diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 0915e4c9f90..8f685a80b66 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -787,7 +787,7 @@ class AccountBankStatementLine(models.Model): or self.statement_id.journal_id.default_debit_account_id.id, 'credit': amount < 0 and -amount or 0.0, 'debit': amount > 0 and amount or 0.0, - 'statement_id': self.statement_id.id, + 'statement_line_id': self.id, 'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False), 'amount_currency': amount_currency, } diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 50a6102b0c8..dd3f32f25f2 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -498,9 +498,9 @@ class AccountInvoice(models.Model): else: account_id = pay_account.id payment_term_id = p.property_supplier_payment_term_id.id - addr = self.partner_id.address_get(['delivery']) - fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=addr['delivery']) + delivery_partner_id = self.get_delivery_partner_id() + fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=delivery_partner_id) # If partner has no warning, check its company if p.invoice_warn == 'no-message' and p.parent_id: @@ -528,6 +528,10 @@ class AccountInvoice(models.Model): return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}} return {} + @api.multi + def get_delivery_partner_id(self): + self.ensure_one() + return self.partner_id.address_get(['delivery'])['delivery'] @api.onchange('journal_id') def _onchange_journal_id(self): @@ -1039,7 +1043,7 @@ class AccountInvoice(models.Model): values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids) - tax_lines = filter(lambda l: l.manual, invoice.tax_line_ids) + tax_lines = invoice.tax_line_ids values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines) if journal_id: diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index d45f61fe50d..220aebf4321 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -304,11 +304,13 @@ class account_journal(models.Model): 'default_type': invoice_type, 'type': invoice_type }) + [action] = self.env.ref('account.%s' % action_name).read() action['context'] = ctx action['domain'] = self._context.get('use_domain', []) - action['views'] = False - action['view_id'] = False + if action_name in ['action_bank_statement_tree', 'action_view_bank_statement_tree']: + action['views'] = False + action['view_id'] = False return action @api.multi diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 827d9de7073..edd0a547b29 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -8,6 +8,7 @@ from odoo.exceptions import RedirectWarning, UserError, ValidationError from odoo.tools.misc import formatLang from odoo.tools import float_is_zero, float_compare from odoo.tools.safe_eval import safe_eval +import odoo.addons.decimal_precision as dp from lxml import etree #---------------------------------------------------------- @@ -244,9 +245,9 @@ class AccountMove(models.Model): journal_id=journal_id) reversed_moves |= reversed_move #unreconcile all lines reversed - aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile) + aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity') aml.remove_move_reconcile() - #reconcile together the reconciliable aml and their newly created counterpart + #reconcile together the reconciliable and the liquidity aml and their newly created counterpart for account in [x.account_id for x in aml]: to_rec = aml.filtered(lambda y: y.account_id == account) to_rec |= reversed_move.line_ids.filtered(lambda y: y.account_id == account) @@ -370,7 +371,7 @@ class AccountMoveLine(models.Model): self.counterpart = ",".join(counterpart) name = fields.Char(string="Label") - quantity = fields.Float(digits=(16, 2), + quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'), help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports.") product_uom_id = fields.Many2one('product.uom', string='Unit of Measure') product_id = fields.Many2one('product.product', string='Product') @@ -780,7 +781,7 @@ class AccountMoveLine(models.Model): """ for datum in data: if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2: - self.process_reconciliation(datum['mv_line_ids'], datum['new_mv_line_dicts']) + self.env['account.move.line'].browse(datum['mv_line_ids']).process_reconciliation(datum['new_mv_line_dicts']) if datum['type'] == 'partner': partners = self.env['res.partner'].browse(datum['id']) @@ -820,6 +821,11 @@ class AccountMoveLine(models.Model): def _get_pair_to_reconcile(self): #field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set) field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + #reconciliation on bank accounts are special cases as we don't want to set them as reconciliable + #but we still want to reconcile entries that are reversed together in order to clear those lines + #in the bank reconciliation report. + if not self[0].account_id.reconcile and self[0].account_id.internal_type == 'liquidity': + field = 'balance' rounding = self[0].company_id.currency_id.rounding if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]): #or if all lines share the same currency @@ -915,7 +921,7 @@ class AccountMoveLine(models.Model): raise UserError(_('To reconcile the entries company should be the same for all entries!')) if len(set(all_accounts)) > 1: raise UserError(_('Entries are not of the same account!')) - if not all_accounts[0].reconcile: + if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'): raise UserError(_('The account %s (%s) is not marked as reconciliable !') % (all_accounts[0].name, all_accounts[0].code)) if len(partners) > 1: raise UserError(_('The partner has to be the same on all lines for receivable and payable accounts!')) @@ -1438,9 +1444,9 @@ class AccountPartialReconcile(models.Model): for rec in self: if not rec.company_id.currency_exchange_journal_id: raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not self.company_id.income_currency_exchange_account_id.id: + if not rec.company_id.income_currency_exchange_account_id.id: raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) - if not self.company_id.expense_currency_exchange_account_id.id: + if not rec.company_id.expense_currency_exchange_account_id.id: raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) move_vals = {'journal_id': rec.company_id.currency_exchange_journal_id.id} @@ -1500,6 +1506,8 @@ class AccountPartialReconcile(models.Model): #amount is the current cash_basis amount minus the one before the reconciliation amount = line.balance * percentage_after - line.balance * percentage_before rounded_amt = line.company_id.currency_id.round(amount) + if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding): + continue if line.tax_line_id and line.tax_line_id.use_cash_basis: if not newly_created_move: newly_created_move = self._create_tax_basis_move() diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index c01d22cac04..ba2503d41a9 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -11,6 +11,7 @@ class ReportPartnerLedger(models.AbstractModel): def _lines(self, data, partner): full_account = [] + currency = self.env['res.currency'] query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] @@ -40,6 +41,7 @@ class ReportPartnerLedger(models.AbstractModel): ) sum += r['debit'] - r['credit'] r['progress'] = sum + r['currency_id'] = currency.browse(r.get('currency_id')) full_account.append(r) return full_account diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 6796c7fb2f6..bad933225a2 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -304,10 +304,10 @@ - customersCustomers - suppliersVendors - othersOtherAccounts - allAll + Customers + Vendors + OtherAccounts + All diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index ab4a3c75587..fb34e891b0b 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -631,7 +631,7 @@ - + form diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 07973558ab0..ade9b2738d7 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -125,18 +125,23 @@ -
+ +
+ + diff --git a/addons/account/views/report_partnerledger.xml b/addons/account/views/report_partnerledger.xml index 0259e5167e8..5294736086c 100644 --- a/addons/account/views/report_partnerledger.xml +++ b/addons/account/views/report_partnerledger.xml @@ -36,7 +36,7 @@ - + @@ -77,8 +77,10 @@ - diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py index b2a99d4bb7e..bea13ffbf35 100644 --- a/addons/account/wizard/account_invoice_refund.py +++ b/addons/account/wizard/account_invoice_refund.py @@ -58,7 +58,6 @@ class AccountInvoiceRefund(models.TransientModel): date = form.date or False description = form.description or inv.name refund = inv.refund(form.date_invoice, date, description, inv.journal_id.id) - refund.compute_taxes() created_inv.append(refund.id) if mode in ('cancel', 'modify'): diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index 6f1251ccaca..83639b5f3a5 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -471,15 +471,14 @@ class AccountAssetDepreciationLine(models.Model): @api.multi def create_move(self, post_move=True): created_moves = self.env['account.move'] + prec = self.env['decimal.precision'].precision_get('Account') for line in self: category_id = line.asset_id.category_id depreciation_date = self.env.context.get('depreciation_date') or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id amount = current_currency.compute(line.amount, company_currency) - sign = (category_id.journal_id.type == 'purchase' or category_id.journal_id.type == 'sale' and 1) or -1 asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids)) - prec = self.env['decimal.precision'].precision_get('Account') move_line_1 = { 'name': asset_name, 'account_id': category_id.account_depreciation_id.id, @@ -489,7 +488,7 @@ class AccountAssetDepreciationLine(models.Model): 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, - 'amount_currency': company_currency != current_currency and - sign * line.amount or 0.0, + 'amount_currency': company_currency != current_currency and - 1.0 * line.amount or 0.0, } move_line_2 = { 'name': asset_name, @@ -500,7 +499,7 @@ class AccountAssetDepreciationLine(models.Model): 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, - 'amount_currency': company_currency != current_currency and sign * line.amount or 0.0, + 'amount_currency': company_currency != current_currency and line.amount or 0.0, } move_vals = { 'ref': line.asset_id.code, diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index da443656d12..e73f009b5d6 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -61,6 +61,9 @@ widgets.SidebarFilter.include({ }, initialize_m2o: function() { this.dfm = new form_common.DefaultFieldManager(this); + if (!this.view.useContacts) { + return; + } this.dfm.extend_field_desc({ partner_id: { relation: "res.partner", diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index df134ff6c4b..49d2738fbef 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -896,7 +896,7 @@ class Lead(models.Model): 'nb_opportunities': 0, } - opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)]) + opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid), ('activity_date_deadline', '!=', False)]) for opp in opportunities: # Expected closing diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 5e5801b5a59..fdc1d18b7ba 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -30,7 +30,7 @@ class EventType(models.Model): 'interval_unit': 'days', 'interval_type': 'before_event', 'template_id': self.env.ref('event.event_reminder') - })] + })] if self.user_has_groups('event.group_email_scheduling') else [] name = fields.Char('Event Category', required=True, translate=True) # registration diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index a53c8e0c9a5..9a34f1660f5 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -596,7 +596,7 @@ - + diff --git a/addons/fleet/models/fleet.py b/addons/fleet/models/fleet.py index 755eb5c17b4..3dfd4538990 100644 --- a/addons/fleet/models/fleet.py +++ b/addons/fleet/models/fleet.py @@ -213,11 +213,11 @@ class FleetVehicle(models.Model): LogContract = self.env['fleet.vehicle.log.contract'] Cost = self.env['fleet.vehicle.cost'] for record in self: - record.odometer_count = Odometer.search_count([('vehicle_id', '=', self.id)]) - record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', self.id)]) - record.service_count = LogService.search_count([('vehicle_id', '=', self.id)]) - record.contract_count = LogContract.search_count([('vehicle_id', '=', self.id)]) - record.cost_count = Cost.search_count([('vehicle_id', '=', self.id), ('parent_id', '=', False)]) + record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)]) + record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', record.id)]) + record.service_count = LogService.search_count([('vehicle_id', '=', record.id)]) + record.contract_count = LogContract.search_count([('vehicle_id', '=', record.id)]) + record.cost_count = Cost.search_count([('vehicle_id', '=', record.id), ('parent_id', '=', False)]) @api.depends('log_contracts') def _compute_contract_reminder(self): diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 89d5215af00..f3a10cfb417 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -172,19 +172,24 @@ class HrExpense(models.Model): ''' main function that is called when trying to create the accounting entries related to an expense ''' + move_group_by_sheet = {} for expense in self: journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id #create the move that will contain the accounting entries acc_date = expense.sheet_id.accounting_date or expense.date - move = self.env['account.move'].create({ - 'journal_id': journal.id, - 'company_id': self.env.user.company_id.id, - 'date': acc_date, - 'ref': expense.sheet_id.name, - # force the name to the default value, to avoid an eventual 'default_name' in the context - # to set it to '' which cause no number to be given to the account.move when posted. - 'name': '/', - }) + if not expense.sheet_id.id in move_group_by_sheet: + move = self.env['account.move'].create({ + 'journal_id': journal.id, + 'company_id': self.env.user.company_id.id, + 'date': acc_date, + 'ref': expense.sheet_id.name, + # force the name to the default value, to avoid an eventual 'default_name' in the context + # to set it to '' which cause no number to be given to the account.move when posted. + 'name': '/', + }) + move_group_by_sheet[expense.sheet_id.id] = move + else: + move = move_group_by_sheet[expense.sheet_id.id] company_currency = expense.company_id.currency_id diff_currency_p = expense.currency_id != company_currency #one account.move.line per expense (+taxes..) @@ -235,38 +240,46 @@ class HrExpense(models.Model): lines = map(lambda x: (0, 0, expense._prepare_move_line(x)), move_lines) move.with_context(dont_create_taxes=True).write({'line_ids': lines}) expense.sheet_id.write({'account_move_id': move.id}) - move.post() if expense.payment_mode == 'company_account': expense.sheet_id.paid_expense_sheets() + for move in move_group_by_sheet.values(): + move.post() return True + @api.multi + def _prepare_move_line_value(self): + self.ensure_one() + if self.account_id: + account = self.account_id + elif self.product_id: + account = self.product_id.product_tmpl_id._get_product_accounts()['expense'] + if not account: + raise UserError( + _("No Expense account found for the product %s (or for its category), please configure one.") % (self.product_id.name)) + else: + account = self.env['ir.property'].with_context(force_company=self.company_id.id).get('property_account_expense_categ_id', 'product.category') + if not account: + raise UserError( + _('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.')) + aml_name = self.employee_id.name + ': ' + self.name.split('\n')[0][:64] + move_line = { + 'type': 'src', + 'name': aml_name, + 'price_unit': self.unit_amount, + 'quantity': self.quantity, + 'price': self.total_amount, + 'account_id': account.id, + 'product_id': self.product_id.id, + 'uom_id': self.product_uom_id.id, + 'analytic_account_id': self.analytic_account_id.id, + } + return move_line + @api.multi def _move_line_get(self): account_move = [] for expense in self: - if expense.account_id: - account = expense.account_id - elif expense.product_id: - account = expense.product_id.product_tmpl_id._get_product_accounts()['expense'] - if not account: - raise UserError(_("No Expense account found for the product %s (or for its category), please configure one.") % (expense.product_id.name)) - else: - account = self.env['ir.property'].with_context(force_company=expense.company_id.id).get('property_account_expense_categ_id', 'product.category') - if not account: - raise UserError(_('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.')) - - aml_name = expense.employee_id.name + ': ' + expense.name.split('\n')[0][:64] - move_line = { - 'type': 'src', - 'name': aml_name, - 'price_unit': expense.unit_amount, - 'quantity': expense.quantity, - 'price': expense.total_amount, - 'account_id': account.id, - 'product_id': expense.product_id.id, - 'uom_id': expense.product_uom_id.id, - 'analytic_account_id': expense.analytic_account_id.id, - } + move_line = expense._prepare_move_line_value() account_move.append(move_line) # Calculate tax lines and adjust base line diff --git a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py index edb11786749..49adf3fed7f 100644 --- a/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_attendance/models/hr_timesheet_sheet.py @@ -10,7 +10,7 @@ class HrTimesheetSheet(models.Model): _inherit = "hr_timesheet_sheet.sheet" attendances_ids = fields.One2many('hr.attendance', 'sheet_id', 'Attendances') - total_attendance = fields.Integer(string='Total Attendance', compute='_compute_total') + total_attendance = fields.Float(string='Total Attendance', compute='_compute_total') total_timesheet = fields.Float(string='Total Timesheet', compute="_compute_total") total_difference = fields.Float(string='Difference', compute="_compute_total") period_ids = fields.One2many('hr_timesheet_sheet.sheet.day', 'sheet_id', string='Period', readonly=True) diff --git a/addons/l10n_at/data/l10n_at_chart_data.xml b/addons/l10n_at/data/l10n_at_chart_data.xml index 8bc33d795cf..b280c11c0e3 100644 --- a/addons/l10n_at/data/l10n_at_chart_data.xml +++ b/addons/l10n_at/data/l10n_at_chart_data.xml @@ -779,7 +779,7 @@ 2500 - + Vorsteuer diff --git a/addons/l10n_cr/data/l10n_cr_state_data.xml b/addons/l10n_cr/data/l10n_cr_state_data.xml index 585012600b2..3c523f44634 100644 --- a/addons/l10n_cr/data/l10n_cr_state_data.xml +++ b/addons/l10n_cr/data/l10n_cr_state_data.xml @@ -43,55 +43,55 @@ Resource: res.partner.title Update partner titles --> - + Corporation Corp. - + Limited Company Ltd. - + Sociedad Anónima Laboral S.A.L. - + Asociation Asoc. - + Government Gov. - + Educational Institution Edu. - + Independant Professional Ind. Prof. - + Doctora Dra. - + Msc. Msc. - + MBA MBA - + Licenciado Lic. - + Licenciada Licda. - + Ingeniero/a Ing. diff --git a/addons/l10n_in_schedule6/__init__.py b/addons/l10n_in_schedule6/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/l10n_in_schedule6/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/l10n_in_schedule6/__openerp__.py b/addons/l10n_in_schedule6/__openerp__.py new file mode 100644 index 00000000000..20b803525d1 --- /dev/null +++ b/addons/l10n_in_schedule6/__openerp__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Indian - Schedule VI Accounting', + 'version': '2.0', + 'description': """ +Indian Accounting: Chart of Account. +==================================== + +Indian accounting chart and localization. + +Schedule VI chart of account for indian accounting. + +Note: The Schedule VI has been revised by MCA and is applicable for all Balance Sheet made after +31st March, 2011. The Format has done away with earlier two options of format of Balance +Sheet, now only Vertical format has been permitted Which is Supported By OpenERP. + """, + 'category': 'Localization', + 'depends': [ + 'account', + ], + 'demo': [], + 'data': [ + 'l10n_in_schedule6_chart.xml', + 'l10n_in_schedule6_tax_template.xml', + 'account_chart_template.yml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/addons/l10n_in_schedule6/account_chart_template.yml b/addons/l10n_in_schedule6/account_chart_template.yml new file mode 100644 index 00000000000..e536be4873d --- /dev/null +++ b/addons/l10n_in_schedule6/account_chart_template.yml @@ -0,0 +1,3 @@ +- + !python {model: account.chart.template, id: indian_chart_template_schedule6}: | + self[0].try_loading_for_current_company() diff --git a/addons/l10n_in_schedule6/l10n_in_schedule6_chart.xml b/addons/l10n_in_schedule6/l10n_in_schedule6_chart.xml new file mode 100644 index 00000000000..5552803bd7a --- /dev/null +++ b/addons/l10n_in_schedule6/l10n_in_schedule6_chart.xml @@ -0,0 +1,1433 @@ + + + + + + 1206 + Transfer Account + + + + + Indian Chart of Accounts - Schedule VI + 12142 + 12141 + 6 + + + + + + + + + + + Authorised Share Capital + 11101 + + + + + + Issued, Subscribed And Paid Up Capital + 11102 + + + + + + Capital Reserve + 11110 + + + + + + Capital Redemption Reserve + 11111 + + + + + + Securities Premium Reserve + 11112 + + + + + + Debenture Redeemption Reserve + 11113 + + + + + + Revaluation Reserve + 11114 + + + + + + Shares Option Outstanding Account + 11115 + + + + + + Other Reserve + 11116 + + + + + + Surplus (Profit and Loss Account) + 11117 + + + + + + Money Received Against Share Warrants + 1112 + + + + + + + + Share Application Money Pending Allotment + 1121 + + + + + + + Bank Loan (Long Term) + 113011 + + + + + + From Others + 113021 + + + + + + Deferred Tax Liabilities (Net) + 1131 + + + + + + Other Long Term Liabilities + 1132 + + + + + + Long Term Provisions + 1133 + + + + + + + + Bank Loan (Short Term) + 114011 + + + + + + Loans And Advances From Related Parties + 11402 + + + + + + Depsoits + 11403 + + + + + + Others + 11404 + + + + + + + Sundry Creditors For Material/Supplies + 11411 + + + + + + Sundry Creditors For Services + 11412 + + + + + + + Excise Duty Payable + 1142111 + + + + + + Education Cess Payable On Excise Duty + 1142112 + + + + + + Higher Education Cess Payable On Excise Duty + 1142113 + + + + + + Service Tax Payable + 1142121 + + + + + + Education Cess Payable On Service Tax + 1142122 + + + + + + Higher Education Cess Payable On Service Tax + 1142123 + + + + + + TDS Paybale + 114213 + + + + + + Central Sales Tax Payable + 114214 + + + + + + VAT Payable + 114215 + + + + + + Out Standing Cheques for Clearance + 11422 + + + + + + Advance From Customers + 11423 + + + + + + + EPF Payable + 114301 + + + + + + Unpaid Remuneration + 114302 + + + + + + Electricity And Power Charges Payable + 114311 + + + + + + Employee Providend Fund Payable + 114312 + + + + + + Internet Expenses Payable + 114313 + + + + + + Mobile Expenses Payable + 114314 + + + + + + Secueity Expenses Payable + 114315 + + + + + + Telephone Expenses Payable + 114316 + + + + + + Wages Payable + 114317 + + + + + + Provision For Taxation + 114318 + + + + + + Provision For Expenses + 114319 + + + + + + + + + Land + 120101 + + + + + + Buildings + 120102 + + + + + + Plant And Equipments + 120103 + + + + + + Furnitures And Fixtures + 120104 + + + + + + Vehicles + 120105 + + + + + + Computer/Laptops + 120106 + + + + + + Office Equipments + 120107 + + + + + + + Goodwill + 120111 + + + + + + + Building Under Construction + 120120 + + + + + + + Intangible Assets Under Development + 12013 + + + + + + + Investment In Property + 12020 + + + + + + Investment In Equity Instrument + 12021 + + + + + + Investment In Preference Shares + 12022 + + + + + + Investment In Government Or Trust Securities + 12023 + + + + + + Investment In Debentures And Bonds + 12024 + + + + + + Investment In Mutual Fund + 12025 + + + + + + Investment In Partnership Firm + 12026 + + + + + + Gold + 120270 + + + + + + Fixed Deposit With Banks-Earmarked + 120271 + + + + + + + Deferred Tax Assets (Net) + 1203 + + + + + + + Secured, Considered Good + 120411 + + + + + + Unsecured, Considered Good + 120412 + + + + + + Doubtful + 120413 + + + + + + Earnest Money Deposit + 1204211 + + + + + + Other Deposit + 1204212 + + + + + + Unsecured, Considered Good + 120422 + + + + + + Doubtful + 120423 + + + + + + Loans And Advances To Related Parties + 12043 + + + + + + Other Loans And Advances + 12044 + + + + + + + Secured, Considered Good + 120511 + + + + + + Unsecured, Considered Good + 120512 + + + + + + Doubtful + 120513 + + + + + + Preliminery Expense + 120521 + + + + + + Software + 120522 + + + + + + Others + 12053 + + + + + + + + Investment In Equity + 12110 + + + + + + Investment In Preference Shares + 12111 + + + + + + Investment In Government Securities + 12112 + + + + + + Investment In Debentures And Bonds + 12113 + + + + + + Investment In Mutual Fund + 12114 + + + + + + Investment In Partnership Firm + 12115 + + + + + + In Short Term Fixed Deposits + 12116 + + + + + + + Raw Material + 12120 + + + + + + Work-In-Progress + 12121 + + + + + + Finished Goods + 12122 + + + + + + Stock-In-Trade + 12123 + + + + + + Stores And Spares + 12124 + + + + + + Loose Tools + 12125 + + + + + + Other With Job Worker + 12126 + + + + + + Goods-In-Transit + 12127 + + + + + + + Secured, Considered Good + 121311 + + + + + + Unsecured, Considered Good + 121312 + + + + + + Doubtful + 121313 + + + + + + Secured, Considered Good + 121321 + + + + + + Unsecured, Considered Good + 121322 + + + + + + Doubtful + 121323 + + + + + + Sundry Debtors + 12133 + + + + + + + + Advance To Associate Concerns + 1215111 + + + + + + Unsecured Recoverable in Cash or in Kind or for Value to be Received + 1215121 + + + + + + Doubtful + 121513 + + + + + + Advance To Suppliers + 121521 + + + + + + Advance Income Tax/Refund Due + 121522 + + + + + + Balance With Revenue Authorities (Indirect Taxes) + 121523 + + + + + + Prepaid Expenses + 121524 + + + + + + + Sales Tax Receivable + 121610 + + + + + + VAT Receivable + 121611 + + + + + + Excise Duty Receivable + 1216121 + + + + + + Education Cess Receivable On Excise Duty + 1216122 + + + + + + Higher Education Cess Receivable On Excise Duty + 1216123 + + + + + + Service Tax Receivable + 1216131 + + + + + + Education Cess Receivable On Service Tax + 1216132 + + + + + + Higher Education Cess Receivable On Service Tax + 1216133 + + + + + + TDS Receivable + 121614 + + + + + + Preliminary Expense + 121621 + + + + + + + + Central Sales(Exclusive of Excise Duty) + 2011 + + + + + + Export Sales + 2012 + + + + + + Sales of Services + 2013 + + + + + + Job Work Receipts + 2014 + + + + + + Product Sales + 2015 + + + + + + Other Operating Income + 2016 + + + + + + + Interest + 2021 + + + + + + Other Financial Services + 2022 + + + + + + Other Non Operating Income + 2023 + + + + + + + Opening Stock + 2101 + + + + + + Raw Material + 21021 + + + + + + Stores And Consumables + 21022 + + + + + + Closing Stock + 2103 + + + + + + + Purchase of Stock-In-Trade + 2111 + + + + + + + Changes In Inventories of Finished Goods, Work-In-Progress And Stock-In-Trade + 212 + + + + + + + Salaries, Bonus, PF And ESIC + 2131 + + + + + + Directors Remuneration + 2132 + + + + + + Directors Setting Fees + 2133 + + + + + + Staff Welfare Expense + 2134 + + + + + + Commision To M.D + 2135 + + + + + + Commision To Joint M.D + 2136 + + + + + + + Interest On Cash Credit Facality + 2141 + + + + + + Interest On Car Loan + 2142 + + + + + + Interest On Other Loans + 2143 + + + + + + Bank Charges + 2144 + + + + + + Bank Gurantee Charges + 2145 + + + + + + + Depreciation + 2151 + + + + + + Preliminary Expenses + 2152 + + + + + + + Jobwork Charges + 21601 + + + + + + Stores and Spares + 21602 + + + + + + Workshop Expense + 21603 + + + + + + Centeral Sales Tax + 21604 + + + + + + Machinery Repair and Maintenance + 21605 + + + + + + Freight Forwarding and Octroi Expenses (Local) + 21606 + + + + + + Electrical Repairing Expense + 21607 + + + + + + Material Testing Expense + 21608 + + + + + + Power and Fuel Charges + 21609 + + + + + + Packing Material Expense + 216091 + + + + + + Measuring Expense + 216092 + + + + + + Processing Labour Charges + 216093 + + + + + + Car Expenses (Including Insurance) + 21611 + + + + + + Travelling Expenses (Including Foreign Travelling) + 21612 + + + + + + Repair And Maintenance Building + 21613 + + + + + + Computer Maintenace + 21614 + + + + + + General Expenses + 21615 + + + + + + Internet And Website Expenses + 21616 + + + + + + Insurance Expenses + 21617 + + + + + + Telephone Expense + 21618 + + + + + + Labour Welfare Expenses + 21619 + + + + + + Legal Expenses + 216191 + + + + + + Postage And Telegram + 216192 + + + + + + Professional Charges + 216193 + + + + + + Security Expesnes + 216194 + + + + + + Stationery Expenses + 216195 + + + + + + Auditors Remuneration + 216196 + + + + + + Rent Rates And Taxes + 216197 + + + + + + Tender Fee And Testing Charges + 216198 + + + + + + Interest On Delayed Payment of Taxes + 216199 + + + + + + Excise/ Service Tax Expenses + 2161991 + + + + + + Advertisement And Publicity + 2161992 + + + + + + Donation + 2161993 + + + + + + Export Expenses + 2161994 + + + + + + Internal Audit Fee + 2161995 + + + + + + Conveyance Expense + 2161996 + + + + + + Packing and Forwarding Expense + 2161997 + + + + + + Annual Service Fees + 2161998 + + + + + + Loan Processing Fees + 2161999 + + + + + + Office Cleaning Expense + 21619991 + + + + + + Prelimenary Expense W/Off + 21619992 + + + + + + Software Expense W/Off + 21619993 + + + + + + Custom Duty + 21619994 + + + + + + Stamping Charge + 21619995 + + + + + + Post and Courrier Charges + 21619996 + + + + + + + + + Foreign Exchange Profit + 2017 + + + + + + Foreign Exchange Loss + 2170 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/l10n_in_schedule6/l10n_in_schedule6_tax_template.xml b/addons/l10n_in_schedule6/l10n_in_schedule6_tax_template.xml new file mode 100644 index 00000000000..4b98a7efafa --- /dev/null +++ b/addons/l10n_in_schedule6/l10n_in_schedule6_tax_template.xml @@ -0,0 +1,842 @@ + + + + + + + Tax to Pay + taxes + + + + C Forms + taxes + + + + C Forms Tax Collected on Sale + taxes + + + + C Forms Tax Paid on Purchase + taxes + + + + Excise Duty + taxes + + + + Excise Duty 12.36% + taxes + + + + Excise Duty Collected on Sales + taxes + + + + Education Cess 2% Excise Duty Collected on Sales + taxes + + + + Higher Education Cess 1% Excise Duty Collected on Sales + taxes + + + + Excise Duty Paid on Purchase + taxes + + + + Education Cess 2% Excise Duty Paid on Purchase + taxes + + + + Higher Education Cess 1% Excise Duty Paid on Purchase + taxes + + + + Sales Tax + taxes + + + + Sales Tax 4% + taxes + + + + Sales Tax 4% Collected on Sale + taxes + + + + Sales Tax 4% Paid on Purchase + taxes + + + + Sales Tax 12% + taxes + + + + Sales Tax 12% Collected on Sale + taxes + + + + Sales Tax 12% Paid on Purchase + taxes + + + + Sales Tax 15% + taxes + + + + Sales Tax 15% Collected on Sale + taxes + + + + Sales Tax 15% Paid on Purchase + taxes + + + + Service Tax + taxes + + + + Service Tax 12.36% + taxes + + + + Service Tax Collected on Sales of Services + taxes + + + + Education Cess 2% Service Tax Collected on Sales of Services + taxes + + + + Higher Education Cess 1% Service Tax Collected on Sales of Services + taxes + + + + Service Tax Paid on Purchase of Services + taxes + + + + Education Cess 2% Service Tax Paid on Purchase of Services + taxes + + + + Higher Education Cess 1% Service Tax Paid on Purchase of Services + taxes + + + + VAT + taxes + + + + VAT 4% + taxes + + + + VAT 4% Collected on Sale + taxes + + + + VAT 4% Paid on Purchase + taxes + + + + VAT 8% + taxes + + + + VAT 8% Collected on Sale + taxes + + + + VAT 8% Paid on Purchase + taxes + + + + VAT 10% + taxes + + + + VAT 10% Collected on Sale + taxes + + + + VAT 10% Paid on Purchase + taxes + + + + VAT 12.5% + taxes + + + + VAT 12.5% Collected on Sale + taxes + + + + VAT 12.5% Paid on Purchase + taxes + + + + Additional VAT 1% + taxes + + + + Additional VAT 1% Collected on Sale + taxes + + + + Additional VAT 1% Paid on Purchase + taxes + + + + Additional VAT 2.5% + taxes + + + + Additional VAT 2.5% Collected on Sale + taxes + + + + Additional VAT 2.5% Paid on Purchase + taxes + + + + Tax Bases + taxes + + + + Base C Forms + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Excise Duty + taxes + + + + Base Excise Duty 12.36% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Sales Tax + taxes + + + + Base Sales Tax 4% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Sales Tax 12% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Sales Tax 15% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Service Tax + taxes + + + + Base Service Tax 12.36% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT + taxes + + + + Base VAT 4% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT 8% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT 10% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base VAT 12.5% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Additional VAT 1% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + Base Additional VAT 2.5% + taxes + + + + Base of Sale + taxes + + + + Base of Purchase + taxes + + + + + (schedule6)Input Sales Tax @ 15% + (schedule6)Input Sales Tax @ 15% + sale + percent + 15 + 10 + + + + + + (schedule6)Input Sales Tax @ 12% + (schedule6)Input Sales Tax @ 12% + sale + percent + 12 + 10 + + + + + + (schedule6)Input Sales Tax @ 4% + (schedule6)Input Sales Tax @ 4% + sale + percent + 4 + 10 + + + + + + (schedule6)Additional Input VAT @ 1% + (schedule6)Additional Input VAT @ 1% + sale + percent + 1 + 15 + + + + + + (schedule6)Input VAT @ 12.5% + (schedule6)Input VAT @ 12.5% + sale + percent + 12.5 + 10 + + + + + + (schedule6)Additional Input VAT @ 2.5% + (schedule6)Additional Input VAT @ 2.5% + sale + percent + 2.5 + 15 + + + + + + (schedule6)Input VAT @ 8% + (schedule6)Input VAT @ 8% + sale + percent + 8 + 10 + + + + + + (schedule6)Input VAT @ 10% + (schedule6)Input VAT @ 10% + sale + percent + 10 + 10 + + + + + + (schedule6)Input Tax 12% + (schedule6)Input Tax 12% + none + percent + 12 + 1 + + + + + + (schedule6)Input Education Cess @ 2% On Excise Duty + (schedule6)Input Education Cess @ 2% On Excise Duty + none + percent + 0.24 + 2 + + + + + + (schedule6)Input Higher Education Cess @ 1% On Excise Duty + (schedule6)Input Higher Education Cess @ 1% On Excise Duty + none + percent + 0.1224 + 3 + + + + + + (schedule6)Input Excise Duty @ 12.36% + (schedule6)Input Excise Duty @ 12.36% + sale + group + 0 + 1 + + + + + + + (schedule6)Input Service Tax 12% + (schedule6)Input Service Tax 12% + none + percent + 12 + 13 + + + + + + (schedule6)Input Education Cess @ 2% On Service Tax + (schedule6)Input Education Cess @ 2% On Service Tax + none + percent + 0.24 + 14 + + + + + + (schedule6)Input Higher Education Cess @ 1% On Service Tax + (schedule6)Input Higher Education Cess @ 1% On Service Tax + none + percent + 0.1224 + 15 + + + + + + (schedule6)Input Service Tax @ 12.36% + (schedule6)Input Service Tax @ 12.36% + sale + group + 12 + True + 8 + + + + + + + (schedule6)Input VAT @ 4% + (schedule6)Input VAT @ 4% + sale + percent + 4 + 10 + + + + + + + + (schedule6)Output Purchase Tax @ 15% + (schedule6)Output Purchase Tax @ 15% + purchase + percent + 15 + 10 + + + + + + (schedule6)Output Purchase Tax @ 12% + (schedule6)Output Purchase Tax @ 12% + purchase + percent + 12 + 10 + + + + + + (schedule6)Output Purchase Tax @ 4% + (schedule6)Output Purchase Tax @ 4% + purchase + percent + 4 + 10 + + + + + + (schedule6)Additional Output VAT @ 1% + (schedule6)Additional Output VAT @ 1% + purchase + percent + 1 + 15 + + + + + + (schedule6)Output VAT @ 12.5% + (schedule6)Output VAT @ 12.5% + purchase + percent + 12.5 + 10 + + + + + + (schedule6)Additional Output VAT @ 2.5% + (schedule6)Additional Output VAT @ 2.5% + purchase + percent + 2.5 + 15 + + + + + + (schedule6)Output VAT @ 8% + (schedule6)Output VAT @ 8% + purchase + percent + 8 + 10 + + + + + + (schedule6)Output VAT @ 10% + (schedule6)Output VAT @ 10% + purchase + percent + 10 + 10 + + + + + + (schedule6)Output Tax 12% + (schedule6)Output Tax 12% + none + percent + 12 + 2 + + + + + + (schedule6)Output Education Cess @ 2% Excise Duty + (schedule6)Output Education Cess @ 2% Excise Duty + none + percent + 0.24 + 3 + + + + + + (schedule6)Output Higher Education Cess @ 1% Excise Duty + (schedule6)Output Higher Education Cess @ 1% Excise Duty + none + percent + 0.1224 + 4 + + + + + + (schedule6)Output Excise Duty @ 12.36% + (schedule6)Output Excise Duty @ 12.36% + purchase + group + 12 + True + 1 + + + + + + + (schedule6)Output Service Tax 12% + (schedule6)Output Service Tax 12% + none + percent + 12 + 9 + + + + + + (schedule6)Output Education Cess @ 2% On Service Tax + (schedule6)Output Education Cess @ 2% On Service Tax + none + percent + 0.24 + 10 + + + + + + (schedule6)Output Higher Education Cess @ 1% On Service Tax + (schedule6)Output Higher Education Cess @ 1% On Service Tax + none + percent + 0.1224 + 11 + + + + + + (schedule6)Output Service Tax @ 12.36% + (schedule6)Output Service Tax @ 12.36% + purchase + True + group + 12 + 8 + + + + + + + (schedule6)Output VAT @ 4% + (schedule6)Output VAT @ 4% + purchase + percent + 4 + 10 + + + + + + + diff --git a/addons/l10n_in_schedule6/static/description/icon.png b/addons/l10n_in_schedule6/static/description/icon.png new file mode 100644 index 00000000000..ddaf38f8b6d Binary files /dev/null and b/addons/l10n_in_schedule6/static/description/icon.png differ diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 4e8a1ada142..62f75314f8d 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1702,9 +1702,10 @@ class MailThread(models.AbstractModel): return self._message_post_process_attachments(attachments, attachment_ids, {'model': attach_model, 'res_id': attach_res_id}) def _message_post_process_attachments(self, attachments, attachment_ids, message_data): - IrAttachment, parameter_attachments = self.env['ir.attachment'], self.env['ir.attachment'] + IrAttachment = self.env['ir.attachment'] m2m_attachment_ids = [] cid_mapping = {} + fname_mapping = {} if attachment_ids: filtered_attachment_ids = self.env['ir.attachment'].sudo().search([ ('res_model', '=', 'mail.compose.message'), @@ -1720,9 +1721,7 @@ class MailThread(models.AbstractModel): name, content = attachment elif len(attachment) == 3: name, content, info = attachment - if info and info.get('cid'): - cid = info['cid'] - cid_mapping[cid] = name + cid = info and info.get('cid') else: continue if isinstance(content, unicode): @@ -1730,13 +1729,16 @@ class MailThread(models.AbstractModel): data_attach = { 'name': name, 'datas': base64.b64encode(str(content)), - 'datas_fname': cid or name, + 'datas_fname': name, 'description': name, 'res_model': message_data['model'], 'res_id': message_data['res_id'], } - parameter_attachments |= IrAttachment.create(data_attach) - m2m_attachment_ids += [(4, attach.id) for attach in parameter_attachments] + new_attachment = IrAttachment.create(data_attach) + m2m_attachment_ids.append((4, new_attachment.id)) + if cid: + cid_mapping[cid] = new_attachment + fname_mapping[name] = new_attachment if cid_mapping and message_data.get('body'): root = lxml.html.fromstring(tools.ustr(message_data['body'])) @@ -1744,12 +1746,11 @@ class MailThread(models.AbstractModel): for node in root.iter('img'): if node.get('src', '').startswith('cid:'): cid = node.get('src').split('cid:')[1] - fname = cid_mapping.get(cid, node.get('data-filename', '')) - attachment = parameter_attachments.filtered(lambda attachment: attachment.datas_fname == cid) + attachment = cid_mapping.get(cid) if not attachment: - attachment = parameter_attachments.filtered(lambda attachment: attachment.datas_fname == fname) + attachment = fname_mapping.get(node.get('data-filename'), '') if attachment: - node.set('src', '/web/image/%s' % attachment.ids[0]) + node.set('src', '/web/image/%s' % attachment.id) postprocessed = True if postprocessed: body = lxml.html.tostring(root, pretty_print=False, encoding='UTF-8') diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 6be82359009..446ae2c877d 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -312,7 +312,7 @@ - + diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index 8dae482529f..3a69530db1a 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -7,8 +7,7 @@ import werkzeug.urls from odoo import api, fields, models, tools - -URL_REGEX = r'(\bhref=[\'"]([^\'"]+)[\'"])' +from openerp.addons.link_tracker.models.link_tracker import URL_REGEX class MailMail(models.Model): diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index 7ecc6274636..25ef11da9a3 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -44,7 +44,12 @@ class ProductProduct(models.Model): read_group_res = self.env['mrp.bom'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id']) mapped_data = dict([(data['product_id'][0], data['product_id_count']) for data in read_group_res]) for product in self: - product.bom_count = mapped_data.get(product.id, 0) + if product.product_tmpl_id.product_variant_count == 1: + bom_count = mapped_data.get(product.id, product.product_tmpl_id.bom_count) + else: + bom_count = mapped_data.get(product.id, 0) + product.bom_count = bom_count + def _compute_mo_count(self): read_group_res = self.env['mrp.production'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id']) diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 150f9cd562e..ad1a50ad738 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -33,7 +33,7 @@ class StockMoveLots(models.Model): def _check_lot_id(self): if self.move_id.product_id.tracking == 'serial': lots = set([]) - for move_lot in self.move_id.move_lot_ids.filtered(lambda r: not r.lot_produced_id): + for move_lot in self.move_id.active_move_lot_ids.filtered(lambda r: not r.lot_produced_id and r.lot_id): if move_lot.lot_id in lots: raise exceptions.UserError(_('You cannot use the same serial number in two different lines.')) if float_compare(move_lot.quantity_done, 1.0, precision_rounding=move_lot.product_id.uom_id.rounding) == 1: @@ -59,6 +59,15 @@ class StockMoveLots(models.Model): self.quantity_done = self.quantity_done - 1 return self.move_id.split_move_lot() + @api.multi + def write(self, vals): + if 'lot_id' in vals: + for movelot in self: + movelot.move_id.production_id.move_raw_ids.mapped('move_lot_ids')\ + .filtered(lambda r: r.done_wo and not r.done_move and r.lot_produced_id == movelot.lot_id)\ + .write({'lot_produced_id': vals['lot_id']}) + return super(StockMoveLots, self).write(vals) + class StockMove(models.Model): _inherit = 'stock.move' diff --git a/addons/mrp_repair/models/mrp_repair.py b/addons/mrp_repair/models/mrp_repair.py index c23e810fd1f..866bde4858b 100644 --- a/addons/mrp_repair/models/mrp_repair.py +++ b/addons/mrp_repair/models/mrp_repair.py @@ -490,7 +490,10 @@ class RepairLine(models.Model): if partner and self.product_id: self.tax_id = partner.property_account_position_id.map_tax(self.product_id.taxes_id, self.product_id, partner).ids if self.product_id: - self.name = self.product_id.display_name + if partner: + self.name = self.product_id.with_context(lang=partner.lang).display_name + else: + self.name = self.product_id.display_name self.product_uom = self.product_id.uom_id.id warning = False diff --git a/addons/mrp_repair/report/mrp_repair_reports.xml b/addons/mrp_repair/report/mrp_repair_reports.xml index db73449155d..9963f87d614 100644 --- a/addons/mrp_repair/report/mrp_repair_reports.xml +++ b/addons/mrp_repair/report/mrp_repair_reports.xml @@ -5,7 +5,7 @@ string="Quotation / Order" id="action_report_mrp_repair_order" model="mrp.repair" - name="mrp_repair.report_mrprepairorder" + name="mrp_repair.report_mrprepairorder2" file="mrp_repair.report_mrprepairorder" report_type="qweb-pdf" /> diff --git a/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml b/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml index abbb6d1881d..93171e959e9 100644 --- a/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml +++ b/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml @@ -2,9 +2,9 @@ + + diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index 28cba8db301..6eef3ae80cc 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -150,7 +150,7 @@ class AuthorizeAPI(): etree.SubElement(root, "transId").text = transaction_id customer = etree.SubElement(root, "customer") etree.SubElement(customer, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8]) - etree.SubElement(customer, "email").text = partner.email + etree.SubElement(customer, "email").text = partner.email or '' response = self._authorize_request(root) res = dict() res['profile_id'] = response.find('customerProfileId').text diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py index bdf35742bec..2fefb1417ef 100644 --- a/addons/payment_paypal/controllers/main.py +++ b/addons/payment_paypal/controllers/main.py @@ -32,16 +32,22 @@ class PaypalController(http.Controller): :param response str: text response, structured in the following way: STATUS\nkey1=value1\nkey2=value2...\n + or STATUS\nError message...\n :rtype tuple(str, dict) :return: tuple containing the STATUS str and the key/value pairs parsed as a dict """ lines = filter(None, response.split('\n')) status = lines.pop(0) - pdt_post = dict(line.split('=', 1) for line in lines) - # html unescape - for post in pdt_post: - pdt_post[post] = urllib.unquote_plus(pdt_post[post]).decode('utf8') + + pdt_post = {} + for line in lines: + split = line.split('=', 1) + if len(split) == 2: + pdt_post[split[0]] = urllib.unquote_plus(split[1]).decode('utf8') + else: + _logger.warning('Paypal: error processing pdt response: %s', line) + return status, pdt_post def paypal_validate_data(self, **post): diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 3c110494630..cd8a1e8e493 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -14,7 +14,10 @@ class PosController(http.Controller): @http.route('/pos/web', type='http', auth='user') def pos_web(self, debug=False, **k): # if user not logged in, log him in - pos_sessions = request.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', request.session.uid)]) + pos_sessions = request.env['pos.session'].search([ + ('state', '=', 'opened'), + ('user_id', '=', request.session.uid), + ('name', 'not like', '(RESCUE FOR')]) if not pos_sessions: return werkzeug.utils.redirect('/web#action=point_of_sale.action_client_pos_menu') pos_sessions.login() diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 83eaecc015f..f16421a02ae 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -31,7 +31,7 @@ class PosConfig(models.Model): def _default_sale_journal(self): journal = self.env.ref('point_of_sale.pos_sale_journal', raise_if_not_found=False) - if journal and journal.company_id == self.env.user.company_id: + if journal and journal.sudo().company_id == self.env.user.company_id: return journal return self._default_invoice_journal() @@ -140,9 +140,12 @@ class PosConfig(models.Model): @api.depends('session_ids') def _compute_current_session(self): for pos_config in self: - session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and not r.state == 'closed') - pos_config.current_session_id = session - pos_config.current_session_state = session.state + session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \ + not r.state == 'closed' and \ + '(RESCUE FOR' not in r.name) + # sessions ordered by id desc + pos_config.current_session_id = session and session[0].id or False + pos_config.current_session_state = session and session[0].state or False @api.depends('session_ids') def _compute_last_session(self): @@ -162,7 +165,8 @@ class PosConfig(models.Model): @api.depends('session_ids') def _compute_current_session_user(self): for pos_config in self: - pos_config.pos_session_username = pos_config.session_ids.filtered(lambda s: s.state == 'opened').user_id.name + session = pos_config.session_ids.filtered(lambda s: s.state == 'opened' and '(RESCUE FOR' not in s.name) + pos_config.pos_session_username = session and session[0].user_id.name or False @api.constrains('company_id', 'stock_location_id') def _check_company_location(self): @@ -210,7 +214,7 @@ class PosConfig(models.Model): @api.model def create(self, values): - IrSequence = self.env['ir.sequence'] + IrSequence = self.env['ir.sequence'].sudo() val = { 'name': _('POS Order %s') % values['name'], 'padding': 4, diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 06d68cfc221..babe056901c 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -66,11 +66,18 @@ class PosOrder(models.Model): closed_session.id, order['name'], order['amount_total']) + rescue_session = PosSession.search([ + ('name', 'like', '(RESCUE FOR %(session)s)' % {'session': closed_session.name}), + ('state', 'not in', ('closed', 'closing_control')), + ], limit=1) + if rescue_session: + _logger.warning('reusing recovery session %s for saving order %s', rescue_session.name, order['name']) + return rescue_session + _logger.warning('attempting to create recovery session for saving order %s', order['name']) new_session = PosSession.create({ 'config_id': closed_session.config_id.id, - 'name': _('(RESCUE FOR %(session)s)') % {'session': closed_session.name}, - 'rescue': True, # avoid conflict with live sessions + 'name': '(RESCUE FOR %(session)s)' % {'session': closed_session.name}, }) # bypass opening_control (necessary when using cash control) new_session.action_pos_session_open() @@ -347,7 +354,12 @@ class PosOrder(models.Model): name = fields.Char(string='Order Ref', required=True, readonly=True, copy=False, default='/') company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id) date_order = fields.Datetime(string='Order Date', readonly=True, index=True, default=fields.Datetime.now) - user_id = fields.Many2one('res.users', string='Salesman', help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", default=lambda self: self.env.uid) + user_id = fields.Many2one( + comodel_name='res.users', string='Salesman', + help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", + default=lambda self: self.env.uid, + states={'done': [('readonly', True)], 'invoiced': [('readonly', True)]}, + ) amount_tax = fields.Float(compute='_compute_amount_all', string='Taxes', digits=0) amount_total = fields.Float(compute='_compute_amount_all', string='Total', digits=0) amount_paid = fields.Float(compute='_compute_amount_all', string='Paid', states={'draft': [('readonly', False)]}, readonly=True, digits=0) @@ -372,12 +384,22 @@ class PosOrder(models.Model): account_move = fields.Many2one('account.move', string='Journal Entry', readonly=True, copy=False) picking_id = fields.Many2one('stock.picking', string='Picking', readonly=True, copy=False) picking_type_id = fields.Many2one('stock.picking.type', related='session_id.config_id.picking_type_id', string="Operation Type") - location_id = fields.Many2one('stock.location', related='session_id.config_id.stock_location_id', string="Location", store=True) + location_id = fields.Many2one( + comodel_name='stock.location', + related='session_id.config_id.stock_location_id', + string="Location", store=True, + readonly=True, + ) note = fields.Text(string='Internal Notes') nb_print = fields.Integer(string='Number of Print', readonly=True, copy=False, default=0) pos_reference = fields.Char(string='Receipt Ref', readonly=True, copy=False) sale_journal = fields.Many2one('account.journal', related='session_id.config_id.journal_id', string='Sales Journal', store=True, readonly=True) - fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', default=lambda self: self._default_session().config_id.default_fiscal_position_id) + fiscal_position_id = fields.Many2one( + comodel_name='account.fiscal.position', string='Fiscal Position', + default=lambda self: self._default_session().config_id.default_fiscal_position_id, + readonly=True, + states={'draft': [('readonly', False)]}, + ) @api.depends('statement_ids', 'lines.price_subtotal_incl', 'lines.discount') def _compute_amount_all(self): @@ -553,6 +575,8 @@ class PosOrder(models.Model): Move = self.env['stock.move'] StockWarehouse = self.env['stock.warehouse'] for order in self: + if not order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu']): + continue address = order.partner_id.address_get(['delivery']) or {} picking_type = order.picking_type_id return_pick_type = order.picking_type_id.return_picking_type_id or order.picking_type_id @@ -582,7 +606,7 @@ class PosOrder(models.Model): 'location_id': location_id, 'location_dest_id': destination_id, } - pos_qty = any([x.qty >= 0 for x in order.lines]) + pos_qty = any([x.qty > 0 for x in order.lines]) if pos_qty: order_picking = Picking.create(picking_vals.copy()) order_picking.message_post(body=message) @@ -620,8 +644,8 @@ class PosOrder(models.Model): # when the pos.config has no picking_type_id set only the moves will be created if moves and not return_picking and not order_picking: - moves.action_confirm() - moves.force_assign() + moves.action_assign() + moves.filtered(lambda m: m.state in ['confirmed', 'waiting']).force_assign() moves.filtered(lambda m: m.product_id.tracking == 'none').action_done() return True @@ -629,7 +653,7 @@ class PosOrder(models.Model): def _force_picking_done(self, picking): """Force picking in order to be set as done.""" self.ensure_one() - picking.action_confirm() + picking.action_assign() picking.force_assign() self.set_pack_operation_lot(picking) if not any([(x.product_id.tracking != 'none') for x in picking.pack_operation_ids]): diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index d7131359f06..07db76c8af8 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -130,12 +130,20 @@ class PosSession(models.Model): @api.constrains('user_id', 'state') def _check_unicity(self): # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user - if self.search_count([('state', 'not in', ('closed', 'closing_control')), ('user_id', '=', self.user_id.id)]) > 1: + if self.search_count([ + ('state', 'not in', ('closed', 'closing_control')), + ('user_id', '=', self.user_id.id), + ('name', 'not like', 'RESCUE FOR'), + ]) > 1: raise ValidationError(_("You cannot create two active sessions with the same responsible!")) @api.constrains('config_id') def _check_pos_config(self): - if self.search_count([('state', '!=', 'closed'), ('config_id', '=', self.config_id.id)]) > 1: + if self.search_count([ + ('state', '!=', 'closed'), + ('config_id', '=', self.config_id.id), + ('name', 'not like', 'RESCUE FOR'), + ]) > 1: raise ValidationError(_("You cannot create two active sessions related to the same point of sale!")) @api.model diff --git a/addons/point_of_sale/report/pos_order_report.py b/addons/point_of_sale/report/pos_order_report.py index a3f7ecc95a1..c493bf852d6 100644 --- a/addons/point_of_sale/report/pos_order_report.py +++ b/addons/point_of_sale/report/pos_order_report.py @@ -12,7 +12,7 @@ class PosOrderReport(models.Model): date = fields.Datetime(string='Date Order', readonly=True) order_id = fields.Many2one('pos.order', string='Order', readonly=True) - partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) + partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_tmpl_id = fields.Many2one('product.template', string='Product Template', readonly=True) state = fields.Selection( diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index 02d1123bcc3..b9370802dd6 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -11,6 +11,7 @@ var CrashManager = require('web.CrashManager'); var _t = core._t; +var _lt = core._lt; var QWeb = core.qweb; /* -------- The Order Selector -------- */ @@ -704,7 +705,7 @@ var Chrome = PosBaseWidget.extend({ 'widget': HeaderButtonWidget, 'append': '.pos-rightheader', 'args': { - label: _t('Close'), + label: _lt('Close'), action: function(){ var self = this; if (!this.confirmed) { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 80ea86b9be9..4cb1cc08296 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1318,7 +1318,9 @@ exports.Orderline = Backbone.Model.extend({ this.trigger('change',this); }, get_unit_price: function(){ - return round_di(this.price || 0, this.pos.dp['Product Price']) + var digits = this.pos.dp['Product Price']; + // round and truncate to mimic _sybmbol_set behavior + return parseFloat(round_di(this.price || 0, digits).toFixed(digits)); }, get_unit_display_price: function(){ if (this.pos.config.iface_tax_included) { diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 5b8933c4cc7..8c4efac4d0e 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1355,6 +1355,16 @@ var ClientListScreenWidget = ScreenWidget.extend({ contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner}))); this.toggle_save_button(); + // Browsers attempt to scroll invisible input elements + // into view (eg. when hidden behind keyboard). They don't + // seem to take into account that some elements are not + // scrollable. + contents.find('input').blur(function() { + setTimeout(function() { + self.$('.window').scrollTop(0); + }, 0); + }); + contents.find('.image-uploader').on('change',function(event){ self.load_image_file(event.target.files[0],function(res){ if (res) { @@ -1404,7 +1414,9 @@ var ReceiptScreenWidget = ScreenWidget.extend({ this.render_change(); this.render_receipt(); - + this.handle_auto_print(); + }, + handle_auto_print: function() { if (this.should_auto_print()) { this.print(); if (this.should_close_immediately()){ @@ -1413,7 +1425,6 @@ var ReceiptScreenWidget = ScreenWidget.extend({ } else { this.lock_screen(false); } - }, should_auto_print: function() { return this.pos.config.iface_print_auto && !this.pos.get_order()._printed; diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index ebf971d2ec3..4c5597979c6 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -10,12 +10,16 @@ class TestPointOfSaleCommon(common.TransactionCase): self.AccountBankStatementLine = self.env['account.bank.statement.line'] self.PosMakePayment = self.env['pos.make.payment'] self.PosOrder = self.env['pos.order'] + self.PosSession = self.env['pos.session'] self.company_id = self.ref('base.main_company') self.product3 = self.env.ref('product.product_product_3') self.product4 = self.env.ref('product.product_product_4') self.partner1 = self.env.ref('base.res_partner_1') self.partner4 = self.env.ref('base.res_partner_4') self.pos_config = self.env.ref('point_of_sale.pos_config_main') + self.carotte = self.env.ref('point_of_sale.carotte') + self.courgette = self.env.ref('point_of_sale.courgette') + self.onions = self.env.ref('point_of_sale.Onions') # create a new session self.pos_order_session0 = self.env['pos.session'].create({ diff --git a/addons/point_of_sale/tests/test_point_of_sale_flow.py b/addons/point_of_sale/tests/test_point_of_sale_flow.py index 42b129d1ffd..2ad3682f826 100644 --- a/addons/point_of_sale/tests/test_point_of_sale_flow.py +++ b/addons/point_of_sale/tests/test_point_of_sale_flow.py @@ -4,8 +4,8 @@ import time import os import odoo -from odoo import tools, report as odoo_report -from odoo.tools import float_compare, test_reports +from odoo import fields, tools, report as odoo_report +from odoo.tools import float_compare, mute_logger, test_reports from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon @@ -451,3 +451,138 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon): # I confirm the bank statement using Confirm button self.AccountBankStatement.button_confirm_bank() + + def test_create_from_ui(self): + """ + Simulation of sales coming from the interface, even after closing the session + """ + # I click on create a new session button + self.pos_config.open_session_cb() + + current_session = self.pos_config.current_session_id + num_starting_orders = len(current_session.order_ids) + + carrot_order = {'data': + {'amount_paid': 0.9, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 0.9, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 42, + 'pack_lot_ids': [], + 'price_unit': 0.9, + 'product_id': self.carotte.id, + 'qty': 1, + 'tax_ids': []}]], + 'name': 'Order 00042-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': 2, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 0.9, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00042-003-0014', + 'user_id': self.env.uid}, + 'id': '00042-003-0014', + 'to_invoice': False} + + zucchini_order = {'data': + {'amount_paid': 1.2, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 1.1, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'pack_lot_ids': [], + 'price_unit': 1.2, + 'product_id': self.courgette.id, + 'qty': 1, + 'tax_ids': []}]], + 'name': 'Order 00043-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 1.2, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00043-003-0014', + 'user_id': self.env.uid}, + 'id': '00043-003-0014', + 'to_invoice': False} + + onions_order = {'data': + {'amount_paid': 1.28, + 'amount_return': 0, + 'amount_tax': 0, + 'amount_total': 1.28, + 'creation_date': fields.Datetime.now(), + 'fiscal_position_id': False, + 'lines': [[0, + 0, + {'discount': 0, + 'id': 3, + 'pack_lot_ids': [], + 'price_unit': 1.28, + 'product_id': self.onions.id, + 'qty': 1, + 'tax_ids': [[6, False, self.onions.taxes_id.ids]]}]], + 'name': 'Order 00044-003-0014', + 'partner_id': False, + 'pos_session_id': current_session.id, + 'sequence_number': self.pos_config.journal_id.id, + 'statement_ids': [[0, + 0, + {'account_id': self.env.user.partner_id.property_account_receivable_id.id, + 'amount': 1.28, + 'journal_id': self.pos_config.journal_ids[0].id, + 'name': fields.Datetime.now(), + 'statement_id': current_session.statement_ids[0].id}]], + 'uid': '00044-003-0014', + 'user_id': self.env.uid}, + 'id': '00044-003-0014', + 'to_invoice': False} + + + # I create an order on an open session + self.PosOrder.create_from_ui([carrot_order]) + self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Submitted order not encoded") + + # I resubmit the same order + self.PosOrder.create_from_ui([carrot_order]) + self.assertEqual(num_starting_orders + 1, len(current_session.order_ids), "Resubmitted order was not skipped") + + # I close the session + current_session.action_pos_session_closing_control() + self.assertEqual(current_session.state, 'closed', "Session was not properly closed") + self.assertFalse(self.pos_config.current_session_id, "Current session not properly recomputed") + + # I keep selling after the session is closed + with mute_logger('odoo.addons.point_of_sale.models.pos_order'): + self.PosOrder.create_from_ui([zucchini_order, onions_order]) + rescue_session = self.PosSession.search([ + ('config_id', '=', self.pos_config.id), + ('state', '=', 'opened'), + ]) + self.assertEqual(len(rescue_session), 1, "One (and only one) rescue session should be created for orphan orders") + self.assertIn("(RESCUE FOR %s)" % current_session.name, rescue_session.name, "Rescue session is not linked to the previous one") + self.assertEqual(len(rescue_session.order_ids), 2, "Rescue session does not contain both orders") + + # I close the rescue session + rescue_session.action_pos_session_closing_control() + self.assertEqual(rescue_session.state, 'closed', "Rescue session was not properly closed") diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml index 1a377c34db8..052a9c9a822 100644 --- a/addons/point_of_sale/views/pos_session_view.xml +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -157,7 +157,7 @@ - + diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index 051d4bee319..6161cc1de20 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -149,7 +149,7 @@ class PortalWizardUser(models.TransientModel): wizard_user.user_id.write({'active': True, 'groups_id': [(4, group_portal.id)]}) # prepare for the signup process wizard_user.user_id.partner_id.signup_prepare() - wizard_user._send_email() + wizard_user.with_context(active_test=True)._send_email() wizard_user.refresh() else: # remove the user (if it exists) from the portal group diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 87559c4913c..237bca65182 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -553,13 +553,34 @@ class PurchaseOrderLine(models.Model): line = super(PurchaseOrderLine, self).create(values) if line.order_id.state == 'purchase': line.order_id._create_picking() + msg = _("Extra line with %s ") % (line.product_id.display_name,) + line.order_id.message_post(body=msg) return line @api.multi def write(self, values): + orders = False + if 'product_qty' in values: + changed_lines = self.filtered(lambda x: x.order_id.state == 'purchase') + if changed_lines: + orders = changed_lines.mapped('order_id') + for order in orders: + order_lines = changed_lines.filtered(lambda x: x.order_id == order) + msg = "" + if any([values['product_qty'] < x.product_qty for x in order_lines]): + msg += "" + _('The ordered quantity has been decreased. Do not forget to take it into account on your bills and receipts.') + '
' + msg += "
    " + for line in order_lines: + msg += "
  • %s:" % (line.product_id.display_name,) + msg += "
    " + _("Ordered Quantity") + ": %s -> %s
    " % (line.product_qty, float(values['product_qty']),) + if line.product_id.type in ('product', 'consu'): + msg += _("Received Quantity") + ": %s
    " % (line.qty_received,) + msg += _("Billed Quantity") + ": %s
  • " % (line.qty_invoiced,) + msg += "
" + order.message_post(body=msg) result = super(PurchaseOrderLine, self).write(values) - orders = self.filtered(lambda x: x.order_id.state == 'purchase').mapped('order_id') - orders._create_picking() + if orders: + orders._create_picking() return result name = fields.Text(string='Description', required=True) @@ -959,6 +980,12 @@ class ProcurementOrder(models.Model): 'group_id': group } + def _make_po_select_supplier(self, suppliers): + """ Method intended to be overridden by customized modules to implement any logic in the + selection of supplier. + """ + return suppliers[0] + @api.multi def make_po(self): cache = {} @@ -968,7 +995,7 @@ class ProcurementOrder(models.Model): if not suppliers: procurement.message_post(body=_('No vendor associated to product %s. Please set one to fix this procurement.') % (procurement.product_id.name)) continue - supplier = suppliers[0] + supplier = procurement._make_po_select_supplier(suppliers) partner = supplier.name gpo = procurement.rule_id.group_propagation_option diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 03d3947d0c2..4c72a9d5058 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -481,8 +481,11 @@ class Report(models.Model): out, err = process.communicate() if process.returncode not in [0, 1]: - raise UserError(_('Wkhtmltopdf failed (error code: %s). ' - 'Message: %s') % (str(process.returncode), err)) + if process.returncode == -11: + message = _('Wkhtmltopdf failed (error code: %s). Memory limit too low or maximum file number of subprocess reached. Message : %s') + else: + message = _('Wkhtmltopdf failed (error code: %s). Message: %s') + raise UserError(message % (str(process.returncode), err[-1000:])) # Save the pdf in attachment if marked if reporthtml[0] is not False: diff --git a/addons/report/models/res_company.py b/addons/report/models/res_company.py index f8cd4bd5236..85683e6edc1 100644 --- a/addons/report/models/res_company.py +++ b/addons/report/models/res_company.py @@ -6,7 +6,7 @@ from odoo import api, fields, models class ResCompany(models.Model): _inherit = 'res.company' - paperformat_id = fields.Many2one('report.paperformat', 'Paper format') + paperformat_id = fields.Many2one('report.paperformat', 'Paper format', default=lambda self: self.env.ref('report.paperformat_euro', raise_if_not_found=False)) external_report_layout = fields.Selection([ ('background', 'Background'), ('boxed', 'Boxed'), diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index a389b5f9691..713308aa7a1 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -84,6 +84,10 @@ class AccountInvoice(models.Model): return report_pages + @api.multi + def get_delivery_partner_id(self): + self.ensure_one() + return self.partner_shipping_id.id or super(AccountInvoice, self).get_delivery_partner_id() class AccountInvoiceLine(models.Model): _inherit = 'account.invoice.line' diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 22d4c0747d1..a30cb13aa78 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -61,7 +61,9 @@ class SaleOrder(models.Model): for inv in invoice_ids: refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)]) - line_invoice_status = [line.invoice_status for line in order.order_line] + # Ignore the status of the deposit product + deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id() + line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id] if order.state not in ('sale', 'done'): invoice_status = 'no' @@ -511,11 +513,11 @@ class SaleOrder(models.Model): group = tax.tax_group_id res.setdefault(group, 0.0) amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty, - product=line.product_id, partner=line.order_partner_id)['taxes'][0]['amount'] + product=line.product_id, partner=self.partner_shipping_id)['taxes'][0]['amount'] res[group] += amount if tax.include_base_amount: base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1, product=line.product_id, - partner=line.order_partner_id)['taxes'][0]['amount'] + partner=self.partner_shipping_id)['taxes'][0]['amount'] res = sorted(res.items(), key=lambda l: l[0].sequence) res = map(lambda l: (l[0].name, l[1]), res) return res @@ -562,7 +564,7 @@ class SaleOrderLine(models.Model): """ for line in self: price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) - taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_id) + taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_shipping_id) line.update({ 'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])), 'price_total': taxes['total_included'], @@ -628,7 +630,7 @@ class SaleOrderLine(models.Model): fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id # If company_id is set, always filter taxes by the company taxes = line.product_id.taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id) - line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_id) if fpos else taxes + line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id) if fpos else taxes @api.multi def _prepare_order_line_procurement(self, group_id=False): @@ -692,16 +694,37 @@ class SaleOrderLine(models.Model): line = super(SaleOrderLine, self).create(values) if line.state == 'sale': line._action_procurement_create() + msg = _("Extra line with %s ") % (line.product_id.display_name,) + line.order_id.message_post(body=msg) return line @api.multi def write(self, values): lines = False + changed_lines = False if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') lines = self.filtered( lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) + changed_lines = self.filtered( + lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) != 0) + if changed_lines: + orders = self.mapped('order_id') + for order in orders: + order_lines = changed_lines.filtered(lambda x: x.order_id == order) + msg = "" + if any([values['product_uom_qty'] < x.product_uom_qty for x in order_lines]): + msg += "" + _('The ordered quantity has been decreased. Do not forget to take it into account on your invoices and delivery orders.') + '' + msg += "
    " + for line in order_lines: + msg += "
  • %s:" % (line.product_id.display_name,) + msg += "
    " + _("Ordered Quantity") + ": %s -> %s
    " % (line.product_uom_qty, float(values['product_uom_qty']),) + if line.product_id.type in ('consu', 'product'): + msg += _("Delivered Quantity") + ": %s
    " % (line.qty_delivered,) + msg += _("Invoiced Quantity") + ": %s
    " % (line.qty_invoiced,) + msg += "
" + order.message_post(body=msg) result = super(SaleOrderLine, self).write(values) if lines: lines._action_procurement_create() @@ -825,10 +848,16 @@ class SaleOrderLine(models.Model): @api.multi def _get_display_price(self, product): # TO DO: move me in master/saas-16 on sale.order - if self.order_id.pricelist_id.discount_policy == 'without_discount': + if self.order_id.pricelist_id.discount_policy == 'with_discount': + return product.with_context(pricelist=self.order_id.pricelist_id.id).price + price, rule_id = self.order_id.pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + pricelist_item = self.env['product.pricelist.item'].browse(rule_id) + if (pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id.discount_policy == 'with_discount'): + price, rule_id = pricelist_item.base_pricelist_id.get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + return price + else: from_currency = self.order_id.company_id.currency_id return from_currency.compute(product.lst_price, self.order_id.pricelist_id.currency_id) - return product.with_context(pricelist=self.order_id.pricelist_id.id).price @api.multi @api.onchange('product_id') @@ -968,7 +997,7 @@ class SaleOrderLine(models.Model): new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) new_list_price = self.env['account.tax']._fix_tax_included_price(new_list_price, self.product_id.taxes_id, self.tax_id) - if price != 0 and new_list_price != 0: + if new_list_price != 0: if self.product_id.company_id and self.order_id.pricelist_id.currency_id != self.product_id.company_id.currency_id: # new_list_price is in company's currency while price in pricelist currency new_list_price = self.env['res.currency'].browse(currency_id).with_context(context_partner).compute(new_list_price, self.order_id.pricelist_id.currency_id) diff --git a/addons/sale/models/sale_analytic.py b/addons/sale/models/sale_analytic.py index acf3fd48682..e937292d818 100644 --- a/addons/sale/models/sale_analytic.py +++ b/addons/sale/models/sale_analytic.py @@ -12,7 +12,19 @@ class SaleOrderLine(models.Model): lines = {} if not domain: # To filter on analyic lines linked to an expense - domain = [('so_line', 'in', self.ids), ('amount', '<=', 0.0)] + expense_type_id = self.env.ref('account.data_account_type_expenses', raise_if_not_found=False) + expense_type_id = expense_type_id and expense_type_id.id + domain = [ + ('so_line', 'in', self.ids), + '|', + ('amount', '<', 0), + '&', + ('amount', '=', 0), + '|', + ('move_id', '=', False), + ('move_id.account_id.user_type_id', '=', expense_type_id) + ] + data = self.env['account.analytic.line'].read_group( domain, ['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index 4e5eee48591..0254c3dbe0e 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -142,5 +142,18 @@ class SaleOrderLine(models.Model): def _compute_analytic(self, domain=None): if not domain: # To filter on analyic lines linked to an expense - domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('project_id', '!=', False)] + expense_type_id = self.env.ref('account.data_account_type_expenses', raise_if_not_found=False) + expense_type_id = expense_type_id and expense_type_id.id + domain = [ + ('so_line', 'in', self.ids), + '|', + '|', + ('amount', '<', 0.0), + ('project_id', '!=', False), + '&', + ('amount', '=', 0), + '|', + ('move_id', '=', False), + ('move_id.account_id.user_type_id', '=', expense_type_id) + ] return super(SaleOrderLine, self)._compute_analytic(domain=domain) diff --git a/addons/sales_team/data/sales_team_data.xml b/addons/sales_team/data/sales_team_data.xml index 4f2397c5731..2af8a2f7992 100644 --- a/addons/sales_team/data/sales_team_data.xml +++ b/addons/sales_team/data/sales_team_data.xml @@ -4,10 +4,12 @@ Sales + Website + diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py index a05520d422b..c4cb8b74301 100644 --- a/addons/sales_team/models/crm_team.py +++ b/addons/sales_team/models/crm_team.py @@ -24,9 +24,11 @@ class CrmTeam(models.Model): def _get_default_team_id(self, user_id=None): if not user_id: user_id = self.env.uid - team_id = self.env['crm.team'].sudo().search( - ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], - limit=1) + company_id = self.sudo(user_id).company_id.id + team_id = self.env['crm.team'].sudo().search([ + '|', ('user_id', '=', user_id), ('member_ids', '=', user_id), + '|', ('company_id', '=', False), ('company_id', 'child_of', [company_id]) + ], limit=1) if not team_id and 'default_team_id' in self.env.context: team_id = self.env['crm.team'].browse(self.env.context.get('default_team_id')) if not team_id: diff --git a/addons/sales_team/static/src/xml/sales_team_dashboard.xml b/addons/sales_team/static/src/xml/sales_team_dashboard.xml index b1bb4a6ca65..060bbca91fd 100644 --- a/addons/sales_team/static/src/xml/sales_team_dashboard.xml +++ b/addons/sales_team/static/src/xml/sales_team_dashboard.xml @@ -84,7 +84,7 @@
TaxBase Amount
+ + Debit Credit BalanceCurrencyCurrency
- + + + +
- + exp. closing diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index 93581c71f2f..b3edca53c0f 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -275,23 +275,27 @@ class ProcurementOrder(models.Model): @api.model def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=False): """ Create procurements based on orderpoints. - :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing each procurement. + :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing + 1000 orderpoints. This is appropriate for batch jobs only. """ - if use_new_cursor: - cr = registry(self._cr.dbname).cursor() - self = self.with_env(self.env(cr=cr)) OrderPoint = self.env['stock.warehouse.orderpoint'] - Procurement = self.env['procurement.order'] - ProcurementAutorundefer = Procurement.with_context(procurement_autorun_defer=True) - procurement_list = [] - orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search( - company_id and [('company_id', '=', company_id)] or [], - order=self._procurement_from_orderpoint_get_order()) + domain = [('company_id', '=', company_id)] if company_id else [] + domain += [('product_id.active', '=', True)] + orderpoints_noprefetch = OrderPoint.with_context(prefetch_fields=False).search(domain, + order=self._procurement_from_orderpoint_get_order()).ids while orderpoints_noprefetch: - orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000].ids) + if use_new_cursor: + cr = registry(self._cr.dbname).cursor() + self = self.with_env(self.env(cr=cr)) + OrderPoint = self.env['stock.warehouse.orderpoint'] + Procurement = self.env['procurement.order'] + ProcurementAutorundefer = Procurement.with_context(procurement_autorun_defer=True) + procurement_list = [] + + orderpoints = OrderPoint.browse(orderpoints_noprefetch[:1000]) orderpoints_noprefetch = orderpoints_noprefetch[1000:] # Calculate groups that can be executed together @@ -341,7 +345,7 @@ class ProcurementOrder(models.Model): except OperationalError: if use_new_cursor: - orderpoints_noprefetch += orderpoint.id + orderpoints_noprefetch += [orderpoint.id] cr.rollback() continue else: @@ -365,8 +369,6 @@ class ProcurementOrder(models.Model): if use_new_cursor: cr.commit() + cr.close() - if use_new_cursor: - cr.commit() - cr.close() return {} diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 020a25ac276..591842e4225 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -349,7 +349,7 @@ class StockMove(models.Model): raise UserError(_('Cannot unreserve a done move')) self.quants_unreserve() if not self.env.context.get('no_state_change'): - waiting = self.filtered(lambda move: move.get_ancestors()) + waiting = self.filtered(lambda move: move.procure_method == 'make_to_order' or move.get_ancestors()) waiting.write({'state': 'waiting'}) (self - waiting).write({'state': 'confirmed'}) @@ -408,15 +408,8 @@ class StockMove(models.Model): type (moves should already have them identical). Otherwise, create a new picking to assign them to. """ Picking = self.env['stock.picking'] - - # If this method is called in batch by a write on a one2many and - # at some point had to create a picking, some next iterations could - # try to find back the created picking. As we look for it by searching - # on some computed fields, we have to force a recompute, else the - # record won't be found. - self.recompute() - for move in self: + recompute = False picking = Picking.search([ ('group_id', '=', move.group_id.id), ('location_id', '=', move.location_id.id), @@ -425,8 +418,17 @@ class StockMove(models.Model): ('printed', '=', False), ('state', 'in', ['draft', 'confirmed', 'waiting', 'partially_available', 'assigned'])], limit=1) if not picking: + recompute = True picking = Picking.create(move._get_new_picking_values()) move.write({'picking_id': picking.id}) + + # If this method is called in batch by a write on a one2many and + # at some point had to create a picking, some next iterations could + # try to find back the created picking. As we look for it by searching + # on some computed fields, we have to force a recompute, else the + # record won't be found. + if recompute: + move.recompute() return True _picking_assign = assign_picking @@ -687,7 +689,7 @@ class StockMove(models.Model): if len(reserved_quant_ids) == 0 and move.partially_available: vals['partially_available'] = False if move.state == 'assigned': - if move.find_move_ancestors(): + if move.procure_method == 'make_to_order' or move.find_move_ancestors(): vals['state'] = 'waiting' else: vals['state'] = 'confirmed' diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index c36ba994f7d..014869345c2 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -19,6 +19,9 @@
+
+ t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/> +

diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index e87d1c3b79c..d8dd43b0794 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1726,4 +1726,69 @@ class TestStockFlow(TestStockCommon): # force assign on the delivery order, it should be assigned picking_out.force_assign() - self.assertEquals(picking_out.state, "assigned") \ No newline at end of file + self.assertEquals(picking_out.state, "assigned") + + def test_74_move_state_waiting_mto(self): + """ This test will check that when a move is unreserved, it state change to 'waiting' if + it has ancestors or is has a 'procure_method' equal to 'make_to_order' else the state + changes to 'confirmed'. + """ + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + move_mto_alone = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'procure_method':'make_to_order'}) + move_with_ancestors = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + the_ancestor = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location, + 'move_dest_id': move_with_ancestors.id}) + other_move = self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + move_mto_alone.action_confirm() + move_with_ancestors.action_confirm() + other_move.action_confirm() + + move_mto_alone.do_unreserve() + move_with_ancestors.do_unreserve() + other_move.do_unreserve() + + self.assertEquals(move_mto_alone.state, "waiting") + self.assertEquals(move_with_ancestors.state, "waiting") + self.assertEquals(other_move.state, "confirmed") + + move_mto_alone.recalculate_move_state() + move_with_ancestors.recalculate_move_state() + other_move.recalculate_move_state() + + self.assertEquals(move_mto_alone.state, "waiting") + self.assertEquals(move_with_ancestors.state, "waiting") + self.assertEquals(other_move.state, "confirmed") diff --git a/addons/stock/views/stock_warehouse_views.xml b/addons/stock/views/stock_warehouse_views.xml index 62d1e3d748c..7a8caea9e5a 100644 --- a/addons/stock/views/stock_warehouse_views.xml +++ b/addons/stock/views/stock_warehouse_views.xml @@ -98,6 +98,7 @@ stock.warehouse + diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 4706bbc6b45..9fec58c20ab 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -188,7 +188,7 @@ class StockMove(models.Model): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} - for move in self.filtered(lambda move: move.location_id.usage == 'supplier' and move.product_id.cost_method == 'average'): + for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move.product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id] # if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use. diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index 2427a8ca0ca..12134cc0f0f 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -129,6 +129,7 @@ class LandedCost(models.Model): if quant.location_id.usage != 'internal': qty_out += quant.qty line._create_accounting_entries(move, qty_out) + move.assert_balanced() cost.write({'state': 'done', 'account_move_id': move.id}) move.post() return True @@ -326,7 +327,7 @@ class AdjustmentLines(models.Model): Generate the account.move.line values to track the landed cost. Afterwards, for the goods that are already out of stock, we should create the out moves """ - AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False) + AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False, recompute=False) base_line = { 'name': self.name, @@ -389,5 +390,4 @@ class AdjustmentLines(models.Model): AccountMoveLine.create(debit_line) AccountMoveLine.create(credit_line) - move.assert_balanced() return True diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 4db03e463eb..39c0c34983c 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -654,6 +654,8 @@ class Database(http.Controller): @http.route('/web/database/create', type='http', auth="none", methods=['POST'], csrf=False) def create(self, master_pwd, name, lang, password, **post): try: + if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', name): + raise Exception(_('Invalid database name. Only alphanumerical characters and underscore are allowed.')) # country code could be = "False" which is actually True in python country_code = post.get('country_code') or False dispatch_rpc('db', 'create_database', [master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code]) @@ -666,6 +668,8 @@ class Database(http.Controller): @http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False) def duplicate(self, master_pwd, name, new_name): try: + if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', new_name): + raise Exception(_('Invalid database name. Only alphanumerical characters and underscore are allowed.')) dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name]) return http.local_redirect('/web/database/manager') except Exception, e: diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js index d3135f86ba6..c74b89af11b 100644 --- a/addons/web/static/src/js/widgets/data_export.js +++ b/addons/web/static/src/js/widgets/data_export.js @@ -207,9 +207,15 @@ var DataExport = Dialog.extend({ this.records = {}; this.dataset = dataset; this.exports = new data.DataSetSearch(this, 'ir.exports', this.dataset.get_context()); - + this.row_index = 0; this.row_index_level = 0; + + // The default for the ".modal_content" element is "max-height: 100%;" + // but we want it to always expand to "height: 100%;" for this modal. + // This can be achieved thanks to LESS modification without touching + // the ".modal-content" rules... but not with Internet explorer (11). + this.$modal.find(".modal-content").css("height", "100%"); }, start: function() { var self = this; @@ -261,7 +267,7 @@ var DataExport = Dialog.extend({ function do_setup_export_formats(formats) { var $fmts = self.$('.o_export_format'); - + _.each(formats, function(format, i) { var $radio = $('', {type: 'radio', value: format.tag, name: 'o_export_format_name'}); var $label = $('

- -u web --stop-after-init + $ python /usr/bin/odoo.py -d -i web_enterprise --stop-after-init * You should be able to connect to your Odoo Enterprise instance using your usual mean of identification. You can then link your database with your Odoo Enterprise Subscription by entering the code you received @@ -61,11 +61,11 @@ have your own favourite. You may need to adapt sections to your usual workflow. * Shutdown your server * Backup your community database * Update the ``--addons-path`` parameter of your launch command (see :ref:`setup/install/source`) -* Update the web module by using +* Install the web_enterprise module by using .. code-block:: console - $ -d -u web --stop-after-init + $ -d -i web_enterprise --stop-after-init Depending on the size of your database, this may take some time. @@ -97,7 +97,7 @@ On Windows .. code-block:: console - $ odoo.exe -d -u web --stop-after-init + $ odoo.exe -d -i web_enterprise --stop-after-init * No need to manually launch the server, the service is running. You should be able to connect to your Odoo Enterprise instance using your usual diff --git a/odoo/addons/base/ir/ir_filters.xml b/odoo/addons/base/ir/ir_filters.xml index d21c6ad25d5..f46ec82c7f6 100644 --- a/odoo/addons/base/ir/ir_filters.xml +++ b/odoo/addons/base/ir/ir_filters.xml @@ -24,7 +24,7 @@ ir.filters.own.rule.delete - [('create_uid', '=', user.id)] + [('user_id', '=', user.id)] diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 8d87d73716e..3ea67a10296 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -9,6 +9,7 @@ from collections import defaultdict from odoo import api, fields, models, SUPERUSER_ID, tools, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.modules.registry import Registry +from odoo.osv import expression from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) @@ -351,6 +352,25 @@ class IrModelFields(models.Model): raise UserError(_("The Selection Options expression is not a valid Pythonic expression." "Please provide an expression in the [('key','Label'), ...] format.")) + @api.constrains('name', 'state') + def _check_name(self): + for field in self: + if field.state == 'manual' and not field.name.startswith('x_'): + raise ValidationError(_("Custom fields must have a name that starts with 'x_' !")) + try: + models.check_pg_name(field.name) + except ValidationError: + msg = _("Field names can only contain characters, digits and underscores (up to 63).") + raise ValidationError(msg) + + @api.constrains('model', 'name') + def _unique_name(self): + # fix on stable branch (to be converted into an SQL constraint) + for field in self: + count = self.search_count([('model', '=', field.model), ('name', '=', field.name)]) + if count > 1: + raise ValidationError(_("Field names must be unique per model.")) + _sql_constraints = [ ('size_gt_zero', 'CHECK (size>=0)', 'Size of the field cannot be negative.'), ] @@ -511,6 +531,10 @@ class IrModelFields(models.Model): This method prevents the modification/deletion of many2one fields that have an inverse one2many, for instance. """ + self = self.filtered(lambda record: record.state == 'manual') + if not self: + return + for record in self: model = self.env[record.model] field = model._fields[record.name] @@ -524,6 +548,23 @@ class IrModelFields(models.Model): msg = _("The field '%s' cannot be removed because the field '%s' depends on it.") raise UserError(msg % (field, model._field_inverses[field][0])) + # remove fields from registry, and check that views are not broken + fields = [self.env[record.model]._pop_field(record.name) for record in self] + domain = expression.OR([('arch_db', 'like', record.name)] for record in self) + views = self.env['ir.ui.view'].search(domain) + try: + for view in views: + view._check_xml() + except Exception: + raise UserError("\n".join([ + _("Cannot rename/delete fields that are still present in views:"), + _("Fields:") + " " + ", ".join(map(str, fields)), + _("View:") + " " + view.name, + ])) + finally: + # the registry has been modified, restore it + self.pool.setup_models(self._cr) + @api.multi def unlink(self): if not self: @@ -566,9 +607,6 @@ class IrModelFields(models.Model): res = super(IrModelFields, self).create(vals) if vals.get('state', 'manual') == 'manual': - if not vals['name'].startswith('x_'): - raise UserError(_("Custom fields must have a name that starts with 'x_' !")) - if vals.get('relation') and not self.env['ir.model'].search([('model', '=', vals['relation'])]): raise UserError(_("Model %s does not exist!") % vals['relation']) @@ -622,12 +660,6 @@ class IrModelFields(models.Model): item._prepare_update() if column_rename: raise UserError(_('Can only rename one field at a time!')) - if vals['name'] in obj._fields: - raise UserError(_('Cannot rename field to %s, because that field already exists!') % vals['name']) - if vals.get('state', 'manual') == 'manual' and not vals['name'].startswith('x_'): - raise UserError(_('New field name must still start with x_ , because it is a custom field!')) - if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']: - raise ValueError('Invalid character in column name') column_rename = (obj._table, item.name, vals['name'], item.index) # We don't check the 'state', because it might come from the context diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py index c1d525c83b3..e40f64c3937 100644 --- a/odoo/addons/base/ir/ir_qweb/fields.py +++ b/odoo/addons/base/ir/ir_qweb/fields.py @@ -330,7 +330,7 @@ class MonetaryConverter(models.AbstractModel): if not options.get('display_currency') and field.type == 'monetary' and field.currency_field: options['display_currency'] = record[field.currency_field] - return self.value_to_html(record[field_name], options) + return super(MonetaryConverter, self).record_to_html(record, field_name, options) TIMEDELTA_UNITS = ( ('year', 3600 * 24 * 365), diff --git a/odoo/addons/base/res/res_company_view.xml b/odoo/addons/base/res/res_company_view.xml index 95bafe4f334..4746deeb1b1 100644 --- a/odoo/addons/base/res/res_company_view.xml +++ b/odoo/addons/base/res/res_company_view.xml @@ -1,7 +1,7 @@ - + res.company.form diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py index e92aaf5315e..da025eeca78 100644 --- a/odoo/addons/base/res/res_users.py +++ b/odoo/addons/base/res/res_users.py @@ -744,7 +744,7 @@ class GroupsView(models.Model): xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace") xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS")) xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8") - view.with_context(lang=None).write({'arch': xml_content}) + view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False}) def get_application_groups(self, domain): """ Return the non-share groups that satisfy ``domain``. """ diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py index 932268d717c..104c4a6dfe1 100644 --- a/odoo/addons/base/tests/test_expression.py +++ b/odoo/addons/base/tests/test_expression.py @@ -683,8 +683,8 @@ class TestAutoJoin(TransactionCase): self.assertEqual(partners, p_aa, "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 3, - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 3 queries (1 in res_partner_bank, 2 on res_partner)") + self.assertEqual(len(self.query_list), 2, + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 2 queries (1 in res_partner_bank, 1 on res_partner)") sql_query = self.query_list[0].get_sql() self.assertIn('res_partner_bank', sql_query[0], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect main table") @@ -692,16 +692,16 @@ class TestAutoJoin(TransactionCase): expected = "%s::text like %s" % (unaccent('"res_partner_bank"."sanitized_acc_number"'), unaccent('%s')) self.assertIn(expected, sql_query[1], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect where condition") - + self.assertEqual(['%' + name_test + '%'], sql_query[2], "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect parameter") - sql_query = self.query_list[2].get_sql() + sql_query = self.query_list[1].get_sql() self.assertIn('res_partner', sql_query[0], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect main table") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect main table") self.assertIn('"res_partner"."id" in (%s)', sql_query[1], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect where condition") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect where condition") self.assertIn(p_aa.id, sql_query[2], - "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect parameter") + "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect parameter") # Do: cascaded one2many without _auto_join self._reinit_mock() @@ -710,8 +710,8 @@ class TestAutoJoin(TransactionCase): self.assertEqual(partners, p_a + p_b, "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 5, - "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 4 on res_partner)") + self.assertEqual(len(self.query_list), 3, + "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 3 queries (1 in res_partner_bank, 2 on res_partner)") # Do: one2many with _auto_join patch_auto_join(partner_obj, 'bank_ids', True) @@ -943,7 +943,7 @@ class TestAutoJoin(TransactionCase): self.assertLessEqual(p_a + p_b, partners, "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result") # Test produced queries - self.assertEqual(len(self.query_list), 5, + self.assertEqual(len(self.query_list), 4, "_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect") # Do: ('child_ids.state_id.country_id.code', 'like', '..') with _auto_join diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 74d220b05bb..9e14f000676 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.exceptions import ValidationError +import odoo +from odoo.exceptions import UserError, ValidationError from odoo.tools import mute_logger import odoo.tests.common as common @@ -181,3 +182,112 @@ class TestServerActions(TestServerActionsBase): self.action.write({ 'child_ids': [(6, 0, [self.action.id])] }) + + +class TestCustomFields(common.TransactionCase): + MODEL = 'res.partner' + + def setUp(self): + # use a test cursor instead of a real cursor + registry = odoo.registry() + registry.enter_test_mode() + fnames = set(registry[self.MODEL]._fields) + + @self.addCleanup + def callback(): + registry.leave_test_mode() + # the tests may have modified the registry, reset it + with registry.cursor() as cr: + registry.clear_manual_fields() + registry.setup_models(cr) + assert set(registry[self.MODEL]._fields) == fnames + + super(TestCustomFields, self).setUp() + + # do not reload the registry after removing a field + self.env = self.env(context={'_force_unlink': True}) + + def create_field(self, name): + """ create a custom field and return it """ + model = self.env['ir.model'].search([('model', '=', self.MODEL)]) + field = self.env['ir.model.fields'].create({ + 'model_id': model.id, + 'name': name, + 'field_description': name, + 'ttype': 'char', + }) + self.assertIn(name, self.env[self.MODEL]._fields) + return field + + def create_view(self, name): + """ create a view with the given field name """ + return self.env['ir.ui.view'].create({ + 'name': 'yet another view', + 'model': self.MODEL, + 'arch': '' % name, + }) + + def test_create_custom(self): + """ custom field names must be start with 'x_' """ + with self.assertRaises(ValidationError): + self.create_field('foo') + + def test_rename_custom(self): + """ custom field names must be start with 'x_' """ + field = self.create_field('x_foo') + with self.assertRaises(ValidationError): + field.name = 'foo' + + def test_create_valid(self): + """ field names must be valid pg identifiers """ + with self.assertRaises(ValidationError): + self.create_field('x_foo bar') + + def test_rename_valid(self): + """ field names must be valid pg identifiers """ + field = self.create_field('x_foo') + with self.assertRaises(ValidationError): + field.name = 'x_foo bar' + + def test_create_unique(self): + """ one cannot create two fields with the same name on a given model """ + self.create_field('x_foo') + with self.assertRaises(ValidationError): + self.create_field('x_foo') + + def test_rename_unique(self): + """ one cannot create two fields with the same name on a given model """ + field1 = self.create_field('x_foo') + field2 = self.create_field('x_bar') + with self.assertRaises(ValidationError): + field2.name = field1.name + + def test_remove_without_view(self): + """ try removing a custom field that does not occur in views """ + field = self.create_field('x_foo') + field.unlink() + + def test_rename_without_view(self): + """ try renaming a custom field that does not occur in views """ + field = self.create_field('x_foo') + field.name = 'x_bar' + + def test_remove_with_view(self): + """ try removing a custom field that occurs in a view """ + field = self.create_field('x_foo') + self.create_view('x_foo') + + # try to delete the field, this should fail but not modify the registry + with self.assertRaises(UserError): + field.unlink() + self.assertIn('x_foo', self.env[self.MODEL]._fields) + + def test_rename_with_view(self): + """ try renaming a custom field that occurs in a view """ + field = self.create_field('x_foo') + self.create_view('x_foo') + + # try to delete the field, this should fail but not modify the registry + with self.assertRaises(UserError): + field.name = 'x_bar' + self.assertIn('x_foo', self.env[self.MODEL]._fields) diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv index 4f05bc6dae3..a368416e743 100644 --- a/odoo/addons/test_new_api/ir.model.access.csv +++ b/odoo/addons/test_new_api/ir.model.access.csv @@ -16,3 +16,4 @@ access_test_new_api_related,access_test_new_api_related,model_test_new_api_relat access_test_new_api_company,access_test_new_api_company,model_test_new_api_company,,1,1,1,1 access_test_new_api_company_attr,access_test_new_api_company_attr,model_test_new_api_company_attr,,1,1,1,1 access_test_new_api_compute_inverse,access_test_new_api_compute_inverse,model_test_new_api_compute_inverse,,1,1,1,1 +access_test_new_api_recursive,access_test_new_api_recursive,model_test_new_api_recursive,,1,1,1,1 diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index d0871a51dcf..c84827d47ce 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -348,4 +348,20 @@ class CompanyDependentAttribute(models.Model): @api.depends('quantity', 'company.foo') def _compute_bar(self): for record in self: - record.bar = (record.company.foo or '') * record.quantity \ No newline at end of file + record.bar = (record.company.foo or '') * record.quantity + + +class ComputeRecursive(models.Model): + _name = 'test_new_api.recursive' + + name = fields.Char(required=True) + parent = fields.Many2one('test_new_api.recursive') + display_name = fields.Char(compute='_compute_display_name', store=True) + + @api.depends('name', 'parent.display_name') + def _compute_display_name(self): + for rec in self: + if rec.parent: + rec.display_name = rec.parent.display_name + " / " + rec.name + else: + rec.display_name = rec.name \ No newline at end of file diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index ca61afa413a..378ace4af9b 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -179,6 +179,29 @@ class TestFields(common.TransactionCase): cath.parent = finn self.assertEqual(ewan.display_name, "Gabriel / Finnley / Catherine / Ewan") + def test_12_recursive_recompute(self): + """ test recomputation on recursively dependent field """ + a = self.env['test_new_api.recursive'].create({'name': 'A'}) + b = self.env['test_new_api.recursive'].create({'name': 'B', 'parent': a.id}) + c = self.env['test_new_api.recursive'].create({'name': 'C', 'parent': b.id}) + d = self.env['test_new_api.recursive'].create({'name': 'D', 'parent': c.id}) + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'A / B') + self.assertEqual(c.display_name, 'A / B / C') + self.assertEqual(d.display_name, 'A / B / C / D') + + b.parent = False + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'B') + self.assertEqual(c.display_name, 'B / C') + self.assertEqual(d.display_name, 'B / C / D') + + b.name = 'X' + self.assertEqual(a.display_name, 'A') + self.assertEqual(b.display_name, 'X') + self.assertEqual(c.display_name, 'X / C') + self.assertEqual(d.display_name, 'X / C / D') + def test_12_cascade(self): """ test computed field depending on computed field """ message = self.env.ref('test_new_api.message_0_0') diff --git a/odoo/fields.py b/odoo/fields.py index d1d25edf05e..2b289227f9f 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -670,6 +670,7 @@ class Field(object): model._field_triggers.add(field, (self, path_str)) elif path: self.recursive = True + model._field_triggers.add(field, (self, '.'.join(path))) ############################################################################ # diff --git a/odoo/models.py b/odoo/models.py index f0a5cb16786..b91f5a81386 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -569,7 +569,7 @@ class BaseModel(object): field = cls._fields.get(name) if not field: _logger.warning("method %s.%s: @constrains parameter %r is not a field name", cls._name, attr, name) - if not (field.store or field.inverse): + elif not (field.store or field.inverse): _logger.warning("method %s.%s: @constrains parameter %r is not writeable", cls._name, attr, name) methods.append(func) @@ -1938,7 +1938,7 @@ class BaseModel(object): def _read_group_resolve_many2one_fields(self, data, fields): many2onefields = {field['field'] for field in fields if field['type'] == 'many2one'} for field in many2onefields: - ids_set = {d[field] for d in data} + ids_set = {d[field] for d in data if d[field]} m2o_records = self.env[self._fields[field].comodel_name].browse(ids_set) data_dict = dict(m2o_records.name_get()) for d in data: @@ -3839,7 +3839,8 @@ class BaseModel(object): """ self.ensure_one() vals = self.copy_data(default)[0] - new = self.create(vals) + # To avoid to create a translation in the lang of the user, copy_translation will do it + new = self.with_context(lang=None).create(vals) self.copy_translations(new) return new diff --git a/odoo/modules/graph.py b/odoo/modules/graph.py index aaf90ae8443..ff2f99188fe 100644 --- a/odoo/modules/graph.py +++ b/odoo/modules/graph.py @@ -80,7 +80,7 @@ class Graph(dict): info = odoo.modules.module.load_information_from_description_file(module) if info and info['installable']: packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version - else: + elif module != 'studio_customization': _logger.warning('module %s: not installable, skipped', module) dependencies = dict([(p, info['depends']) for p, info in packages]) @@ -112,10 +112,7 @@ class Graph(dict): unmet_deps = filter(lambda p: p not in self, dependencies[package]) _logger.error('module %s: Unmet dependencies: %s', package, ', '.join(unmet_deps)) - result = len(self) - len_graph - if result != len(module_list): - _logger.warning('Some modules were not loaded.') - return result + return len(self) - len_graph def __iter__(self): diff --git a/odoo/modules/module.py b/odoo/modules/module.py index ecf95be088b..d15df6dba59 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -302,7 +302,7 @@ def load_information_from_description_file(module, mod_path=None): :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...) """ if not mod_path: - mod_path = get_module_path(module) + mod_path = get_module_path(module, downloaded=True) manifest_file = module_manifest(mod_path) if manifest_file: # default values for descriptor diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index eea14988eb7..bef1cb1c523 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -876,8 +876,7 @@ class expression(object): # Making search easier when there is a left operand as one2many or many2many elif len(path) > 1 and field.store and field.type in ('many2many', 'one2many'): right_ids = comodel.search([('.'.join(path[1:]), operator, right)]).ids - table_ids = model.with_context(active_test=False).search([(path[0], 'in', right_ids)]).ids - leaf.leaf = ('id', 'in', table_ids) + leaf.leaf = (path[0], 'in', right_ids) push(leaf) elif not field.store: @@ -980,15 +979,16 @@ class expression(object): rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2 if operator in HIERARCHY_FUNCS: - def _rec_convert(ids): - if comodel == model: - return ids - return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator) - ids2 = to_ids(right, comodel) dom = HIERARCHY_FUNCS[operator]('id', ids2, comodel) ids2 = comodel.search(dom).ids - push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model)) + if comodel == model: + push(create_substitution_leaf(leaf, ('id', 'in', ids2), model)) + else: + subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) + # avoid flattening of argument in to_sql() + subquery = cr.mogrify(subquery, [tuple(ids2)]) + push(create_substitution_leaf(leaf, ('id', 'inselect', (subquery, [])), internal=True)) else: call_null_m2m = True if right is not False: @@ -1014,8 +1014,11 @@ class expression(object): operator = 'in' # operator changed because ids are directly related to main object else: call_null_m2m = False - m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' - push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), model)) + subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect' + subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2) + # avoid flattening of argument in to_sql() + subquery = cr.mogrify(subquery, [tuple(filter(None, res_ids))]) + push(create_substitution_leaf(leaf, ('id', subop, (subquery, [])), internal=True)) if call_null_m2m: m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'