diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f0372c241f6..b62213a584a 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -284,9 +284,9 @@ class AccountMove(models.Model): # ========================================================= # ==== Business fields ==== - fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', readonly=True, - states={'draft': [('readonly', False)]}, - check_company=True, + fiscal_position_id = fields.Many2one( + 'account.fiscal.position', string='Fiscal Position', readonly=True, + states={'draft': [('readonly', False)]}, check_company=True, domain="[('company_id', '=', company_id)]", ondelete="restrict", help="Fiscal positions are used to adapt taxes and accounts for particular customers or sales orders/invoices. " "The default value comes from the customer.") @@ -502,9 +502,9 @@ class AccountMove(models.Model): self.partner_bank_id = bank_ids and bank_ids[0] # Find the new fiscal position. - delivery_partner_id = self._get_invoice_delivery_partner_id() - self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position( - self.partner_id.id, delivery_id=delivery_partner_id) + delivery_partner = self.env['res.partner'].browse(self._get_invoice_delivery_partner_id()) + self.fiscal_position_id = self.env['account.fiscal.position']._get_fiscal_position( + self.partner_id, delivery=delivery_partner) self._recompute_dynamic_lines() if warning: return {'warning': warning} diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index cec7ffcb54e..18325b1c518 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -424,7 +424,7 @@ class AccountReconcileModel(models.Model): if line.tax_ids: taxes = line.tax_ids - detected_fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(partner_id) + detected_fiscal_position = self.env['account.fiscal.position']._get_fiscal_position(self.env['res.partner'].browse(partner_id)) if detected_fiscal_position: taxes = detected_fiscal_position.map_tax(taxes) writeoff_line['tax_ids'] = [Command.set(taxes.ids)] diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 2a3767d9563..69fc1c66767 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -206,27 +206,22 @@ class AccountFiscalPosition(models.Model): return fpos @api.model - def get_fiscal_position(self, partner_id, delivery_id=None): + def _get_fiscal_position(self, partner, delivery=None): """ :return: fiscal position found (recordset) :rtype: :class:`account.fiscal.position` """ - if not partner_id: + if not partner: return self.env['account.fiscal.position'] - # This can be easily overridden to apply more complex fiscal rules - PartnerObj = self.env['res.partner'] - partner = PartnerObj.browse(partner_id) - # if no delivery use invoicing - if delivery_id: - delivery = PartnerObj.browse(delivery_id) - else: + if not delivery: delivery = partner # partner manually set fiscal position always win - if delivery.property_account_position_id or partner.property_account_position_id: - return delivery.property_account_position_id or partner.property_account_position_id + manual_fiscal_position = delivery.property_account_position_id or partner.property_account_position_id + if manual_fiscal_position: + return manual_fiscal_position # First search only matching VAT positions vat_required = bool(partner.vat) diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index e138e442bf4..443c6cb9a5a 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -80,11 +80,13 @@ class ResConfigSettings(models.TransientModel): group_show_line_subtotals_tax_excluded = fields.Boolean( "Show line subtotals without taxes (B2B)", implied_group='account.group_show_line_subtotals_tax_excluded', - group='base.group_portal,base.group_user,base.group_public') + group='base.group_portal,base.group_user,base.group_public', + compute='_compute_group_show_line_subtotals', store=True, readonly=False) group_show_line_subtotals_tax_included = fields.Boolean( "Show line subtotals with taxes (B2C)", implied_group='account.group_show_line_subtotals_tax_included', - group='base.group_portal,base.group_user,base.group_public') + group='base.group_portal,base.group_user,base.group_public', + compute='_compute_group_show_line_subtotals', store=True, readonly=False) group_show_sale_receipts = fields.Boolean(string='Sale Receipt', implied_group='account.group_sale_receipts') group_show_purchase_receipts = fields.Boolean(string='Purchase Receipt', @@ -154,18 +156,11 @@ class ResConfigSettings(models.TransientModel): self.has_chart_of_accounts = bool(self.company_id.chart_template_id) self.has_accounting_entries = self.env['account.chart.template'].existing_accounting(self.company_id) - @api.onchange('show_line_subtotals_tax_selection') - def _onchange_sale_tax(self): - if self.show_line_subtotals_tax_selection == "tax_excluded": - self.update({ - 'group_show_line_subtotals_tax_included': False, - 'group_show_line_subtotals_tax_excluded': True, - }) - else: - self.update({ - 'group_show_line_subtotals_tax_included': True, - 'group_show_line_subtotals_tax_excluded': False, - }) + @api.depends('show_line_subtotals_tax_selection') + def _compute_group_show_line_subtotals(self): + for wizard in self: + wizard.group_show_line_subtotals_tax_included = wizard.show_line_subtotals_tax_selection == "tax_included" + wizard.group_show_line_subtotals_tax_excluded = wizard.show_line_subtotals_tax_selection == "tax_excluded" @api.onchange('group_analytic_accounting') def onchange_analytic_accounting(self): diff --git a/addons/account/tests/test_fiscal_position.py b/addons/account/tests/test_fiscal_position.py index 5bcb87d1aba..24ef5a47c9c 100644 --- a/addons/account/tests/test_fiscal_position.py +++ b/addons/account/tests/test_fiscal_position.py @@ -64,7 +64,7 @@ class TestFiscalPosition(common.TransactionCase): def test_10_fp_country(self): def assert_fp(partner, expected_pos, message): self.assertEqual( - self.fp.get_fiscal_position(partner.id).id, + self.fp._get_fiscal_position(partner).id, expected_pos.id, message) diff --git a/addons/account/tests/test_settings.py b/addons/account/tests/test_settings.py index 2aabe3345ce..595ff571405 100644 --- a/addons/account/tests/test_settings.py +++ b/addons/account/tests/test_settings.py @@ -19,21 +19,18 @@ class TestSettings(AccountTestInvoicingCommon): def switch_tax_settings(self, config): config.show_line_subtotals_tax_selection = "tax_excluded" - config._onchange_sale_tax() config.flush() config.execute() self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True) self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), False) config.show_line_subtotals_tax_selection = "tax_included" - config._onchange_sale_tax() config.flush() config.execute() self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), False) self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_included'), True) config.show_line_subtotals_tax_selection = "tax_excluded" - config._onchange_sale_tax() config.flush() config.execute() self.assertEqual(self.env.user.has_group('account.group_show_line_subtotals_tax_excluded'), True) diff --git a/addons/account/wizard/accrued_orders.py b/addons/account/wizard/accrued_orders.py index 1c348914334..9a6533e8c61 100644 --- a/addons/account/wizard/accrued_orders.py +++ b/addons/account/wizard/accrued_orders.py @@ -155,7 +155,7 @@ class AccruedExpenseRevenue(models.TransientModel): o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_delivered() o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_invoiced() o.order_line.with_context(accrual_entry_date=self.date)._compute_untaxed_amount_invoiced() - o.order_line.with_context(accrual_entry_date=self.date)._get_to_invoice_qty() + o.order_line.with_context(accrual_entry_date=self.date)._compute_qty_to_invoice() lines = o.order_line.filtered( lambda l: fields.Float.compare( l.qty_to_invoice, diff --git a/addons/delivery_mondialrelay/wizard/choose_delivery_carrier.py b/addons/delivery_mondialrelay/wizard/choose_delivery_carrier.py index 70507e63822..89f3a5c2de3 100644 --- a/addons/delivery_mondialrelay/wizard/choose_delivery_carrier.py +++ b/addons/delivery_mondialrelay/wizard/choose_delivery_carrier.py @@ -56,6 +56,5 @@ class ChooseDeliveryCarrier(models.TransientModel): }) if partner_shipping != self.order_id.partner_shipping_id: self.order_id.partner_shipping_id = partner_shipping - self.order_id.onchange_partner_shipping_id() return super().button_confirm() diff --git a/addons/event_booth_sale/models/sale_order_line.py b/addons/event_booth_sale/models/sale_order_line.py index 68da88c5db7..a57ca3b9c22 100644 --- a/addons/event_booth_sale/models/sale_order_line.py +++ b/addons/event_booth_sale/models/sale_order_line.py @@ -64,10 +64,13 @@ class SaleOrderLine(models.Model): if self.event_booth_pending_ids and (not self.event_id or self.event_id != self.event_booth_pending_ids.event_id): self.event_booth_pending_ids = None - @api.onchange('event_booth_pending_ids') - def _onchange_event_booth_pending_ids(self): - # trigger the description computation - self.product_id_change() + @api.depends('event_booth_pending_ids') + def _compute_name(self): + """Override to add the compute dependency. + + The custom name logic can be found below in get_sale_order_line_multiline_description_sale. + """ + super()._compute_name() def _update_event_booths(self, set_paid=False): for so_line in self.filtered('is_event_booth'): diff --git a/addons/event_sale/models/event_ticket.py b/addons/event_sale/models/event_ticket.py index 278cd105cca..f684e8b50a1 100644 --- a/addons/event_sale/models/event_ticket.py +++ b/addons/event_sale/models/event_ticket.py @@ -43,6 +43,12 @@ class EventTemplateTicket(models.Model): if not ticket.description: ticket.description = False + # TODO clean this feature in master + # Feature broken by design, depending on the hacky `price` field on products + # context_dependent, core part of the pricelist mess + # This field usage should be restricted to the UX, and any use in effective + # price computation should be replaced by clear calls to the pricelist API + @api.depends_context('uom', 'qty', 'pricelist') # Cf product.price context dependencies @api.depends('product_id', 'price') def _compute_price_reduce(self): for ticket in self: diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 7e12b878747..685571079c6 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -57,14 +57,15 @@ class SaleOrder(models.Model): class SaleOrderLine(models.Model): - _inherit = 'sale.order.line' event_id = fields.Many2one( 'event.event', string='Event', + compute="_compute_event_id", store=True, readonly=False, precompute=True, help="Choose an event and it will automatically create a registration for this event.") event_ticket_id = fields.Many2one( 'event.event.ticket', string='Event Ticket', + compute="_compute_event_ticket_id", store=True, readonly=False, precompute=True, help="Choose an event ticket and it will automatically create a registration for this event ticket.") event_ok = fields.Boolean(compute='_compute_event_ok') @@ -110,28 +111,44 @@ class SaleOrderLine(models.Model): RegistrationSudo.create(registrations_vals) return True - @api.onchange('product_id') - def _onchange_product_id(self): - # We reset the event when keeping it would lead to an inconstitent state. - # We need to do it this way because the only relation between the product and the event is through the corresponding tickets. - if self.event_id and (not self.product_id or self.product_id.id not in self.event_id.mapped('event_ticket_ids.product_id.id')): - self.event_id = None + @api.depends('product_id') + def _compute_event_id(self): + event_lines = self.filtered(lambda line: line.product_id and line.product_id.detailed_type == 'event') + (self - event_lines).event_id = False + for line in event_lines: + if line.product_id not in line.event_id.event_ticket_ids.product_id: + line.event_id = False - @api.onchange('event_id') - def _onchange_event_id(self): - # We reset the ticket when keeping it would lead to an inconstitent state. - if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id): - self.event_ticket_id = None + @api.depends('event_id') + def _compute_event_ticket_id(self): + event_lines = self.filtered('event_id') + (self - event_lines).event_ticket_id = False + for line in event_lines: + if line.event_id != line.event_ticket_id.event_id: + line.event_ticket_id = False - @api.onchange('product_uom', 'product_uom_qty') - def product_uom_change(self): - if not self.event_ticket_id: - super(SaleOrderLine, self).product_uom_change() + @api.depends('event_ticket_id') + def _compute_price_unit(self): + """Do not update the price on qty/uom change""" + event_lines = self.filtered('event_ticket_id') + super(SaleOrderLine, self-event_lines)._compute_price_unit() + for line in event_lines: + if not line.product_id or line._origin.product_id != line.product_id: + super(SaleOrderLine, line)._compute_price_unit() - @api.onchange('event_ticket_id') - def _onchange_event_ticket_id(self): - # we call this to force update the default name - self.product_id_change() + @api.depends('event_ticket_id') + def _compute_discount(self): + """Do not compute the discount on event lines, it's always included in the price.""" + event_lines = self.filtered('event_ticket_id') + super(SaleOrderLine, self-event_lines)._compute_discount() + + @api.depends('event_ticket_id') + def _compute_name(self): + """Override to add the compute dependency. + + The custom name logic can be found below in get_sale_order_line_multiline_description_sale. + """ + super()._compute_name() def unlink(self): self._unlink_associated_registrations() @@ -160,6 +177,12 @@ class SaleOrderLine(models.Model): def _get_display_price(self, product): if self.event_ticket_id and self.event_id: - return self.event_ticket_id.with_context(pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id).price_reduce + # FIXME this is strange + # price_reduce is the price after discount + # shouldn't we leave the discount computation to sale + # and use the non reduced price here (aka price field) + return self.event_ticket_id.with_context( + pricelist=self.order_id.pricelist_id.id, + uom=self.product_uom.id).price_reduce else: return super()._get_display_price(product) diff --git a/addons/event_sale/tests/test_event_sale.py b/addons/event_sale/tests/test_event_sale.py index 68346f72eb4..1d043e9ea45 100644 --- a/addons/event_sale/tests/test_event_sale.py +++ b/addons/event_sale/tests/test_event_sale.py @@ -49,10 +49,7 @@ class TestEventSale(TestEventSaleCommon): cls.env['sale.order.line'].create({ 'product_id': product.id, 'price_unit': 190.50, - 'product_uom': cls.env.ref('uom.product_uom_unit').id, - 'product_uom_qty': 1.0, 'order_id': cls.sale_order.id, - 'name': 'sales order line', 'event_id': cls.event_0.id, 'event_ticket_id': cls.ticket.id, }) @@ -238,16 +235,11 @@ class TestEventSale(TestEventSaleCommon): 'pricelist_id': pricelist.id, }) sol = self.env['sale.order.line'].create({ - 'name': event.name, 'product_id': event_product.product_variant_id.id, - 'product_uom_qty': 1, - 'product_uom': event_product.uom_id.id, - 'price_unit': event_product.list_price, 'order_id': so.id, 'event_id': event.id, 'event_ticket_id': event_ticket.id, }) - sol.product_id_change() self.assertEqual(so.amount_total, 660.0, "Ticket is $1000 but the event product is on a pricelist 10 -> 6. So, $600 + a 10% tax.") @users('user_salesman') diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index c8a888bc27e..7423a4eca29 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -39,11 +39,12 @@ class AccountAnalyticLine(models.Model): return [] task_id = fields.Many2one( - 'project.task', 'Task', compute='_compute_task_id', store=True, readonly=False, index=True, + 'project.task', 'Task', index=True, + compute='_compute_task_id', store=True, readonly=False, domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), ('project_id', '=?', project_id)]") project_id = fields.Many2one( - 'project.project', 'Project', compute='_compute_project_id', store=True, readonly=False, - domain=_domain_project_id) + 'project.project', 'Project', domain=_domain_project_id, + compute='_compute_project_id', store=True, readonly=False) user_id = fields.Many2one(compute='_compute_user_id', store=True, readonly=False) employee_id = fields.Many2one('hr.employee', "Employee", domain=_domain_employee_id) department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True, compute_sudo=True) diff --git a/addons/l10n_ar/models/account_fiscal_position.py b/addons/l10n_ar/models/account_fiscal_position.py index c032a689285..2726d59017e 100644 --- a/addons/l10n_ar/models/account_fiscal_position.py +++ b/addons/l10n_ar/models/account_fiscal_position.py @@ -12,30 +12,26 @@ class AccountFiscalPosition(models.Model): 'should be auto-detected') @api.model - def get_fiscal_position(self, partner_id, delivery_id=None): + def _get_fiscal_position(self, partner, delivery=None): """ Take into account the partner afip responsibility in order to auto-detect the fiscal position """ - company = self.env.company - if company.country_id.code == "AR": - PartnerObj = self.env['res.partner'] - partner = PartnerObj.browse(partner_id) + if self.env.company.country_id.code != "AR": + return super()._get_fiscal_position(partner, delivery=delivery) - # if no delivery use invoicing - if delivery_id: - delivery = PartnerObj.browse(delivery_id) - else: - delivery = partner + # if no delivery use invoicing + if not delivery: + delivery = partner - # partner manually set fiscal position always win - if delivery.property_account_position_id or partner.property_account_position_id: - return delivery.property_account_position_id or partner.property_account_position_id - domain = [ - ('auto_apply', '=', True), - ('l10n_ar_afip_responsibility_type_ids', '=', self.env['res.partner'].browse( - partner_id).l10n_ar_afip_responsibility_type_id.id), - ('company_id', '=', company.id), - ] - return self.sudo().search(domain, limit=1) - return super().get_fiscal_position(partner_id, delivery_id=delivery_id) + # partner manually set fiscal position always win + manual_fiscal_position = delivery.property_account_position_id or partner.property_account_position_id + if manual_fiscal_position: + return manual_fiscal_position + + domain = [ + ('auto_apply', '=', True), + ('l10n_ar_afip_responsibility_type_ids', '=', partner.l10n_ar_afip_responsibility_type_id.id), + ('company_id', '=', self.env.company.id), + ] + return self.sudo().search(domain, limit=1) @api.onchange('l10n_ar_afip_responsibility_type_ids', 'country_group_id', 'country_id', 'zip_from', 'zip_to') def _onchange_afip_responsibility(self): diff --git a/addons/mrp_repair/models/repair.py b/addons/mrp_repair/models/repair.py index 47971ecc002..7f526a56f7c 100644 --- a/addons/mrp_repair/models/repair.py +++ b/addons/mrp_repair/models/repair.py @@ -48,7 +48,7 @@ class RepairLine(models.Model): tax = self.env['account.tax'] if partner: partner_invoice = self.repair_id.partner_invoice_id or partner - fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=self.repair_id.address_id.id) + fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id) taxes = self.product_id.taxes_id.filtered(lambda x: x.company_id == self.repair_id.company_id) tax = fpos.map_tax(taxes) return { diff --git a/addons/pos_sale/models/pos_order.py b/addons/pos_sale/models/pos_order.py index 9c01cfbb806..7171de5e6a4 100644 --- a/addons/pos_sale/models/pos_order.py +++ b/addons/pos_sale/models/pos_order.py @@ -51,7 +51,6 @@ class PosOrder(models.Model): 'discount': line.discount, 'sequence': sale_lines and sale_lines[-1].sequence + 1 or 10, }) - sale_line._compute_tax_id() line.sale_order_line_id = sale_line so_lines = order.lines.mapped('sale_order_line_id') diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 858b4fbc1e2..659290d0eef 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -936,8 +936,8 @@ class Task(models.Model): index=True, copy=False, readonly=True) - project_id = fields.Many2one('project.project', string='Project', - compute='_compute_project_id', recursive=True, store=True, readonly=False, + project_id = fields.Many2one('project.project', string='Project', recursive=True, + compute='_compute_project_id', store=True, readonly=False, index=True, tracking=True, check_company=True, change_default=True) # Defines in which project the task will be displayed / taken into account in statistics. # Example: 1 task A with 1 subtask B in project P @@ -967,8 +967,8 @@ class Task(models.Model): search='_search_personal_stage_type_id', help="The current user's personal task stage.") partner_id = fields.Many2one('res.partner', - string='Customer', - compute='_compute_partner_id', recursive=True, store=True, readonly=False, tracking=True, + string='Customer', recursive=True, tracking=True, + compute='_compute_partner_id', store=True, readonly=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") partner_is_company = fields.Boolean(related='partner_id.is_company', readonly=True) commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id') diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index bfd1e3ed51d..9151b6e810e 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -308,7 +308,7 @@ class PurchaseOrder(models.Model): self.fiscal_position_id = False self.currency_id = self.env.company.currency_id.id else: - self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id) + self.fiscal_position_id = self.env['account.fiscal.position']._get_fiscal_position(self.partner_id) self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.company.currency_id.id return {} @@ -572,15 +572,15 @@ class PurchaseOrder(models.Model): if not journal: raise UserError(_('Please define an accounting purchase journal for the company %s (%s).') % (self.company_id.name, self.company_id.id)) - partner_invoice_id = self.partner_id.address_get(['invoice'])['invoice'] + partner_invoice = self.env['res.partner'].browse(self.partner_id.address_get(['invoice'])['invoice']) invoice_vals = { 'ref': self.partner_ref or '', 'move_type': move_type, 'narration': self.notes, 'currency_id': self.currency_id.id, 'invoice_user_id': self.user_id and self.user_id.id or self.env.user.id, - 'partner_id': partner_invoice_id, - 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(partner_invoice_id)).id, + 'partner_id': partner_invoice.id, + 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id._get_fiscal_position(partner_invoice)).id, 'payment_reference': self.partner_ref or '', 'partner_bank_id': self.partner_id.bank_ids[:1].id, 'invoice_origin': self.name, @@ -953,7 +953,7 @@ class PurchaseOrderLine(models.Model): def _compute_tax_id(self): for line in self: line = line.with_company(line.company_id) - fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id.get_fiscal_position(line.order_id.partner_id.id) + fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_id.partner_id) # filter taxes by company taxes = line.product_id.supplier_taxes_id.filtered(lambda r: r.company_id == line.env.company) line.taxes_id = fpos.map_tax(taxes) diff --git a/addons/purchase_requisition/models/purchase.py b/addons/purchase_requisition/models/purchase.py index 1e4e595db01..2fb39475351 100644 --- a/addons/purchase_requisition/models/purchase.py +++ b/addons/purchase_requisition/models/purchase.py @@ -24,7 +24,7 @@ class PurchaseOrder(models.Model): payment_term = partner.property_supplier_payment_term_id FiscalPosition = self.env['account.fiscal.position'] - fpos = FiscalPosition.with_company(self.company_id).get_fiscal_position(partner.id) + fpos = FiscalPosition.with_company(self.company_id)._get_fiscal_position(partner) self.partner_id = partner.id self.fiscal_position_id = fpos.id diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py index 2feab33b18c..e0bf8e34488 100644 --- a/addons/purchase_stock/models/stock_rule.py +++ b/addons/purchase_stock/models/stock_rule.py @@ -270,7 +270,7 @@ class StockRule(models.Model): partner = values['supplier'].partner_id purchase_date = procurement_date_planned - relativedelta(days=supplier_delay) - fpos = self.env['account.fiscal.position'].with_company(company_id).get_fiscal_position(partner.id) + fpos = self.env['account.fiscal.position'].with_company(company_id)._get_fiscal_position(partner) gpo = self.group_propagation_option group = (gpo == 'fixed' and self.group_id.id) or \ diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py index 2b67d2db2c9..56366cdd857 100644 --- a/addons/repair/models/repair.py +++ b/addons/repair/models/repair.py @@ -342,7 +342,7 @@ class Repair(models.Model): current_invoices_list = grouped_invoices_vals[(partner_invoice.id, currency.id, company.id)] if not group or len(current_invoices_list) == 0: - fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=repair.address_id.id) + fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=repair.address_id) invoice_vals = { 'move_type': 'out_invoice', 'partner_id': partner_invoice.id, @@ -717,7 +717,7 @@ class RepairLine(models.Model): self.product_uom = product.uom_id.id if self.type != 'remove': if partner: - fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=self.repair_id.address_id.id) + fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id) taxes = self.product_id.taxes_id.filtered(lambda x: x.company_id == self.repair_id.company_id) self.tax_id = fpos.map_tax(taxes) warning = False @@ -795,7 +795,7 @@ class RepairFee(models.Model): pricelist = self.repair_id.pricelist_id if partner and self.product_id: - fpos = self.env['account.fiscal.position'].get_fiscal_position(partner_invoice.id, delivery_id=self.repair_id.address_id.id) + fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id) taxes = self.product_id.taxes_id.filtered(lambda x: x.company_id == self.repair_id.company_id) self.tax_id = fpos.map_tax(taxes) if partner: diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py index acd7ffed417..89993271f72 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError + class AccountMove(models.Model): _name = 'account.move' _inherit = ['account.move', 'utm.mixin'] @@ -14,6 +15,7 @@ class AccountMove(models.Model): team_id = fields.Many2one( 'crm.team', string='Sales Team', default=_get_invoice_default_sale_team, + compute='_compute_team_id', store=True, readonly=False, ondelete="set null", tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") partner_shipping_id = fields.Many2one( @@ -23,22 +25,25 @@ class AccountMove(models.Model): states={'draft': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Delivery address for current invoice.") + # UTMs - enforcing the fact that we want to 'set null' when relation is unlinked campaign_id = fields.Many2one(ondelete='set null') medium_id = fields.Many2one(ondelete='set null') source_id = fields.Many2one(ondelete='set null') - @api.onchange('partner_shipping_id', 'company_id') - def _onchange_partner_shipping_id(self): - """ - Trigger the change of fiscal position when the shipping address is modified. - """ - delivery_partner_id = self._get_invoice_delivery_partner_id() - fiscal_position = self.env['account.fiscal.position'].with_company(self.company_id).get_fiscal_position( - self.partner_id.id, delivery_id=delivery_partner_id) + fiscal_position_id = fields.Many2one( + compute='_compute_fiscal_position_id', store=True) - if fiscal_position: - self.fiscal_position_id = fiscal_position + + @api.depends('partner_shipping_id', 'company_id') + def _compute_fiscal_position_id(self): + # Trigger the change of fiscal position when the shipping address is modified. + for move in self: + delivery_partner = self.env['res.partner'].browse(move._get_invoice_delivery_partner_id()) + fiscal_position = self.env['account.fiscal.position'].with_company( + move.company_id)._get_fiscal_position(move.partner_id, delivery=delivery_partner) + if fiscal_position: + move.fiscal_position_id = fiscal_position def unlink(self): downpayment_lines = self.mapped('line_ids.sale_line_ids').filtered(lambda line: line.is_downpayment and line.invoice_lines <= self.mapped('line_ids')) @@ -58,10 +63,14 @@ class AccountMove(models.Model): return res - @api.onchange('invoice_user_id') - def onchange_user_id(self): - if self.invoice_user_id and self.invoice_user_id.sale_team_id: - self.team_id = self.env['crm.team']._get_default_team_id(user_id=self.invoice_user_id.id, domain=[('company_id', '=', self.company_id.id)]) + @api.depends('invoice_user_id') + def _compute_team_id(self): + for move in self: + if not move.invoice_user_id.sale_team_id: + continue + move.team_id = self.env['crm.team']._get_default_team_id( + user_id=move.invoice_user_id.id, + domain=[('company_id', '=', move.company_id.id)]) def _reverse_moves(self, default_values_list=None, cancel=False): # OVERRIDE diff --git a/addons/sale/models/account_move_line.py b/addons/sale/models/account_move_line.py index 9b8d08746d3..5c983ac1414 100644 --- a/addons/sale/models/account_move_line.py +++ b/addons/sale/models/account_move_line.py @@ -118,8 +118,6 @@ class AccountMoveLine(models.Model): # create the sale lines in batch new_sale_lines = self.env['sale.order.line'].create(sale_line_values_to_create) - for sol in new_sale_lines: - sol._onchange_discount() # build result map by replacing index with newly created record of sale.order.line result = {} @@ -158,7 +156,7 @@ class AccountMoveLine(models.Model): last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1) last_sequence = last_so_line.sequence + 1 if last_so_line else 100 - fpos = order.fiscal_position_id or order.fiscal_position_id.get_fiscal_position(order.partner_id.id) + fpos = order.fiscal_position_id or order.fiscal_position_id._get_fiscal_position(order.partner_id) taxes = fpos.map_tax(self.product_id.taxes_id) return { diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index ae3bef2b262..24035f903c3 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -23,9 +23,11 @@ class ProductTemplate(models.Model): sale_line_warn = fields.Selection(WARNING_MESSAGE, 'Sales Order Line', help=WARNING_HELP, required=True, default="no-message") sale_line_warn_msg = fields.Text('Message for Sales Order Line') expense_policy = fields.Selection( - [('no', 'No'), ('cost', 'At cost'), ('sales_price', 'Sales price')], - string='Re-Invoice Expenses', - default='no', + [('no', 'No'), + ('cost', 'At cost'), + ('sales_price', 'Sales price') + ], string='Re-Invoice Expenses', default='no', + compute='_compute_expense_policy', store=True, readonly=False, help="Expenses and vendor bills can be re-invoiced to a customer." "With this option, a validated expense can be re-invoice to a customer at its cost or sales price.") visible_expense_policy = fields.Boolean("Re-Invoice Policy visible", compute='_compute_visible_expense_policy') @@ -49,17 +51,15 @@ class ProductTemplate(models.Model): product_template.visible_expense_policy = visibility - @api.onchange('sale_ok') - def _change_sale_ok(self): - if not self.sale_ok: - self.expense_policy = 'no' + @api.depends('sale_ok') + def _compute_expense_policy(self): + self.filtered(lambda t: not t.sale_ok).expense_policy = 'no' @api.depends('product_variant_ids.sales_count') def _compute_sales_count(self): for product in self: product.sales_count = float_round(sum([p.sales_count for p in product.with_context(active_test=False).product_variant_ids]), precision_rounding=product.uom_id.rounding) - @api.constrains('company_id') def _check_sale_product_company(self): """Ensure the product is not being restricted to a single company while diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index fac1701aeba..feac5f926be 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime, timedelta -from functools import partial +from datetime import timedelta from itertools import groupby import json from odoo import api, fields, models, SUPERUSER_ID, _ from odoo.exceptions import AccessError, UserError, ValidationError -from odoo.tools.misc import formatLang from odoo.osv import expression from odoo.tools import float_is_zero, html_keep_url, is_html_empty @@ -24,7 +22,7 @@ class SaleOrder(models.Model): if self.env['ir.config_parameter'].sudo().get_param('sale.use_quotation_validity_days'): days = self.env.company.quotation_validity_days if days > 0: - return fields.Date.to_string(datetime.now() + timedelta(days)) + return fields.Datetime.now() + timedelta(days) return False def _get_default_require_signature(self): @@ -105,29 +103,9 @@ class SaleOrder(models.Model): return super(SaleOrder, self).get_empty_list_help(help) @api.model - def _default_note_url(self): + def _get_note_url(self): return self.env.company.get_base_url() - @api.model - def _default_note(self): - use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms') - if use_invoice_terms and self.env.company.terms_type == "html": - baseurl = html_keep_url(self._default_note_url() + '/terms') - return _('Terms & Conditions: %s', baseurl) - return use_invoice_terms and self.env.company.invoice_terms or '' - - @api.model - def _get_default_team(self): - return self.env['crm.team']._get_default_team_id() - - @api.onchange('fiscal_position_id') - def _compute_tax_id(self): - """ - Trigger the recompute of the taxes if the fiscal position is changed on the SO. - """ - for order in self: - order.order_line._compute_tax_id() - def _search_invoice_ids(self, operator, value): if operator == 'in' and value: self.env.cr.execute(""" @@ -161,7 +139,9 @@ class SaleOrder(models.Model): return [('id', 'not in', order_ids)] return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)] - name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New')) + name = fields.Char( + string='Order Reference', required=True, copy=False, readonly=True, + states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New')) origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.") client_order_ref = fields.Char(string='Customer Reference', copy=False) reference = fields.Char(string='Payment Ref.', copy=False, @@ -186,29 +166,33 @@ class SaleOrder(models.Model): create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.") user_id = fields.Many2one( - 'res.users', string='Salesperson', index=True, tracking=2, default=lambda self: self.env.user, + 'res.users', string='Salesperson', index=True, tracking=2, + compute='_compute_user_id', store=True, readonly=False, precompute=True, domain=lambda self: [('groups_id', 'in', self.env.ref('sales_team.group_sale_salesman').id)]) partner_id = fields.Many2one( - 'res.partner', string='Customer', readonly=True, - states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, + 'res.partner', string='Customer', readonly=False, + states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)]}, required=True, change_default=True, index=True, tracking=1, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) partner_invoice_id = fields.Many2one( - 'res.partner', string='Invoice Address', - readonly=True, required=True, - states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, + 'res.partner', string='Invoice Address', required=True, + compute='_compute_partner_invoice_id', store=True, readonly=False, precompute=True, + states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) partner_shipping_id = fields.Many2one( - 'res.partner', string='Delivery Address', readonly=True, required=True, - states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, + 'res.partner', string='Delivery Address', required=True, + compute='_compute_partner_shipping_id', store=True, readonly=False, precompute=True, + states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) pricelist_id = fields.Many2one( - 'product.pricelist', string='Pricelist', check_company=True, # Unrequired company - required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, + 'product.pricelist', string='Pricelist', required=False, check_company=True, # Unrequired company + compute='_compute_pricelist_id', store=True, precompute=True, readonly=False, + states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=1, help="If you change the pricelist, only newly added lines will be affected.") - currency_id = fields.Many2one(related='pricelist_id.currency_id', depends=["pricelist_id"], store=True, ondelete="restrict") + currency_id = fields.Many2one( + related='pricelist_id.currency_id', depends=["pricelist_id"], store=True, precompute=True, ondelete="restrict") analytic_account_id = fields.Many2one( 'account.analytic.account', 'Analytic Account', readonly=True, copy=False, check_company=True, # Unrequired company @@ -227,21 +211,28 @@ class SaleOrder(models.Model): ('no', 'Nothing to Invoice') ], string='Invoice Status', compute='_get_invoice_status', store=True) - note = fields.Html('Terms and conditions', default=_default_note) + note = fields.Html( + 'Terms and conditions', + compute='_compute_note', store=True, readonly=False, precompute=True) terms_type = fields.Selection(related='company_id.terms_type') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, compute='_amount_all', tracking=5) tax_totals_json = fields.Char(compute='_compute_tax_totals_json') amount_tax = fields.Monetary(string='Taxes', store=True, compute='_amount_all') amount_total = fields.Monetary(string='Total', store=True, compute='_amount_all', tracking=4) - currency_rate = fields.Float("Currency Rate", compute='_compute_currency_rate', store=True, digits=(12, 6), help='The rate of the currency to the currency of rate 1 applicable at the date of the order') + currency_rate = fields.Float( + string="Currency Rate", + compute='_compute_currency_rate', store=True, digits=(12, 6), precompute=True, + help='The rate of the currency to the currency of rate 1 applicable at the date of the order') payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Terms', check_company=True, # Unrequired company + compute='_compute_payment_term_id', store=True, readonly=False, precompute=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) fiscal_position_id = fields.Many2one( 'account.fiscal.position', string='Fiscal Position', domain="[('company_id', '=', company_id)]", check_company=True, + compute='_compute_fiscal_position_id', store=True, readonly=False, precompute=True, help="Fiscal positions are used to adapt taxes and accounts for particular customers or sales orders/invoices." "The default value comes from the customer.") tax_country_id = fields.Many2one( @@ -252,18 +243,20 @@ class SaleOrder(models.Model): team_id = fields.Many2one( 'crm.team', 'Sales Team', ondelete="set null", tracking=True, - change_default=True, default=_get_default_team, check_company=True, # Unrequired company + compute='_compute_team_id', store=True, readonly=False, precompute=True, + change_default=True, check_company=True, # Unrequired company domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") signature = fields.Image('Signature', help='Signature received through the portal.', copy=False, attachment=True, max_width=1024, max_height=1024) signed_by = fields.Char('Signed By', help='Name of the person that signed the SO.', copy=False) signed_on = fields.Datetime('Signed On', help='Date of the signature.', copy=False) - commitment_date = fields.Datetime('Delivery Date', copy=False, - states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - help="This is the delivery date promised to the customer. " - "If set, the delivery order will be scheduled based on " - "this date rather than product lead times.") + commitment_date = fields.Datetime( + 'Delivery Date', copy=False, + states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, + help="This is the delivery date promised to the customer. " + "If set, the delivery order will be scheduled based on " + "this date rather than product lead times.") expected_date = fields.Datetime("Expected Date", compute='_compute_expected_date', store=False, # Note: can not be stored since depends on today() help="Delivery date you can promise to the customer, computed from the minimum lead time of the order lines.") amount_undiscounted = fields.Float('Amount Before Discount', compute='_compute_amount_undiscounted', digits=0) @@ -274,9 +267,11 @@ class SaleOrder(models.Model): string='Transactions', copy=False, readonly=True) authorized_transaction_ids = fields.Many2many('payment.transaction', compute='_compute_authorized_transaction_ids', string='Authorized Transactions', copy=False) - show_update_pricelist = fields.Boolean(string='Has Pricelist Changed', - help="Technical Field, True if the pricelist was changed;\n" - " this will then display a recomputation button") + show_update_pricelist = fields.Boolean( + string='Has Pricelist Changed', + compute='_compute_show_update_pricelist', store=True, readonly=True, precompute=True, + help="Technical Field, True if the pricelist was changed;\n" + " this will then display a recomputation button") tag_ids = fields.Many2many('crm.tag', 'sale_order_tag_rel', 'order_id', 'tag_id', string='Tags') # UTMs - enforcing the fact that we want to 'set null' when relation is unlinked campaign_id = fields.Many2one(ondelete='set null') @@ -300,16 +295,26 @@ class SaleOrder(models.Model): bad_products=', '.join(bad_products.mapped('display_name')), )) - @api.depends('pricelist_id', 'date_order', 'company_id') + @api.depends('currency_id', 'date_order', 'company_id') def _compute_currency_rate(self): + cache = {} for order in self: + order_date = order.date_order.date() if not order.company_id: - order.currency_rate = order.currency_id.with_context(date=order.date_order).rate or 1.0 + order.currency_rate = order.currency_id.with_context(date=order_date).rate or 1.0 continue - elif order.company_id.currency_id and order.currency_id: # the following crashes if any one is undefined - order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order) - else: + elif not order.currency_id: order.currency_rate = 1.0 + else: + key = (order.company_id.id, order_date, order.currency_id.id) + if key not in cache: + cache[key] = self.env['res.currency']._get_conversion_rate( + from_currency=order.company_id.currency_id, + to_currency=order.currency_id, + company=order.company_id, + date=order_date, + ) + order.currency_rate = cache[key] def _compute_access_url(self): super(SaleOrder, self)._compute_access_url() @@ -333,7 +338,7 @@ class SaleOrder(models.Model): dt = line._expected_date() dates_list.append(dt) if dates_list: - order.expected_date = fields.Datetime.to_string(min(dates_list)) + order.expected_date = min(dates_list) else: order.expected_date = False @@ -350,6 +355,9 @@ class SaleOrder(models.Model): tax_totals = account_move._get_tax_totals(order.partner_id, tax_lines_data, order.amount_total, order.amount_untaxed, order.currency_id) order.tax_totals_json = json.dumps(tax_totals) + # YTI TODO: Convert into compute method, however this introduces + # a behavior change that breaks the test test_reservation_method_w_sale + # Note: This should be the case @api.onchange('expected_date') def _onchange_expected_date(self): self.commitment_date = self.expected_date @@ -398,66 +406,77 @@ class SaleOrder(models.Model): return self.env.ref('sale.mt_order_sent') return super(SaleOrder, self)._track_subtype(init_values) - @api.onchange('partner_shipping_id', 'partner_id', 'company_id') - def onchange_partner_shipping_id(self): + @api.depends('partner_shipping_id', 'partner_id', 'company_id') + def _compute_fiscal_position_id(self): """ Trigger the change of fiscal position when the shipping address is modified. """ - self.fiscal_position_id = self.env['account.fiscal.position'].with_company(self.company_id).get_fiscal_position(self.partner_id.id, self.partner_shipping_id.id) - return {} + cache = {} + for order in self: + if not order.partner_id: + order.fiscal_position_id = False + continue + key = (order.company_id.id, order.partner_id.id, order.partner_shipping_id.id) + if key not in cache: + cache[key] = self.env['account.fiscal.position'].with_company( + order.company_id + )._get_fiscal_position(order.partner_id, order.partner_shipping_id) + order.fiscal_position_id = cache[key] - @api.onchange('partner_id') - def onchange_partner_id(self): - """ - Update the following fields when the partner is changed: - - Pricelist - - Payment terms - - Invoice address - - Delivery address - - Sales Team - """ - if not self.partner_id: - self.update({ - 'partner_invoice_id': False, - 'partner_shipping_id': False, - 'fiscal_position_id': False, - }) + @api.depends('partner_id') + def _compute_partner_invoice_id(self): + for order in self: + order.partner_invoice_id = order.partner_id.address_get(['invoice'])['invoice'] if order.partner_id else False + + @api.depends('partner_id') + def _compute_partner_shipping_id(self): + for order in self: + order.partner_shipping_id = order.partner_id.address_get(['delivery'])['delivery'] if order.partner_id else False + + @api.depends('partner_id') + def _compute_pricelist_id(self): + for order in self.filtered(lambda o: o.partner_id): + order = order.with_company(order.company_id) + order.pricelist_id = order.partner_id.property_product_pricelist + + @api.depends('partner_id') + def _compute_note(self): + use_invoice_terms = self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms') + if not use_invoice_terms: return - - self = self.with_company(self.company_id) - - addr = self.partner_id.address_get(['delivery', 'invoice']) - partner_user = self.partner_id.user_id or self.partner_id.commercial_partner_id.user_id - values = { - 'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False, - 'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False, - 'partner_invoice_id': addr['invoice'], - 'partner_shipping_id': addr['delivery'], - } - user_id = partner_user.id - if not self.env.context.get('not_self_saleperson'): - user_id = user_id or self.env.context.get('default_user_id', self.env.uid) - if user_id and self.user_id.id != user_id: - values['user_id'] = user_id - - if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'): - if self.terms_type == 'html' and self.env.company.invoice_terms_html: - baseurl = html_keep_url(self.get_base_url() + '/terms') - values['note'] = _('Terms & Conditions: %s', baseurl) + for order in self: + order = order.with_company(order.company_id) + if order.terms_type == 'html' and self.env.company.invoice_terms_html: + baseurl = html_keep_url(order._get_note_url() + '/terms') + order.note = _('Terms & Conditions: %s', baseurl) elif not is_html_empty(self.env.company.invoice_terms): - values['note'] = self.with_context(lang=self.partner_id.lang).env.company.invoice_terms - if not self.env.context.get('not_self_saleperson') or not self.team_id: - values['team_id'] = self.env['crm.team'].with_context( - default_team_id=self.partner_id.team_id.id - )._get_default_team_id(domain=['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], user_id=user_id) - self.update(values) + order.note = order.with_context(lang=order.partner_id.lang).env.company.invoice_terms - @api.onchange('user_id') - def onchange_user_id(self): - if self.user_id: - self.team_id = self.env['crm.team'].with_context( - default_team_id=self.team_id.id - )._get_default_team_id(user_id=self.user_id.id, domain=None) + @api.depends('partner_id') + def _compute_payment_term_id(self): + for order in self: + order = order.with_company(order.company_id) + order.payment_term_id = order.partner_id.property_payment_term_id + + @api.depends('partner_id') + def _compute_user_id(self): + for order in self: + order.user_id = order.partner_id.user_id or order.partner_id.commercial_partner_id.user_id or self.env.user + + @api.depends('partner_id', 'user_id') + def _compute_team_id(self): + cached_teams = {} + for order in self: + default_team_id = (order.team_id or order.partner_id.team_id).id + user_id = order.user_id.id + company_id = order.company_id.id + key = (default_team_id, user_id, company_id) + if key not in cached_teams: + cached_teams[key] = self.env['crm.team'].with_context( + default_team_id=default_team_id + )._get_default_team_id( + user_id=user_id, domain=[('company_id', 'in', [company_id, False])]) + order.team_id = cached_teams[key] @api.onchange('partner_id') def _onchange_partner_id_warning(self): @@ -476,7 +495,6 @@ class SaleOrder(models.Model): if partner.sale_warn == 'block': self.update({'partner_id': False, 'partner_invoice_id': False, 'partner_shipping_id': False, 'pricelist_id': False}) - return { 'warning': { 'title': _("Warning for %s", partner.name), @@ -496,12 +514,10 @@ class SaleOrder(models.Model): } } - @api.onchange('pricelist_id', 'order_line') - def _onchange_pricelist_id(self): - if self.order_line and self.pricelist_id and self._origin.pricelist_id != self.pricelist_id: - self.show_update_pricelist = True - else: - self.show_update_pricelist = False + @api.depends('pricelist_id', 'order_line') + def _compute_show_update_pricelist(self): + for order in self: + order.show_update_pricelist = order.order_line and order.pricelist_id and order._origin.pricelist_id != self.pricelist_id def update_prices(self): self.ensure_one() @@ -525,25 +541,19 @@ class SaleOrder(models.Model): self.show_update_pricelist = False self.message_post(body=_("Product prices have been recomputed according to pricelist %s ", self.pricelist_id.display_name)) - @api.model - def create(self, vals): - if 'company_id' in vals: - self = self.with_company(vals['company_id']) - if vals.get('name', _('New')) == _('New'): - seq_date = None - if 'date_order' in vals: - seq_date = fields.Datetime.context_timestamp(self, fields.Datetime.to_datetime(vals['date_order'])) - vals['name'] = self.env['ir.sequence'].next_by_code('sale.order', sequence_date=seq_date) or _('New') + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if 'company_id' in vals: + self = self.with_company(vals['company_id']) + if vals.get('name', _('New')) == _('New'): + seq_date = fields.Datetime.context_timestamp( + self, fields.Datetime.to_datetime(vals['date_order']) + ) if 'date_order' in vals else None + vals['name'] = self.env['ir.sequence'].next_by_code( + 'sale.order', sequence_date=seq_date) or _('New') - # Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined - if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']): - partner = self.env['res.partner'].browse(vals.get('partner_id')) - addr = partner.address_get(['delivery', 'invoice']) - vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) - vals['partner_shipping_id'] = vals.setdefault('partner_shipping_id', addr['delivery']) - vals['pricelist_id'] = vals.setdefault('pricelist_id', partner.property_product_pricelist.id) - result = super(SaleOrder, self).create(vals) - return result + return super().create(vals_list) def _compute_field_value(self, field): if field.name == 'invoice_status' and not self.env.context.get('mail_activity_automation_skip'): @@ -624,7 +634,7 @@ class SaleOrder(models.Model): 'team_id': self.team_id.id, 'partner_id': self.partner_invoice_id.id, 'partner_shipping_id': self.partner_shipping_id.id, - 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(self.partner_invoice_id.id)).id, + 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id._get_fiscal_position(self.partner_invoice_id)).id, 'partner_bank_id': self.company_id.partner_id.bank_ids[:1].id, 'journal_id': journal.id, # company comes from the journal 'invoice_origin': self.name, @@ -1064,10 +1074,6 @@ class SaleOrder(models.Model): self.ensure_one() return self.transaction_ids._get_last() - @api.model - def _get_customer_lead(self, product_tmpl_id): - return False - def _get_report_base_filename(self): self.ensure_one() return '%s %s' % (self.type_name, self.name) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 6e3e02ac7ca..8572c0b488c 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -23,7 +23,7 @@ class SaleOrderLine(models.Model): - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to invoice. This is also hte default value if the conditions of no other status is met. - to invoice: we refer to the quantity to invoice of the line. Refer to method - `_get_to_invoice_qty()` for more information on how this quantity is calculated. + `_compute_qty_to_invoice()` for more information on how this quantity is calculated. - upselling: this is possible only for a product invoiced on ordered quantities for which we delivered more than expected. The could arise if, for example, a project took more time than expected but we decided not to invoice the extra cost to the client. This @@ -49,7 +49,10 @@ class SaleOrderLine(models.Model): def _expected_date(self): self.ensure_one() - order_date = fields.Datetime.from_string(self.order_id.date_order if self.order_id.date_order and self.order_id.state in ['sale', 'done'] else fields.Datetime.now()) + if self.state in ['sale', 'done'] and self.order_id.date_order: + order_date = self.order_id.date_order + else: + order_date = fields.Datetime.now() return order_date + timedelta(days=self.customer_lead or 0.0) @api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id') @@ -59,7 +62,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_shipping_id) + taxes = line.tax_id.compute_all(price, line.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_shipping_id) line.update({ 'price_tax': taxes['total_included'] - taxes['total_excluded'], 'price_total': taxes['total_included'], @@ -68,7 +71,7 @@ class SaleOrderLine(models.Model): if self.env.context.get('import_file', False) and not self.env.user.user_has_groups('account.group_account_manager'): line.tax_id.invalidate_cache(['invoice_repartition_line_ids'], [line.tax_id.id]) - @api.depends('product_id', 'order_id.state', 'qty_invoiced', 'qty_delivered') + @api.depends('product_id', 'state', 'qty_invoiced', 'qty_delivered') def _compute_product_updatable(self): for line in self: if line.state in ['done', 'cancel'] or (line.state == 'sale' and (line.qty_invoiced > 0 or line.qty_delivered > 0)): @@ -77,14 +80,14 @@ class SaleOrderLine(models.Model): line.product_updatable = True # no trigger product_id.invoice_policy to avoid retroactively changing SO - @api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state') - def _get_to_invoice_qty(self): + @api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'state') + def _compute_qty_to_invoice(self): """ Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is calculated from the ordered quantity. Otherwise, the quantity delivered is used. """ for line in self: - if line.order_id.state in ['sale', 'done']: + if line.state in ['sale', 'done']: if line.product_id.invoice_policy == 'order': line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced else: @@ -134,38 +137,23 @@ class SaleOrderLine(models.Model): for line in self: line.price_reduce_taxexcl = line.price_subtotal / line.product_uom_qty if line.product_uom_qty else 0.0 + @api.depends('product_id') def _compute_tax_id(self): for line in self: line = line.with_company(line.company_id) - fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id.get_fiscal_position(line.order_partner_id.id) - # If company_id is set, always filter taxes by the company taxes = line.product_id.taxes_id.filtered(lambda t: t.company_id == line.env.company) - line.tax_id = fpos.map_tax(taxes) - - @api.model - def _prepare_add_missing_fields(self, values): - """ Deduce missing required fields from the onchange """ - res = {} - onchange_fields = ['name', 'price_unit', 'product_uom', 'tax_id'] - if values.get('order_id') and values.get('product_id') and any(f not in values for f in onchange_fields): - line = self.new(values) - line.product_id_change() - for field in onchange_fields: - if field not in values: - res[field] = line._fields[field].convert_to_write(line[field], line) - return res + if not line.product_id or not taxes: + # Nothing to map + line.tax_id = False + continue + # If company_id is set, always filter taxes by the company + line.tax_id = line.order_id.fiscal_position_id.map_tax(taxes) @api.model_create_multi def create(self, vals_list): - for values in vals_list: - if values.get('display_type', self.default_get(['display_type'])['display_type']): - values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom=False, customer_lead=0) - - values.update(self._prepare_add_missing_fields(values)) - lines = super().create(vals_list) for line in lines: - if line.product_id and line.order_id.state == 'sale': + if line.product_id and line.state == 'sale': msg = _("Extra line with %s", line.product_id.display_name) line.order_id.message_post(body=msg) # create an analytic account if at least an expense product @@ -211,7 +199,7 @@ class SaleOrderLine(models.Model): # Prevent writing on a locked SO. protected_fields = self._get_protected_fields() - if 'done' in self.mapped('order_id.state') and any(f in values.keys() for f in protected_fields): + if 'done' in self.mapped('state') and any(f in values.keys() for f in protected_fields): protected_fields_modified = list(set(protected_fields) & set(values.keys())) fields = self.env['ir.model.fields'].search([ ('name', 'in', protected_fields_modified), ('model', '=', self._name) @@ -225,7 +213,9 @@ class SaleOrderLine(models.Model): return result order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False) - name = fields.Text(string='Description', required=True) + name = fields.Text( + string='Description', required=True, + compute='_compute_name', store=True, readonly=False, precompute=True) sequence = fields.Integer(string='Sequence', default=10) invoice_lines = fields.Many2many('account.move.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False) @@ -234,19 +224,30 @@ class SaleOrderLine(models.Model): ('invoiced', 'Fully Invoiced'), ('to invoice', 'To Invoice'), ('no', 'Nothing to Invoice') - ], string='Invoice Status', compute='_compute_invoice_status', store=True, default='no') - price_unit = fields.Float('Unit Price', required=True, digits='Product Price', default=0.0) + ], string='Invoice Status', compute='_compute_invoice_status', store=True) + price_unit = fields.Float( + 'Unit Price', required=True, digits='Product Price', + compute='_compute_price_unit', store=True, readonly=False, precompute=True) - price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) - price_tax = fields.Float(compute='_compute_amount', string='Total Tax', store=True) - price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) + price_reduce = fields.Float( + string='Price Reduce', digits='Product Price', + compute='_compute_price_reduce', store=True, precompute=True) + price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True, precompute=True) + price_tax = fields.Float(compute='_compute_amount', string='Total Tax', store=True, precompute=True) + price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True, precompute=True) + price_reduce_taxinc = fields.Monetary( + compute='_compute_price_reduce_taxinc', string='Price Reduce Tax inc', store=True, precompute=True) + price_reduce_taxexcl = fields.Monetary( + compute='_compute_price_reduce_taxexcl', string='Price Reduce Tax excl', store=True, precompute=True) - price_reduce = fields.Float(compute='_compute_price_reduce', string='Price Reduce', digits='Product Price', store=True) - tax_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) - price_reduce_taxinc = fields.Monetary(compute='_compute_price_reduce_taxinc', string='Price Reduce Tax inc', store=True) - price_reduce_taxexcl = fields.Monetary(compute='_compute_price_reduce_taxexcl', string='Price Reduce Tax excl', store=True) + tax_id = fields.Many2many( + 'account.tax', string='Taxes', + compute='_compute_tax_id', store=True, readonly=False, precompute=True, + domain=['|', ('active', '=', False), ('active', '=', True)]) - discount = fields.Float(string='Discount (%)', digits='Discount', default=0.0) + discount = fields.Float( + string='Discount (%)', digits='Discount', + compute='_compute_discount', store=True, readonly=False, precompute=True) product_id = fields.Many2one( 'product.product', string='Product', domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", @@ -254,42 +255,54 @@ class SaleOrderLine(models.Model): product_template_id = fields.Many2one( 'product.template', string='Product Template', related="product_id.product_tmpl_id", domain=[('sale_ok', '=', True)]) - product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product', default=True) - product_uom_qty = fields.Float(string='Quantity', digits='Product Unit of Measure', required=True, default=1.0) - product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict") + product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product') + product_uom_qty = fields.Float( + string='Quantity', digits='Product Unit of Measure', required=True, + compute='_compute_product_uom_qty', store=True, readonly=False, precompute=True) + product_uom = fields.Many2one( + 'uom.uom', string='Unit of Measure', + compute='_compute_product_uom', store=True, readonly=False, precompute=True, + domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict") product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') product_uom_readonly = fields.Boolean(compute='_compute_product_uom_readonly') - product_custom_attribute_value_ids = fields.One2many('product.attribute.custom.value', 'sale_order_line_id', string="Custom Values", copy=True) + product_custom_attribute_value_ids = fields.One2many( + 'product.attribute.custom.value', 'sale_order_line_id', + string="Custom Values", copy=True, + compute='_compute_custom_attribute_values', store=True, readonly=False, precompute=True) # M2M holding the values of product.attribute with create_variant field set to 'no_variant' # It allows keeping track of the extra_price associated to those attribute values and add them to the SO line description - product_no_variant_attribute_value_ids = fields.Many2many('product.template.attribute.value', string="Extra Values", ondelete='restrict') + product_no_variant_attribute_value_ids = fields.Many2many( + 'product.template.attribute.value', string="Extra Values", ondelete='restrict', + compute='_compute_no_variant_attribute_values', store=True, readonly=False, precompute=True) - qty_delivered_method = fields.Selection([ - ('manual', 'Manual'), - ('analytic', 'Analytic From Expenses') - ], string="Method to update delivered qty", compute='_compute_qty_delivered_method', store=True, + qty_delivered_method = fields.Selection( + [('manual', 'Manual'), + ('analytic', 'Analytic From Expenses')], string="Method to update delivered qty", + compute='_compute_qty_delivered_method', store=True, precompute=True, help="According to product configuration, the delivered quantity can be automatically computed by mechanism :\n" " - Manual: the quantity is set manually on the line\n" " - Analytic From expenses: the quantity is the quantity sum from posted expenses\n" " - Timesheet: the quantity is the sum of hours recorded on tasks linked to this sale line\n" " - Stock Moves: the quantity comes from confirmed pickings\n") - qty_delivered = fields.Float('Delivered Quantity', copy=False, compute='_compute_qty_delivered', inverse='_inverse_qty_delivered', store=True, digits='Product Unit of Measure', default=0.0) - qty_delivered_manual = fields.Float('Delivered Manually', copy=False, digits='Product Unit of Measure', default=0.0) + qty_delivered = fields.Float( + 'Delivered Quantity', copy=False, + compute='_compute_qty_delivered', store=True, readonly=False, + digits='Product Unit of Measure') qty_to_invoice = fields.Float( - compute='_get_to_invoice_qty', string='To Invoice Quantity', store=True, + compute='_compute_qty_to_invoice', string='To Invoice Quantity', store=True, digits='Product Unit of Measure') qty_invoiced = fields.Float( compute='_compute_qty_invoiced', string='Invoiced Quantity', store=True, digits='Product Unit of Measure') - untaxed_amount_invoiced = fields.Monetary("Untaxed Invoiced Amount", compute='_compute_untaxed_amount_invoiced', store=True) - untaxed_amount_to_invoice = fields.Monetary("Untaxed Amount To Invoice", compute='_compute_untaxed_amount_to_invoice', store=True) + untaxed_amount_invoiced = fields.Monetary( + "Untaxed Invoiced Amount", + compute='_compute_untaxed_amount_invoiced', store=True) + untaxed_amount_to_invoice = fields.Monetary( + "Untaxed Amount To Invoice", + compute='_compute_untaxed_amount_to_invoice', store=True) - salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson') - currency_id = fields.Many2one(related='order_id.currency_id', depends=['order_id.currency_id'], store=True, string='Currency') - company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, index=True) - order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer') analytic_tag_ids = fields.Many2many( 'account.analytic.tag', string='Analytic Tags', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") @@ -299,19 +312,40 @@ class SaleOrderLine(models.Model): string="Is a down payment", help="Down payments are made when creating invoices from a sales order." " They are not copied when duplicating a sales order.") + # Related stored fields from the order + currency_id = fields.Many2one( + related='order_id.currency_id', depends=['order_id.currency_id'], + store=True, string='Currency', precompute=True) + company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, index=True, precompute=True) + order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer', precompute=True) + salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', precompute=True) state = fields.Selection( - related='order_id.state', string='Order Status', copy=False, store=True) + related='order_id.state', string='Order Status', copy=False, store=True, precompute=True) customer_lead = fields.Float( - 'Lead Time', required=True, default=0.0, + 'Lead Time', required=True, + compute="_compute_customer_lead", store=True, readonly=False, precompute=True, help="Number of days between the order confirmation and the shipping of the products to the customer") display_type = fields.Selection([ ('line_section', "Section"), ('line_note', "Note")], default=False, help="Technical field for UX purpose.") - product_packaging_id = fields.Many2one('product.packaging', string='Packaging', default=False, domain="[('sales', '=', True), ('product_id','=',product_id)]", check_company=True) - product_packaging_qty = fields.Float('Packaging Quantity') + product_packaging_id = fields.Many2one( + 'product.packaging', string='Packaging', + compute='_compute_product_packaging_id', store=True, readonly=False, precompute=True, + domain="[('sales', '=', True), ('product_id','=',product_id)]", + check_company=True) + product_packaging_qty = fields.Float( + 'Packaging Quantity', + compute='_compute_product_packaging_qty', store=True, readonly=False, precompute=True) + + # This computed default is necessary + # because the ORM doesn't provide a way to remove a field default on inheritance + # if a default is specified in sale, it disables the compute in sale_stock + # when a SOL is created with a product but without customer_lead values. + def _compute_customer_lead(self): + self.customer_lead = 0.0 @api.depends('state') def _compute_product_uom_readonly(self): @@ -334,7 +368,11 @@ class SaleOrderLine(models.Model): else: # service and consu line.qty_delivered_method = 'manual' - @api.depends('qty_delivered_method', 'qty_delivered_manual', 'analytic_line_ids.so_line', 'analytic_line_ids.unit_amount', 'analytic_line_ids.product_uom_id') + @api.depends( + 'qty_delivered_method', + 'analytic_line_ids.so_line', + 'analytic_line_ids.unit_amount', + 'analytic_line_ids.product_uom_id') def _compute_qty_delivered(self): """ This method compute the delivered quantity of the SO lines: it covers the case provide by sale module, aka expense/vendor bills (sum of unit_amount of AAL), and manual case. @@ -347,10 +385,6 @@ class SaleOrderLine(models.Model): mapping = lines_by_analytic._get_delivered_quantity_by_analytic([('amount', '<=', 0.0)]) for so_line in lines_by_analytic: so_line.qty_delivered = mapping.get(so_line.id or so_line._origin.id, 0.0) - # compute for manual lines - for line in self: - if line.qty_delivered_method == 'manual': - line.qty_delivered = line.qty_delivered_manual or 0.0 def _get_delivered_quantity_by_analytic(self, additional_domain): """ Compute and write the delivered quantity of current SO lines, based on their related @@ -391,26 +425,16 @@ class SaleOrderLine(models.Model): return result - @api.onchange('qty_delivered') - def _inverse_qty_delivered(self): - """ When writing on qty_delivered, if the value should be modify manually (`qty_delivered_method` = 'manual' only), - then we put the value in `qty_delivered_manual`. Otherwise, `qty_delivered_manual` should be False since the - delivered qty is automatically compute by other mecanisms. - """ + @api.depends('product_id', 'product_uom_qty', 'product_uom') + def _compute_product_packaging_id(self): for line in self: - if line.qty_delivered_method == 'manual': - line.qty_delivered_manual = line.qty_delivered - else: - line.qty_delivered_manual = 0.0 - - @api.onchange('product_id', 'product_uom_qty', 'product_uom') - def _onchange_suggest_packaging(self): - # remove packaging if not match the product - if self.product_packaging_id.product_id != self.product_id: - self.product_packaging_id = False - # suggest biggest suitable packaging - if self.product_id and self.product_uom_qty and self.product_uom: - self.product_packaging_id = self.product_id.packaging_ids.filtered('sales')._find_suitable_product_packaging(self.product_uom_qty, self.product_uom) + # remove packaging if not match the product + if line.product_packaging_id.product_id != line.product_id: + line.product_packaging_id = False + # Find biggest suitable packaging + if line.product_id and line.product_uom_qty and line.product_uom: + line.product_packaging_id = line.product_id.packaging_ids.filtered( + 'sales')._find_suitable_product_packaging(line.product_uom_qty, line.product_uom) @api.onchange('product_packaging_id') def _onchange_product_packaging_id(self): @@ -430,23 +454,41 @@ class SaleOrderLine(models.Model): }, } - @api.onchange('product_packaging_id', 'product_uom', 'product_uom_qty') - def _onchange_update_product_packaging_qty(self): - if not self.product_packaging_id: - self.product_packaging_qty = False - else: - packaging_uom = self.product_packaging_id.product_uom_id - packaging_uom_qty = self.product_uom._compute_quantity(self.product_uom_qty, packaging_uom) - self.product_packaging_qty = float_round(packaging_uom_qty / self.product_packaging_id.qty, precision_rounding=packaging_uom.rounding) + @api.depends('product_packaging_id', 'product_uom', 'product_uom_qty') + def _compute_product_packaging_qty(self): + for line in self: + if not line.product_packaging_id: + line.product_packaging_qty = False + else: + packaging_uom = line.product_packaging_id.product_uom_id + packaging_uom_qty = line.product_uom._compute_quantity(line.product_uom_qty, packaging_uom) + line.product_packaging_qty = float_round( + packaging_uom_qty / line.product_packaging_id.qty, + precision_rounding=packaging_uom.rounding) - @api.onchange('product_packaging_qty') - def _onchange_product_packaging_qty(self): - if self.product_packaging_id: - packaging_uom = self.product_packaging_id.product_uom_id - qty_per_packaging = self.product_packaging_id.qty - product_uom_qty = packaging_uom._compute_quantity(self.product_packaging_qty * qty_per_packaging, self.product_uom) - if float_compare(product_uom_qty, self.product_uom_qty, precision_rounding=self.product_uom.rounding) != 0: - self.product_uom_qty = product_uom_qty + @api.depends('product_id') + def _compute_product_uom(self): + for line in self: + if not line.product_uom or (line.product_id.uom_id.id != line.product_uom.id): + line.product_uom = line.product_id.uom_id + + @api.depends('display_type', 'product_id', 'product_packaging_qty') + def _compute_product_uom_qty(self): + for line in self: + if line.display_type: + line.product_uom_qty = 0.0 + continue + + # Default value = 1.0 + line.product_uom_qty = line.product_uom_qty or 1.0 + if not line.product_packaging_id: + continue + packaging_uom = line.product_packaging_id.product_uom_id + qty_per_packaging = line.product_packaging_id.qty + product_uom_qty = packaging_uom._compute_quantity( + line.product_packaging_qty * qty_per_packaging, line.product_uom) + if float_compare(product_uom_qty, line.product_uom_qty, precision_rounding=line.product_uom.rounding) != 0: + line.product_uom_qty = product_uom_qty @api.depends('invoice_lines', 'invoice_lines.price_total', 'invoice_lines.move_id.state', 'invoice_lines.move_id.move_type') def _compute_untaxed_amount_invoiced(self): @@ -495,7 +537,7 @@ class SaleOrderLine(models.Model): # `price_reduce_taxexcl` cannot be used as it is computed from `price_subtotal` field. (see upper Note) price_subtotal = line.tax_id.compute_all( price_reduce, - currency=line.order_id.currency_id, + currency=line.currency_id, quantity=uom_qty_to_consider, product=line.product_id, partner=line.order_id.partner_shipping_id)['total_excluded'] @@ -595,43 +637,43 @@ class SaleOrderLine(models.Model): # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) + @api.depends('product_id') + def _compute_name(self): + for line in self: + if not line.product_id: + continue + product = line.product_id.with_context(lang=line.order_partner_id.lang) + line.name = line.get_sale_order_line_multiline_description_sale(product) + + @api.depends('product_id') + def _compute_custom_attribute_values(self): + for line in self: + if not line.product_id: + line.product_custom_attribute_value_ids = False + continue + valid_values = line.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids + # remove the is_custom values that don't belong to this template + for pacv in line.product_custom_attribute_value_ids: + if pacv.custom_product_template_attribute_value_id not in valid_values: + line.product_custom_attribute_value_ids -= pacv + + @api.depends('product_id') + def _compute_no_variant_attribute_values(self): + for line in self: + if not line.product_id: + line.product_no_variant_attribute_value_ids = False + continue + valid_values = line.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids + # remove the no_variant attributes that don't belong to this template + for ptav in line.product_no_variant_attribute_value_ids: + if ptav._origin not in valid_values: + line.product_no_variant_attribute_value_ids -= ptav + @api.onchange('product_id') - def product_id_change(self): + def _onchange_product_id_warning(self): if not self.product_id: return - valid_values = self.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids - # remove the is_custom values that don't belong to this template - for pacv in self.product_custom_attribute_value_ids: - if pacv.custom_product_template_attribute_value_id not in valid_values: - self.product_custom_attribute_value_ids -= pacv - - # remove the no_variant attributes that don't belong to this template - for ptav in self.product_no_variant_attribute_value_ids: - if ptav._origin not in valid_values: - self.product_no_variant_attribute_value_ids -= ptav - - vals = {} - if not self.product_uom or (self.product_id.uom_id.id != self.product_uom.id): - vals['product_uom'] = self.product_id.uom_id - vals['product_uom_qty'] = self.product_uom_qty or 1.0 - - product = self.product_id.with_context( - lang=get_lang(self.env, self.order_id.partner_id.lang).code, - partner=self.order_id.partner_id, - quantity=vals.get('product_uom_qty') or self.product_uom_qty, - date=self.order_id.date_order, - pricelist=self.order_id.pricelist_id.id, - uom=self.product_uom.id - ) - - vals.update(name=self.get_sale_order_line_multiline_description_sale(product)) - - self._compute_tax_id() - - if self.order_id.pricelist_id and self.order_id.partner_id: - vals['price_unit'] = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id) - self.update(vals) - + product = self.product_id if product.sale_line_warn != 'no-message': if product.sale_line_warn == 'block': self.product_id = False @@ -643,22 +685,25 @@ class SaleOrderLine(models.Model): } } - @api.onchange('product_uom', 'product_uom_qty') - def product_uom_change(self): - if not self.product_uom or not self.product_id: - self.price_unit = 0.0 - return - if self.order_id.pricelist_id and self.order_id.partner_id: - product = self.product_id.with_context( - lang=self.order_id.partner_id.lang, - partner=self.order_id.partner_id, - quantity=self.product_uom_qty, - date=self.order_id.date_order, - pricelist=self.order_id.pricelist_id.id, - uom=self.product_uom.id, - fiscal_position=self.env.context.get('fiscal_position') - ) - self.price_unit = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id) + @api.depends('product_id', 'product_uom', 'product_uom_qty') + def _compute_price_unit(self): + for line in self: + if not line.product_uom or not line.product_id: + line.price_unit = 0.0 + continue + if line.order_id.pricelist_id and line.order_partner_id: + product = line.product_id.with_context( + partner=line.order_partner_id, + quantity=line.product_uom_qty, + date=line.order_id.date_order, + pricelist=line.order_id.pricelist_id.id, + uom=line.product_uom.id, + fiscal_position=self.env.context.get('fiscal_position') + ) + line.price_unit = self.env['account.tax']._fix_tax_included_price_company( + line._get_display_price(product), product.taxes_id, line.tax_id, line.company_id) + else: + line.price_unit = 0.0 def name_get(self): result = [] @@ -745,42 +790,51 @@ class SaleOrderLine(models.Model): 'tax_id', 'analytic_tag_ids' ] - def _onchange_product_id_set_customer_lead(self): - pass - - @api.onchange('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id') - def _onchange_discount(self): - if not (self.product_id and self.product_uom and - self.order_id.partner_id and self.order_id.pricelist_id and - self.order_id.pricelist_id.discount_policy == 'without_discount' and - self.env.user.has_group('product.group_discount_per_so_line')): + @api.depends('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id') + def _compute_discount(self): + if not self.env.user.has_group('product.group_discount_per_so_line'): + # Do not compute discounts if the feature is not enabled. return - self.discount = 0.0 - product = self.product_id.with_context( - lang=self.order_id.partner_id.lang, - partner=self.order_id.partner_id, - quantity=self.product_uom_qty, - date=self.order_id.date_order, - pricelist=self.order_id.pricelist_id.id, - uom=self.product_uom.id, - fiscal_position=self.env.context.get('fiscal_position') - ) + for line in self: + if not (line.product_id and line.product_uom and + line.order_partner_id and line.order_id.pricelist_id and + line.order_id.pricelist_id.discount_policy == 'without_discount'): + continue - product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id) + line.discount = 0.0 + product = line.product_id.with_context( + partner=line.order_partner_id, + quantity=line.product_uom_qty, + date=line.order_id.date_order, + pricelist=line.order_id.pricelist_id.id, + uom=line.product_uom.id, + fiscal_position=self.env.context.get('fiscal_position')) - price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) - new_list_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + product_context = dict( + self.env.context, + partner_id=line.order_partner_id.id, + date=line.order_id.date_order, + uom=line.product_uom.id) - if new_list_price != 0: - if self.order_id.pricelist_id.currency_id != currency: - # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency - new_list_price = currency._convert( - new_list_price, self.order_id.pricelist_id.currency_id, - self.order_id.company_id or self.env.company, self.order_id.date_order or fields.Date.today()) - discount = (new_list_price - price) / new_list_price * 100 - if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0): - self.discount = discount + price, rule_id = line.order_id.pricelist_id.with_context(product_context).get_product_price_rule( + line.product_id, + line.product_uom_qty or 1.0, + line.order_partner_id) + new_list_price, currency = line.with_context(product_context)._get_real_price_currency( + product, rule_id, line.product_uom_qty, + line.product_uom, line.order_id.pricelist_id.id) + + if new_list_price != 0: + if line.order_id.pricelist_id.currency_id != currency: + # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency + new_list_price = currency._convert( + new_list_price, line.order_id.pricelist_id.currency_id, + line.order_id.company_id or self.env.company, + line.order_id.date_order or fields.Date.today()) + discount = (new_list_price - price) / new_list_price * 100 + if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0): + line.discount = discount def _is_delivery(self): self.ensure_one() @@ -817,13 +871,13 @@ class SaleOrderLine(models.Model): # display the no_variant attributes, except those that are also # displayed by a custom (avoid duplicate description) for ptav in (no_variant_ptavs - custom_ptavs): - name += "\n" + ptav.with_context(lang=self.order_id.partner_id.lang).display_name + name += "\n" + ptav.with_context(lang=self.order_partner_id.lang).display_name # Sort the values according to _order settings, because it doesn't work for virtual records in onchange custom_values = sorted(self.product_custom_attribute_value_ids, key=lambda r: (r.custom_product_template_attribute_value_id.id, r.id)) # display the is_custom values for pacv in custom_values: - name += "\n" + pacv.with_context(lang=self.order_id.partner_id.lang).display_name + name += "\n" + pacv.with_context(lang=self.order_partner_id.lang).display_name return name diff --git a/addons/sale/populate/sale_order.py b/addons/sale/populate/sale_order.py index 95aec55a207..38462afd304 100644 --- a/addons/sale/populate/sale_order.py +++ b/addons/sale/populate/sale_order.py @@ -76,9 +76,10 @@ class SaleOrderLine(models.Model): product_ids = self.env.registry.populated_models["product.product"] # If we want more advanced products with multiple variants # add a populate dependency on product template and the following lines - product_ids += self.env["product.product"].search([ - ('product_tmpl_id', 'in', self.env.registry.populated_models["product.template"]) - ]).ids + if 'product.template' in self.env.registry.populated_models: + product_ids += self.env["product.product"].search([ + ('product_tmpl_id', 'in', self.env.registry.populated_models["product.template"]) + ]).ids self.env['product.product'].browse(product_ids).read(['uom_id']) # prefetch all uom_id diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index 1a5f79fbc8c..b75fb04a21b 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import test_sale_product_attribute_value_config -from . import test_sale_to_invoice -from . import test_sale_order -from . import test_sale_pricelist +from . import test_access_rights +from . import test_accrued_sale_orders from . import test_onchange from . import test_payment_flow +from . import test_performance from . import test_reinvoice -from . import test_access_rights +from . import test_sale_flow +from . import test_sale_order +from . import test_sale_pricelist +from . import test_sale_product_attribute_value_config from . import test_sale_refund from . import test_sale_signature -from . import test_sale_flow -from . import test_accrued_sale_orders from . import test_sale_tax_totals +from . import test_sale_to_invoice diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py index d3322797b1b..8845e5f7f3f 100644 --- a/addons/sale/tests/test_onchange.py +++ b/addons/sale/tests/test_onchange.py @@ -1,282 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests import Form -from odoo.tests.common import TransactionCase +from odoo.tests import tagged, TransactionCase -class TestOnchangeProductId(TransactionCase): - """Test that when an included tax is mapped by a fiscal position, the included tax must be - subtracted to the price of the product. - """ - - def setUp(self): - super(TestOnchangeProductId, self).setUp() - self.fiscal_position_model = self.env['account.fiscal.position'] - self.fiscal_position_tax_model = self.env['account.fiscal.position.tax'] - self.tax_model = self.env['account.tax'] - self.so_model = self.env['sale.order'] - self.po_line_model = self.env['sale.order.line'] - self.res_partner_model = self.env['res.partner'] - self.product_tmpl_model = self.env['product.template'] - self.product_model = self.env['product.product'] - self.product_uom_model = self.env['uom.uom'] - self.pricelist_model = self.env['product.pricelist'] - - def test_onchange_product_id(self): - - uom_id = self.product_uom_model.search([('name', '=', 'Units')])[0] - pricelist = self.pricelist_model.search([('name', '=', 'Public Pricelist')])[0] - - partner_id = self.res_partner_model.create(dict(name="George")) - tax_include_id = self.tax_model.create(dict(name="Include tax", - amount='21.00', - price_include=True, - type_tax_use='sale')) - tax_exclude_id = self.tax_model.create(dict(name="Exclude tax", - amount='0.00', - type_tax_use='sale')) - - product_tmpl_id = self.product_tmpl_model.create(dict(name="Voiture", - list_price=121, - taxes_id=[(6, 0, [tax_include_id.id])])) - - product_id = product_tmpl_id.product_variant_id - - fp_id = self.fiscal_position_model.create(dict(name="fiscal position", sequence=1)) - - fp_tax_id = self.fiscal_position_tax_model.create(dict(position_id=fp_id.id, - tax_src_id=tax_include_id.id, - tax_dest_id=tax_exclude_id.id)) - - # Create the SO with one SO line and apply a pricelist and fiscal position on it - order_form = Form(self.env['sale.order'].with_context(tracking_disable=True)) - order_form.partner_id = partner_id - order_form.pricelist_id = pricelist - order_form.fiscal_position_id = fp_id - with order_form.order_line.new() as line: - line.name = product_id.name - line.product_id = product_id - line.product_uom_qty = 1.0 - line.product_uom = uom_id - sale_order = order_form.save() - - # Check the unit price of SO line - self.assertEqual(100, sale_order.order_line[0].price_unit, "The included tax must be subtracted to the price") - - def test_pricelist_application(self): - """ Test different prices are correctly applied based on dates """ - support_product = self.env['product.product'].create({ - 'name': 'Virtual Home Staging', - 'list_price': 100, - }) - partner = self.res_partner_model.create(dict(name="George")) - - christmas_pricelist = self.env['product.pricelist'].create({ - 'name': 'Christmas pricelist', - 'item_ids': [(0, 0, { - 'date_start': "2017-12-01", - 'date_end': "2017-12-24", - 'compute_price': 'percentage', - 'base': 'list_price', - 'percent_price': 20, - 'applied_on': '3_global', - 'name': 'Pre-Christmas discount' - }), (0, 0, { - 'date_start': "2017-12-25", - 'date_end': "2017-12-31", - 'compute_price': 'percentage', - 'base': 'list_price', - 'percent_price': 50, - 'applied_on': '3_global', - 'name': 'Post-Christmas super-discount' - })] - }) - - # Create the SO with pricelist based on date - order_form = Form(self.env['sale.order'].with_context(tracking_disable=True)) - order_form.partner_id = partner - order_form.date_order = '2017-12-20' - order_form.pricelist_id = christmas_pricelist - with order_form.order_line.new() as line: - line.product_id = support_product - so = order_form.save() - # Check the unit price and subtotal of SO line - self.assertEqual(so.order_line[0].price_unit, 80, "First date pricelist rule not applied") - self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity') - - # Change order date of the SO and check the unit price and subtotal of SO line - with Form(so) as order: - order.date_order = '2017-12-30' - with order.order_line.edit(0) as line: - line.product_id = support_product - - self.assertEqual(so.order_line[0].price_unit, 50, "Second date pricelist rule not applied") - self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity') - - def test_pricelist_uom_discount(self): - """ Test prices and discounts are correctly applied based on date and uom""" - computer_case = self.env['product.product'].create({ - 'name': 'Drawer Black', - 'list_price': 100, - }) - partner = self.res_partner_model.create(dict(name="George")) - categ_unit_id = self.ref('uom.product_uom_categ_unit') - goup_discount_id = self.ref('product.group_discount_per_so_line') - self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]}) - new_uom = self.env['uom.uom'].create({ - 'name': '10 units', - 'factor_inv': 10, - 'uom_type': 'bigger', - 'rounding': 1.0, - 'category_id': categ_unit_id - }) - christmas_pricelist = self.env['product.pricelist'].create({ - 'name': 'Christmas pricelist', - 'discount_policy': 'without_discount', - 'item_ids': [(0, 0, { - 'date_start': "2017-12-01", - 'date_end': "2017-12-30", - 'compute_price': 'percentage', - 'base': 'list_price', - 'percent_price': 10, - 'applied_on': '3_global', - 'name': 'Christmas discount' - })] - }) - - so = self.env['sale.order'].create({ - 'partner_id': partner.id, - 'date_order': '2017-12-20', - 'pricelist_id': christmas_pricelist.id, - }) - - order_line = self.env['sale.order.line'].new({ - 'order_id': so.id, - 'product_id': computer_case.id, - }) - - # force compute uom and prices - order_line.product_id_change() - order_line.product_uom_change() - order_line._onchange_discount() - self.assertEqual(order_line.price_subtotal, 90, "Christmas discount pricelist rule not applied") - self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%") - order_line.product_uom = new_uom - order_line.product_uom_change() - order_line._onchange_discount() - self.assertEqual(order_line.price_subtotal, 900, "Christmas discount pricelist rule not applied") - self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%") - - def test_pricelist_based_on_other(self): - """ Test price and discount are correctly applied with a pricelist based on an other one""" - computer_case = self.env['product.product'].create({ - 'name': 'Drawer Black', - 'list_price': 100, - }) - partner = self.res_partner_model.create(dict(name="George")) - goup_discount_id = self.ref('product.group_discount_per_so_line') - self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]}) - - first_pricelist = self.env['product.pricelist'].create({ - 'name': 'First pricelist', - 'discount_policy': 'without_discount', - 'item_ids': [(0, 0, { - 'compute_price': 'percentage', - 'base': 'list_price', - 'percent_price': 10, - 'applied_on': '3_global', - 'name': 'First discount' - })] - }) - - second_pricelist = self.env['product.pricelist'].create({ - 'name': 'Second pricelist', - 'discount_policy': 'without_discount', - 'item_ids': [(0, 0, { - 'compute_price': 'formula', - 'base': 'pricelist', - 'base_pricelist_id': first_pricelist.id, - 'price_discount': 10, - 'applied_on': '3_global', - 'name': 'Second discount' - })] - }) - - so = self.env['sale.order'].create({ - 'partner_id': partner.id, - 'date_order': '2018-07-11', - 'pricelist_id': second_pricelist.id, - }) - - order_line = self.env['sale.order.line'].new({ - 'order_id': so.id, - 'product_id': computer_case.id, - }) - - # force compute uom and prices - order_line.product_id_change() - order_line._onchange_discount() - self.assertEqual(order_line.price_subtotal, 81, "Second pricelist rule not applied") - self.assertEqual(order_line.discount, 19, "Second discount not applied") - - def test_pricelist_with_other_currency(self): - """ Test prices are correctly applied with a pricelist with an other currency""" - computer_case = self.env['product.product'].create({ - 'name': 'Drawer Black', - 'list_price': 100, - }) - computer_case.list_price = 100 - partner = self.res_partner_model.create(dict(name="George")) - categ_unit_id = self.ref('uom.product_uom_categ_unit') - other_currency = self.env['res.currency'].create({'name': 'other currency', - 'symbol': 'other'}) - self.env['res.currency.rate'].create({'name': '2018-07-11', - 'rate': 2.0, - 'currency_id': other_currency.id, - 'company_id': self.env.company.id}) - self.env['res.currency.rate'].search( - [('currency_id', '=', self.env.company.currency_id.id)] - ).unlink() - new_uom = self.env['uom.uom'].create({ - 'name': '10 units', - 'factor_inv': 10, - 'uom_type': 'bigger', - 'rounding': 1.0, - 'category_id': categ_unit_id - }) - - # This pricelist doesn't show the discount - first_pricelist = self.env['product.pricelist'].create({ - 'name': 'First pricelist', - 'currency_id': other_currency.id, - 'discount_policy': 'with_discount', - 'item_ids': [(0, 0, { - 'compute_price': 'percentage', - 'base': 'list_price', - 'percent_price': 10, - 'applied_on': '3_global', - 'name': 'First discount' - })] - }) - - so = self.env['sale.order'].create({ - 'partner_id': partner.id, - 'date_order': '2018-07-12', - 'pricelist_id': first_pricelist.id, - }) - - order_line = self.env['sale.order.line'].new({ - 'order_id': so.id, - 'product_id': computer_case.id, - }) - - # force compute uom and prices - order_line.product_id_change() - self.assertEqual(order_line.price_unit, 180, "First pricelist rule not applied") - order_line.product_uom = new_uom - order_line.product_uom_change() - self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied") +@tagged('post_install', '-at_install') +class TestSaleOnchanges(TransactionCase): def test_sale_warnings(self): """Test warnings & SO/SOL updates when partner/products with sale warnings are used.""" @@ -320,7 +49,7 @@ class TestOnchangeProductId(TransactionCase): 'order_id': sale_order.id, 'product_id': product_with_warning.id, }) - warning = sale_order_line.product_id_change() + warning = sale_order_line._onchange_product_id_warning() self.assertDictEqual(warning, { 'warning': { 'title': "Warning for Test Product", @@ -329,7 +58,7 @@ class TestOnchangeProductId(TransactionCase): }) sale_order_line.product_id = product_with_block_warning - warning = sale_order_line.product_id_change() + warning = sale_order_line._onchange_product_id_warning() self.assertDictEqual(warning, { 'warning': { diff --git a/addons/sale/tests/test_performance.py b/addons/sale/tests/test_performance.py new file mode 100644 index 00000000000..1e7ad76b249 --- /dev/null +++ b/addons/sale/tests/test_performance.py @@ -0,0 +1,15 @@ +from odoo.tests import tagged + +from odoo.addons.base.tests.common import PerformanceCommon + + +@tagged('post_install', '-at_install') +class TestSalePerformance(PerformanceCommon): + + def test_batch_creation_sale_orders(self): + """Enforce sale.order create overrides always support batch creation""" + self.assertModelCreateMulti('sale.order') + + def test_batch_creation_sale_order_line(self): + """Enforce sale.order.line create overrides always support batch creation""" + self.assertModelCreateMulti('sale.order.line') diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py index be414b81e8c..c328070e520 100644 --- a/addons/sale/tests/test_reinvoice.py +++ b/addons/sale/tests/test_reinvoice.py @@ -38,28 +38,18 @@ class TestReInvoice(TestSaleCommon): """ Test vendor bill at cost for product based on ordered and delivered quantities. """ # create SO line and confirm SO (with only one line) sale_order_line1 = self.env['sale.order.line'].create({ - 'name': self.company_data['product_order_cost'].name, 'product_id': self.company_data['product_order_cost'].id, 'product_uom_qty': 2, 'qty_delivered': 1, - 'product_uom': self.company_data['product_order_cost'].uom_id.id, - 'price_unit': self.company_data['product_order_cost'].list_price, 'order_id': self.sale_order.id, }) - sale_order_line1.product_id_change() sale_order_line2 = self.env['sale.order.line'].create({ - 'name': self.company_data['product_delivery_cost'].name, 'product_id': self.company_data['product_delivery_cost'].id, 'product_uom_qty': 4, 'qty_delivered': 1, - 'product_uom': self.company_data['product_delivery_cost'].uom_id.id, - 'price_unit': self.company_data['product_delivery_cost'].list_price, 'order_id': self.sale_order.id, }) - sale_order_line2.product_id_change() - self.sale_order.onchange_partner_id() - self.sale_order._compute_tax_id() self.sale_order.action_confirm() # create invoice lines and validate it @@ -158,26 +148,17 @@ class TestReInvoice(TestSaleCommon): """ # create SO line and confirm SO (with only one line) sale_order_line1 = self.env['sale.order.line'].create({ - 'name': self.company_data['product_delivery_sales_price'].name, 'product_id': self.company_data['product_delivery_sales_price'].id, 'product_uom_qty': 2, 'qty_delivered': 1, - 'product_uom': self.company_data['product_delivery_sales_price'].uom_id.id, - 'price_unit': self.company_data['product_delivery_sales_price'].list_price, 'order_id': self.sale_order.id, }) - sale_order_line1.product_id_change() sale_order_line2 = self.env['sale.order.line'].create({ - 'name': self.company_data['product_order_sales_price'].name, 'product_id': self.company_data['product_order_sales_price'].id, 'product_uom_qty': 3, 'qty_delivered': 1, - 'product_uom': self.company_data['product_order_sales_price'].uom_id.id, - 'price_unit': self.company_data['product_order_sales_price'].list_price, 'order_id': self.sale_order.id, }) - sale_order_line2.product_id_change() - self.sale_order._compute_tax_id() self.sale_order.action_confirm() # create invoice lines and validate it @@ -237,15 +218,11 @@ class TestReInvoice(TestSaleCommon): """ Test invoicing vendor bill with no policy. Check nothing happen. """ # confirm SO sale_order_line = self.env['sale.order.line'].create({ - 'name': self.company_data['product_delivery_no'].name, 'product_id': self.company_data['product_delivery_no'].id, 'product_uom_qty': 2, 'qty_delivered': 1, - 'product_uom': self.company_data['product_delivery_no'].uom_id.id, - 'price_unit': self.company_data['product_delivery_no'].list_price, 'order_id': self.sale_order.id, }) - self.sale_order._compute_tax_id() self.sale_order.action_confirm() # create invoice lines and validate it @@ -264,28 +241,16 @@ class TestReInvoice(TestSaleCommon): def test_not_reinvoicing_invoiced_so_lines(self): """ Test that invoiced SO lines are not re-invoiced. """ so_line1 = self.env['sale.order.line'].create({ - 'name': self.company_data['product_delivery_cost'].name, 'product_id': self.company_data['product_delivery_cost'].id, - 'product_uom_qty': 1, - 'product_uom': self.company_data['product_delivery_cost'].uom_id.id, - 'price_unit': self.company_data['product_delivery_cost'].list_price, 'discount': 100.00, 'order_id': self.sale_order.id, }) - so_line1.product_id_change() so_line2 = self.env['sale.order.line'].create({ - 'name': self.company_data['product_delivery_sales_price'].name, 'product_id': self.company_data['product_delivery_sales_price'].id, - 'product_uom_qty': 1, - 'product_uom': self.company_data['product_delivery_sales_price'].uom_id.id, - 'price_unit': self.company_data['product_delivery_sales_price'].list_price, 'discount': 100.00, 'order_id': self.sale_order.id, }) - so_line2.product_id_change() - self.sale_order.onchange_partner_id() - self.sale_order._compute_tax_id() self.sale_order.action_confirm() for line in self.sale_order.order_line: diff --git a/addons/sale/tests/test_sale_flow.py b/addons/sale/tests/test_sale_flow.py index c18d645f9b1..3095b5345da 100644 --- a/addons/sale/tests/test_sale_flow.py +++ b/addons/sale/tests/test_sale_flow.py @@ -69,11 +69,7 @@ class TestSaleFlow(TestSaleCommonBase): }), ], }) - for line in sale_order.order_line: - line.product_id_change() - sale_order.onchange_partner_id() - sale_order._compute_tax_id() sale_order.action_confirm() self.assertRecordValues(sale_order.order_line, [ diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 803c2f7a720..04f09818e01 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -4,6 +4,7 @@ from datetime import timedelta from freezegun import freeze_time from odoo import fields +from odoo.fields import Command from odoo.exceptions import UserError, AccessError from odoo.tests import tagged, Form from odoo.tools import float_compare @@ -49,42 +50,84 @@ class TestSaleOrder(TestSaleCommon): 'pricelist_id': cls.company_data['default_pricelist'].id, }) cls.sol_product_order = cls.env['sale.order.line'].create({ - 'name': cls.company_data['product_order_no'].name, 'product_id': cls.company_data['product_order_no'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_order_no'].uom_id.id, - 'price_unit': cls.company_data['product_order_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_deliver = cls.env['sale.order.line'].create({ - 'name': cls.company_data['product_service_delivery'].name, 'product_id': cls.company_data['product_service_delivery'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_service_delivery'].uom_id.id, - 'price_unit': cls.company_data['product_service_delivery'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_order = cls.env['sale.order.line'].create({ - 'name': cls.company_data['product_service_order'].name, 'product_id': cls.company_data['product_service_order'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_service_order'].uom_id.id, - 'price_unit': cls.company_data['product_service_order'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_product_deliver = cls.env['sale.order.line'].create({ - 'name': cls.company_data['product_delivery_no'].name, 'product_id': cls.company_data['product_delivery_no'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_delivery_no'].uom_id.id, - 'price_unit': cls.company_data['product_delivery_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) + def test_computes_auto_fill(self): + free_product = self.env['product.product'].create({ + 'name': 'Free product', + 'list_price': 0.0, + }) + dummy_product = self.env['product.product'].create({ + 'name': 'Dummy product', + 'list_price': 0.0, + }) + # Test pre-computes of lines with order + order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [ + Command.create({ + 'display_type': 'line_section', + 'name': 'Dummy section', + }), + Command.create({ + 'display_type': 'line_section', + 'name': 'Dummy section', + }), + Command.create({ + 'product_id': free_product.id, + }), + Command.create({ + 'product_id': dummy_product.id, + }) + ] + }) + + # Test pre-computes of lines creation alone + # Ensures the creation works fine even if the computes + # are triggered after the defaults + order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + }) + self.env['sale.order.line'].create([ + { + 'display_type': 'line_section', + 'name': 'Dummy section', + 'order_id': order.id, + }, { + 'display_type': 'line_section', + 'name': 'Dummy section', + 'order_id': order.id, + }, { + 'product_id': free_product.id, + 'order_id': order.id, + }, { + 'product_id': dummy_product.id, + 'order_id': order.id, + } + ]) + def test_sale_order(self): """ Test the sales order flow (invoicing and quantity updates) - Invoice repeatedly while varrying delivered quantities and check that invoice are always what we expect @@ -281,11 +324,6 @@ class TestSaleOrder(TestSaleCommon): self.sol_serv_order.write({'tax_id': [(4, tax_exclude.id)]}) self.sol_product_deliver.write({'tax_id': [(4, tax_exclude.id)]}) - # Trigger onchange to reset discount, unit price, subtotal, ... - for line in self.sale_order.order_line: - line.product_id_change() - line._onchange_discount() - for line in self.sale_order.order_line: if line.tax_id.price_include: price = line.price_unit * line.product_uom_qty - line.price_tax @@ -294,9 +332,10 @@ class TestSaleOrder(TestSaleCommon): self.assertEqual(float_compare(line.price_subtotal, price, precision_digits=2), 0) - self.assertEqual(self.sale_order.amount_total, - self.sale_order.amount_untaxed + self.sale_order.amount_tax, - 'Taxes should be applied') + self.assertAlmostEqual( + self.sale_order.amount_total, + self.sale_order.amount_untaxed + self.sale_order.amount_tax, + places=2) def test_so_create_multicompany(self): """Check that only taxes of the right company are applied on the lines.""" @@ -314,8 +353,9 @@ class TestSaleOrder(TestSaleCommon): 'company_id': self.company_data['company'].id, }) so_1.write({ - 'order_line': [(0, False, {'product_id': product_shared.product_variant_id.id, 'order_id': so_1.id})], + 'order_line': [Command.create({'product_id': product_shared.product_variant_id.id, 'order_id': so_1.id})], }) + self.assertEqual(so_1.order_line.product_uom_qty, 1) self.assertEqual(so_1.order_line.tax_id, self.company_data['default_tax_sale'], 'Only taxes from the right company are put by default') @@ -461,9 +501,6 @@ class TestSaleOrder(TestSaleCommon): }) ] }) - for line in sales_order.order_line: - # Create values autofill does not compute discount. - line._onchange_discount() so_line_1 = sales_order.order_line[0] so_line_2 = sales_order.order_line[1] @@ -492,8 +529,6 @@ class TestSaleOrder(TestSaleCommon): }) ] }) - for line in sales_order.order_line: - line._onchange_discount() so_line_1 = sales_order.order_line[0] so_line_2 = sales_order.order_line[1] @@ -512,7 +547,6 @@ class TestSaleOrder(TestSaleCommon): sale_order = self.env['sale.order'].create({ 'partner_id': partner.id, }) - sale_order.onchange_partner_id() self.assertEqual(sale_order.team_id.id, self.crm_team0.id, 'Should assign to team of sales person') def test_assign_sales_team_from_partner_team(self): @@ -525,7 +559,6 @@ class TestSaleOrder(TestSaleCommon): sale_order = self.env['sale.order'].create({ 'partner_id': partner.id, }) - sale_order.onchange_partner_id() self.assertEqual(sale_order.team_id.id, self.crm_team1.id, 'Should assign to team of partner') def test_assign_sales_team_when_changing_user(self): @@ -536,7 +569,6 @@ class TestSaleOrder(TestSaleCommon): 'team_id': self.crm_team1.id }) sale_order.user_id = self.user_in_team - sale_order.onchange_user_id() self.assertEqual(sale_order.team_id.id, self.crm_team0.id, 'Should assign to team of sales person') def test_keep_sales_team_when_changing_user_with_no_team(self): @@ -546,7 +578,6 @@ class TestSaleOrder(TestSaleCommon): 'team_id': self.crm_team1.id }) sale_order.user_id = self.user_not_in_team - sale_order.onchange_user_id() self.assertEqual(sale_order.team_id.id, self.crm_team1.id, 'Should not reset the team to default') def test_onchange_packaging_00(self): @@ -736,3 +767,42 @@ class TestSaleOrder(TestSaleCommon): # (541,26 / 1,15) * ,98 * 38 = 17527,410782609 ~= 17527.41 self.assertEqual(line.price_subtotal, 17527.41) self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal) + + def test_so_tax_mapping(self): + uom = self.env['uom.uom'].search([('name', '=', 'Units')], limit=1) + pricelist = self.env['product.pricelist'].search([('name', '=', 'Public Pricelist')], limit=1) + + partner = self.env['res.partner'].create(dict(name="George")) + tax_include = self.env['account.tax'].create(dict(name="Include tax", + amount='21.00', + price_include=True, + type_tax_use='sale')) + tax_exclude = self.env['account.tax'].create(dict(name="Exclude tax", + amount='0.00', + type_tax_use='sale')) + + product_tmpl = self.env['product.template'].create(dict(name="Voiture", + list_price=121, + taxes_id=[(6, 0, [tax_include.id])])) + + product = product_tmpl.product_variant_id + + fpos = self.env['account.fiscal.position'].create(dict(name="fiscal position", sequence=1)) + self.env['account.fiscal.position.tax'].create({ + 'position_id': fpos.id, + 'tax_src_id': tax_include.id, + 'tax_dest_id': tax_exclude.id, + }) + + sale_order = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'pricelist_id': pricelist.id, + 'fiscal_position_id': fpos.id, + 'order_line': [Command.create({ + 'product_id': product.id, + 'product_uom': uom.id, + })] + }) + + # Check the unit price of SO line + self.assertEqual(100, sale_order.order_line[0].price_unit, "The included tax must be subtracted to the price") diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index 7c0fd5c5970..38c51028ed6 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from .common import TestSaleCommon +from odoo.fields import Command from odoo.tests import tagged @@ -79,38 +80,26 @@ class TestSaleOrder(TestSaleCommon): 'pricelist_id': cls.company_data['default_pricelist'].id, }) cls.sol_product_order = SaleOrderLine.create({ - 'name': cls.company_data['product_order_no'].name, 'product_id': cls.company_data['product_order_no'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_order_no'].uom_id.id, - 'price_unit': cls.company_data['product_order_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_deliver = SaleOrderLine.create({ - 'name': cls.company_data['product_service_delivery'].name, 'product_id': cls.company_data['product_service_delivery'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_service_delivery'].uom_id.id, - 'price_unit': cls.company_data['product_service_delivery'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_order = SaleOrderLine.create({ - 'name': cls.company_data['product_service_order'].name, 'product_id': cls.company_data['product_service_order'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_service_order'].uom_id.id, - 'price_unit': cls.company_data['product_service_order'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_prod_deliver = SaleOrderLine.create({ - 'name': cls.company_data['product_delivery_no'].name, 'product_id': cls.company_data['product_delivery_no'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_delivery_no'].uom_id.id, - 'price_unit': cls.company_data['product_delivery_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) @@ -119,10 +108,9 @@ class TestSaleOrder(TestSaleCommon): """ Test SO with the pricelist and check unit price appeared on its lines """ # Change the pricelist self.sale_order.write({'pricelist_id': self.pricelist_discount_incl.id}) - # Trigger onchange to reset discount, unit price, subtotal, ... - for line in self.sale_order.order_line: - line.product_id_change() - line._onchange_discount() + # Trigger manually the computation for discount, unit price, subtotal, ... + self.sale_order.update_prices() + # Check that pricelist of the SO has been applied on the sale order lines or not for line in self.sale_order.order_line: if line.product_id == self.company_data['product_order_no']: @@ -143,10 +131,8 @@ class TestSaleOrder(TestSaleCommon): # Change the pricelist self.sale_order.write({'pricelist_id': self.pricelist_discount_excl.id}) - # Trigger onchange to reset discount, unit price, subtotal, ... - for line in self.sale_order.order_line: - line.product_id_change() - line._onchange_discount() + # Trigger manually the computation for discount, unit price, subtotal, ... + self.sale_order.update_prices() # Check pricelist of the SO apply or not on order lines where pricelist contains formula that add 15% on the cost price for line in self.sale_order.order_line: @@ -161,3 +147,206 @@ class TestSaleOrder(TestSaleCommon): else: # no discount for the rest self.assertEqual(line.discount, 0.0, 'Pricelist of SO should not be applied on an order line') self.assertEqual(line.price_unit, line.product_id.list_price, 'Unit price of order line should be a sale price as the pricelist not applied on the other category\'s product') + + def test_pricelist_application(self): + """ Test different prices are correctly applied based on dates """ + support_product = self.env['product.product'].create({ + 'name': 'Virtual Home Staging', + 'list_price': 100, + }) + partner = self.env['res.partner'].create(dict(name="George")) + + christmas_pricelist = self.env['product.pricelist'].create({ + 'name': 'Christmas pricelist', + 'item_ids': [(0, 0, { + 'date_start': "2017-12-01", + 'date_end': "2017-12-24", + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 20, + 'applied_on': '3_global', + 'name': 'Pre-Christmas discount' + }), (0, 0, { + 'date_start': "2017-12-25", + 'date_end': "2017-12-31", + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 50, + 'applied_on': '3_global', + 'name': 'Post-Christmas super-discount' + })] + }) + + # Create the SO with pricelist based on date + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'date_order': '2017-12-20', + 'pricelist_id': christmas_pricelist.id, + 'order_line': [Command.create({'product_id': support_product.id})] + }) + # Check the unit price and subtotal of SO line + self.assertEqual(so.order_line[0].price_unit, 80, "First date pricelist rule not applied") + self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity') + + # Change order date of the SO and check the unit price and subtotal of SO line + so.date_order = '2017-12-30' + so.update_prices() + + self.assertEqual(so.order_line[0].price_unit, 50, "Second date pricelist rule not applied") + self.assertEqual(so.order_line[0].price_subtotal, so.order_line[0].price_unit * so.order_line[0].product_uom_qty, 'Total of SO line should be a multiplication of unit price and ordered quantity') + + def test_pricelist_uom_discount(self): + """ Test prices and discounts are correctly applied based on date and uom""" + computer_case = self.env['product.product'].create({ + 'name': 'Drawer Black', + 'list_price': 100, + }) + partner = self.env['res.partner'].create(dict(name="George")) + categ_unit_id = self.ref('uom.product_uom_categ_unit') + goup_discount_id = self.ref('product.group_discount_per_so_line') + self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]}) + new_uom = self.env['uom.uom'].create({ + 'name': '10 units', + 'factor_inv': 10, + 'uom_type': 'bigger', + 'rounding': 1.0, + 'category_id': categ_unit_id + }) + christmas_pricelist = self.env['product.pricelist'].create({ + 'name': 'Christmas pricelist', + 'discount_policy': 'without_discount', + 'item_ids': [(0, 0, { + 'date_start': "2017-12-01", + 'date_end': "2017-12-30", + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 10, + 'applied_on': '3_global', + 'name': 'Christmas discount' + })] + }) + + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'date_order': '2017-12-20', + 'pricelist_id': christmas_pricelist.id, + }) + + order_line = self.env['sale.order.line'].create({ + 'order_id': so.id, + 'product_id': computer_case.id, + }) + + self.assertEqual(order_line.price_subtotal, 90, "Christmas discount pricelist rule not applied") + self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%") + order_line.product_uom = new_uom + self.assertEqual(order_line.price_subtotal, 900, "Christmas discount pricelist rule not applied") + self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%") + + def test_pricelist_based_on_other(self): + """ Test price and discount are correctly applied with a pricelist based on an other one""" + computer_case = self.env['product.product'].create({ + 'name': 'Drawer Black', + 'list_price': 100, + }) + partner = self.env['res.partner'].create(dict(name="George")) + goup_discount_id = self.ref('product.group_discount_per_so_line') + self.env.user.write({'groups_id': [(4, goup_discount_id, 0)]}) + + first_pricelist = self.env['product.pricelist'].create({ + 'name': 'First pricelist', + 'discount_policy': 'without_discount', + 'item_ids': [(0, 0, { + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 10, + 'applied_on': '3_global', + 'name': 'First discount' + })] + }) + + second_pricelist = self.env['product.pricelist'].create({ + 'name': 'Second pricelist', + 'discount_policy': 'without_discount', + 'item_ids': [(0, 0, { + 'compute_price': 'formula', + 'base': 'pricelist', + 'base_pricelist_id': first_pricelist.id, + 'price_discount': 10, + 'applied_on': '3_global', + 'name': 'Second discount' + })] + }) + + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'date_order': '2018-07-11', + 'pricelist_id': second_pricelist.id, + }) + + order_line = self.env['sale.order.line'].create({ + 'order_id': so.id, + 'product_id': computer_case.id, + }) + + self.assertEqual(order_line.price_subtotal, 81, "Second pricelist rule not applied") + self.assertEqual(order_line.discount, 19, "Second discount not applied") + + def test_pricelist_with_other_currency(self): + """ Test prices are correctly applied with a pricelist with an other currency""" + computer_case = self.env['product.product'].create({ + 'name': 'Drawer Black', + 'list_price': 100, + }) + # computer_case.list_price = 100 + partner = self.env['res.partner'].create(dict(name="George")) + categ_unit_id = self.ref('uom.product_uom_categ_unit') + other_currency = self.env['res.currency'].create({ + 'name': 'other currency', + 'symbol': 'other' + }) + self.env['res.currency.rate'].create({'name': '2018-07-11', + 'rate': 2.0, + 'currency_id': other_currency.id, + 'company_id': self.env.company.id + }) + self.env['res.currency.rate'].search( + [('currency_id', '=', self.env.company.currency_id.id)] + ).unlink() + new_uom = self.env['uom.uom'].create({ + 'name': '10 units', + 'factor_inv': 10, + 'uom_type': 'bigger', + 'rounding': 1.0, + 'category_id': categ_unit_id + }) + + # This pricelist doesn't show the discount + first_pricelist = self.env['product.pricelist'].create({ + 'name': 'First pricelist', + 'currency_id': other_currency.id, + 'discount_policy': 'with_discount', + 'item_ids': [(0, 0, { + 'compute_price': 'percentage', + 'base': 'list_price', + 'percent_price': 10, + 'applied_on': '3_global', + 'name': 'First discount' + })] + }) + + so = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'date_order': '2018-07-12', + 'pricelist_id': first_pricelist.id, + }) + + order_line = self.env['sale.order.line'].new({ + 'order_id': so.id, + 'product_id': computer_case.id, + }) + + # force compute uom and prices + self.assertEqual(order_line.price_unit, 180, "First pricelist rule not applied") + order_line.product_uom = new_uom + self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied") diff --git a/addons/sale/tests/test_sale_refund.py b/addons/sale/tests/test_sale_refund.py index eee8c0a1e2c..a1c478ae845 100644 --- a/addons/sale/tests/test_sale_refund.py +++ b/addons/sale/tests/test_sale_refund.py @@ -21,38 +21,26 @@ class TestSaleToInvoice(TestSaleCommon): }) SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True) cls.sol_prod_order = SaleOrderLine.create({ - 'name': cls.company_data['product_order_no'].name, 'product_id': cls.company_data['product_order_no'].id, 'product_uom_qty': 5, - 'product_uom': cls.company_data['product_order_no'].uom_id.id, - 'price_unit': cls.company_data['product_order_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_deliver = SaleOrderLine.create({ - 'name': cls.company_data['product_service_delivery'].name, 'product_id': cls.company_data['product_service_delivery'].id, 'product_uom_qty': 4, - 'product_uom': cls.company_data['product_service_delivery'].uom_id.id, - 'price_unit': cls.company_data['product_service_delivery'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_order = SaleOrderLine.create({ - 'name': cls.company_data['product_service_order'].name, 'product_id': cls.company_data['product_service_order'].id, 'product_uom_qty': 3, - 'product_uom': cls.company_data['product_service_order'].uom_id.id, - 'price_unit': cls.company_data['product_service_order'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_prod_deliver = SaleOrderLine.create({ - 'name': cls.company_data['product_delivery_no'].name, 'product_id': cls.company_data['product_delivery_no'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_delivery_no'].uom_id.id, - 'price_unit': cls.company_data['product_delivery_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) @@ -329,11 +317,8 @@ class TestSaleToInvoice(TestSaleCommon): 'pricelist_id': self.company_data['default_pricelist'].id, }) sol_product = self.env['sale.order.line'].create({ - 'name': self.company_data['product_order_no'].name, 'product_id': self.company_data['product_order_no'].id, 'product_uom_qty': 5, - 'product_uom': self.company_data['product_order_no'].uom_id.id, - 'price_unit': self.company_data['product_order_no'].list_price, 'order_id': sale_order_refund.id, 'tax_id': False, }) diff --git a/addons/sale/tests/test_sale_to_invoice.py b/addons/sale/tests/test_sale_to_invoice.py index 4291e735a85..32ff1f61301 100644 --- a/addons/sale/tests/test_sale_to_invoice.py +++ b/addons/sale/tests/test_sale_to_invoice.py @@ -22,38 +22,26 @@ class TestSaleToInvoice(TestSaleCommon): }) SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True) cls.sol_prod_order = SaleOrderLine.create({ - 'name': cls.company_data['product_order_no'].name, 'product_id': cls.company_data['product_order_no'].id, 'product_uom_qty': 5, - 'product_uom': cls.company_data['product_order_no'].uom_id.id, - 'price_unit': cls.company_data['product_order_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_deliver = SaleOrderLine.create({ - 'name': cls.company_data['product_service_delivery'].name, 'product_id': cls.company_data['product_service_delivery'].id, 'product_uom_qty': 4, - 'product_uom': cls.company_data['product_service_delivery'].uom_id.id, - 'price_unit': cls.company_data['product_service_delivery'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_serv_order = SaleOrderLine.create({ - 'name': cls.company_data['product_service_order'].name, 'product_id': cls.company_data['product_service_order'].id, 'product_uom_qty': 3, - 'product_uom': cls.company_data['product_service_order'].uom_id.id, - 'price_unit': cls.company_data['product_service_order'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) cls.sol_prod_deliver = SaleOrderLine.create({ - 'name': cls.company_data['product_delivery_no'].name, 'product_id': cls.company_data['product_delivery_no'].id, 'product_uom_qty': 2, - 'product_uom': cls.company_data['product_delivery_no'].uom_id.id, - 'price_unit': cls.company_data['product_delivery_no'].list_price, 'order_id': cls.sale_order.id, 'tax_id': False, }) @@ -296,11 +284,8 @@ class TestSaleToInvoice(TestSaleCommon): 'order_id': sale_order.id, }) sol_prod_deliver = SaleOrderLine.create({ - 'name': self.company_data['product_order_no'].name, 'product_id': self.company_data['product_order_no'].id, 'product_uom_qty': 5, - 'product_uom': self.company_data['product_order_no'].uom_id.id, - 'price_unit': self.company_data['product_order_no'].list_price, 'order_id': sale_order.id, 'tax_id': False, }) @@ -339,11 +324,8 @@ class TestSaleToInvoice(TestSaleCommon): SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True) sol_prod_deliver = SaleOrderLine.create({ - 'name': self.company_data['product_order_no'].name, 'product_id': self.company_data['product_order_no'].id, 'product_uom_qty': 5, - 'product_uom': self.company_data['product_order_no'].uom_id.id, - 'price_unit': self.company_data['product_order_no'].list_price, 'order_id': sale_order.id, 'tax_id': False, }) diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index f95f20692c5..addeb51dccd 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -378,7 +378,6 @@ /> - @@ -528,7 +527,6 @@ }" optional="show" /> - 0% for BE)") self.assertEqual(round(combination_info['list_price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 0% for BE)") -@tagged('post_install', '-at_install') +@tagged('post_install', '-at_install', 'product_pricelist') class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon): def test_cart_update_with_fpos(self): # We will test that the mapping of an 10% included tax by a 0% by a fiscal position is taken into account when updating the cart @@ -174,18 +174,14 @@ class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon): 'partner_id': self.env.user.partner_id.id, }) sol = self.env['sale.order.line'].create({ - 'name': test_product.name, 'product_id': test_product.product_variant_id.id, - 'product_uom_qty': 1, - 'product_uom': test_product.uom_id.id, - 'price_unit': test_product.list_price, 'order_id': so.id, 'tax_id': [(6, 0, [tax10.id])], }) self.assertEqual(round(sol.price_total), 110.0, "110$ with 10% included tax") so.pricelist_id = pricelist so.fiscal_position_id = fpos - sol.product_id_change() + sol._compute_tax_id() with MockRequest(self.env, website=current_website, sale_order_id=so.id): so._cart_update(product_id=test_product.product_variant_id.id, line_id=sol.id, set_qty=1) self.assertEqual(round(sol.price_total), 50, "100$ with 50% discount + 0% tax (mapped from fp 10% -> 0%)") diff --git a/addons/website_sale_delivery_mondialrelay/controllers/controllers.py b/addons/website_sale_delivery_mondialrelay/controllers/controllers.py index 6b495769d9f..5f2178f7de4 100644 --- a/addons/website_sale_delivery_mondialrelay/controllers/controllers.py +++ b/addons/website_sale_delivery_mondialrelay/controllers/controllers.py @@ -31,7 +31,6 @@ class MondialRelay(http.Controller): }) if order.partner_shipping_id != partner_shipping: order.partner_shipping_id = partner_shipping - order.onchange_partner_shipping_id() return { 'address': request.env['ir.qweb']._render('website_sale.address_on_payment', { diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index ea52c6a91cf..83c0dd6a798 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -224,9 +224,10 @@ class Partner(models.Model): contact_address = fields.Char(compute='_compute_contact_address', string='Complete Address') # technical field used for managing commercial fields - commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', - compute='_compute_commercial_partner', recursive=True, - store=True, index=True) + commercial_partner_id = fields.Many2one( + 'res.partner', string='Commercial Entity', + compute='_compute_commercial_partner', store=True, + recursive=True, index=True) commercial_company_name = fields.Char('Company Name Entity', compute='_compute_commercial_company_name', store=True) company_name = fields.Char('Company Name') diff --git a/odoo/addons/base/tests/common.py b/odoo/addons/base/tests/common.py index 0fd845209db..7bd8a2d5a75 100644 --- a/odoo/addons/base/tests/common.py +++ b/odoo/addons/base/tests/common.py @@ -8,6 +8,27 @@ from odoo.tests.common import TransactionCase, HttpCase from odoo import Command +class PerformanceCommon(TransactionCase): + + def assertModelCreateMulti(self, model_name, vals_list=None): + """Ensures model_name records can be created in batch. + + :param str model_name: name of the model on which batch record creation should be verified + :param list vals_list: example list of values, if needed + """ + vals_list = vals_list or [dict(), dict()] + if not isinstance(vals_list, list): + raise ValueError("Invalid arguments for assertModelCreateMulti") + assert len(vals_list) > 1, "Batch creation cannot be tested with only one value." + def patched_create(self, values_list): + if len(values_list) != len(vals_list): + raise ValueError(f"Model {model_name} doesn't support batch records creation.") + # Do not create anything + return self + with patch('odoo.models.BaseModel.create', patched_create): + self.env[model_name].create(vals_list) + + class TransactionCaseWithUserDemo(TransactionCase): def setUp(self):