From 7b82fc1fe1047439ff42f3285384ab97cfb57323 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Fri, 20 Mar 2020 18:19:34 +0100 Subject: [PATCH 01/65] [IMP] sale_stock: Add query counter test on SO batch creation This is be a correct metric to ensure the onchanges convertion into compute methods using precompute attribute is going in the right direction. --- addons/sale_stock/tests/__init__.py | 1 + addons/sale_stock/tests/test_create_perf.py | 56 +++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 addons/sale_stock/tests/test_create_perf.py diff --git a/addons/sale_stock/tests/__init__.py b/addons/sale_stock/tests/__init__.py index 3dde82064a6..5c84998bb9b 100644 --- a/addons/sale_stock/tests/__init__.py +++ b/addons/sale_stock/tests/__init__.py @@ -10,3 +10,4 @@ from . import test_sale_order_dates from . import test_sale_stock_multicompany from . import test_sale_stock_accrued_entries from . import test_sale_stock_access_rights +from . import test_create_perf diff --git a/addons/sale_stock/tests/test_create_perf.py b/addons/sale_stock/tests/test_create_perf.py new file mode 100644 index 00000000000..962cc585c53 --- /dev/null +++ b/addons/sale_stock/tests/test_create_perf.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import time + +from odoo.tests import common, tagged +from odoo.tests.common import users, warmup + +_logger = logging.getLogger(__name__) + + +@tagged('so_batch_perf') +class TestPERF(common.TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ENTITIES = 50 + + cls.products = cls.env['product.product'].create([{ + 'name': 'Product %s' % i, + 'list_price': 1 + 10 * i, + 'type': 'service', + } for i in range(10)]) + + cls.partners = cls.env['res.partner'].create([{ + 'name': 'Partner %s' % i, + } for i in range(cls.ENTITIES)]) + + cls.salesmans = cls.env.ref('base.user_admin') | cls.env.ref('base.user_demo') + + cls.env['base'].flush() + + @users('admin') + @warmup + def test_sales_orders_batch_creation_perf(self): + MSG = "Model %s, %i records, %s, time %.2f" + + vals_list = [{ + "partner_id": self.partners[i].id, + "user_id": self.salesmans[i % 2].id, + "order_line": [ + (0, 0, {"display_type": "line_note", "name": "NOTE"}) + ] + [ + (0, 0, {'product_id': product.id}) for product in self.products + ], + } for i in range(self.ENTITIES)] + + with self.assertQueryCount(admin=2753): + t0 = time.time() + self.env["sale.order"].create(vals_list) + t1 = time.time() + _logger.info(MSG, 'sale.order', self.ENTITIES, "BATCH", t1 - t0) + self.env.cr.flush() + _logger.info(MSG, 'sale.order', self.ENTITIES, "FLUSH", time.time() - t1) From ebd719e0fa90e86b2b873929163d944e2f3fda34 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 27 Oct 2021 15:43:08 +0200 Subject: [PATCH 02/65] [IMP] website_sale: Convert _onchange_sale_delivery_settings into compute method --- .../models/res_config_settings.py | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/addons/website_sale/models/res_config_settings.py b/addons/website_sale/models/res_config_settings.py index 816d74984d7..22c9e49d8e8 100644 --- a/addons/website_sale/models/res_config_settings.py +++ b/addons/website_sale/models/res_config_settings.py @@ -33,6 +33,11 @@ class ResConfigSettings(models.TransientModel): cart_add_on_page = fields.Boolean("Stay on page after adding to cart", related='website_id.cart_add_on_page', readonly=False) terms_url = fields.Char(compute='_compute_terms_url', string="URL", help="A preview will be available at this URL.") + module_delivery = fields.Boolean( + compute='_compute_module_delivery', store=True, readonly=False) + module_website_sale_delivery = fields.Boolean( + compute='_compute_module_delivery', store=True, readonly=False) + @api.depends('website_id') def _compute_terms_url(self): for record in self: @@ -53,23 +58,11 @@ class ResConfigSettings(models.TransientModel): ) return res - @api.onchange('sale_delivery_settings') - def _onchange_sale_delivery_settings(self): - if self.sale_delivery_settings == 'none': - self.update({ - 'module_delivery': False, - 'module_website_sale_delivery': False, - }) - elif self.sale_delivery_settings == 'internal': - self.update({ - 'module_delivery': True, - 'module_website_sale_delivery': False, - }) - else: - self.update({ - 'module_delivery': True, - 'module_website_sale_delivery': True, - }) + @api.depends('sale_delivery_settings') + def _compute_module_delivery(self): + for wizard in self: + wizard.module_delivery = wizard.sale_delivery_settings in ['internal', 'website'] + wizard.module_website_sale_delivery = wizard.sale_delivery_settings == 'website' @api.onchange('group_discount_per_so_line') def _onchange_group_discount_per_so_line(self): From 40b2bc6f6541571e23c57d1faa955a9b3cc2e717 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 27 Oct 2021 15:47:04 +0200 Subject: [PATCH 03/65] [IMP] website_sale: Convert _onchange_group_discount_per_so_line into compute method --- addons/website_sale/models/res_config_settings.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/addons/website_sale/models/res_config_settings.py b/addons/website_sale/models/res_config_settings.py index 22c9e49d8e8..1963ca921f7 100644 --- a/addons/website_sale/models/res_config_settings.py +++ b/addons/website_sale/models/res_config_settings.py @@ -37,6 +37,8 @@ class ResConfigSettings(models.TransientModel): compute='_compute_module_delivery', store=True, readonly=False) module_website_sale_delivery = fields.Boolean( compute='_compute_module_delivery', store=True, readonly=False) + group_product_pricelist = fields.Boolean( + compute='_compute_group_product_pricelist', store=True, readonly=False) @api.depends('website_id') def _compute_terms_url(self): @@ -64,12 +66,11 @@ class ResConfigSettings(models.TransientModel): wizard.module_delivery = wizard.sale_delivery_settings in ['internal', 'website'] wizard.module_website_sale_delivery = wizard.sale_delivery_settings == 'website' - @api.onchange('group_discount_per_so_line') - def _onchange_group_discount_per_so_line(self): - if self.group_discount_per_so_line: - self.update({ - 'group_product_pricelist': True, - }) + @api.depends('group_discount_per_so_line') + def _compute_group_product_pricelist(self): + self.filtered(lambda w: w.group_discount_per_so_line).update({ + 'group_product_pricelist': True, + }) def action_update_terms(self): self.ensure_one() From 550eb0b73c521ca0c19a31967dc3aa6a5c2516ea Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 27 Oct 2021 15:53:02 +0200 Subject: [PATCH 04/65] [IMP] account: Convert _onchange_sale_tax into compute method --- addons/account/models/res_config_settings.py | 23 ++++++++------------ addons/account/tests/test_settings.py | 3 --- addons/website_sale/tests/test_customize.py | 1 - 3 files changed, 9 insertions(+), 18 deletions(-) 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_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/website_sale/tests/test_customize.py b/addons/website_sale/tests/test_customize.py index f15086100c1..b29b9fc9e6e 100644 --- a/addons/website_sale/tests/test_customize.py +++ b/addons/website_sale/tests/test_customize.py @@ -308,7 +308,6 @@ class TestUi(HttpCaseWithUserDemo, HttpCaseWithUserPortal): # activate b2c config = self.env['res.config.settings'].create({}) config.show_line_subtotals_tax_selection = "tax_included" - config._onchange_sale_tax() config.execute() self.start_tour("/", 'shop_list_view_b2c', login="admin") From b9194406ecc8cf99d9747c2164838f3e092e0ec4 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 27 Oct 2021 16:42:38 +0200 Subject: [PATCH 05/65] [IMP] account: Avoid multiple rebrowse in get_fiscal_position + Make it private, as it is not supposed to be called from the webclient. --- addons/account/models/account_move.py | 6 +-- .../account/models/account_reconcile_model.py | 2 +- addons/account/models/partner.py | 17 +++------ addons/account/tests/test_fiscal_position.py | 2 +- .../l10n_ar/models/account_fiscal_position.py | 38 +++++++++---------- addons/mrp_repair/models/repair.py | 2 +- addons/purchase/models/purchase.py | 10 ++--- .../purchase_requisition/models/purchase.py | 2 +- addons/purchase_stock/models/stock_rule.py | 2 +- addons/repair/models/repair.py | 6 +-- addons/sale/models/account_move.py | 6 +-- addons/sale/models/account_move_line.py | 2 +- addons/sale/models/sale_order.py | 2 +- addons/sale/models/sale_order_line.py | 2 +- .../sale/wizard/sale_make_invoice_advance.py | 2 +- addons/sale_coupon/models/sale_order.py | 2 +- addons/sale_purchase/models/sale_order.py | 2 +- .../website_sale/models/product_template.py | 2 +- addons/website_sale/models/website.py | 2 +- 19 files changed, 50 insertions(+), 59 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f0372c241f6..05e1b70f524 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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/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/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/purchase/models/purchase.py b/addons/purchase/models/purchase.py index bfd1e3ed51d..6641a460fe3 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, + '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..90db9859f7b 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -33,9 +33,9 @@ class AccountMove(models.Model): """ 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) + delivery_partner_id = self.env['res.partner'].browse(self._get_invoice_delivery_partner_id()) + fiscal_position = self.env['account.fiscal.position'].with_company(self.company_id)._get_fiscal_position( + self.partner_id, delivery=delivery_partner_id) if fiscal_position: self.fiscal_position_id = fiscal_position diff --git a/addons/sale/models/account_move_line.py b/addons/sale/models/account_move_line.py index 9b8d08746d3..6315c6591da 100644 --- a/addons/sale/models/account_move_line.py +++ b/addons/sale/models/account_move_line.py @@ -158,7 +158,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/sale_order.py b/addons/sale/models/sale_order.py index fac1701aeba..07cacdc6464 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -624,7 +624,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, diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 6e3e02ac7ca..78604ab87ca 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -137,7 +137,7 @@ class SaleOrderLine(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_partner_id.id) + fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_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) diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index cb70230be49..590a1080f21 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -78,7 +78,7 @@ class SaleAdvancePaymentInv(models.TransientModel): 'invoice_user_id': order.user_id.id, 'narration': order.note, 'partner_id': order.partner_invoice_id.id, - 'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id.get_fiscal_position(order.partner_id.id)).id, + 'fiscal_position_id': (order.fiscal_position_id or order.fiscal_position_id._get_fiscal_position(order.partner_id)).id, 'partner_shipping_id': order.partner_shipping_id.id, 'currency_id': order.pricelist_id.currency_id.id, 'payment_reference': order.reference, diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py index 5a7339a32a8..c2f54dcbd6c 100644 --- a/addons/sale_coupon/models/sale_order.py +++ b/addons/sale_coupon/models/sale_order.py @@ -509,7 +509,7 @@ class SaleOrderLine(models.Model): # In case of a free product, retrieving the tax on the line instead of the product won't affect the behavior. for line in reward_lines: 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) + fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_id) # If company_id is set, always filter taxes by the company taxes = line.tax_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id) line.tax_id = fpos.map_tax(taxes) diff --git a/addons/sale_purchase/models/sale_order.py b/addons/sale_purchase/models/sale_order.py index 9515721e998..004295edfea 100644 --- a/addons/sale_purchase/models/sale_order.py +++ b/addons/sale_purchase/models/sale_order.py @@ -193,7 +193,7 @@ class SaleOrderLine(models.Model): """ self.ensure_one() partner_supplier = supplierinfo.partner_id - fpos = self.env['account.fiscal.position'].sudo().get_fiscal_position(partner_supplier.id) + fpos = self.env['account.fiscal.position'].sudo()._get_fiscal_position(partner_supplier) date_order = self._purchase_get_date_order(supplierinfo) return { 'partner_id': partner_supplier.id, diff --git a/addons/website_sale/models/product_template.py b/addons/website_sale/models/product_template.py index a50e6fc25a1..d00d3bbb990 100644 --- a/addons/website_sale/models/product_template.py +++ b/addons/website_sale/models/product_template.py @@ -183,7 +183,7 @@ class ProductTemplate(models.Model): company_id = current_website.company_id tax_display = self.user_has_groups('account.group_show_line_subtotals_tax_excluded') and 'total_excluded' or 'total_included' - fpos = self.env['account.fiscal.position'].sudo().get_fiscal_position(partner.id) + fpos = self.env['account.fiscal.position'].sudo()._get_fiscal_position(partner) product_taxes = product.sudo().taxes_id.filtered(lambda x: x.company_id == company_id) taxes = fpos.map_tax(product_taxes) diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 4067e9db146..6e1498ff934 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -277,7 +277,7 @@ class Website(models.Model): fpos_id = ( self.env['account.fiscal.position'].sudo() .with_company(sale_order.company_id.id) - .get_fiscal_position(sale_order.partner_id.id, delivery_id=sale_order.partner_shipping_id.id) + ._get_fiscal_position(sale_order.partner_id, delivery=sale_order.partner_shipping_id) ).id if sale_order.fiscal_position_id.id != fpos_id: sale_order = None From f0f30828fc2786a2f13bc85ef322543e79c438ae Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 27 Oct 2021 16:43:29 +0200 Subject: [PATCH 06/65] [IMP] sale: Convert onchange_partner_shipping_id into compute method --- .../wizard/choose_delivery_carrier.py | 1 - addons/sale/models/sale_order.py | 15 +++++++++++---- .../wizard/project_create_sale_order.py | 1 - addons/website_sale/controllers/main.py | 1 - addons/website_sale/models/website.py | 8 +------- .../controllers/controllers.py | 1 - 6 files changed, 12 insertions(+), 15 deletions(-) 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/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 07cacdc6464..dad902d7cb9 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -242,6 +242,7 @@ class SaleOrder(models.Model): 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, 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( @@ -398,13 +399,19 @@ 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: + key = (order.company_id, order.partner_id, order.partner_shipping_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): diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index 82f36387ea5..7459f4cfd19 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -118,7 +118,6 @@ class ProjectCreateSalesOrder(models.TransientModel): 'company_id': self.project_id.company_id.id, }) sale_order.onchange_partner_id() - sale_order.onchange_partner_shipping_id() # rewrite the user as the onchange_partner_id erases it sale_order.write({'user_id': self.project_id.user_id.id}) sale_order.onchange_user_id() diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index e75441b12b1..507ac2552b1 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -925,7 +925,6 @@ class WebsiteSale(http.Controller): if redirection: return redirection - order.onchange_partner_shipping_id() order.order_line._compute_tax_id() request.session['sale_last_order_id'] = order.id request.website.sale_get_order(update_pricelist=True) diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 6e1498ff934..8d2cf5a4f87 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -305,16 +305,11 @@ class Website(models.Model): sale_order = self.env['sale.order'].with_company(request.website.company_id.id).with_user(SUPERUSER_ID).create(so_data) # set fiscal position - if request.website.partner_id.id != partner.id: - sale_order.onchange_partner_shipping_id() - else: # For public user, fiscal position based on geolocation + if request.website.partner_id.id == partner.id: # For public user, fiscal position based on geolocation country_code = request.session['geoip'].get('country_code') if country_code: country_id = request.env['res.country'].search([('code', '=', country_code)], limit=1).id sale_order.fiscal_position_id = request.env['account.fiscal.position'].sudo().with_company(request.website.company_id.id)._get_fpos_by_region(country_id) - else: - # if no geolocation, use the public user fp - sale_order.onchange_partner_shipping_id() request.session['sale_order_id'] = sale_order.id @@ -336,7 +331,6 @@ class Website(models.Model): sale_order.write({'partner_id': partner.id}) sale_order.with_context(not_self_saleperson=True).onchange_partner_id() sale_order.write({'partner_invoice_id': partner.id}) - sale_order.onchange_partner_shipping_id() # fiscal position sale_order['payment_term_id'] = self.sale_get_payment_term(partner) # check the pricelist : update it if the pricelist is not the 'forced' one 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', { From 2806c5fd9f00128b24e49d6a84a9dd03abff489a Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Thu, 28 Oct 2021 14:23:35 +0200 Subject: [PATCH 07/65] [IMP] sale: Convert onchange_partner_id into compute method --- addons/sale/models/sale_order.py | 88 +++++++++++-------- addons/sale/tests/test_reinvoice.py | 2 - addons/sale/tests/test_sale_flow.py | 1 - addons/sale/tests/test_sale_order.py | 2 - addons/sale_management/models/sale_order.py | 11 +-- addons/sale_timesheet/tests/test_reinvoice.py | 1 - .../wizard/project_create_sale_order.py | 1 - addons/website_sale/controllers/main.py | 3 +- addons/website_sale/models/website.py | 3 +- 9 files changed, 57 insertions(+), 55 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index dad902d7cb9..588f26c23e1 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -187,6 +187,7 @@ class SaleOrder(models.Model): user_id = fields.Many2one( 'res.users', string='Salesperson', index=True, tracking=2, default=lambda self: self.env.user, + compute='_compute_order_info_from_partner', store=True, readonly=False, 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, @@ -195,12 +196,14 @@ class SaleOrder(models.Model): 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)]}, + readonly=False, required=True, + states={'draft': [('done', True)], 'cancel': [('readonly', True)]}, + compute='_compute_order_info_from_partner', store=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', readonly=False, required=True, + states={'draft': [('done', True)], 'cancel': [('readonly', True)]}, + compute='_compute_order_info_from_partner', store=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) pricelist_id = fields.Many2one( @@ -227,7 +230,9 @@ 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', default=_default_note, + compute='_compute_order_info_from_partner', store=True, readonly=False) terms_type = fields.Selection(related='company_id.terms_type') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, compute='_amount_all', tracking=5) @@ -238,6 +243,7 @@ class SaleOrder(models.Model): payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Terms', check_company=True, # Unrequired company + compute='_compute_order_info_from_partner', store=True, readonly=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) fiscal_position_id = fields.Many2one( 'account.fiscal.position', string='Fiscal Position', @@ -253,6 +259,7 @@ class SaleOrder(models.Model): team_id = fields.Many2one( 'crm.team', 'Sales Team', ondelete="set null", tracking=True, + compute='_compute_order_info_from_partner', store=True, readonly=False, change_default=True, default=_get_default_team, check_company=True, # Unrequired company domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") @@ -413,8 +420,8 @@ class SaleOrder(models.Model): )._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): + @api.depends('partner_id') + def _compute_order_info_from_partner(self): """ Update the following fields when the partner is changed: - Pricelist @@ -422,42 +429,45 @@ class SaleOrder(models.Model): - Invoice address - Delivery address - Sales Team + - User + - Note """ - if not self.partner_id: - self.update({ - 'partner_invoice_id': False, - 'partner_shipping_id': False, - 'fiscal_position_id': False, - }) - return + for order in self: + if not order.partner_id: + order.update({ + 'partner_invoice_id': False, + 'partner_shipping_id': False, + 'fiscal_position_id': False, + }) + continue - self = self.with_company(self.company_id) + order = order.with_company(order.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 + addr = order.partner_id.address_get(['delivery', 'invoice']) + partner_user = order.partner_id.user_id or order.partner_id.commercial_partner_id.user_id + values = { + 'pricelist_id': order.partner_id.property_product_pricelist and order.partner_id.property_product_pricelist.id or False, + 'payment_term_id': order.partner_id.property_payment_term_id and order.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 order.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) - 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) + if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'): + if order.terms_type == 'html' and self.env.company.invoice_terms_html: + baseurl = html_keep_url(order.get_base_url() + '/terms') + values['note'] = _('Terms & Conditions: %s', baseurl) + elif not is_html_empty(self.env.company.invoice_terms): + values['note'] = order.with_context(lang=order.partner_id.lang).env.company.invoice_terms + if not self.env.context.get('not_self_saleperson') or not order.team_id: + values['team_id'] = self.env['crm.team'].with_context( + default_team_id=order.partner_id.team_id.id + )._get_default_team_id(domain=['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], user_id=user_id) + order.update(values) @api.onchange('user_id') def onchange_user_id(self): diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py index be414b81e8c..1a9fc279c16 100644 --- a/addons/sale/tests/test_reinvoice.py +++ b/addons/sale/tests/test_reinvoice.py @@ -58,7 +58,6 @@ class TestReInvoice(TestSaleCommon): }) sale_order_line2.product_id_change() - self.sale_order.onchange_partner_id() self.sale_order._compute_tax_id() self.sale_order.action_confirm() @@ -284,7 +283,6 @@ class TestReInvoice(TestSaleCommon): }) so_line2.product_id_change() - self.sale_order.onchange_partner_id() self.sale_order._compute_tax_id() self.sale_order.action_confirm() diff --git a/addons/sale/tests/test_sale_flow.py b/addons/sale/tests/test_sale_flow.py index c18d645f9b1..b5d10c67439 100644 --- a/addons/sale/tests/test_sale_flow.py +++ b/addons/sale/tests/test_sale_flow.py @@ -72,7 +72,6 @@ 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() diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 803c2f7a720..37d50eff4f7 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -512,7 +512,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 +524,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): diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 4bbc71c34de..5fa998ce4fa 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -50,11 +50,12 @@ class SaleOrder(models.Model): default['validity_date'] = fields.Date.context_today(self) + timedelta(self.sale_order_template_id.number_of_days) return super(SaleOrder, self).copy(default=default) - @api.onchange('partner_id') - def onchange_partner_id(self): - super(SaleOrder, self).onchange_partner_id() - template = self.sale_order_template_id.with_context(lang=self.partner_id.lang) - self.note = template.note if not is_html_empty(template.note) else self.note + @api.depends('partner_id') + def _compute_order_info_from_partner(self): + super(SaleOrder, self)._compute_order_info_from_partner() + for order in self: + template = order.sale_order_template_id.with_context(lang=order.partner_id.lang) + order.note = template.note if not is_html_empty(template.note) else order.note def _compute_line_data_for_template_change(self, line): return { diff --git a/addons/sale_timesheet/tests/test_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py index 42054d046e9..5b815f56f98 100644 --- a/addons/sale_timesheet/tests/test_reinvoice.py +++ b/addons/sale_timesheet/tests/test_reinvoice.py @@ -72,7 +72,6 @@ class TestReInvoice(TestCommonSaleTimesheet): }) sale_order_line2.product_id_change() - self.sale_order.onchange_partner_id() self.sale_order._compute_tax_id() self.sale_order.action_confirm() diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index 7459f4cfd19..f89f3999191 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -117,7 +117,6 @@ class ProjectCreateSalesOrder(models.TransientModel): 'client_order_ref': self.project_id.name, 'company_id': self.project_id.company_id.id, }) - sale_order.onchange_partner_id() # rewrite the user as the onchange_partner_id erases it sale_order.write({'user_id': self.project_id.user_id.id}) sale_order.onchange_user_id() diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 507ac2552b1..eecfcccd1c4 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -834,8 +834,7 @@ class WebsiteSale(http.Controller): else: partner_id = self._checkout_form_save(mode, post, kw) if mode[1] == 'billing': - order.partner_id = partner_id - order.with_context(not_self_saleperson=True).onchange_partner_id() + order.with_context(not_self_saleperson=True).partner_id = partner_id # This is the *only* thing that the front end user will see/edit anyway when choosing billing address order.partner_invoice_id = partner_id if not kw.get('use_same'): diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 8d2cf5a4f87..7a587aaad2f 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -328,8 +328,7 @@ class Website(models.Model): fiscal_position = sale_order.fiscal_position_id.id # change the partner, and trigger the onchange - sale_order.write({'partner_id': partner.id}) - sale_order.with_context(not_self_saleperson=True).onchange_partner_id() + sale_order.with_context(not_self_saleperson=True).write({'partner_id': partner.id}) sale_order.write({'partner_invoice_id': partner.id}) sale_order['payment_term_id'] = self.sale_get_payment_term(partner) From 5fea243e1a50fe2df7957b5f0d3b5b70146da56f Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Thu, 28 Oct 2021 14:26:36 +0200 Subject: [PATCH 08/65] [IMP] sale: Convert _onchange_pricelist_id into compute method --- addons/sale/models/sale_order.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 588f26c23e1..bdfef64c6e4 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -282,9 +282,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, + 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') @@ -513,12 +515,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() From c10db4492e01d4a506cb5ea1ac3067f0f1f4bb2d Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Thu, 28 Oct 2021 14:42:17 +0200 Subject: [PATCH 09/65] [IMP] sale: Convert _compute_tax_id into real compute method --- addons/pos_sale/models/pos_order.py | 1 - addons/sale/models/sale_order.py | 8 -------- addons/sale/models/sale_order_line.py | 8 +++++--- addons/sale/tests/test_reinvoice.py | 4 ---- addons/sale/tests/test_sale_flow.py | 1 - addons/sale_coupon/models/sale_order.py | 1 + addons/sale_coupon/tests/test_program_numbers.py | 2 -- addons/sale_expense/tests/test_sale_expense.py | 1 - addons/sale_management/models/sale_order.py | 2 -- addons/sale_timesheet/tests/test_reinvoice.py | 3 --- 10 files changed, 6 insertions(+), 25 deletions(-) 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/sale/models/sale_order.py b/addons/sale/models/sale_order.py index bdfef64c6e4..b448d4b34da 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -120,14 +120,6 @@ class SaleOrder(models.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(""" diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 78604ab87ca..0b21c11a03c 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -134,6 +134,7 @@ 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('order_id.fiscal_position_id') def _compute_tax_id(self): for line in self: line = line.with_company(line.company_id) @@ -242,7 +243,10 @@ class SaleOrderLine(models.Model): price_total = fields.Monetary(compute='_compute_amount', string='Total', store=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)]) + tax_id = fields.Many2many( + 'account.tax', string='Taxes', + compute='_compute_tax_id', store=True, readonly=False, + 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) @@ -626,8 +630,6 @@ class SaleOrderLine(models.Model): 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) diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py index 1a9fc279c16..11f68dd4e97 100644 --- a/addons/sale/tests/test_reinvoice.py +++ b/addons/sale/tests/test_reinvoice.py @@ -58,7 +58,6 @@ class TestReInvoice(TestSaleCommon): }) sale_order_line2.product_id_change() - self.sale_order._compute_tax_id() self.sale_order.action_confirm() # create invoice lines and validate it @@ -176,7 +175,6 @@ class TestReInvoice(TestSaleCommon): '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 @@ -244,7 +242,6 @@ class TestReInvoice(TestSaleCommon): '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 @@ -283,7 +280,6 @@ class TestReInvoice(TestSaleCommon): }) so_line2.product_id_change() - 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 b5d10c67439..8d07096001b 100644 --- a/addons/sale/tests/test_sale_flow.py +++ b/addons/sale/tests/test_sale_flow.py @@ -72,7 +72,6 @@ class TestSaleFlow(TestSaleCommonBase): for line in sale_order.order_line: line.product_id_change() - sale_order._compute_tax_id() sale_order.action_confirm() self.assertRecordValues(sale_order.order_line, [ diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py index c2f54dcbd6c..0ab92f0385a 100644 --- a/addons/sale_coupon/models/sale_order.py +++ b/addons/sale_coupon/models/sale_order.py @@ -501,6 +501,7 @@ class SaleOrderLine(models.Model): related_program_lines |= line.order_id.order_line.filtered(lambda l: l.product_id.id == related_program.discount_line_product_id.id) - line return super(SaleOrderLine, self | related_program_lines).unlink() + @api.depends('order_id.fiscal_position_id') def _compute_tax_id(self): reward_lines = self.filtered('is_reward_line') super(SaleOrderLine, self - reward_lines)._compute_tax_id() diff --git a/addons/sale_coupon/tests/test_program_numbers.py b/addons/sale_coupon/tests/test_program_numbers.py index 5323a03a58b..45628c631bf 100644 --- a/addons/sale_coupon/tests/test_program_numbers.py +++ b/addons/sale_coupon/tests/test_program_numbers.py @@ -188,7 +188,6 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon): order.recompute_coupon_lines() self.assertEqual(len(order.order_line.ids), 1, "We should not get the reduction line since we dont have 320$ tax excluded (cabinet is 320$ tax included)") sol1.tax_id.price_include = False - sol1._compute_tax_id() order.recompute_coupon_lines() self.assertEqual(len(order.order_line.ids), 2, "We should now get the reduction line since we have 320$ tax included (cabinet is 320$ tax included)") # Name | Qty | price_unit | Tax | HTVA | TVAC | TVA | @@ -390,7 +389,6 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon): self.assertEqual(len(order.order_line.ids), 12, "Order should contains 5 regular product lines, 3 free product lines and 4 discount lines (one for every tax)") # -- This is a test inside the test - order.order_line._compute_tax_id() self.assertEqual(order.amount_total, 1711, "Recomputing tax on sale order lines should not change total amount") self.assertEqual(order.amount_untaxed, 1435.46, "Recomputing tax on sale order lines should not change untaxed amount") self.assertEqual(len(order.order_line.ids), 12, "Recomputing tax on sale order lines should not change number of order line") diff --git a/addons/sale_expense/tests/test_sale_expense.py b/addons/sale_expense/tests/test_sale_expense.py index cb969636278..02f060a35b2 100644 --- a/addons/sale_expense/tests/test_sale_expense.py +++ b/addons/sale_expense/tests/test_sale_expense.py @@ -26,7 +26,6 @@ class TestSaleExpense(TestExpenseCommon, TestSaleCommon): })], 'pricelist_id': self.env.ref('product.list0').id, }) - so._compute_tax_id() so.action_confirm() so._create_analytic_account() # normally created at so confirmation when you use the right products init_price = so.amount_total diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 5fa998ce4fa..2b97877cf13 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -131,7 +131,6 @@ class SaleOrder(models.Model): order_lines.append((0, 0, data)) self.order_line = order_lines - self.order_line._compute_tax_id() # then, process the list of optional products from the template option_lines = [(5, 0, 0)] @@ -258,7 +257,6 @@ class SaleOrderOption(models.Model): values = self._get_values_to_add_to_order() order_line = self.env['sale.order.line'].create(values) - order_line._compute_tax_id() self.write({'line_id': order_line.id}) if sale_order: diff --git a/addons/sale_timesheet/tests/test_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py index 5b815f56f98..4171279dacb 100644 --- a/addons/sale_timesheet/tests/test_reinvoice.py +++ b/addons/sale_timesheet/tests/test_reinvoice.py @@ -72,7 +72,6 @@ class TestReInvoice(TestCommonSaleTimesheet): }) sale_order_line2.product_id_change() - self.sale_order._compute_tax_id() self.sale_order.action_confirm() self.assertEqual(sale_order_line1.qty_delivered_method, 'timesheet', "Delivered quantity of 'service' SO line should be computed by timesheet amount") @@ -174,7 +173,6 @@ class TestReInvoice(TestCommonSaleTimesheet): 'order_id': self.sale_order.id, }) sale_order_line2.product_id_change() - self.sale_order._compute_tax_id() self.sale_order.action_confirm() # let's log some timesheets (on the project created by sale_order_line1) @@ -259,7 +257,6 @@ class TestReInvoice(TestCommonSaleTimesheet): 'price_unit': self.company_data['product_order_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 From ec84b7e7bde07154550312a2e79b6ecb3e988fea Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Fri, 29 Oct 2021 15:10:10 +0200 Subject: [PATCH 10/65] [IMP] sale: Add pre_compute=True on necessary fields --- addons/sale/models/sale_order.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index b448d4b34da..d79b536d333 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -182,25 +182,26 @@ class SaleOrder(models.Model): compute='_compute_order_info_from_partner', store=True, readonly=False, 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, + 'res.partner', string='Customer', readonly=True, pre_compute=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, 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=False, required=True, + readonly=False, required=True, pre_compute=True, states={'draft': [('done', True)], 'cancel': [('readonly', True)]}, compute='_compute_order_info_from_partner', store=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) partner_shipping_id = fields.Many2one( 'res.partner', string='Delivery Address', readonly=False, required=True, states={'draft': [('done', True)], 'cancel': [('readonly', True)]}, - compute='_compute_order_info_from_partner', store=True, + compute='_compute_order_info_from_partner', store=True, pre_compute=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)]}, + required=True, readonly=True, pre_compute=True, + states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, 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") From f503e69d3bc56bfad1d9405ee0b76dbd975c4872 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Fri, 29 Oct 2021 15:45:40 +0200 Subject: [PATCH 11/65] [IMP] sale: Write create methods in api_create_multi mode --- addons/sale/models/sale_order.py | 40 ++++++++++--------- .../models/sale_order.py | 8 ++-- .../models/sale_order_template.py | 8 ++-- addons/sale_stock/models/sale_order.py | 14 ++++--- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index d79b536d333..5583e54dc5d 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -7,6 +7,7 @@ from itertools import groupby import json from odoo import api, fields, models, SUPERUSER_ID, _ +from odoo.fields import Datetime from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.misc import formatLang from odoo.osv import expression @@ -336,7 +337,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 = Datetime.to_string(min(dates_list)) else: order.expected_date = False @@ -535,25 +536,26 @@ 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 = Datetime.context_timestamp( + self, 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 + # 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) + return super(SaleOrder, self).create(vals_list) def _compute_field_value(self, field): if field.name == 'invoice_status' and not self.env.context.get('mail_activity_automation_skip'): diff --git a/addons/sale_quotation_builder/models/sale_order.py b/addons/sale_quotation_builder/models/sale_order.py index 22c88388fc5..a4c391ccdd0 100644 --- a/addons/sale_quotation_builder/models/sale_order.py +++ b/addons/sale_quotation_builder/models/sale_order.py @@ -42,10 +42,10 @@ class SaleOrderLine(models.Model): website_description = fields.Html('Website Description', sanitize=False, translate=html_translate, sanitize_form=False) - @api.model - def create(self, values): - values = self._inject_quotation_description(values) - return super(SaleOrderLine, self).create(values) + @api.model_create_multi + def create(self, vals_list): + vals_list = [self._inject_quotation_description(vals) for vals in vals_list] + return super(SaleOrderLine, self).create(vals_list) def write(self, values): values = self._inject_quotation_description(values) diff --git a/addons/sale_quotation_builder/models/sale_order_template.py b/addons/sale_quotation_builder/models/sale_order_template.py index c7fe6a08f6a..5c4b8d86111 100644 --- a/addons/sale_quotation_builder/models/sale_order_template.py +++ b/addons/sale_quotation_builder/models/sale_order_template.py @@ -31,10 +31,10 @@ class SaleOrderTemplateLine(models.Model): self.website_description = self.product_id.quotation_description return ret - @api.model - def create(self, values): - values = self._inject_quotation_description(values) - return super(SaleOrderTemplateLine, self).create(values) + @api.model_create_multi + def create(self, vals_list): + vals_list = [self._inject_quotation_description(vals) for vals in vals_list] + return super(SaleOrderTemplateLine, self).create(vals_list) def write(self, values): values = self._inject_quotation_description(values) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index dc31e57b068..6d98f0cd958 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -88,12 +88,14 @@ class SaleOrder(models.Model): expected_date = min(dates_list) if order.picking_policy == 'direct' else max(dates_list) order.expected_date = fields.Datetime.to_string(expected_date) - @api.model - def create(self, vals): - if 'warehouse_id' not in vals and 'company_id' in vals: - user = self.env['res.users'].browse(vals.get('user_id', False)) - vals['warehouse_id'] = user.with_company(vals.get('company_id'))._get_default_warehouse_id().id - return super().create(vals) + @api.model_create_multi + def create(self, vals_list): + # YTI TODO: Achieve this with a compute method + for vals in vals_list: + if 'warehouse_id' not in vals and 'company_id' in vals: + user = self.env['res.users'].browse(vals.get('user_id', False)) + vals['warehouse_id'] = user.with_company(vals.get('company_id'))._get_default_warehouse_id().id + return super().create(vals_list) def write(self, values): if values.get('order_line') and self.state == 'sale': From f9f68adb2cdfdfd16bc24c82acae015ddfb9b83a Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Fri, 29 Oct 2021 16:09:16 +0200 Subject: [PATCH 12/65] [IMP] sale: Convert onchange_user_id into a compute method --- addons/purchase/models/purchase.py | 2 +- addons/sale/models/sale_order.py | 55 +++++++++---------- addons/sale/tests/test_sale_order.py | 2 - addons/sale_stock/models/sale_order.py | 37 ++++++------- .../wizard/project_create_sale_order.py | 1 - 5 files changed, 43 insertions(+), 54 deletions(-) diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 6641a460fe3..9151b6e810e 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -579,7 +579,7 @@ class PurchaseOrder(models.Model): '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, + '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, diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 5583e54dc5d..cf392896f71 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -117,10 +117,6 @@ class SaleOrder(models.Model): 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() - def _search_invoice_ids(self, operator, value): if operator == 'in' and value: self.env.cr.execute(""" @@ -179,30 +175,31 @@ 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, - compute='_compute_order_info_from_partner', store=True, readonly=False, + 'res.users', string='Salesperson', index=True, tracking=2, + compute='_compute_user_id', store=True, readonly=False, pre_compute=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, pre_compute=True, - states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, + 'res.partner', string='Customer', readonly=False, pre_compute=True, + 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=False, required=True, pre_compute=True, - states={'draft': [('done', True)], 'cancel': [('readonly', True)]}, + states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, compute='_compute_order_info_from_partner', store=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) partner_shipping_id = fields.Many2one( 'res.partner', string='Delivery Address', readonly=False, required=True, - states={'draft': [('done', True)], 'cancel': [('readonly', True)]}, + states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, compute='_compute_order_info_from_partner', store=True, pre_compute=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, pre_compute=True, - states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, + required=False, readonly=False, store=True, + compute='_compute_order_info_from_partner', pre_compute=True, + 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") @@ -253,8 +250,8 @@ class SaleOrder(models.Model): team_id = fields.Many2one( 'crm.team', 'Sales Team', ondelete="set null", tracking=True, - compute='_compute_order_info_from_partner', store=True, readonly=False, - change_default=True, default=_get_default_team, check_company=True, # Unrequired company + compute='_compute_team_id', store=True, readonly=False, pre_compute=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) @@ -440,18 +437,12 @@ class SaleOrder(models.Model): order = order.with_company(order.company_id) addr = order.partner_id.address_get(['delivery', 'invoice']) - partner_user = order.partner_id.user_id or order.partner_id.commercial_partner_id.user_id values = { 'pricelist_id': order.partner_id.property_product_pricelist and order.partner_id.property_product_pricelist.id or False, 'payment_term_id': order.partner_id.property_payment_term_id and order.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 order.user_id.id != user_id: - values['user_id'] = user_id if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'): if order.terms_type == 'html' and self.env.company.invoice_terms_html: @@ -459,18 +450,22 @@ class SaleOrder(models.Model): values['note'] = _('Terms & Conditions: %s', baseurl) elif not is_html_empty(self.env.company.invoice_terms): values['note'] = order.with_context(lang=order.partner_id.lang).env.company.invoice_terms - if not self.env.context.get('not_self_saleperson') or not order.team_id: - values['team_id'] = self.env['crm.team'].with_context( - default_team_id=order.partner_id.team_id.id - )._get_default_team_id(domain=['|', ('company_id', '=', self.company_id.id), ('company_id', '=', False)], user_id=user_id) order.update(values) - @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_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): + for order in self: + order.team_id = self.env['crm.team'].with_context( + default_team_id=(order.team_id or order.partner_id.team_id).id + )._get_default_team_id( + user_id=self.user_id.id, + domain=[('company_id', 'in', [self.company_id.id, False])] + ) @api.onchange('partner_id') def _onchange_partner_id_warning(self): diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 37d50eff4f7..a944d96ebea 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -534,7 +534,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): @@ -544,7 +543,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): diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 6d98f0cd958..3c4343a87c6 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -17,12 +17,6 @@ _logger = logging.getLogger(__name__) class SaleOrder(models.Model): _inherit = "sale.order" - @api.model - def _default_warehouse_id(self): - # !!! Any change to the default value may have to be repercuted - # on _init_column() below. - return self.env.user._get_default_warehouse_id() - incoterm = fields.Many2one( 'account.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") @@ -34,9 +28,10 @@ class SaleOrder(models.Model): ,help="If you deliver all products at once, the delivery order will be scheduled based on the greatest " "product lead time. Otherwise, it will be based on the shortest.") warehouse_id = fields.Many2one( - 'stock.warehouse', string='Warehouse', - required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, - default=_default_warehouse_id, check_company=True) + 'stock.warehouse', string='Warehouse', required=True, + compute='_compute_warehouse_id', store=True, readonly=False, pre_compute=True, + states={'sale': [('readonly', True)], 'done': [('readonly', False)], 'cancel': [('readonly', False)]}, + check_company=True) picking_ids = fields.One2many('stock.picking', 'sale_id', string='Transfers') delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) @@ -155,17 +150,19 @@ class SaleOrder(models.Model): for order in self: order.delivery_count = len(order.picking_ids) - @api.onchange('company_id') - def _onchange_company_id(self): - if self.company_id: - warehouse_id = self.env['ir.default'].get_model_defaults('sale.order').get('warehouse_id') - self.warehouse_id = warehouse_id or self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id - - @api.onchange('user_id') - def onchange_user_id(self): - super().onchange_user_id() - if self.state in ['draft', 'sent']: - self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id + @api.depends('user_id', 'company_id') + def _compute_warehouse_id(self): + default_warehouse_id = self.env['ir.default'].get_model_defaults('sale.order').get('warehouse_id') + for order in self: + if order.company_id and order.company_id != order._origin.company_id: + warehouse = default_warehouse_id + else: + warehouse = self.env['stock.warehouse'] + if order.state in ['draft', 'sent']: + order.warehouse_id = warehouse or order.user_id.with_company(order.company_id.id)._get_default_warehouse_id() + # In case we create a record in another state (eg: demo data, or business code) + if not order.warehouse_id: + order.warehouse_id = self.env.user._get_default_warehouse_id() @api.onchange('partner_shipping_id') def _onchange_partner_shipping_id(self): diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index f89f3999191..ca368aeaf6a 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -119,7 +119,6 @@ class ProjectCreateSalesOrder(models.TransientModel): }) # rewrite the user as the onchange_partner_id erases it sale_order.write({'user_id': self.project_id.user_id.id}) - sale_order.onchange_user_id() # create the sale lines, the map (optional), and assign existing timesheet to sale lines self._make_billable(sale_order) From 6c198eb31fd75af57af75a5083ae8aa11cca44ce Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 12:21:22 +0100 Subject: [PATCH 13/65] [IMP] sale: Convert onchange_user_id on account.move into compute method --- addons/sale/models/account_move.py | 13 +++++++++---- addons/sale_quotation_builder/models/sale_order.py | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py index 90db9859f7b..54ca3113233 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -14,6 +14,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( @@ -58,10 +59,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_quotation_builder/models/sale_order.py b/addons/sale_quotation_builder/models/sale_order.py index a4c391ccdd0..fb81d21650c 100644 --- a/addons/sale_quotation_builder/models/sale_order.py +++ b/addons/sale_quotation_builder/models/sale_order.py @@ -10,6 +10,7 @@ class SaleOrder(models.Model): website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate, sanitize_form=False) + # YTI TODO: This should be converted @api.onchange('partner_id') def onchange_update_description_lang(self): if not self.sale_order_template_id: From d626f348fead0b959b1036ac5d6d81881d90fcfe Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 12:38:34 +0100 Subject: [PATCH 14/65] [FIX] sale: Fix bad float comparison in tests --- addons/sale/tests/test_sale_order.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index a944d96ebea..ff0627180a9 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -294,9 +294,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.""" From a88e7c8717248a3c2117b81fb8a3b3767297d93f Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 12:43:16 +0100 Subject: [PATCH 15/65] [IMP] sale: Add comment about onchange convertion issues --- addons/sale/models/sale_order.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index cf392896f71..42275f3b6ea 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -197,7 +197,7 @@ class SaleOrder(models.Model): pricelist_id = fields.Many2one( 'product.pricelist', string='Pricelist', check_company=True, # Unrequired company - required=False, readonly=False, store=True, + required=False, readonly=False, store=True, compute='_compute_order_info_from_partner', pre_compute=True, states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=1, @@ -258,11 +258,12 @@ class SaleOrder(models.Model): 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) @@ -351,6 +352,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 introduce + # 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 From 6a4f437c9903758cd5b26c39b0eb920bccb44ee3 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 12:45:54 +0100 Subject: [PATCH 16/65] [IMP] sale: Convert _onchange_product_id_set_customer_lead into compute method --- addons/sale/models/sale_order_line.py | 3 --- addons/sale_product_matrix/models/sale_order.py | 1 - addons/sale_stock/models/sale_order.py | 9 ++++++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 0b21c11a03c..5eed45c08b9 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -747,9 +747,6 @@ 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 diff --git a/addons/sale_product_matrix/models/sale_order.py b/addons/sale_product_matrix/models/sale_order.py index 8c31ee77a53..c43c87737f8 100644 --- a/addons/sale_product_matrix/models/sale_order.py +++ b/addons/sale_product_matrix/models/sale_order.py @@ -125,7 +125,6 @@ class SaleOrder(models.Model): for line in self.order_line.filtered(lambda line: line.product_id.id in product_ids): res = line.product_id_change() or res line._onchange_discount() - line._onchange_product_id_set_customer_lead() return res def _get_matrix(self, product_template): diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 3c4343a87c6..7586e9a25d7 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -280,6 +280,8 @@ class SaleOrderLine(models.Model): qty_to_deliver = fields.Float(compute='_compute_qty_to_deliver', digits='Product Unit of Measure') is_mto = fields.Boolean(compute='_compute_is_mto') display_qty_widget = fields.Boolean(compute='_compute_qty_to_deliver') + customer_lead = fields.Float( + compute='_compute_customer_lead', store=True, readonly=False, pre_compute=True) @api.depends('product_type', 'product_uom_qty', 'qty_delivered', 'state', 'move_ids', 'product_uom') def _compute_qty_to_deliver(self): @@ -470,9 +472,10 @@ class SaleOrderLine(models.Model): else: line.product_updatable = False - @api.onchange('product_id') - def _onchange_product_id_set_customer_lead(self): - self.customer_lead = self.product_id.sale_delay + @api.depends('product_id') + def _compute_customer_lead(self): + for order in self: + order.customer_lead = order.product_id.sale_delay def _prepare_procurement_values(self, group_id=False): """ Prepare specific key for moves or other components that will be created from a stock rule From 836f463c9778195a00bcf2f5290ddef18d8e75d5 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 12:53:30 +0100 Subject: [PATCH 17/65] [IMP] sale: Convert product_uom_change into compute method --- addons/event_sale/models/sale_order.py | 7 ++--- addons/sale/models/sale_order_line.py | 38 ++++++++++++++------------ addons/sale/tests/test_onchange.py | 3 -- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 7e12b878747..201ea8824f3 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -123,10 +123,9 @@ class SaleOrderLine(models.Model): 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.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('product_uom', 'product_uom_qty') + def _compute_price_unit(self): + super(SaleOrderLine, self.filtered(lambda l: not l.event_ticket_id))._compute_price_unit() @api.onchange('event_ticket_id') def _onchange_event_ticket_id(self): diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 5eed45c08b9..926f7c22b7b 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -236,7 +236,9 @@ class SaleOrderLine(models.Model): ('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) + price_unit = fields.Float( + 'Unit Price', required=True, digits='Product Price', + compute='_compute_price_unit', store=True, readonly=False) price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) price_tax = fields.Float(compute='_compute_amount', string='Total Tax', store=True) @@ -645,22 +647,24 @@ 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_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_id.partner_id: + product = line.product_id.with_context( + lang=line.order_id.partner_id.lang, + partner=line.order_id.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) def name_get(self): result = [] diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py index d3322797b1b..aef1a6f1aee 100644 --- a/addons/sale/tests/test_onchange.py +++ b/addons/sale/tests/test_onchange.py @@ -158,12 +158,10 @@ class TestOnchangeProductId(TransactionCase): # 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%") @@ -275,7 +273,6 @@ class TestOnchangeProductId(TransactionCase): 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") def test_sale_warnings(self): From 687a4c97e3a1d8b0d6a169f36ebad1b5996671d7 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 12:59:25 +0100 Subject: [PATCH 18/65] [IMP] sale: Convert _inverse_qty_delivered into computed method --- addons/sale/models/sale_order_line.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 926f7c22b7b..5063a220983 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -280,8 +280,13 @@ class SaleOrderLine(models.Model): " - 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_delivered_manual = fields.Float( + 'Delivered Manually', copy=False, digits='Product Unit of Measure', + compute='_compute_qty_delivered_manual', store=True, readonly=False) qty_to_invoice = fields.Float( compute='_get_to_invoice_qty', string='To Invoice Quantity', store=True, digits='Product Unit of Measure') @@ -397,17 +402,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('qty_delivered', 'qty_delivered_method') + def _compute_qty_delivered_manual(self): + """ When writing on qty_delivered, if the value should be + modified 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 computed by other mecanisms. """ for line in self: - if line.qty_delivered_method == 'manual': - line.qty_delivered_manual = line.qty_delivered - else: - line.qty_delivered_manual = 0.0 + line.qty_delivered_manual = line.qty_delivered if line.qty_delivered_method == 'manual' else 0.0 @api.onchange('product_id', 'product_uom_qty', 'product_uom') def _onchange_suggest_packaging(self): From d80758697d25f08d806bb4c57a5f3a1c3ea7bbea Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 13:02:22 +0100 Subject: [PATCH 19/65] [IMP] sale: Convert _onchange_suggest_packaging into compute method --- addons/sale/models/sale_order_line.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 5063a220983..bbd3575c9ae 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -321,7 +321,11 @@ class SaleOrderLine(models.Model): ('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_id = fields.Many2one( + 'product.packaging', string='Packaging', + compute='_compute_product_packaging_id', store=True, readonly=False, + domain="[('sales', '=', True), ('product_id','=',product_id)]", + check_company=True) product_packaging_qty = fields.Float('Packaging Quantity') @api.depends('state') @@ -413,14 +417,16 @@ class SaleOrderLine(models.Model): for line in self: line.qty_delivered_manual = line.qty_delivered if line.qty_delivered_method == 'manual' else 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) + @api.depends('product_id', 'product_uom_qty', 'product_uom') + def _compute_product_packaging_id(self): + for line in self: + # remove packaging if not match the product + if line.product_packaging_id.product_id != line.product_id: + line.product_packaging_id = False + # suggest 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): From ccdd113260c1a3c8e225f6d726cfe4da36a4df92 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 13:04:13 +0100 Subject: [PATCH 20/65] [IMP] sale: Convert _onchange_update_product_packaging_qty into compute method --- addons/sale/models/sale_order_line.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index bbd3575c9ae..68ebf42fe5f 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -326,7 +326,8 @@ class SaleOrderLine(models.Model): compute='_compute_product_packaging_id', store=True, readonly=False, domain="[('sales', '=', True), ('product_id','=',product_id)]", check_company=True) - product_packaging_qty = fields.Float('Packaging Quantity') + product_packaging_qty = fields.Float( + 'Packaging Quantity', compute='_compute_product_packaging_qty', store=True, readonly=False) @api.depends('state') def _compute_product_uom_readonly(self): @@ -446,14 +447,17 @@ 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): From b16b3e32f7013baca4d47c5457d36d9086f3dde0 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 2 Nov 2021 13:09:48 +0100 Subject: [PATCH 21/65] [IMP] sale: Convert _onchange_product_packaging_qty into compute method --- addons/sale/models/sale_order_line.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 68ebf42fe5f..7ef61f31538 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -261,7 +261,9 @@ class SaleOrderLine(models.Model): '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_qty = fields.Float( + string='Quantity', digits='Product Unit of Measure', required=True, default=1.0, + compute='_compute_product_uom_qty', store=True, readonly=False, pre_compute=True) product_uom = fields.Many2one('uom.uom', string='Unit of Measure', 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') @@ -459,14 +461,17 @@ class SaleOrderLine(models.Model): 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_packaging_qty') + def _compute_product_uom_qty(self): + for line in self: + 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): From 8a6093c612cfd40af476f5f6c70bc0d897efad90 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 8 Nov 2021 10:05:25 +0100 Subject: [PATCH 22/65] [FIX] sale: Add misc missing computation triggers --- addons/sale/models/sale_order_line.py | 6 ++++-- addons/sale_coupon/models/sale_order.py | 2 +- .../tests/test_sale_product_configurator_ui.py | 2 ++ addons/sale_timesheet/tests/test_project_billing.py | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 7ef61f31538..3876473c526 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -134,7 +134,7 @@ 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('order_id.fiscal_position_id') + @api.depends('order_id.fiscal_position_id', 'tax_id.price_include') def _compute_tax_id(self): for line in self: line = line.with_company(line.company_id) @@ -666,7 +666,9 @@ class SaleOrderLine(models.Model): } } - @api.depends('product_uom', 'product_uom_qty') + @api.depends( + 'product_uom', 'product_uom_qty', 'product_id', + 'order_id.partner_id') def _compute_price_unit(self): for line in self: if not line.product_uom or not line.product_id: diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py index 0ab92f0385a..4d10d67889c 100644 --- a/addons/sale_coupon/models/sale_order.py +++ b/addons/sale_coupon/models/sale_order.py @@ -501,7 +501,7 @@ class SaleOrderLine(models.Model): related_program_lines |= line.order_id.order_line.filtered(lambda l: l.product_id.id == related_program.discount_line_product_id.id) - line return super(SaleOrderLine, self | related_program_lines).unlink() - @api.depends('order_id.fiscal_position_id') + @api.depends('order_id.fiscal_position_id', 'tax_id.price_include', 'product_id.taxes_id.price_include') def _compute_tax_id(self): reward_lines = self.filtered('is_reward_line') super(SaleOrderLine, self - reward_lines)._compute_tax_id() diff --git a/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py b/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py index 95ab4df737a..0e3ed37f991 100644 --- a/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py +++ b/addons/sale_product_configurator/tests/test_sale_product_configurator_ui.py @@ -148,6 +148,8 @@ class TestUi(odoo.tests.HttpCase, TestProductConfiguratorCommon): # Remove tax from Conference Chair and Chair floor protection self.product_product_conf_chair.taxes_id = None self.product_product_conf_chair_floor_protect.taxes_id = None + # YTI FIXME: This tour requires sale_management, as the Sale + # root menu item is clicked self.start_tour("/web", 'sale_product_configurator_pricelist_tour', login="admin") def test_06_product_configurator_optional_products(self): diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py index 3cd6bcc949c..e59e7d7ffdd 100644 --- a/addons/sale_timesheet/tests/test_project_billing.py +++ b/addons/sale_timesheet/tests/test_project_billing.py @@ -5,7 +5,7 @@ from odoo.fields import Command from odoo.tests import tagged from odoo.tests.common import Form -@tagged('post_install', '-at_install') +@tagged('post_install', '-at_install', 'project_billing') class TestProjectBilling(TestCommonSaleTimesheet): """ This test suite provide checks for miscellaneous small things. """ @@ -230,7 +230,7 @@ class TestProjectBilling(TestCommonSaleTimesheet): self.assertFalse(line1.project_id, "Sale line 1 should be linked to the 'non billable' project") self.assertEqual(line2.project_id, self.project_non_billable, "Sale line 3 should be linked to the 'non billable' project") - self.assertEqual(line1.price_unit, 15, "The unit price of SOL 1 should be 15") + self.assertEqual(line1.price_unit, 13, "The unit price of SOL 1 should be 15") self.assertEqual(line1.product_uom_qty, 3, "The ordered qty of SOL 1 should be 3") self.assertEqual(line2.product_uom_qty, 2, "The ordered qty of SOL 2 should be 2") From 916e92aa30d6ed9cd03399138f6f85683d7c6812 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 9 Nov 2021 11:52:50 +0100 Subject: [PATCH 23/65] [FIX] sale: Split product_id_change into several compute methods --- .../models/sale_order_line.py | 7 +- addons/event_sale/models/sale_order.py | 18 ++- addons/event_sale/tests/test_event_sale.py | 2 - addons/sale/models/sale_order.py | 7 +- addons/sale/models/sale_order_line.py | 121 ++++++++++-------- addons/sale/tests/test_onchange.py | 8 +- addons/sale/tests/test_reinvoice.py | 6 - addons/sale/tests/test_sale_flow.py | 2 - addons/sale/tests/test_sale_order.py | 1 - addons/sale/tests/test_sale_pricelist.py | 2 - addons/sale_management/models/sale_order.py | 20 +-- .../sale_product_matrix/models/sale_order.py | 3 - addons/sale_timesheet/tests/test_reinvoice.py | 4 - .../sale_timesheet/tests/test_sale_service.py | 4 - .../tests/test_sale_timesheet.py | 6 - ...ite_sale_product_attribute_value_config.py | 1 - 16 files changed, 97 insertions(+), 115 deletions(-) diff --git a/addons/event_booth_sale/models/sale_order_line.py b/addons/event_booth_sale/models/sale_order_line.py index 68da88c5db7..c6f873aacab 100644 --- a/addons/event_booth_sale/models/sale_order_line.py +++ b/addons/event_booth_sale/models/sale_order_line.py @@ -64,10 +64,9 @@ 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): + 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/sale_order.py b/addons/event_sale/models/sale_order.py index 201ea8824f3..d81abfb76f4 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -125,12 +125,18 @@ class SaleOrderLine(models.Model): @api.depends('product_uom', 'product_uom_qty') def _compute_price_unit(self): - super(SaleOrderLine, self.filtered(lambda l: not l.event_ticket_id))._compute_price_unit() + # We want to recompute the price if the product changes + # Not for other triggers like product_uom or product_uom_qty + # for event SOL + if self._origin.product_id != self.product_id: + super()._compute_price_unit() + else: + super(SaleOrderLine, self.filtered(lambda l: not l.event_ticket_id))._compute_price_unit() - @api.onchange('event_ticket_id') - def _onchange_event_ticket_id(self): + @api.depends('event_ticket_id') + def _compute_name(self): # we call this to force update the default name - self.product_id_change() + super()._compute_name() def unlink(self): self._unlink_associated_registrations() @@ -159,6 +165,8 @@ 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 + 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..8991aa7ea64 100644 --- a/addons/event_sale/tests/test_event_sale.py +++ b/addons/event_sale/tests/test_event_sale.py @@ -242,12 +242,10 @@ class TestEventSale(TestEventSaleCommon): '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/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 42275f3b6ea..d7630227e8c 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -150,7 +150,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, @@ -352,7 +354,7 @@ 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 introduce + # 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') @@ -488,7 +490,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), diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 3876473c526..f0e93e3bb5e 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -143,27 +143,11 @@ class SaleOrderLine(models.Model): 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 - @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': @@ -226,7 +210,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, pre_compute=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) @@ -238,7 +224,7 @@ class SaleOrderLine(models.Model): ], string='Invoice Status', compute='_compute_invoice_status', store=True, default='no') price_unit = fields.Float( 'Unit Price', required=True, digits='Product Price', - compute='_compute_price_unit', store=True, readonly=False) + compute='_compute_price_unit', store=True, readonly=False, pre_compute=True) price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) price_tax = fields.Float(compute='_compute_amount', string='Total Tax', store=True) @@ -264,14 +250,22 @@ class SaleOrderLine(models.Model): product_uom_qty = fields.Float( string='Quantity', digits='Product Unit of Measure', required=True, default=1.0, compute='_compute_product_uom_qty', store=True, readonly=False, pre_compute=True) - product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]", ondelete="restrict") + product_uom = fields.Many2one( + 'uom.uom', string='Unit of Measure', + compute='_compute_product_uom', store=True, readonly=False, pre_compute=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) # 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) qty_delivered_method = fields.Selection([ ('manual', 'Manual'), @@ -325,11 +319,12 @@ class SaleOrderLine(models.Model): product_packaging_id = fields.Many2one( 'product.packaging', string='Packaging', - compute='_compute_product_packaging_id', store=True, readonly=False, + compute='_compute_product_packaging_id', store=True, readonly=False, pre_compute=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) + 'Packaging Quantity', + compute='_compute_product_packaging_qty', store=True, readonly=False, pre_compute=True) @api.depends('state') def _compute_product_uom_readonly(self): @@ -461,9 +456,17 @@ class SaleOrderLine(models.Model): packaging_uom_qty / line.product_packaging_id.qty, precision_rounding=packaging_uom.rounding) - @api.depends('product_packaging_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('product_packaging_qty', 'product_id') def _compute_product_uom_qty(self): for line in self: + if not line.product_uom or (line.product_id.uom_id.id != line.product_uom.id): + 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 @@ -620,41 +623,47 @@ 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=get_lang(line.env, line.order_id.partner_id.lang).code, + partner=line.order_id.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) + 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: + 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: + 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): 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)) - - 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 @@ -671,8 +680,8 @@ class SaleOrderLine(models.Model): 'order_id.partner_id') def _compute_price_unit(self): for line in self: + line.price_unit = 0.0 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_id.partner_id: product = line.product_id.with_context( diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py index aef1a6f1aee..95e3a2997d6 100644 --- a/addons/sale/tests/test_onchange.py +++ b/addons/sale/tests/test_onchange.py @@ -151,13 +151,12 @@ class TestOnchangeProductId(TransactionCase): 'pricelist_id': christmas_pricelist.id, }) - order_line = self.env['sale.order.line'].new({ + order_line = self.env['sale.order.line'].create({ '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, 90, "Christmas discount pricelist rule not applied") self.assertEqual(order_line.discount, 10, "Christmas discount not equalt to 10%") @@ -207,13 +206,11 @@ class TestOnchangeProductId(TransactionCase): 'pricelist_id': second_pricelist.id, }) - order_line = self.env['sale.order.line'].new({ + order_line = self.env['sale.order.line'].create({ '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") @@ -270,7 +267,6 @@ class TestOnchangeProductId(TransactionCase): }) # 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 self.assertEqual(order_line.price_unit, 1800, "First pricelist rule not applied") diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py index 11f68dd4e97..14633a4e053 100644 --- a/addons/sale/tests/test_reinvoice.py +++ b/addons/sale/tests/test_reinvoice.py @@ -46,7 +46,6 @@ class TestReInvoice(TestSaleCommon): '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, @@ -56,7 +55,6 @@ class TestReInvoice(TestSaleCommon): '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.action_confirm() @@ -164,7 +162,6 @@ class TestReInvoice(TestSaleCommon): '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, @@ -174,7 +171,6 @@ class TestReInvoice(TestSaleCommon): '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.action_confirm() # create invoice lines and validate it @@ -268,7 +264,6 @@ class TestReInvoice(TestSaleCommon): '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, @@ -278,7 +273,6 @@ class TestReInvoice(TestSaleCommon): 'discount': 100.00, 'order_id': self.sale_order.id, }) - so_line2.product_id_change() self.sale_order.action_confirm() diff --git a/addons/sale/tests/test_sale_flow.py b/addons/sale/tests/test_sale_flow.py index 8d07096001b..3095b5345da 100644 --- a/addons/sale/tests/test_sale_flow.py +++ b/addons/sale/tests/test_sale_flow.py @@ -69,8 +69,6 @@ class TestSaleFlow(TestSaleCommonBase): }), ], }) - for line in sale_order.order_line: - line.product_id_change() sale_order.action_confirm() diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index ff0627180a9..e260359d351 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -283,7 +283,6 @@ class TestSaleOrder(TestSaleCommon): # 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: diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index 7c0fd5c5970..a7dc070cec4 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -121,7 +121,6 @@ class TestSaleOrder(TestSaleCommon): 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() # Check that pricelist of the SO has been applied on the sale order lines or not for line in self.sale_order.order_line: @@ -145,7 +144,6 @@ class TestSaleOrder(TestSaleCommon): 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() # Check pricelist of the SO apply or not on order lines where pricelist contains formula that add 15% on the cost price diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 2b97877cf13..5086e31ec52 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -177,16 +177,16 @@ class SaleOrderLine(models.Model): sale_order_option_ids = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines') - # Take the description on the order template if the product is present in it - @api.onchange('product_id') - def product_id_change(self): - domain = super(SaleOrderLine, self).product_id_change() - if self.product_id and self.order_id.sale_order_template_id: - for line in self.order_id.sale_order_template_id.sale_order_template_line_ids: - if line.product_id == self.product_id: - self.name = line.with_context(lang=self.order_id.partner_id.lang).name + self._get_sale_order_line_multiline_description_variants() - break - return domain + @api.depends('product_id') + def _compute_name(self): + # Take the description on the order template if the product is present in it + super()._compute_name() + for line in self: + if line.product_id and line.order_id.sale_order_template_id: + for line in line.order_id.sale_order_template_id.sale_order_template_line_ids: + if line.product_id == line.product_id: + line.name = line.with_context(lang=line.order_id.partner_id.lang).name + line._get_sale_order_line_multiline_description_variants() + break class SaleOrderOption(models.Model): diff --git a/addons/sale_product_matrix/models/sale_order.py b/addons/sale_product_matrix/models/sale_order.py index c43c87737f8..4b11fd966ba 100644 --- a/addons/sale_product_matrix/models/sale_order.py +++ b/addons/sale_product_matrix/models/sale_order.py @@ -116,16 +116,13 @@ class SaleOrder(models.Model): product_no_variant_attribute_value_ids=no_variant_attribute_values.ids) )) if product_ids: - res = False if new_lines: # Add new SO lines self.update(dict(order_line=new_lines)) # Recompute prices for new/modified lines for line in self.order_line.filtered(lambda line: line.product_id.id in product_ids): - res = line.product_id_change() or res line._onchange_discount() - return res def _get_matrix(self, product_template): """Return the matrix of the given product, updated with current SOLines quantities. diff --git a/addons/sale_timesheet/tests/test_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py index 4171279dacb..699ac0026fb 100644 --- a/addons/sale_timesheet/tests/test_reinvoice.py +++ b/addons/sale_timesheet/tests/test_reinvoice.py @@ -61,7 +61,6 @@ class TestReInvoice(TestCommonSaleTimesheet): '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, @@ -70,7 +69,6 @@ class TestReInvoice(TestCommonSaleTimesheet): '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.action_confirm() @@ -162,7 +160,6 @@ class TestReInvoice(TestCommonSaleTimesheet): '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, @@ -172,7 +169,6 @@ class TestReInvoice(TestCommonSaleTimesheet): '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.action_confirm() # let's log some timesheets (on the project created by sale_order_line1) diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index 7b2765aa15a..5b8f0ca2a5c 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -141,7 +141,6 @@ class TestSaleService(TestCommonSaleTimesheet): 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': self.sale_order.id, }) - so_line_deliver_global_project.product_id_change() self.sale_order.action_confirm() task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_global_project.id)]) @@ -187,7 +186,6 @@ class TestSaleService(TestCommonSaleTimesheet): 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': self.sale_order.id, }) - so_line_deliver_new_task_project.product_id_change() self.sale_order.action_confirm() task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_deliver_new_task_project.id)]) @@ -510,8 +508,6 @@ class TestSaleService(TestCommonSaleTimesheet): 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': self.sale_order.id, }) - so_line_deliver_new_task_project.product_id_change() - so_line_deliver_new_task_project_2.product_id_change() self.sale_order.action_confirm() project = so_line_deliver_new_task_project.project_id diff --git a/addons/sale_timesheet/tests/test_sale_timesheet.py b/addons/sale_timesheet/tests/test_sale_timesheet.py index f7d87d58727..b0bff62ae92 100644 --- a/addons/sale_timesheet/tests/test_sale_timesheet.py +++ b/addons/sale_timesheet/tests/test_sale_timesheet.py @@ -50,8 +50,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'price_unit': self.product_order_timesheet2.list_price, 'order_id': sale_order.id, }) - so_line_ordered_project_only.product_id_change() - so_line_ordered_global_project.product_id_change() sale_order.action_confirm() task_serv2 = self.env['project.task'].search([('sale_line_id', '=', so_line_ordered_global_project.id)]) project_serv1 = self.env['project.project'].search([('sale_line_id', '=', so_line_ordered_project_only.id)]) @@ -188,8 +186,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': sale_order.id, }) - so_line_deliver_global_project.product_id_change() - so_line_deliver_task_project.product_id_change() # confirm SO sale_order.action_confirm() @@ -415,8 +411,6 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': sale_order.id, }) - so_line_deliver_global_project.product_id_change() - so_line_deliver_task_project.product_id_change() # confirm SO sale_order.action_confirm() diff --git a/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py index 9c04244c79e..cf47621e3f1 100644 --- a/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py +++ b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py @@ -185,7 +185,6 @@ class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon): 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() 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%)") From 1003ef0e703a909ee96c93c4876b9b05f1104aae Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 9 Nov 2021 13:32:46 +0100 Subject: [PATCH 24/65] [IMP] sale: Convert _onchange_discount into compute method --- addons/sale/models/account_move_line.py | 2 - addons/sale/models/sale_order_line.py | 72 +++++++++++-------- addons/sale/tests/test_onchange.py | 4 -- addons/sale/tests/test_sale_order.py | 9 --- addons/sale/tests/test_sale_pricelist.py | 6 -- addons/sale_management/models/sale_order.py | 2 +- .../sale_product_matrix/models/sale_order.py | 11 +-- addons/website_sale/models/sale_order.py | 2 +- 8 files changed, 47 insertions(+), 61 deletions(-) diff --git a/addons/sale/models/account_move_line.py b/addons/sale/models/account_move_line.py index 6315c6591da..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 = {} diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index f0e93e3bb5e..7c1e64ed496 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -238,7 +238,9 @@ class SaleOrderLine(models.Model): 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) - discount = fields.Float(string='Discount (%)', digits='Discount', default=0.0) + discount = fields.Float( + string='Discount (%)', digits='Discount', + compute='_compute_discount', store=True, readonly=False) product_id = fields.Many2one( 'product.product', string='Product', domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", @@ -781,39 +783,49 @@ class SaleOrderLine(models.Model): 'tax_id', 'analytic_tag_ids' ] - @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')): - return + @api.depends('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id') + def _compute_discount(self): + for line in self: + if not (line.product_id and line.product_uom and + line.order_id.partner_id and line.order_id.pricelist_id and + line.order_id.pricelist_id.discount_policy == 'without_discount' and + self.env.user.has_group('product.group_discount_per_so_line')): + continue - 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') - ) + line.discount = 0.0 + product = line.product_id.with_context( + lang=line.order_id.partner_id.lang, + partner=line.order_id.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')) - 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) + product_context = dict( + self.env.context, + partner_id=line.order_id.partner_id.id, + date=line.order_id.date_order, + uom=line.product_uom.id) - 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) + 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_id.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 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 + 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() diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py index 95e3a2997d6..6a894ca6d03 100644 --- a/addons/sale/tests/test_onchange.py +++ b/addons/sale/tests/test_onchange.py @@ -156,12 +156,9 @@ class TestOnchangeProductId(TransactionCase): 'product_id': computer_case.id, }) - # force compute uom and prices - 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._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%") @@ -211,7 +208,6 @@ class TestOnchangeProductId(TransactionCase): 'product_id': computer_case.id, }) - 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") diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index e260359d351..36493593930 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -281,10 +281,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._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 @@ -461,9 +457,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 +485,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] diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index a7dc070cec4..bd3588c380c 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -119,9 +119,6 @@ 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._onchange_discount() # 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']: @@ -142,9 +139,6 @@ 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._onchange_discount() # 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: diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 5086e31ec52..49bfa8d2f35 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -239,7 +239,7 @@ class SaleOrderOption(models.Model): # To compute the discount a so line is created in cache values = self._get_values_to_add_to_order() new_sol = self.env['sale.order.line'].new(values) - new_sol._onchange_discount() + new_sol._compute_discount() self.discount = new_sol.discount if self.order_id.pricelist_id and self.order_id.partner_id: self.price_unit = new_sol._get_display_price(product) diff --git a/addons/sale_product_matrix/models/sale_order.py b/addons/sale_product_matrix/models/sale_order.py index 4b11fd966ba..7f6ab29955f 100644 --- a/addons/sale_product_matrix/models/sale_order.py +++ b/addons/sale_product_matrix/models/sale_order.py @@ -115,14 +115,9 @@ class SaleOrder(models.Model): product_uom_qty=qty, product_no_variant_attribute_value_ids=no_variant_attribute_values.ids) )) - if product_ids: - if new_lines: - # Add new SO lines - self.update(dict(order_line=new_lines)) - - # Recompute prices for new/modified lines - for line in self.order_line.filtered(lambda line: line.product_id.id in product_ids): - line._onchange_discount() + if product_ids and new_lines: + # Add new SO lines + self.update(dict(order_line=new_lines)) def _get_matrix(self, product_template): """Return the matrix of the given product, updated with current SOLines quantities. diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 431817cec7e..7977fa164e9 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -116,7 +116,7 @@ class SaleOrder(models.Model): discount = 0 if order.pricelist_id.discount_policy == 'without_discount': - # This part is pretty much a copy-paste of the method '_onchange_discount' of + # This part is pretty much a copy-paste of the method '_compute_discount' of # 'sale.order.line'. price, rule_id = order.pricelist_id.with_context(product_context).get_product_price_rule(product, qty or 1.0, order.partner_id) pu, currency = request.env['sale.order.line'].with_context(product_context)._get_real_price_currency(product, rule_id, qty, product.uom_id, order.pricelist_id.id) From 728cb1b15f76a5cb3eecaf968fa0cfc47d26a7f9 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 9 Nov 2021 13:40:41 +0100 Subject: [PATCH 25/65] [IMP] sale: Remove useless pre-precessing on SOL creation This is not needed anymore since the last ORM changes from this PR with the pre_compute attribute. --- addons/sale/models/sale_order.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index d7630227e8c..60c72793e6c 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -548,13 +548,6 @@ class SaleOrder(models.Model): 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) return super(SaleOrder, self).create(vals_list) def _compute_field_value(self, field): From 052b15e823bb24c3034e442bcfddd007d1cf662b Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 9 Nov 2021 13:52:00 +0100 Subject: [PATCH 26/65] [IMP] sale: Convert _change_sale_ok into compute method --- addons/sale/models/product_template.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index ae3bef2b262..8e1fb3d68b9 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).update({'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 From 01557bf36edb22c668c865822060404443dc871c Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 10 Nov 2021 11:30:26 +0100 Subject: [PATCH 27/65] [FIX] sale: Split _compute_order_info_from_partner into several methods --- addons/sale/models/sale_order.py | 73 ++++++++++----------- addons/sale/tests/test_sale_order.py | 2 +- addons/sale/tests/test_sale_pricelist.py | 9 ++- addons/sale_management/models/sale_order.py | 4 +- 4 files changed, 47 insertions(+), 41 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 60c72793e6c..548e6b5a9a4 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -189,18 +189,18 @@ class SaleOrder(models.Model): 'res.partner', string='Invoice Address', readonly=False, required=True, pre_compute=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - compute='_compute_order_info_from_partner', store=True, + compute='_compute_partner_invoice_id', store=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) partner_shipping_id = fields.Many2one( 'res.partner', string='Delivery Address', readonly=False, required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - compute='_compute_order_info_from_partner', store=True, pre_compute=True, + compute='_compute_partner_shipping_id', store=True, pre_compute=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) pricelist_id = fields.Many2one( 'product.pricelist', string='Pricelist', check_company=True, # Unrequired company required=False, readonly=False, store=True, - compute='_compute_order_info_from_partner', pre_compute=True, + compute='_compute_pricelist_id', pre_compute=True, 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.") @@ -225,7 +225,7 @@ class SaleOrder(models.Model): note = fields.Html( 'Terms and conditions', default=_default_note, - compute='_compute_order_info_from_partner', store=True, readonly=False) + compute='_compute_note', store=True, readonly=False) terms_type = fields.Selection(related='company_id.terms_type') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, compute='_amount_all', tracking=5) @@ -236,7 +236,7 @@ class SaleOrder(models.Model): payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Terms', check_company=True, # Unrequired company - compute='_compute_order_info_from_partner', store=True, readonly=False, + compute='_compute_payment_term_id', store=True, readonly=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) fiscal_position_id = fields.Many2one( 'account.fiscal.position', string='Fiscal Position', @@ -412,6 +412,9 @@ class SaleOrder(models.Model): """ cache = {} for order in self: + if not order.partner_id: + order.fiscal_position_id = False + continue key = (order.company_id, order.partner_id, order.partner_shipping_id) if key not in cache: cache[key] = self.env['account.fiscal.position'].with_company( @@ -420,43 +423,39 @@ class SaleOrder(models.Model): order.fiscal_position_id = cache[key] @api.depends('partner_id') - def _compute_order_info_from_partner(self): - """ - Update the following fields when the partner is changed: - - Pricelist - - Payment terms - - Invoice address - - Delivery address - - Sales Team - - User - - Note - """ + def _compute_partner_invoice_id(self): for order in self: - if not order.partner_id: - order.update({ - 'partner_invoice_id': False, - 'partner_shipping_id': False, - 'fiscal_position_id': False, - }) - continue + 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 - addr = order.partner_id.address_get(['delivery', 'invoice']) - values = { - 'pricelist_id': order.partner_id.property_product_pricelist and order.partner_id.property_product_pricelist.id or False, - 'payment_term_id': order.partner_id.property_payment_term_id and order.partner_id.property_payment_term_id.id or False, - 'partner_invoice_id': addr['invoice'], - 'partner_shipping_id': addr['delivery'], - } + @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 + 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_base_url() + '/terms') + order.note = _('Terms & Conditions: %s', baseurl) + elif not is_html_empty(self.env.company.invoice_terms): + order.note = order.with_context(lang=order.partner_id.lang).env.company.invoice_terms - if self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'): - if order.terms_type == 'html' and self.env.company.invoice_terms_html: - baseurl = html_keep_url(order.get_base_url() + '/terms') - values['note'] = _('Terms & Conditions: %s', baseurl) - elif not is_html_empty(self.env.company.invoice_terms): - values['note'] = order.with_context(lang=order.partner_id.lang).env.company.invoice_terms - order.update(values) + @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): diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 36493593930..feb3376e303 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -11,7 +11,7 @@ from odoo.tools import float_compare from .common import TestSaleCommon -@tagged('post_install', '-at_install') +@tagged('post_install', '-at_install', 'sale_order') class TestSaleOrder(TestSaleCommon): @classmethod diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index bd3588c380c..70592b12973 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -5,7 +5,7 @@ from .common import TestSaleCommon from odoo.tests import tagged -@tagged('post_install', '-at_install') +@tagged('post_install', '-at_install', 'sale_pricelist') class TestSaleOrder(TestSaleCommon): @classmethod @@ -119,6 +119,10 @@ 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 manually the computation for discount, unit price, subtotal, ... + self.sale_order.order_line._compute_price_unit() + self.sale_order.order_line._compute_discount() + # 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']: @@ -139,6 +143,9 @@ class TestSaleOrder(TestSaleCommon): # Change the pricelist self.sale_order.write({'pricelist_id': self.pricelist_discount_excl.id}) + # Trigger manually the computation for discount, unit price, subtotal, ... + self.sale_order.order_line._compute_price_unit() + self.sale_order.order_line._compute_discount() # 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: diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 49bfa8d2f35..a3c3aabc18d 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -51,8 +51,8 @@ class SaleOrder(models.Model): return super(SaleOrder, self).copy(default=default) @api.depends('partner_id') - def _compute_order_info_from_partner(self): - super(SaleOrder, self)._compute_order_info_from_partner() + def _compute_note(self): + super(SaleOrder, self)._compute_note() for order in self: template = order.sale_order_template_id.with_context(lang=order.partner_id.lang) order.note = template.note if not is_html_empty(template.note) else order.note From fecff7119f61ff04f40c71438f800a72ac88aff7 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 10 Nov 2021 13:27:55 +0100 Subject: [PATCH 28/65] [IMP] sale: Don't recompute taxes on tax configuration change This should be done manually --- addons/sale/models/sale_order_line.py | 6 ++++-- addons/sale/tests/test_sale_pricelist.py | 6 ++---- addons/sale_coupon/models/sale_order.py | 2 +- addons/sale_coupon/tests/test_program_numbers.py | 4 +++- addons/sale_management/models/sale_order.py | 2 +- .../test_website_sale_product_attribute_value_config.py | 5 +++-- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 7c1e64ed496..778061745da 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -134,7 +134,7 @@ 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('order_id.fiscal_position_id', 'tax_id.price_include') + @api.depends('product_id') def _compute_tax_id(self): for line in self: line = line.with_company(line.company_id) @@ -682,8 +682,8 @@ class SaleOrderLine(models.Model): 'order_id.partner_id') def _compute_price_unit(self): for line in self: - line.price_unit = 0.0 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_id.partner_id: product = line.product_id.with_context( @@ -697,6 +697,8 @@ class SaleOrderLine(models.Model): ) 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 = [] diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index 70592b12973..a1db9f509ac 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -120,8 +120,7 @@ class TestSaleOrder(TestSaleCommon): # Change the pricelist self.sale_order.write({'pricelist_id': self.pricelist_discount_incl.id}) # Trigger manually the computation for discount, unit price, subtotal, ... - self.sale_order.order_line._compute_price_unit() - self.sale_order.order_line._compute_discount() + 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: @@ -144,8 +143,7 @@ class TestSaleOrder(TestSaleCommon): # Change the pricelist self.sale_order.write({'pricelist_id': self.pricelist_discount_excl.id}) # Trigger manually the computation for discount, unit price, subtotal, ... - self.sale_order.order_line._compute_price_unit() - self.sale_order.order_line._compute_discount() + 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: diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py index 4d10d67889c..7b234ca1de6 100644 --- a/addons/sale_coupon/models/sale_order.py +++ b/addons/sale_coupon/models/sale_order.py @@ -501,7 +501,7 @@ class SaleOrderLine(models.Model): related_program_lines |= line.order_id.order_line.filtered(lambda l: l.product_id.id == related_program.discount_line_product_id.id) - line return super(SaleOrderLine, self | related_program_lines).unlink() - @api.depends('order_id.fiscal_position_id', 'tax_id.price_include', 'product_id.taxes_id.price_include') + @api.depends('product_id') def _compute_tax_id(self): reward_lines = self.filtered('is_reward_line') super(SaleOrderLine, self - reward_lines)._compute_tax_id() diff --git a/addons/sale_coupon/tests/test_program_numbers.py b/addons/sale_coupon/tests/test_program_numbers.py index 45628c631bf..b5659b497dc 100644 --- a/addons/sale_coupon/tests/test_program_numbers.py +++ b/addons/sale_coupon/tests/test_program_numbers.py @@ -6,7 +6,7 @@ from odoo.exceptions import UserError from odoo.tests import tagged -@tagged('post_install', '-at_install') +@tagged('post_install', '-at_install', 'program_numbers') class TestSaleCouponProgramNumbers(TestSaleCouponCommon): def setUp(self): @@ -188,6 +188,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon): order.recompute_coupon_lines() self.assertEqual(len(order.order_line.ids), 1, "We should not get the reduction line since we dont have 320$ tax excluded (cabinet is 320$ tax included)") sol1.tax_id.price_include = False + sol1._compute_tax_id() order.recompute_coupon_lines() self.assertEqual(len(order.order_line.ids), 2, "We should now get the reduction line since we have 320$ tax included (cabinet is 320$ tax included)") # Name | Qty | price_unit | Tax | HTVA | TVAC | TVA | @@ -389,6 +390,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon): self.assertEqual(len(order.order_line.ids), 12, "Order should contains 5 regular product lines, 3 free product lines and 4 discount lines (one for every tax)") # -- This is a test inside the test + order.order_line._compute_tax_id() self.assertEqual(order.amount_total, 1711, "Recomputing tax on sale order lines should not change total amount") self.assertEqual(order.amount_untaxed, 1435.46, "Recomputing tax on sale order lines should not change untaxed amount") self.assertEqual(len(order.order_line.ids), 12, "Recomputing tax on sale order lines should not change number of order line") diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index a3c3aabc18d..3f1e3f24d8a 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -50,7 +50,7 @@ class SaleOrder(models.Model): default['validity_date'] = fields.Date.context_today(self) + timedelta(self.sale_order_template_id.number_of_days) return super(SaleOrder, self).copy(default=default) - @api.depends('partner_id') + @api.depends('partner_id', 'sale_order_template_id') def _compute_note(self): super(SaleOrder, self)._compute_note() for order in self: diff --git a/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py index cf47621e3f1..51629cd40a5 100644 --- a/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py +++ b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py @@ -6,7 +6,7 @@ from odoo.tests import tagged from odoo.addons.website.tools import MockRequest -@tagged('post_install', '-at_install') +@tagged('post_install', '-at_install', 'product_attribute') class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCommon): def test_get_combination_info(self): @@ -132,7 +132,7 @@ class TestWebsiteSaleProductAttributeValueConfig(TestSaleProductAttributeValueCo self.assertEqual(round(combination_info['price'], 2), 434.78, "434.78$ + 0% tax (mapped from fp 15% -> 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 @@ -185,6 +185,7 @@ class TestWebsiteSaleProductPricelist(TestSaleProductAttributeValueCommon): self.assertEqual(round(sol.price_total), 110.0, "110$ with 10% included tax") so.pricelist_id = pricelist so.fiscal_position_id = fpos + 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%)") From de4911acebd4b96b097feb1caf375574f89bdd72 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 10 Nov 2021 17:32:02 +0100 Subject: [PATCH 29/65] [IMP] sale: Remove qty_delivered_manual field This field is useless since the new compute method from 13.0. And also introduces side effect / loop recomputations. --- addons/sale/models/sale_order_line.py | 24 +++++------------------- addons/sale/views/sale_views.xml | 2 -- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 778061745da..48d8d4923f2 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -282,9 +282,6 @@ class SaleOrderLine(models.Model): 'Delivered Quantity', copy=False, compute='_compute_qty_delivered', store=True, readonly=False, digits='Product Unit of Measure') - qty_delivered_manual = fields.Float( - 'Delivered Manually', copy=False, digits='Product Unit of Measure', - compute='_compute_qty_delivered_manual', store=True, readonly=False) qty_to_invoice = fields.Float( compute='_get_to_invoice_qty', string='To Invoice Quantity', store=True, digits='Product Unit of Measure') @@ -349,7 +346,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. @@ -362,10 +363,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 @@ -406,17 +403,6 @@ class SaleOrderLine(models.Model): return result - @api.depends('qty_delivered', 'qty_delivered_method') - def _compute_qty_delivered_manual(self): - """ When writing on qty_delivered, if the value should be - modified 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 computed by other mecanisms. - """ - for line in self: - line.qty_delivered_manual = line.qty_delivered if line.qty_delivered_method == 'manual' else 0.0 - @api.depends('product_id', 'product_uom_qty', 'product_uom') def _compute_product_packaging_id(self): for line in self: 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" /> - Date: Wed, 10 Nov 2021 18:28:17 +0100 Subject: [PATCH 30/65] [FIX] sale_timesheet: Avoid recomputing price_unit --- addons/sale_timesheet/tests/test_project_billing.py | 2 +- addons/sale_timesheet/wizard/project_create_sale_order.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py index e59e7d7ffdd..c306fc865df 100644 --- a/addons/sale_timesheet/tests/test_project_billing.py +++ b/addons/sale_timesheet/tests/test_project_billing.py @@ -230,7 +230,7 @@ class TestProjectBilling(TestCommonSaleTimesheet): self.assertFalse(line1.project_id, "Sale line 1 should be linked to the 'non billable' project") self.assertEqual(line2.project_id, self.project_non_billable, "Sale line 3 should be linked to the 'non billable' project") - self.assertEqual(line1.price_unit, 13, "The unit price of SOL 1 should be 15") + self.assertEqual(line1.price_unit, 15.0, "The unit price of SOL 1 should be 15.0") self.assertEqual(line1.product_uom_qty, 3, "The ordered qty of SOL 1 should be 3") self.assertEqual(line2.product_uom_qty, 2, "The ordered qty of SOL 2 should be 2") diff --git a/addons/sale_timesheet/wizard/project_create_sale_order.py b/addons/sale_timesheet/wizard/project_create_sale_order.py index ca368aeaf6a..1cf01a6303e 100644 --- a/addons/sale_timesheet/wizard/project_create_sale_order.py +++ b/addons/sale_timesheet/wizard/project_create_sale_order.py @@ -170,6 +170,8 @@ class ProjectCreateSalesOrder(models.TransientModel): sale_order_line.with_context({'no_update_planned_hours': True}).write({ 'product_uom_qty': sale_order_line.qty_delivered }) + # Avoid recomputing price_unit + self.env.remove_to_compute(self.env['sale.order.line']._fields['price_unit'], sale_order_line) self.project_id.write({ 'sale_order_id': sale_order.id, @@ -242,9 +244,10 @@ class ProjectCreateSalesOrder(models.TransientModel): 'so_line': map_entry.sale_line_id.id }) map_entry.sale_line_id.with_context({'no_update_planned_hours': True}).write({ - 'product_uom_qty': map_entry.sale_line_id.qty_delivered + 'product_uom_qty': map_entry.sale_line_id.qty_delivered, }) - + # Avoid recomputing price_unit + self.env.remove_to_compute(self.env['sale.order.line']._fields['price_unit'], map_entry.sale_line_id) return map_entries From 54fa80c24fce1d16e2098c2c09382d9d2f48eb46 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 17 Nov 2021 13:53:29 +0100 Subject: [PATCH 31/65] [FIX][IMP] sale_*: clean customer_lead code * remove useless hook _get_customer_lead, since the field is now computed * Add inverse to properly modify existing stock moves when the customer delay is modified --- addons/sale/models/sale_order.py | 4 ---- addons/sale_management/models/sale_order.py | 1 - addons/sale_stock/models/sale_order.py | 21 ++++++++++----------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 548e6b5a9a4..fa6bc9fcc57 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1068,10 +1068,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_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 3f1e3f24d8a..070079e6477 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -125,7 +125,6 @@ class SaleOrder(models.Model): 'product_uom_qty': line.product_uom_qty, 'product_id': line.product_id.id, 'product_uom': line.product_uom_id.id, - 'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id), }) order_lines.append((0, 0, data)) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 7586e9a25d7..e83962e2d57 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -233,11 +233,6 @@ class SaleOrder(models.Model): invoice_vals['invoice_incoterm_id'] = self.incoterm.id return invoice_vals - @api.model - def _get_customer_lead(self, product_tmpl_id): - super(SaleOrder, self)._get_customer_lead(product_tmpl_id) - return product_tmpl_id.sale_delay - def _log_decrease_ordered_quantity(self, documents, cancel=False): def _render_note_exception_quantity_so(rendering_context): @@ -281,7 +276,8 @@ class SaleOrderLine(models.Model): is_mto = fields.Boolean(compute='_compute_is_mto') display_qty_widget = fields.Boolean(compute='_compute_qty_to_deliver') customer_lead = fields.Float( - compute='_compute_customer_lead', store=True, readonly=False, pre_compute=True) + compute='_compute_customer_lead', store=True, readonly=False, pre_compute=True, + inverse='_inverse_customer_lead') @api.depends('product_type', 'product_uom_qty', 'qty_delivered', 'state', 'move_ids', 'product_uom') def _compute_qty_to_deliver(self): @@ -435,9 +431,6 @@ class SaleOrderLine(models.Model): res = super(SaleOrderLine, self).write(values) if lines: lines._action_launch_stock_rule(previous_product_uom_qty) - if 'customer_lead' in values and self.state == 'sale' and not self.order_id.commitment_date: - # Propagate deadline on related stock move - self.move_ids.date_deadline = self.order_id.date_order + timedelta(days=self.customer_lead or 0.0) return res @api.depends('order_id.state') @@ -474,8 +467,14 @@ class SaleOrderLine(models.Model): @api.depends('product_id') def _compute_customer_lead(self): - for order in self: - order.customer_lead = order.product_id.sale_delay + for line in self: + line.customer_lead = line.product_id.sale_delay + + def _inverse_customer_lead(self): + for line in self: + if line.state == 'sale' and not line.order_id.commitment_date: + # Propagate deadline on related stock move + line.move_ids.date_deadline = line.order_id.date_order + timedelta(days=line.customer_lead or 0.0) def _prepare_procurement_values(self, group_id=False): """ Prepare specific key for moves or other components that will be created from a stock rule From 2de16205d2b336c23d87280e4fe6ce68f87e8855 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 17 Nov 2021 14:21:33 +0100 Subject: [PATCH 32/65] [IMP] sale: clean date(time) conversions * remove conversions from & to strings, odoo manages date(times) natively * clean imports --- addons/sale/models/sale_order.py | 13 +++++-------- addons/sale/models/sale_order_line.py | 5 ++++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index fa6bc9fcc57..efb74612014 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -1,15 +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.fields import Datetime 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 @@ -25,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): @@ -337,7 +334,7 @@ class SaleOrder(models.Model): dt = line._expected_date() dates_list.append(dt) if dates_list: - order.expected_date = Datetime.to_string(min(dates_list)) + order.expected_date = min(dates_list) else: order.expected_date = False @@ -541,8 +538,8 @@ class SaleOrder(models.Model): if 'company_id' in vals: self = self.with_company(vals['company_id']) if vals.get('name', _('New')) == _('New'): - seq_date = Datetime.context_timestamp( - self, Datetime.to_datetime(vals['date_order']) + 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') diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 48d8d4923f2..befa64b8fb9 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -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') From 1d854b0bb65e4577dcb769535111f5b487056626 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 17 Nov 2021 15:54:49 +0100 Subject: [PATCH 33/65] [IMP] sale: correctly consider display_type in SOL computes --- addons/sale/models/sale_order_line.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index befa64b8fb9..9668e7cde3c 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -148,9 +148,6 @@ class SaleOrderLine(models.Model): @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) lines = super().create(vals_list) for line in lines: if line.product_id and line.order_id.state == 'sale': @@ -453,9 +450,13 @@ class SaleOrderLine(models.Model): 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('product_packaging_qty', 'product_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 + if not line.product_uom or (line.product_id.uom_id.id != line.product_uom.id): line.product_uom_qty = line.product_uom_qty or 1.0 if not line.product_packaging_id: From 1d3b9f6be6f664eaa344028a96926f15fc59428f Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 17 Nov 2021 15:55:38 +0100 Subject: [PATCH 34/65] [IMP] sale: minor perf improvement & cleanup --- addons/sale/models/sale_order_line.py | 9 ++++++--- addons/sale/tests/test_onchange.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 9668e7cde3c..08a88ce024e 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -652,7 +652,7 @@ class SaleOrderLine(models.Model): 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 product = self.product_id @@ -777,11 +777,14 @@ class SaleOrderLine(models.Model): @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 + for line in self: if not (line.product_id and line.product_uom and line.order_id.partner_id and line.order_id.pricelist_id and - line.order_id.pricelist_id.discount_policy == 'without_discount' and - self.env.user.has_group('product.group_discount_per_so_line')): + line.order_id.pricelist_id.discount_policy == 'without_discount'): continue line.discount = 0.0 diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py index 6a894ca6d03..198d4702420 100644 --- a/addons/sale/tests/test_onchange.py +++ b/addons/sale/tests/test_onchange.py @@ -309,7 +309,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", @@ -318,7 +318,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': { From 302ad13b3dc8cd147f1670a8ca9fc3ca3746b002 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 17 Nov 2021 16:10:58 +0100 Subject: [PATCH 35/65] [IMP] sale,account: follow same pattern for all compute methods --- addons/account/wizard/accrued_orders.py | 2 +- addons/sale/models/sale_order_line.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 08a88ce024e..687e1881b37 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 @@ -81,7 +81,7 @@ class SaleOrderLine(models.Model): # 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): + 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. @@ -283,7 +283,7 @@ class SaleOrderLine(models.Model): 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, From 31d10b862d790f95ccf357b898f56c2216229356 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 11:34:40 +0100 Subject: [PATCH 36/65] [IMP] event_sale: convert onchanges to compute + review comments --- .../models/sale_order_line.py | 4 ++ addons/event_sale/models/event_ticket.py | 6 ++ addons/event_sale/models/sale_order.py | 58 ++++++++++++------- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/addons/event_booth_sale/models/sale_order_line.py b/addons/event_booth_sale/models/sale_order_line.py index c6f873aacab..a57ca3b9c22 100644 --- a/addons/event_booth_sale/models/sale_order_line.py +++ b/addons/event_booth_sale/models/sale_order_line.py @@ -66,6 +66,10 @@ class SaleOrderLine(models.Model): @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): 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 d81abfb76f4..09540b474c4 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, pre_compute=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, pre_compute=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,32 +111,43 @@ 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.depends('product_uom', 'product_uom_qty') + @api.depends('event_ticket_id') def _compute_price_unit(self): - # We want to recompute the price if the product changes - # Not for other triggers like product_uom or product_uom_qty - # for event SOL - if self._origin.product_id != self.product_id: - super()._compute_price_unit() - else: - super(SaleOrderLine, self.filtered(lambda l: not l.event_ticket_id))._compute_price_unit() + """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.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): - # we call this to force update the default name + """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): @@ -165,6 +177,10 @@ class SaleOrderLine(models.Model): def _get_display_price(self, product): if self.event_ticket_id and self.event_id: + # 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 From 52a0f2cc6ae15be675424c994aa51da503ca0ba9 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 13:41:16 +0100 Subject: [PATCH 37/65] [IMP] sale: keep only onchange tests in test_onchange file --- addons/sale/tests/test_onchange.py | 263 +---------------------- addons/sale/tests/test_sale_order.py | 42 +++- addons/sale/tests/test_sale_pricelist.py | 206 +++++++++++++++++- 3 files changed, 247 insertions(+), 264 deletions(-) diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py index 198d4702420..25a0cd2f1e4 100644 --- a/addons/sale/tests/test_onchange.py +++ b/addons/sale/tests/test_onchange.py @@ -1,271 +1,10 @@ # -*- 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 -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'].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.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'].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.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 - 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") +class TestSaleOnchanges(TransactionCase): def test_sale_warnings(self): """Test warnings & SO/SOL updates when partner/products with sale warnings are used.""" diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index feb3376e303..6e4a44cce5e 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 @@ -11,7 +12,7 @@ from odoo.tools import float_compare from .common import TestSaleCommon -@tagged('post_install', '-at_install', 'sale_order') +@tagged('post_install', '-at_install') class TestSaleOrder(TestSaleCommon): @classmethod @@ -723,3 +724,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)) + fp_tax_id = 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 a1db9f509ac..f4fd05947a8 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -2,10 +2,11 @@ # 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 -@tagged('post_install', '-at_install', 'sale_pricelist') +@tagged('post_install', '-at_install') class TestSaleOrder(TestSaleCommon): @classmethod @@ -158,3 +159,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") From 3314036f71278c0654a714ff1656e4847978f90b Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 16:24:56 +0100 Subject: [PATCH 38/65] [IMP] sale: pre-compute stored related fields --- addons/sale/models/sale_order_line.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 687e1881b37..f1931c2676d 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -292,10 +292,6 @@ class SaleOrderLine(models.Model): 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)]") @@ -305,8 +301,13 @@ 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', pre_compute=True) + company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, index=True, pre_compute=True) + order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer', pre_compute=True) + salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', pre_compute=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, pre_compute=True) customer_lead = fields.Float( 'Lead Time', required=True, default=0.0, From 886d6093f152b21123aeee0bad4da36f29e784f3 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 16:26:21 +0100 Subject: [PATCH 39/65] [IMP] sale: remove default for fully computed fields Will be triggered and returned by default_get calls for nothing --- addons/sale/models/sale_order_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index f1931c2676d..f04a9958dd1 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -248,7 +248,7 @@ 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_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, default=1.0, compute='_compute_product_uom_qty', store=True, readonly=False, pre_compute=True) From bc945ef3f238c47d59d86e1d0e325cadc2a56bb9 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 16:27:36 +0100 Subject: [PATCH 40/65] [FIX] sale: remove wrong default qty on lines Compute was never called because of the compute, which lead to wrong values (raising due to the SQL constraints). Integrate the default in the compute directly. --- addons/sale/models/sale_order_line.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index f04a9958dd1..05dcb5d95c9 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -250,7 +250,7 @@ class SaleOrderLine(models.Model): related="product_id.product_tmpl_id", domain=[('sale_ok', '=', True)]) 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, default=1.0, + string='Quantity', digits='Product Unit of Measure', required=True, compute='_compute_product_uom_qty', store=True, readonly=False, pre_compute=True) product_uom = fields.Many2one( 'uom.uom', string='Unit of Measure', @@ -458,8 +458,8 @@ class SaleOrderLine(models.Model): line.product_uom_qty = 0.0 continue - if not line.product_uom or (line.product_id.uom_id.id != line.product_uom.id): - line.product_uom_qty = line.product_uom_qty or 1.0 + # 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 From d9f9826b9383c0336776aa783eff560712b3f6b1 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 16:59:02 +0100 Subject: [PATCH 41/65] [FIX] sale(_stock): do not override customer_lead compute with default --- addons/sale/models/sale_order_line.py | 10 +++- addons/sale_stock/models/sale_order.py | 1 + .../tests/test_sale_stock_lead_time.py | 50 +++++++++++++++---- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 05dcb5d95c9..5334de559e5 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -310,7 +310,8 @@ class SaleOrderLine(models.Model): related='order_id.state', string='Order Status', copy=False, store=True, pre_compute=True) customer_lead = fields.Float( - 'Lead Time', required=True, default=0.0, + 'Lead Time', required=True, + compute="_compute_customer_lead", store=True, readonly=False, pre_compute=True, help="Number of days between the order confirmation and the shipping of the products to the customer") display_type = fields.Selection([ @@ -326,6 +327,13 @@ class SaleOrderLine(models.Model): 'Packaging Quantity', compute='_compute_product_packaging_qty', store=True, readonly=False, pre_compute=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): for line in self: diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index e83962e2d57..968a6d1337f 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -467,6 +467,7 @@ class SaleOrderLine(models.Model): @api.depends('product_id') def _compute_customer_lead(self): + super()._compute_customer_lead() # Reset customer_lead when the product is modified for line in self: line.customer_lead = line.product_id.sale_delay diff --git a/addons/sale_stock/tests/test_sale_stock_lead_time.py b/addons/sale_stock/tests/test_sale_stock_lead_time.py index 579159c9721..0b32ba55bb4 100644 --- a/addons/sale_stock/tests/test_sale_stock_lead_time.py +++ b/addons/sale_stock/tests/test_sale_stock_lead_time.py @@ -28,16 +28,17 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon): # Create sale order of product_1 order = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, - 'partner_invoice_id': self.partner_a.id, - 'partner_shipping_id': self.partner_a.id, 'pricelist_id': self.company_data['default_pricelist'].id, 'picking_policy': 'direct', 'warehouse_id': self.company_data['default_warehouse'].id, - 'order_line': [(0, 0, {'name': self.test_product_order.name, - 'product_id': self.test_product_order.id, - 'product_uom_qty': 10, - 'product_uom': self.env.ref('uom.product_uom_unit').id, - 'customer_lead': self.test_product_order.sale_delay})]}) + 'order_line': [(0, 0, { + 'product_id': self.test_product_order.id, + 'product_uom_qty': 10, + 'product_uom': self.env.ref('uom.product_uom_unit').id, + })] + }) + + self.assertEqual(order.order_line.customer_lead, self.test_product_order.sale_delay) # Confirm our standard sale order order.action_confirm() @@ -46,8 +47,8 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon): self.assertTrue(order.picking_ids, "Picking should be created.") # Check schedule date of picking - out_date = fields.Datetime.from_string(order.date_order) + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead) - min_date = fields.Datetime.from_string(order.picking_ids[0].scheduled_date) + out_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead) + min_date = order.picking_ids[0].scheduled_date self.assertTrue(abs(min_date - out_date) <= timedelta(seconds=1), 'Schedule date of picking should be equal to: order date + Customer Lead Time - Sales Safety Days.') def test_01_product_route_level_delays(self): @@ -206,3 +207,34 @@ class TestSaleStockLeadTime(TestSaleCommon, ValuationReconciliationTestCommon): self.assertEqual(pack.date_deadline, new_deadline) new_deadline -= timedelta(days=pick.move_ids.rule_id.delay) self.assertEqual(pick.date_deadline, new_deadline) + + def test_03_product_company_level_delays(self): + """Partial duplicate of test_02 to make sure there is no default value specified in sale + that disables the computation of the customer_lead. + """ + order = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'pricelist_id': self.company_data['default_pricelist'].id, + 'picking_policy': 'direct', + 'warehouse_id': self.company_data['default_warehouse'].id, + }) + + order_line = self.env['sale.order.line'].create({ + 'product_id': self.test_product_order.id, + 'product_uom_qty': 10, + 'product_uom': self.env.ref('uom.product_uom_unit').id, + 'order_id': order.id, + }) + + self.assertEqual(order_line.customer_lead, self.test_product_order.sale_delay) + + # Confirm our standard sale order + order.action_confirm() + + # Check the picking crated or not + self.assertTrue(order.picking_ids, "Picking should be created.") + + # Check schedule date of picking + out_date = order.date_order + timedelta(days=self.test_product_order.sale_delay) - timedelta(days=self.env.company.security_lead) + min_date = order.picking_ids[0].scheduled_date + self.assertTrue(abs(min_date - out_date) <= timedelta(seconds=1), 'Schedule date of picking should be equal to: order date + Customer Lead Time - Sales Safety Days.') From 2905c565d23496c93b7999e2bbeea824ebb55741 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 17:00:03 +0100 Subject: [PATCH 42/65] [FIX] sale: avoid defaults on compute stored fields Leave the computation fully to the compute method, to avoid side-effects and wrong values in db. --- addons/sale/models/sale_order_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 5334de559e5..a9cb0f4a2ce 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -221,7 +221,7 @@ 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') + ], string='Invoice Status', compute='_compute_invoice_status', store=True, pre_compute=True) price_unit = fields.Float( 'Unit Price', required=True, digits='Product Price', compute='_compute_price_unit', store=True, readonly=False, pre_compute=True) From 47f6416d8c73f378fb2e8392131f538b7bfcb16c Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 17:32:50 +0100 Subject: [PATCH 43/65] [IMP] sale: test improvements * Verify the pre-compute logic, from the SO side (recurisvely) and from the SOL side. --- addons/sale/tests/test_sale_order.py | 57 +++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 6e4a44cce5e..9a00446db56 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -86,6 +86,60 @@ class TestSaleOrder(TestSaleCommon): '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, + }) + order_lines = 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 @@ -311,8 +365,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') From 096e8a3b95a811738a76a97010c473692d005788 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 17:37:21 +0100 Subject: [PATCH 44/65] [FIX] sale: do not recomputed price_unit on partner change --- addons/sale/models/sale_order_line.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index a9cb0f4a2ce..e5ff21f1df8 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -676,9 +676,7 @@ class SaleOrderLine(models.Model): } } - @api.depends( - 'product_uom', 'product_uom_qty', 'product_id', - 'order_id.partner_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: From da35c78dd81f2c7b860b662f2b29034f546e63e7 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Thu, 18 Nov 2021 17:40:53 +0100 Subject: [PATCH 45/65] [IMP] sale: rely on related fields instead of taking them from the order --- addons/sale/models/sale_order_line.py | 30 +++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index e5ff21f1df8..2a4dd458911 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -62,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'], @@ -71,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)): @@ -80,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') + @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: @@ -150,7 +150,7 @@ class SaleOrderLine(models.Model): def create(self, vals_list): 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 @@ -196,7 +196,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) @@ -524,7 +524,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'] @@ -682,10 +682,10 @@ class SaleOrderLine(models.Model): 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_id.partner_id: + if line.order_id.pricelist_id and line.order_partner_id: product = line.product_id.with_context( lang=line.order_id.partner_id.lang, - partner=line.order_id.partner_id, + partner=line.order_partner_id, quantity=line.product_uom_qty, date=line.order_id.date_order, pricelist=line.order_id.pricelist_id.id, @@ -790,14 +790,14 @@ class SaleOrderLine(models.Model): for line in self: if not (line.product_id and line.product_uom and - line.order_id.partner_id and line.order_id.pricelist_id and + line.order_partner_id and line.order_id.pricelist_id and line.order_id.pricelist_id.discount_policy == 'without_discount'): continue line.discount = 0.0 product = line.product_id.with_context( lang=line.order_id.partner_id.lang, - partner=line.order_id.partner_id, + partner=line.order_partner_id, quantity=line.product_uom_qty, date=line.order_id.date_order, pricelist=line.order_id.pricelist_id.id, @@ -806,14 +806,14 @@ class SaleOrderLine(models.Model): product_context = dict( self.env.context, - partner_id=line.order_id.partner_id.id, + partner_id=line.order_partner_id.id, date=line.order_id.date_order, uom=line.product_uom.id) 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_id.partner_id) + 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) @@ -864,13 +864,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 From 9a1e3987020417603d4f2dfca522e13a386edcc7 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Fri, 19 Nov 2021 13:22:22 +0100 Subject: [PATCH 46/65] [IMP] sale_stock: remove unnecessary create override managed by a compute method now --- addons/sale_stock/models/sale_order.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 968a6d1337f..3ba3e81a5a0 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -83,15 +83,6 @@ class SaleOrder(models.Model): expected_date = min(dates_list) if order.picking_policy == 'direct' else max(dates_list) order.expected_date = fields.Datetime.to_string(expected_date) - @api.model_create_multi - def create(self, vals_list): - # YTI TODO: Achieve this with a compute method - for vals in vals_list: - if 'warehouse_id' not in vals and 'company_id' in vals: - user = self.env['res.users'].browse(vals.get('user_id', False)) - vals['warehouse_id'] = user.with_company(vals.get('company_id'))._get_default_warehouse_id().id - return super().create(vals_list) - def write(self, values): if values.get('order_line') and self.state == 'sale': for order in self: From 280cf015e1351c6c9e1a93749a87c7061870b624 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 22 Nov 2021 12:31:32 +0100 Subject: [PATCH 47/65] [IMP] sale: Use compute method for notes + translate field in default value --- addons/sale/models/sale_order.py | 16 ++++------------ addons/website_sale/models/sale_order.py | 4 ++-- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index efb74612014..519deddb0fb 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -103,17 +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 '' - def _search_invoice_ids(self, operator, value): if operator == 'in' and value: self.env.cr.execute(""" @@ -221,8 +213,8 @@ class SaleOrder(models.Model): ], string='Invoice Status', compute='_get_invoice_status', store=True) note = fields.Html( - 'Terms and conditions', default=_default_note, - compute='_compute_note', store=True, readonly=False) + 'Terms and conditions', + compute='_compute_note', store=True, readonly=False, pre_compute=True) terms_type = fields.Selection(related='company_id.terms_type') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, compute='_amount_all', tracking=5) @@ -443,7 +435,7 @@ class SaleOrder(models.Model): 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_base_url() + '/terms') + 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): order.note = order.with_context(lang=order.partner_id.lang).env.company.invoice_terms diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 7977fa164e9..fb53371f80a 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -30,11 +30,11 @@ class SaleOrder(models.Model): help='Website through which this order was placed.') @api.model - def _default_note_url(self): + def _get_note_url(self): website = self.env['website'].get_current_website() if website: return website.get_base_url() - return super()._default_note_url() + return super()._get_note_url() @api.depends('order_line') def _compute_website_order_line(self): From 3c07a3fecdee25c0d9db53c5293334f6ce5e1c67 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Thu, 18 Nov 2021 17:41:24 +0100 Subject: [PATCH 48/65] [IMP] sale: Apply misc styling improvements + remove dead context keys --- addons/sale/models/account_move.py | 4 ++-- addons/sale/models/product_template.py | 2 +- addons/sale/models/sale_order.py | 19 +++++++++---------- addons/sale/models/sale_order_line.py | 12 ++---------- addons/sale/tests/__init__.py | 14 +++++++------- addons/sale/tests/test_sale_order.py | 4 ++-- addons/sale_coupon/models/sale_order.py | 1 - .../sale_coupon/tests/test_program_numbers.py | 2 +- addons/sale_management/models/sale_order.py | 4 ++-- .../sale_product_matrix/models/sale_order.py | 5 +---- .../models/sale_order.py | 4 ++-- .../models/sale_order_template.py | 2 +- .../tests/test_project_billing.py | 2 +- addons/website_sale/controllers/main.py | 2 +- addons/website_sale/models/website.py | 8 +++++--- 15 files changed, 37 insertions(+), 48 deletions(-) diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py index 54ca3113233..ba9fb504bdc 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -34,9 +34,9 @@ class AccountMove(models.Model): """ Trigger the change of fiscal position when the shipping address is modified. """ - delivery_partner_id = self.env['res.partner'].browse(self._get_invoice_delivery_partner_id()) + delivery_partner = self.env['res.partner'].browse(self._get_invoice_delivery_partner_id()) fiscal_position = self.env['account.fiscal.position'].with_company(self.company_id)._get_fiscal_position( - self.partner_id, delivery=delivery_partner_id) + self.partner_id, delivery=delivery_partner) if fiscal_position: self.fiscal_position_id = fiscal_position diff --git a/addons/sale/models/product_template.py b/addons/sale/models/product_template.py index 8e1fb3d68b9..24035f903c3 100644 --- a/addons/sale/models/product_template.py +++ b/addons/sale/models/product_template.py @@ -53,7 +53,7 @@ class ProductTemplate(models.Model): @api.depends('sale_ok') def _compute_expense_policy(self): - self.filtered(lambda t: not t.sale_ok).update({'expense_policy': 'no'}) + self.filtered(lambda t: not t.sale_ok).expense_policy = 'no' @api.depends('product_variant_ids.sales_count') def _compute_sales_count(self): diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 519deddb0fb..5d03d692da7 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -175,25 +175,24 @@ class SaleOrder(models.Model): 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=False, required=True, pre_compute=True, + 'res.partner', string='Invoice Address', required=True, + compute='_compute_partner_invoice_id', store=True, readonly=False, pre_compute=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - compute='_compute_partner_invoice_id', store=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) partner_shipping_id = fields.Many2one( - 'res.partner', string='Delivery Address', readonly=False, required=True, + 'res.partner', string='Delivery Address', required=True, + compute='_compute_partner_shipping_id', store=True, readonly=False, pre_compute=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - compute='_compute_partner_shipping_id', store=True, pre_compute=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",) pricelist_id = fields.Many2one( - 'product.pricelist', string='Pricelist', check_company=True, # Unrequired company - required=False, readonly=False, store=True, - compute='_compute_pricelist_id', pre_compute=True, + 'product.pricelist', string='Pricelist', required=False, check_company=True, # Unrequired company + compute='_compute_pricelist_id', store=True, pre_compute=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, pre_compute=True, ondelete="restrict") analytic_account_id = fields.Many2one( 'account.analytic.account', 'Analytic Account', readonly=True, copy=False, check_company=True, # Unrequired company @@ -536,7 +535,7 @@ class SaleOrder(models.Model): vals['name'] = self.env['ir.sequence'].next_by_code( 'sale.order', sequence_date=seq_date) or _('New') - return super(SaleOrder, self).create(vals_list) + 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'): diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 2a4dd458911..a81b6f8ec01 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -418,7 +418,7 @@ class SaleOrderLine(models.Model): # remove packaging if not match the product if line.product_packaging_id.product_id != line.product_id: line.product_packaging_id = False - # suggest biggest suitable packaging + # 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) @@ -629,13 +629,7 @@ class SaleOrderLine(models.Model): for line in self: if not line.product_id: continue - product = line.product_id.with_context( - lang=get_lang(line.env, line.order_id.partner_id.lang).code, - partner=line.order_id.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) + 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') @@ -684,7 +678,6 @@ class SaleOrderLine(models.Model): continue if line.order_id.pricelist_id and line.order_partner_id: product = line.product_id.with_context( - lang=line.order_id.partner_id.lang, partner=line.order_partner_id, quantity=line.product_uom_qty, date=line.order_id.date_order, @@ -796,7 +789,6 @@ class SaleOrderLine(models.Model): line.discount = 0.0 product = line.product_id.with_context( - lang=line.order_id.partner_id.lang, partner=line.order_partner_id, quantity=line.product_uom_qty, date=line.order_id.date_order, diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index 1a5f79fbc8c..f86f2e9323f 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -1,16 +1,16 @@ # -*- 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_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_sale_order.py b/addons/sale/tests/test_sale_order.py index 9a00446db56..01bac878df6 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -122,7 +122,7 @@ class TestSaleOrder(TestSaleCommon): order = self.env['sale.order'].create({ 'partner_id': self.partner_a.id, }) - order_lines = self.env['sale.order.line'].create([ + self.env['sale.order.line'].create([ { 'display_type': 'line_section', 'name': 'Dummy section', @@ -800,7 +800,7 @@ class TestSaleOrder(TestSaleCommon): product = product_tmpl.product_variant_id fpos = self.env['account.fiscal.position'].create(dict(name="fiscal position", sequence=1)) - fp_tax_id = self.env['account.fiscal.position.tax'].create({ + self.env['account.fiscal.position.tax'].create({ 'position_id': fpos.id, 'tax_src_id': tax_include.id, 'tax_dest_id': tax_exclude.id, diff --git a/addons/sale_coupon/models/sale_order.py b/addons/sale_coupon/models/sale_order.py index 7b234ca1de6..c2f54dcbd6c 100644 --- a/addons/sale_coupon/models/sale_order.py +++ b/addons/sale_coupon/models/sale_order.py @@ -501,7 +501,6 @@ class SaleOrderLine(models.Model): related_program_lines |= line.order_id.order_line.filtered(lambda l: l.product_id.id == related_program.discount_line_product_id.id) - line return super(SaleOrderLine, self | related_program_lines).unlink() - @api.depends('product_id') def _compute_tax_id(self): reward_lines = self.filtered('is_reward_line') super(SaleOrderLine, self - reward_lines)._compute_tax_id() diff --git a/addons/sale_coupon/tests/test_program_numbers.py b/addons/sale_coupon/tests/test_program_numbers.py index b5659b497dc..5323a03a58b 100644 --- a/addons/sale_coupon/tests/test_program_numbers.py +++ b/addons/sale_coupon/tests/test_program_numbers.py @@ -6,7 +6,7 @@ from odoo.exceptions import UserError from odoo.tests import tagged -@tagged('post_install', '-at_install', 'program_numbers') +@tagged('post_install', '-at_install') class TestSaleCouponProgramNumbers(TestSaleCouponCommon): def setUp(self): diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 070079e6477..db5f5f0cfe0 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -52,8 +52,8 @@ class SaleOrder(models.Model): @api.depends('partner_id', 'sale_order_template_id') def _compute_note(self): - super(SaleOrder, self)._compute_note() - for order in self: + super()._compute_note() + for order in self.filtered('sale_order_template_id'): template = order.sale_order_template_id.with_context(lang=order.partner_id.lang) order.note = template.note if not is_html_empty(template.note) else order.note diff --git a/addons/sale_product_matrix/models/sale_order.py b/addons/sale_product_matrix/models/sale_order.py index 7f6ab29955f..a01c40fda2d 100644 --- a/addons/sale_product_matrix/models/sale_order.py +++ b/addons/sale_product_matrix/models/sale_order.py @@ -45,7 +45,6 @@ class SaleOrder(models.Model): if self.grid and self.grid_update: grid = json.loads(self.grid) product_template = self.env['product.template'].browse(grid['product_template_id']) - product_ids = set() dirty_cells = grid['changes'] Attrib = self.env['product.template.attribute.value'] default_so_line_vals = {} @@ -69,8 +68,6 @@ class SaleOrder(models.Model): if not diff: continue - product_ids.add(product.id) - # TODO keep qty check? cannot be 0 because we only get cell changes ... if order_lines: if qty == 0: @@ -115,7 +112,7 @@ class SaleOrder(models.Model): product_uom_qty=qty, product_no_variant_attribute_value_ids=no_variant_attribute_values.ids) )) - if product_ids and new_lines: + if new_lines: # Add new SO lines self.update(dict(order_line=new_lines)) diff --git a/addons/sale_quotation_builder/models/sale_order.py b/addons/sale_quotation_builder/models/sale_order.py index fb81d21650c..09686774f8f 100644 --- a/addons/sale_quotation_builder/models/sale_order.py +++ b/addons/sale_quotation_builder/models/sale_order.py @@ -46,11 +46,11 @@ class SaleOrderLine(models.Model): @api.model_create_multi def create(self, vals_list): vals_list = [self._inject_quotation_description(vals) for vals in vals_list] - return super(SaleOrderLine, self).create(vals_list) + return super().create(vals_list) def write(self, values): values = self._inject_quotation_description(values) - return super(SaleOrderLine, self).write(values) + return super().write(values) def _inject_quotation_description(self, values): values = dict(values or {}) diff --git a/addons/sale_quotation_builder/models/sale_order_template.py b/addons/sale_quotation_builder/models/sale_order_template.py index 5c4b8d86111..b55008926bc 100644 --- a/addons/sale_quotation_builder/models/sale_order_template.py +++ b/addons/sale_quotation_builder/models/sale_order_template.py @@ -34,7 +34,7 @@ class SaleOrderTemplateLine(models.Model): @api.model_create_multi def create(self, vals_list): vals_list = [self._inject_quotation_description(vals) for vals in vals_list] - return super(SaleOrderTemplateLine, self).create(vals_list) + return super().create(vals_list) def write(self, values): values = self._inject_quotation_description(values) diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py index c306fc865df..2022ca80355 100644 --- a/addons/sale_timesheet/tests/test_project_billing.py +++ b/addons/sale_timesheet/tests/test_project_billing.py @@ -5,7 +5,7 @@ from odoo.fields import Command from odoo.tests import tagged from odoo.tests.common import Form -@tagged('post_install', '-at_install', 'project_billing') +@tagged('post_install', '-at_install') class TestProjectBilling(TestCommonSaleTimesheet): """ This test suite provide checks for miscellaneous small things. """ diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index eecfcccd1c4..cb75ad3e720 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -834,7 +834,7 @@ class WebsiteSale(http.Controller): else: partner_id = self._checkout_form_save(mode, post, kw) if mode[1] == 'billing': - order.with_context(not_self_saleperson=True).partner_id = partner_id + order.partner_id = partner_id # This is the *only* thing that the front end user will see/edit anyway when choosing billing address order.partner_invoice_id = partner_id if not kw.get('use_same'): diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 7a587aaad2f..88e67e3e70b 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -328,9 +328,11 @@ class Website(models.Model): fiscal_position = sale_order.fiscal_position_id.id # change the partner, and trigger the onchange - sale_order.with_context(not_self_saleperson=True).write({'partner_id': partner.id}) - sale_order.write({'partner_invoice_id': partner.id}) - sale_order['payment_term_id'] = self.sale_get_payment_term(partner) + sale_order.write({ + 'partner_id': partner.id, + 'partner_invoice_id': partner.id, + 'payment_term_id': self.sale_get_payment_term(partner), + }) # check the pricelist : update it if the pricelist is not the 'forced' one values = {} From 795aa94cef8fbf92828d1553052411dff3507fe0 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 22 Nov 2021 12:34:36 +0100 Subject: [PATCH 49/65] [IMP] sale: Make several fields pre_compute=True to avoid double computations --- addons/sale/models/sale_order.py | 2 +- addons/sale/models/sale_order_line.py | 38 ++++++++++++++++----------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 5d03d692da7..f443528f5c7 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -266,7 +266,7 @@ class SaleOrder(models.Model): string='Authorized Transactions', copy=False) show_update_pricelist = fields.Boolean( string='Has Pricelist Changed', - compute='_compute_show_update_pricelist', store=True, readonly=True, + compute='_compute_show_update_pricelist', store=True, readonly=True, pre_compute=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') diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index a81b6f8ec01..0614f13e74e 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -230,17 +230,19 @@ class SaleOrderLine(models.Model): 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(compute='_compute_price_reduce', string='Price Reduce', digits='Product Price', store=True) + price_reduce = fields.Float( + string='Price Reduce', digits='Product Price', + compute='_compute_price_reduce', store=True, pre_compute=True) tax_id = fields.Many2many( 'account.tax', string='Taxes', - compute='_compute_tax_id', store=True, readonly=False, + compute='_compute_tax_id', store=True, readonly=False, pre_compute=True, 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) discount = fields.Float( string='Discount (%)', digits='Discount', - compute='_compute_discount', store=True, readonly=False) + compute='_compute_discount', store=True, readonly=False, pre_compute=True) product_id = fields.Many2one( 'product.product', string='Product', domain="[('sale_ok', '=', True), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", @@ -261,18 +263,18 @@ class SaleOrderLine(models.Model): 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) + compute='_compute_custom_attribute_values', store=True, readonly=False, pre_compute=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', - compute='_compute_no_variant_attribute_values', store=True, readonly=False) + compute='_compute_no_variant_attribute_values', store=True, readonly=False, pre_compute=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, pre_compute=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" @@ -280,17 +282,21 @@ class SaleOrderLine(models.Model): " - Stock Moves: the quantity comes from confirmed pickings\n") qty_delivered = fields.Float( 'Delivered Quantity', copy=False, - compute='_compute_qty_delivered', store=True, readonly=False, + compute='_compute_qty_delivered', store=True, readonly=False, pre_compute=True, digits='Product Unit of Measure') qty_to_invoice = fields.Float( - compute='_compute_qty_to_invoice', string='To Invoice Quantity', store=True, + compute='_compute_qty_to_invoice', string='To Invoice Quantity', store=True, pre_compute=True, digits='Product Unit of Measure') qty_invoiced = fields.Float( - compute='_compute_qty_invoiced', string='Invoiced Quantity', store=True, + compute='_compute_qty_invoiced', string='Invoiced Quantity', store=True, pre_compute=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, pre_compute=True) + untaxed_amount_to_invoice = fields.Monetary( + "Untaxed Amount To Invoice", + compute='_compute_untaxed_amount_to_invoice', store=True, pre_compute=True) analytic_tag_ids = fields.Many2many( 'account.analytic.tag', string='Analytic Tags', @@ -302,7 +308,9 @@ class SaleOrderLine(models.Model): " 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', pre_compute=True) + currency_id = fields.Many2one( + related='order_id.currency_id', depends=['order_id.currency_id'], + store=True, string='Currency', pre_compute=True) company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, index=True, pre_compute=True) order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer', pre_compute=True) salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', pre_compute=True) From 28b66bad3160606f5b2bc648f42717ded5bebb70 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 22 Nov 2021 12:35:02 +0100 Subject: [PATCH 50/65] [FIX] sale: Erase product attributes values if no product on SOL --- addons/sale/models/sale_order_line.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 0614f13e74e..f4964e84599 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -644,6 +644,7 @@ class SaleOrderLine(models.Model): 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 @@ -655,6 +656,7 @@ class SaleOrderLine(models.Model): 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 From 5914f5fe24eab8d0c8a0feade0befdfd572d3b20 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 22 Nov 2021 12:35:23 +0100 Subject: [PATCH 51/65] [FIX] sale_stock: Retrieve default warehouse according to SO company --- addons/sale_stock/models/sale_order.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 3ba3e81a5a0..a84243d4d5d 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -24,8 +24,8 @@ class SaleOrder(models.Model): ('direct', 'As soon as possible'), ('one', 'When all products are ready')], string='Shipping Policy', required=True, readonly=True, default='direct', - states={'draft': [('readonly', False)], 'sent': [('readonly', False)]} - ,help="If you deliver all products at once, the delivery order will be scheduled based on the greatest " + states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, + help="If you deliver all products at once, the delivery order will be scheduled based on the greatest " "product lead time. Otherwise, it will be based on the shortest.") warehouse_id = fields.Many2one( 'stock.warehouse', string='Warehouse', required=True, @@ -143,8 +143,9 @@ class SaleOrder(models.Model): @api.depends('user_id', 'company_id') def _compute_warehouse_id(self): - default_warehouse_id = self.env['ir.default'].get_model_defaults('sale.order').get('warehouse_id') for order in self: + default_warehouse_id = self.env['ir.default'].with_company( + order.company_id.id).get_model_defaults('sale.order').get('warehouse_id') if order.company_id and order.company_id != order._origin.company_id: warehouse = default_warehouse_id else: From 586307006609b6e6d5a016b27bbab01d469d1990 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 22 Nov 2021 13:04:40 +0100 Subject: [PATCH 52/65] [IMP] sale: Remove useless SO line values on creation Purpose ======= Those are now the default values that are computed from the field definition. --- addons/event_sale/tests/test_event_sale.py | 6 --- addons/sale/tests/test_reinvoice.py | 23 -------- addons/sale/tests/test_sale_order.py | 12 ----- addons/sale/tests/test_sale_pricelist.py | 12 ----- addons/sale/tests/test_sale_refund.py | 15 ------ addons/sale/tests/test_sale_to_invoice.py | 18 ------- addons/sale_gift_card/models/sale_order.py | 2 - .../sale_project/tests/test_sale_project.py | 12 ----- .../sale_purchase/tests/test_access_rights.py | 3 -- .../sale_purchase/tests/test_sale_purchase.py | 18 ------- addons/sale_timesheet/tests/common.py | 2 - .../sale_timesheet/tests/common_reporting.py | 15 ------ .../tests/test_project_billing.py | 12 ----- addons/sale_timesheet/tests/test_reinvoice.py | 15 ------ addons/sale_timesheet/tests/test_reporting.py | 9 ---- .../sale_timesheet/tests/test_sale_service.py | 54 ------------------- .../tests/test_sale_timesheet.py | 39 -------------- .../tests/test_upsell_warning.py | 2 - .../stock_dropshipping/tests/test_dropship.py | 2 - ...ite_sale_product_attribute_value_config.py | 4 -- 20 files changed, 275 deletions(-) diff --git a/addons/event_sale/tests/test_event_sale.py b/addons/event_sale/tests/test_event_sale.py index 8991aa7ea64..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,10 +235,7 @@ 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, 'order_id': so.id, 'event_id': event.id, 'event_ticket_id': event_ticket.id, diff --git a/addons/sale/tests/test_reinvoice.py b/addons/sale/tests/test_reinvoice.py index 14633a4e053..c328070e520 100644 --- a/addons/sale/tests/test_reinvoice.py +++ b/addons/sale/tests/test_reinvoice.py @@ -38,21 +38,15 @@ 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_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, }) @@ -154,21 +148,15 @@ 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_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, }) self.sale_order.action_confirm() @@ -230,12 +218,9 @@ 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.action_confirm() @@ -256,20 +241,12 @@ 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_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, }) diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index 01bac878df6..04f09818e01 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -50,38 +50,26 @@ 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, }) diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index f4fd05947a8..38c51028ed6 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -80,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, }) 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_gift_card/models/sale_order.py b/addons/sale_gift_card/models/sale_order.py index ca20389a610..5237ce90425 100644 --- a/addons/sale_gift_card/models/sale_order.py +++ b/addons/sale_gift_card/models/sale_order.py @@ -43,8 +43,6 @@ class SaleOrder(models.Model): self.env["sale.order.line"].create({ 'product_id': pay_gift_card_id.id, 'price_unit': - amount, - 'product_uom_qty': 1, - 'product_uom': pay_gift_card_id.uom_id.id, 'gift_card_id': gift_card.id, 'order_id': self.id }) diff --git a/addons/sale_project/tests/test_sale_project.py b/addons/sale_project/tests/test_sale_project.py index d4b9ac0e6a9..326eb31ab2d 100644 --- a/addons/sale_project/tests/test_sale_project.py +++ b/addons/sale_project/tests/test_sale_project.py @@ -91,38 +91,26 @@ class TestSaleProject(TransactionCase): 'partner_shipping_id': partner.id, }) so_line_order_no_task = SaleOrderLine.create({ - 'name': self.product_order_service1.name, 'product_id': self.product_order_service1.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_service1.uom_id.id, - 'price_unit': self.product_order_service1.list_price, 'order_id': sale_order.id, }) so_line_order_task_in_global = SaleOrderLine.create({ - 'name': self.product_order_service2.name, 'product_id': self.product_order_service2.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_service2.uom_id.id, - 'price_unit': self.product_order_service2.list_price, 'order_id': sale_order.id, }) so_line_order_new_task_new_project = SaleOrderLine.create({ - 'name': self.product_order_service3.name, 'product_id': self.product_order_service3.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_service3.uom_id.id, - 'price_unit': self.product_order_service3.list_price, 'order_id': sale_order.id, }) so_line_order_only_project = SaleOrderLine.create({ - 'name': self.product_order_service4.name, 'product_id': self.product_order_service4.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_service4.uom_id.id, - 'price_unit': self.product_order_service4.list_price, 'order_id': sale_order.id, }) sale_order.action_confirm() diff --git a/addons/sale_purchase/tests/test_access_rights.py b/addons/sale_purchase/tests/test_access_rights.py index 5240bc0ed19..936c3adc8f2 100644 --- a/addons/sale_purchase/tests/test_access_rights.py +++ b/addons/sale_purchase/tests/test_access_rights.py @@ -39,11 +39,8 @@ class TestAccessRights(TestCommonSalePurchaseNoChart): }) sol_service_purchase = self.env['sale.order.line'].with_user(self.user_salesperson).create({ - 'name': self.service_purchase_1.name, 'product_id': self.service_purchase_1.id, 'product_uom_qty': 4, - 'product_uom': self.service_purchase_1.uom_id.id, - 'price_unit': self.service_purchase_1.list_price, 'order_id': sale_order.id, 'tax_id': False, }) diff --git a/addons/sale_purchase/tests/test_sale_purchase.py b/addons/sale_purchase/tests/test_sale_purchase.py index 7214a023400..d0f6005d2ce 100644 --- a/addons/sale_purchase/tests/test_sale_purchase.py +++ b/addons/sale_purchase/tests/test_sale_purchase.py @@ -22,29 +22,20 @@ class TestSalePurchase(TestCommonSalePurchaseNoChart): 'pricelist_id': cls.company_data['default_pricelist'].id, }) cls.sol1_service_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': 1, - '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_1.id, 'tax_id': False, }) cls.sol1_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_1.id, 'tax_id': False, }) cls.sol1_service_purchase_1 = cls.env['sale.order.line'].create({ - 'name': cls.service_purchase_1.name, 'product_id': cls.service_purchase_1.id, 'product_uom_qty': 4, - 'product_uom': cls.service_purchase_1.uom_id.id, - 'price_unit': cls.service_purchase_1.list_price, 'order_id': cls.sale_order_1.id, 'tax_id': False, }) @@ -56,29 +47,20 @@ class TestSalePurchase(TestCommonSalePurchaseNoChart): 'pricelist_id': cls.company_data['default_pricelist'].id, }) cls.sol2_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': 5, - '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_2.id, 'tax_id': False, }) cls.sol2_service_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': 6, - '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_2.id, 'tax_id': False, }) cls.sol2_service_purchase_2 = cls.env['sale.order.line'].create({ - 'name': cls.service_purchase_2.name, 'product_id': cls.service_purchase_2.id, 'product_uom_qty': 7, - 'product_uom': cls.service_purchase_2.uom_id.id, - 'price_unit': cls.service_purchase_2.list_price, 'order_id': cls.sale_order_2.id, 'tax_id': False, }) diff --git a/addons/sale_timesheet/tests/common.py b/addons/sale_timesheet/tests/common.py index 2645a985537..b65e01ad870 100644 --- a/addons/sale_timesheet/tests/common.py +++ b/addons/sale_timesheet/tests/common.py @@ -351,10 +351,8 @@ class TestCommonSaleTimesheet(TestSaleCommon): }) self.env['sale.order.line'].create([{ 'order_id': self.so.id, - 'name': self.product_delivery_timesheet1.name, 'product_id': self.product_delivery_timesheet1.id, 'product_uom_qty': 10, - 'price_unit': self.product_delivery_timesheet1.list_price }, { 'order_id': self.so.id, 'product_id': self.product_delivery_timesheet2.id, diff --git a/addons/sale_timesheet/tests/common_reporting.py b/addons/sale_timesheet/tests/common_reporting.py index 9ca5549e197..03814923b24 100644 --- a/addons/sale_timesheet/tests/common_reporting.py +++ b/addons/sale_timesheet/tests/common_reporting.py @@ -52,19 +52,13 @@ class TestCommonReporting(TestCommonSaleTimesheet): 'analytic_account_id': cls.analytic_account_1.id, }) cls.so_line_deliver_project = cls.env['sale.order.line'].create({ - 'name': cls.product_delivery_timesheet3.name, 'product_id': cls.product_delivery_timesheet3.id, 'product_uom_qty': 5, - 'product_uom': cls.product_delivery_timesheet3.uom_id.id, - 'price_unit': cls.product_delivery_timesheet3.list_price, 'order_id': cls.sale_order_1.id, }) cls.so_line_deliver_task = cls.env['sale.order.line'].create({ - 'name': cls.product_delivery_timesheet2.name, 'product_id': cls.product_delivery_timesheet2.id, 'product_uom_qty': 7, - 'product_uom': cls.product_delivery_timesheet2.uom_id.id, - 'price_unit': cls.product_delivery_timesheet2.list_price, 'order_id': cls.sale_order_1.id, }) @@ -75,19 +69,13 @@ class TestCommonReporting(TestCommonSaleTimesheet): 'analytic_account_id': cls.analytic_account_2.id, }) cls.so_line_order_project = cls.env['sale.order.line'].create({ - 'name': cls.product_order_timesheet3.name, 'product_id': cls.product_order_timesheet3.id, 'product_uom_qty': 5, - 'product_uom': cls.product_order_timesheet3.uom_id.id, - 'price_unit': cls.product_order_timesheet3.list_price, 'order_id': cls.sale_order_2.id, }) cls.so_line_order_task = cls.env['sale.order.line'].create({ - 'name': cls.product_order_timesheet2.name, 'product_id': cls.product_order_timesheet2.id, 'product_uom_qty': 7, - 'product_uom': cls.product_order_timesheet2.uom_id.id, - 'price_unit': cls.product_order_timesheet2.list_price, 'order_id': cls.sale_order_2.id, }) @@ -98,11 +86,8 @@ class TestCommonReporting(TestCommonSaleTimesheet): 'analytic_account_id': cls.analytic_account_3.id, }) cls.so_line_deliver_manual_project = cls.env['sale.order.line'].create({ - 'name': cls.product_delivery_manual3.name, 'product_id': cls.product_delivery_manual3.id, 'product_uom_qty': 11, - 'product_uom': cls.product_delivery_manual3.uom_id.id, - 'price_unit': cls.product_delivery_manual3.list_price, 'order_id': cls.sale_order_3.id, }) diff --git a/addons/sale_timesheet/tests/test_project_billing.py b/addons/sale_timesheet/tests/test_project_billing.py index 2022ca80355..c7078522942 100644 --- a/addons/sale_timesheet/tests/test_project_billing.py +++ b/addons/sale_timesheet/tests/test_project_billing.py @@ -36,20 +36,14 @@ class TestProjectBilling(TestCommonSaleTimesheet): }) cls.so1_line_order_no_task = SaleOrderLine.create({ - 'name': cls.product_order_timesheet1.name, 'product_id': cls.product_order_timesheet1.id, 'product_uom_qty': 10, - 'product_uom': cls.product_order_timesheet1.uom_id.id, - 'price_unit': cls.product_order_timesheet1.list_price, 'order_id': cls.sale_order_1.id, }) cls.so1_line_deliver_no_task = SaleOrderLine.create({ - 'name': cls.product_delivery_timesheet1.name, 'product_id': cls.product_delivery_timesheet1.id, 'product_uom_qty': 10, - 'product_uom': cls.product_delivery_timesheet1.uom_id.id, - 'price_unit': cls.product_delivery_timesheet1.list_price, 'order_id': cls.sale_order_1.id, }) # Sale Order 2, creates 2 project billed at task rate @@ -60,19 +54,13 @@ class TestProjectBilling(TestCommonSaleTimesheet): }) cls.so2_line_deliver_project_task = SaleOrderLine.create({ 'order_id': cls.sale_order_2.id, - 'name': cls.product_delivery_timesheet3.name, 'product_id': cls.product_delivery_timesheet3.id, 'product_uom_qty': 5, - 'product_uom': cls.product_delivery_timesheet3.uom_id.id, - 'price_unit': cls.product_delivery_timesheet3.list_price }) cls.so2_line_deliver_project_template = SaleOrderLine.create({ 'order_id': cls.sale_order_2.id, - 'name': cls.product_delivery_timesheet5.name, 'product_id': cls.product_delivery_timesheet5.id, 'product_uom_qty': 7, - 'product_uom': cls.product_delivery_timesheet5.uom_id.id, - 'price_unit': cls.product_delivery_timesheet5.list_price }) cls.sale_order_2.action_confirm() diff --git a/addons/sale_timesheet/tests/test_reinvoice.py b/addons/sale_timesheet/tests/test_reinvoice.py index 699ac0026fb..f11a533ddf0 100644 --- a/addons/sale_timesheet/tests/test_reinvoice.py +++ b/addons/sale_timesheet/tests/test_reinvoice.py @@ -54,19 +54,13 @@ class TestReInvoice(TestCommonSaleTimesheet): """ 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, - '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_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, - '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, }) @@ -152,21 +146,15 @@ class TestReInvoice(TestCommonSaleTimesheet): """ # 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_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, }) self.sale_order.action_confirm() @@ -245,12 +233,9 @@ class TestReInvoice(TestCommonSaleTimesheet): """ 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_order_no'].name, 'product_id': self.company_data['product_order_no'].id, 'product_uom_qty': 2, 'qty_delivered': 1, - 'product_uom': self.company_data['product_order_no'].uom_id.id, - 'price_unit': self.company_data['product_order_no'].list_price, 'order_id': self.sale_order.id, }) self.sale_order.action_confirm() diff --git a/addons/sale_timesheet/tests/test_reporting.py b/addons/sale_timesheet/tests/test_reporting.py index 23efb406f79..1f2ee2b4020 100644 --- a/addons/sale_timesheet/tests/test_reporting.py +++ b/addons/sale_timesheet/tests/test_reporting.py @@ -420,11 +420,8 @@ class TestReporting(TestCommonReporting): # simulate the auto creation of the SO line for expense, like we confirm a vendor bill. so_line_expense = self.env['sale.order.line'].create({ - 'name': self.product_expense.name, 'product_id': self.product_expense.id, 'product_uom_qty': 0.0, - 'product_uom': self.product_expense.uom_id.id, - 'price_unit': self.product_expense.list_price, # reinvoice at sales price 'order_id': self.sale_order_1.id, 'is_expense': True, }) @@ -575,11 +572,8 @@ class TestReporting(TestCommonReporting): # simulate the auto creation of the SO line for expense, like we confirm a vendor bill. so_line_expense = self.env['sale.order.line'].create({ - 'name': self.product_expense.name, 'product_id': self.product_expense.id, 'product_uom_qty': 0.0, - 'product_uom': self.product_expense.uom_id.id, - 'price_unit': self.product_expense.list_price, # reinvoice at sales price 'order_id': self.sale_order_2.id, 'is_expense': True, }) @@ -701,11 +695,8 @@ class TestReporting(TestCommonReporting): currency = self.env.company.currency_id rounding = currency.rounding so_line_deliver_no_task = self.env['sale.order.line'].create({ - 'name': self.product_delivery_manual1.name, 'product_id': self.product_delivery_manual1.id, 'product_uom_qty': 50, - 'product_uom': self.product_delivery_manual1.uom_id.id, - 'price_unit': self.product_delivery_manual1.list_price, 'order_id': self.sale_order_2.id, }) so_line_deliver_no_task.write({'qty_delivered': 1.0}) diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py index 5b8f0ca2a5c..64d753caec2 100644 --- a/addons/sale_timesheet/tests/test_sale_service.py +++ b/addons/sale_timesheet/tests/test_sale_service.py @@ -27,8 +27,6 @@ class TestSaleService(TestCommonSaleTimesheet): 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 50, - 'product_uom': self.product_delivery_timesheet2.uom_id.id, - 'price_unit': self.product_delivery_timesheet2.list_price }) self.sale_order.order_line._compute_product_updatable() @@ -79,11 +77,8 @@ class TestSaleService(TestCommonSaleTimesheet): }) self.env['sale.order.line'].create({ - 'name': product_service_task.name, 'product_id': product_service_task.id, 'product_uom_qty': 10, - 'product_uom': product_service_task.uom_id.id, - 'price_unit': product_service_task.list_price, 'order_id': self.sale_order.id, }) @@ -102,11 +97,9 @@ class TestSaleService(TestCommonSaleTimesheet): uom_days = self.env.ref('uom.product_uom_day') sale_order_line = self.env['sale.order.line'].create({ 'order_id': self.sale_order.id, - 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 5, 'product_uom': uom_days.id, - 'price_unit': self.product_delivery_timesheet3.list_price }) self.sale_order.action_confirm() task = self.env['project.task'].search([('sale_line_id', '=', sale_order_line.id)]) @@ -134,11 +127,8 @@ class TestSaleService(TestCommonSaleTimesheet): def test_task_so_line_assignation(self): # create SO line and confirm it so_line_deliver_global_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 10, - 'product_uom': self.product_delivery_timesheet2.uom_id.id, - 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': self.sale_order.id, }) self.sale_order.action_confirm() @@ -179,11 +169,8 @@ class TestSaleService(TestCommonSaleTimesheet): def test_delivered_quantity(self): # create SO line and confirm it so_line_deliver_new_task_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 10, - 'product_uom': self.product_delivery_timesheet3.uom_id.id, - 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': self.sale_order.id, }) self.sale_order.action_confirm() @@ -229,11 +216,8 @@ class TestSaleService(TestCommonSaleTimesheet): the ordered quantity of a SO line that have created a task should update the planned hours of this task. """ so_line1 = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 7, - 'product_uom': self.product_delivery_timesheet3.uom_id.id, - 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': self.sale_order.id, }) @@ -316,43 +300,28 @@ class TestSaleService(TestCommonSaleTimesheet): # create 5 so lines so_line1 = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet5.name, 'product_id': self.product_delivery_timesheet5.id, 'product_uom_qty': 11, - 'product_uom': self.product_delivery_timesheet5.uom_id.id, - 'price_unit': self.product_delivery_timesheet5.list_price, 'order_id': self.sale_order.id, }) so_line2 = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet4.name, 'product_id': self.product_order_timesheet4.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_timesheet4.uom_id.id, - 'price_unit': self.product_order_timesheet4.list_price, 'order_id': self.sale_order.id, }) so_line3 = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet5.name, 'product_id': self.product_delivery_timesheet5.id, 'product_uom_qty': 5, - 'product_uom': self.product_delivery_timesheet5.uom_id.id, - 'price_unit': self.product_delivery_timesheet5.list_price, 'order_id': self.sale_order.id, }) so_line4 = self.env['sale.order.line'].create({ - 'name': self.product_delivery_manual3.name, 'product_id': self.product_delivery_manual3.id, 'product_uom_qty': 4, - 'product_uom': self.product_delivery_manual3.uom_id.id, - 'price_unit': self.product_delivery_manual3.list_price, 'order_id': self.sale_order.id, }) so_line5 = self.env['sale.order.line'].create({ - 'name': product_deli_ts_tmpl.name, 'product_id': product_deli_ts_tmpl.id, 'product_uom_qty': 8, - 'product_uom': product_deli_ts_tmpl.uom_id.id, - 'price_unit': product_deli_ts_tmpl.list_price, 'order_id': self.sale_order.id, }) @@ -410,27 +379,18 @@ class TestSaleService(TestCommonSaleTimesheet): self.assertEqual(self.sale_order.analytic_account_id, self.analytic_account_sale, "Changing the project on the SO should set the analytic account accordingly.") so_line1 = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet3.name, 'product_id': self.product_order_timesheet3.id, 'product_uom_qty': 11, - 'product_uom': self.product_order_timesheet3.uom_id.id, - 'price_unit': self.product_order_timesheet3.list_price, 'order_id': self.sale_order.id, }) so_line2 = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet3.name, 'product_id': self.product_order_timesheet3.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_timesheet3.uom_id.id, - 'price_unit': self.product_order_timesheet3.list_price, 'order_id': self.sale_order.id, }) so_line3 = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet4.name, 'product_id': self.product_order_timesheet4.id, 'product_uom_qty': 5, - 'product_uom': self.product_order_timesheet4.uom_id.id, - 'price_unit': self.product_order_timesheet4.list_price, 'order_id': self.sale_order.id, }) @@ -467,11 +427,8 @@ class TestSaleService(TestCommonSaleTimesheet): """ so_line1 = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet3.name, 'product_id': self.product_order_timesheet3.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_timesheet3.uom_id.id, - 'price_unit': self.product_order_timesheet3.list_price, 'order_id': self.sale_order.id, }) @@ -493,19 +450,14 @@ class TestSaleService(TestCommonSaleTimesheet): """ Test if subtasks and tasks are billed on the correct SO line """ # create SO line and confirm it so_line_deliver_new_task_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 10, - 'product_uom': self.product_delivery_timesheet3.uom_id.id, - 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': self.sale_order.id, }) so_line_deliver_new_task_project_2 = self.env['sale.order.line'].create({ 'name': self.product_delivery_timesheet3.name + "(2)", 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 10, - 'product_uom': self.product_delivery_timesheet3.uom_id.id, - 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': self.sale_order.id, }) self.sale_order.action_confirm() @@ -552,11 +504,8 @@ class TestSaleService(TestCommonSaleTimesheet): """ Changing the ordered quantity of a SO line that have created a task should update the planned hours of this task """ sale_order_line = self.env['sale.order.line'].create({ 'order_id': self.sale_order.id, - 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 50, - 'product_uom': self.product_delivery_timesheet2.uom_id.id, - 'price_unit': self.product_delivery_timesheet2.list_price }) self.sale_order.action_confirm() @@ -576,11 +525,8 @@ class TestSaleService(TestCommonSaleTimesheet): def test_copy_billable_project_and_task(self): sale_order_line = self.env['sale.order.line'].create({ 'order_id': self.sale_order.id, - 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 5, - 'product_uom': self.product_delivery_timesheet3.uom_id.id, - 'price_unit': self.product_delivery_timesheet3.list_price }) self.sale_order.action_confirm() task = self.env['project.task'].search([('sale_line_id', '=', sale_order_line.id)]) diff --git a/addons/sale_timesheet/tests/test_sale_timesheet.py b/addons/sale_timesheet/tests/test_sale_timesheet.py index b0bff62ae92..c5743272622 100644 --- a/addons/sale_timesheet/tests/test_sale_timesheet.py +++ b/addons/sale_timesheet/tests/test_sale_timesheet.py @@ -35,19 +35,13 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'pricelist_id': self.company_data['default_pricelist'].id, }) so_line_ordered_project_only = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet4.name, 'product_id': self.product_order_timesheet4.id, 'product_uom_qty': 10, - 'product_uom': self.product_order_timesheet4.uom_id.id, - 'price_unit': self.product_order_timesheet4.list_price, 'order_id': sale_order.id, }) so_line_ordered_global_project = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet2.name, 'product_id': self.product_order_timesheet2.id, 'product_uom_qty': 50, - 'product_uom': self.product_order_timesheet2.uom_id.id, - 'price_unit': self.product_order_timesheet2.list_price, 'order_id': sale_order.id, }) sale_order.action_confirm() @@ -109,11 +103,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): # add so line with produdct "create task in new project". so_line_ordered_task_in_project = self.env['sale.order.line'].create({ - 'name': self.product_order_timesheet3.name, 'product_id': self.product_order_timesheet3.id, 'product_uom_qty': 3, - 'product_uom': self.product_order_timesheet3.uom_id.id, - 'price_unit': self.product_order_timesheet3.list_price, 'order_id': sale_order.id, }) @@ -171,19 +162,13 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'pricelist_id': self.company_data['default_pricelist'].id, }) so_line_deliver_global_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 50, - 'product_uom': self.product_delivery_timesheet2.uom_id.id, - 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': sale_order.id, }) so_line_deliver_task_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 20, - 'product_uom': self.product_delivery_timesheet3.uom_id.id, - 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': sale_order.id, }) @@ -248,11 +233,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): # add a line on SO so_line_deliver_only_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet4.name, 'product_id': self.product_delivery_timesheet4.id, 'product_uom_qty': 5, - 'product_uom': self.product_delivery_timesheet4.uom_id.id, - 'price_unit': self.product_delivery_timesheet4.list_price, 'order_id': sale_order.id, }) self.assertEqual(len(sale_order.project_ids), 2, "No new project should have been created by the SO, when selling 'project only' product, since it reuse the one from 'new task in new project'.") @@ -303,19 +285,13 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'pricelist_id': self.company_data['default_pricelist'].id, }) so_line_manual_global_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_manual2.name, 'product_id': self.product_delivery_manual2.id, 'product_uom_qty': 50, - 'product_uom': self.product_delivery_manual2.uom_id.id, - 'price_unit': self.product_delivery_manual2.list_price, 'order_id': sale_order.id, }) so_line_manual_only_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_manual4.name, 'product_id': self.product_delivery_manual4.id, 'product_uom_qty': 20, - 'product_uom': self.product_delivery_manual4.uom_id.id, - 'price_unit': self.product_delivery_manual4.list_price, 'order_id': sale_order.id, }) @@ -396,19 +372,13 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'display_type': 'line_section', }) so_line_deliver_global_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 50, - 'product_uom': self.product_delivery_timesheet2.uom_id.id, - 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': sale_order.id, }) so_line_deliver_task_project = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 20, - 'product_uom': self.product_delivery_timesheet3.uom_id.id, - 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': sale_order.id, }) @@ -603,19 +573,13 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): 'pricelist_id': self.company_data['default_pricelist'].id, }) so1_product_global_project_so_line = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 50, - 'product_uom': self.product_delivery_timesheet2.uom_id.id, - 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': sale_order1.id, }) so2_product_global_project_so_line = self.env['sale.order.line'].create({ - 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 20, - 'product_uom': self.product_delivery_timesheet2.uom_id.id, - 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': sale_order2.id, }) @@ -674,11 +638,8 @@ class TestSaleTimesheet(TestCommonSaleTimesheet): uom_days = self.env.ref('uom.product_uom_day') sale_order_line = self.env['sale.order.line'].create({ 'order_id': sale_order.id, - 'name': self.product_order_timesheet3.name, 'product_id': self.product_order_timesheet3.id, - 'product_uom_qty': 1, 'product_uom': uom_days.id, - 'price_unit': self.product_order_timesheet3.list_price, }) sale_order.action_confirm() task = sale_order_line.task_id diff --git a/addons/sale_timesheet/tests/test_upsell_warning.py b/addons/sale_timesheet/tests/test_upsell_warning.py index 8fc8f5f6584..6811ce3cc1a 100644 --- a/addons/sale_timesheet/tests/test_upsell_warning.py +++ b/addons/sale_timesheet/tests/test_upsell_warning.py @@ -37,10 +37,8 @@ class TestUpsellWarning(TestCommonSaleTimesheet): self.env['sale.order.line'].create({ 'order_id': so.id, - 'name': self.product_order_timesheet1.name, 'product_id': self.product_order_timesheet1.id, 'product_uom_qty': 10, - 'price_unit': self.product_order_timesheet1.list_price, }) so.action_confirm() diff --git a/addons/stock_dropshipping/tests/test_dropship.py b/addons/stock_dropshipping/tests/test_dropship.py index 21a2d237062..4b0345b1dc5 100644 --- a/addons/stock_dropshipping/tests/test_dropship.py +++ b/addons/stock_dropshipping/tests/test_dropship.py @@ -55,10 +55,8 @@ class TestDropship(common.TransactionCase): # Create a new so line sol2 = self.env['sale.order.line'].create({ 'order_id': so.id, - 'name': prod.name, 'product_id': prod.id, 'product_uom_qty': 3.00, - 'product_uom': prod.uom_id.id, 'price_unit': 12, }) # there is a new line diff --git a/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py index 51629cd40a5..756938cbd71 100644 --- a/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py +++ b/addons/website_sale/tests/test_website_sale_product_attribute_value_config.py @@ -174,11 +174,7 @@ 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])], }) From 48401aa9a0d40db1e0711063ed27e6a9f761e89e Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 22 Nov 2021 13:21:48 +0100 Subject: [PATCH 53/65] [IMP] sale: Convert _onchange_partner_shipping_id into computed field --- addons/account/models/account_move.py | 6 +++--- addons/sale/models/account_move.py | 24 ++++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 05e1b70f524..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.") diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py index ba9fb504bdc..5b12b143188 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'] @@ -24,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 = self.env['res.partner'].browse(self._get_invoice_delivery_partner_id()) - fiscal_position = self.env['account.fiscal.position'].with_company(self.company_id)._get_fiscal_position( - self.partner_id, delivery=delivery_partner) + fiscal_position_id = fields.Many2one( + compute='_compute_fiscal_position_id', store=True, pre_compute=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')) From 232d66069a2dab0653b7c851c3c3cda8fe7d7f63 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 22 Nov 2021 13:21:44 +0100 Subject: [PATCH 54/65] [IMP] sale: Make some field precomputed --- addons/event_sale/models/sale_order.py | 4 +- addons/hr_timesheet/models/hr_timesheet.py | 7 ++- addons/project/models/project.py | 8 +-- addons/sale/models/account_move.py | 2 +- addons/sale/models/sale_order.py | 22 +++---- addons/sale/models/sale_order_line.py | 62 ++++++++++--------- addons/sale_margin/models/sale_order.py | 6 +- addons/sale_project/models/project.py | 10 +-- addons/sale_stock/models/sale_order.py | 4 +- addons/sale_timesheet/models/account.py | 4 +- addons/sale_timesheet/models/project.py | 5 +- .../models/project_sale_line_employee_map.py | 4 +- addons/stock/models/stock_move.py | 3 +- odoo/addons/base/models/res_partner.py | 7 ++- 14 files changed, 80 insertions(+), 68 deletions(-) diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 09540b474c4..685571079c6 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -61,11 +61,11 @@ class SaleOrderLine(models.Model): event_id = fields.Many2one( 'event.event', string='Event', - compute="_compute_event_id", store=True, readonly=False, pre_compute=True, + 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, pre_compute=True, + 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') diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index c8a888bc27e..c1390dc4fd0 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, precompute=True, 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, precompute=True) 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/project/models/project.py b/addons/project/models/project.py index 858b4fbc1e2..8b4a008d350 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, precompute=True, 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, precompute=True, 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/sale/models/account_move.py b/addons/sale/models/account_move.py index 5b12b143188..89993271f72 100644 --- a/addons/sale/models/account_move.py +++ b/addons/sale/models/account_move.py @@ -32,7 +32,7 @@ class AccountMove(models.Model): source_id = fields.Many2one(ondelete='set null') fiscal_position_id = fields.Many2one( - compute='_compute_fiscal_position_id', store=True, pre_compute=True) + compute='_compute_fiscal_position_id', store=True) @api.depends('partner_shipping_id', 'company_id') diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index f443528f5c7..f87a9758eed 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -167,32 +167,32 @@ class SaleOrder(models.Model): user_id = fields.Many2one( 'res.users', string='Salesperson', index=True, tracking=2, - compute='_compute_user_id', store=True, readonly=False, pre_compute=True, + 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=False, pre_compute=True, + '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', required=True, - compute='_compute_partner_invoice_id', store=True, readonly=False, pre_compute=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', required=True, - compute='_compute_partner_shipping_id', store=True, readonly=False, pre_compute=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', required=False, check_company=True, # Unrequired company - compute='_compute_pricelist_id', store=True, pre_compute=True, readonly=False, + 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, pre_compute=True, ondelete="restrict") + 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 @@ -213,7 +213,7 @@ class SaleOrder(models.Model): note = fields.Html( 'Terms and conditions', - compute='_compute_note', store=True, readonly=False, pre_compute=True) + 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) @@ -224,12 +224,12 @@ class SaleOrder(models.Model): 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, + 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, + 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( @@ -240,7 +240,7 @@ class SaleOrder(models.Model): team_id = fields.Many2one( 'crm.team', 'Sales Team', ondelete="set null", tracking=True, - compute='_compute_team_id', store=True, readonly=False, pre_compute=True, + 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)]") @@ -266,7 +266,7 @@ class SaleOrder(models.Model): string='Authorized Transactions', copy=False) show_update_pricelist = fields.Boolean( string='Has Pricelist Changed', - compute='_compute_show_update_pricelist', store=True, readonly=True, pre_compute=True, + 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') diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index f4964e84599..da4cb582fd0 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -212,7 +212,7 @@ class SaleOrderLine(models.Model): order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False) name = fields.Text( string='Description', required=True, - compute='_compute_name', store=True, readonly=False, pre_compute=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) @@ -221,28 +221,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, pre_compute=True) + ], string='Invoice Status', compute='_compute_invoice_status', store=True, precompute=True) price_unit = fields.Float( 'Unit Price', required=True, digits='Product Price', - compute='_compute_price_unit', store=True, readonly=False, pre_compute=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) + compute='_compute_price_unit', store=True, readonly=False, precompute=True) price_reduce = fields.Float( string='Price Reduce', digits='Product Price', - compute='_compute_price_reduce', store=True, pre_compute=True) + 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) + tax_id = fields.Many2many( 'account.tax', string='Taxes', - compute='_compute_tax_id', store=True, readonly=False, pre_compute=True, + compute='_compute_tax_id', store=True, readonly=False, precompute=True, 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) discount = fields.Float( string='Discount (%)', digits='Discount', - compute='_compute_discount', store=True, readonly=False, pre_compute=True) + 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)]", @@ -253,28 +255,28 @@ class SaleOrderLine(models.Model): 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, pre_compute=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, pre_compute=True, + 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, - compute='_compute_custom_attribute_values', store=True, readonly=False, pre_compute=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', - compute='_compute_no_variant_attribute_values', store=True, readonly=False, pre_compute=True) + 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, pre_compute=True, + 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" @@ -282,21 +284,21 @@ class SaleOrderLine(models.Model): " - Stock Moves: the quantity comes from confirmed pickings\n") qty_delivered = fields.Float( 'Delivered Quantity', copy=False, - compute='_compute_qty_delivered', store=True, readonly=False, pre_compute=True, + compute='_compute_qty_delivered', store=True, readonly=False, precompute=True, digits='Product Unit of Measure') qty_to_invoice = fields.Float( - compute='_compute_qty_to_invoice', string='To Invoice Quantity', store=True, pre_compute=True, + compute='_compute_qty_to_invoice', string='To Invoice Quantity', store=True, precompute=True, digits='Product Unit of Measure') qty_invoiced = fields.Float( - compute='_compute_qty_invoiced', string='Invoiced Quantity', store=True, pre_compute=True, + compute='_compute_qty_invoiced', string='Invoiced Quantity', store=True, precompute=True, digits='Product Unit of Measure') untaxed_amount_invoiced = fields.Monetary( "Untaxed Invoiced Amount", - compute='_compute_untaxed_amount_invoiced', store=True, pre_compute=True) + compute='_compute_untaxed_amount_invoiced', store=True, precompute=True) untaxed_amount_to_invoice = fields.Monetary( "Untaxed Amount To Invoice", - compute='_compute_untaxed_amount_to_invoice', store=True, pre_compute=True) + compute='_compute_untaxed_amount_to_invoice', store=True, precompute=True) analytic_tag_ids = fields.Many2many( 'account.analytic.tag', string='Analytic Tags', @@ -310,16 +312,16 @@ class SaleOrderLine(models.Model): # Related stored fields from the order currency_id = fields.Many2one( related='order_id.currency_id', depends=['order_id.currency_id'], - store=True, string='Currency', pre_compute=True) - company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, index=True, pre_compute=True) - order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer', pre_compute=True) - salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', pre_compute=True) + 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, pre_compute=True) + related='order_id.state', string='Order Status', copy=False, store=True, precompute=True) customer_lead = fields.Float( 'Lead Time', required=True, - compute="_compute_customer_lead", store=True, readonly=False, pre_compute=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([ @@ -328,12 +330,12 @@ class SaleOrderLine(models.Model): product_packaging_id = fields.Many2one( 'product.packaging', string='Packaging', - compute='_compute_product_packaging_id', store=True, readonly=False, pre_compute=True, + 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, pre_compute=True) + 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 diff --git a/addons/sale_margin/models/sale_order.py b/addons/sale_margin/models/sale_order.py index 7f897968245..e1d0696862a 100644 --- a/addons/sale_margin/models/sale_order.py +++ b/addons/sale_margin/models/sale_order.py @@ -8,12 +8,12 @@ class SaleOrderLine(models.Model): margin = fields.Float( "Margin", compute='_compute_margin', - digits='Product Price', store=True, groups="base.group_user") + digits='Product Price', store=True, groups="base.group_user", precompute=True) margin_percent = fields.Float( - "Margin (%)", compute='_compute_margin', store=True, groups="base.group_user") + "Margin (%)", compute='_compute_margin', store=True, groups="base.group_user", precompute=True) purchase_price = fields.Float( string='Cost', compute="_compute_purchase_price", - digits='Product Price', store=True, readonly=False, + digits='Product Price', store=True, readonly=False, precompute=True, groups="base.group_user") @api.depends('product_id', 'company_id', 'currency_id', 'product_uom') diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 871175fb83e..6910bc29ed3 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -12,7 +12,7 @@ class Project(models.Model): sale_line_id = fields.Many2one( 'sale.order.line', 'Sales Order Item', copy=False, - compute="_compute_sale_line_id", store=True, readonly=False, index=True, + compute="_compute_sale_line_id", store=True, readonly=False, index=True, precompute=True, domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', '=?', partner_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Sales order item to which the project is linked. Link the timesheet entry to the sales order item defined on the project. " "Only applies on tasks without sale order item defined, and if the employee is not in the 'Employee/Sales Order Item Mapping' of the project.") @@ -139,8 +139,10 @@ class ProjectTask(models.Model): sale_order_id = fields.Many2one('sale.order', 'Sales Order', compute='_compute_sale_order_id', store=True, help="Sales order to which the task is linked.") sale_line_id = fields.Many2one( - 'sale.order.line', 'Sales Order Item', domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]", - compute='_compute_sale_line', recursive=True, store=True, readonly=False, copy=False, tracking=True, index=True, + 'sale.order.line', 'Sales Order Item', + copy=False, tracking=True, index=True, recursive=True, + compute='_compute_sale_line', store=True, readonly=False, precompute=True, + domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]", help="Sales Order Item to which the time spent on this task will be added, in order to be invoiced to your customer.") project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_id.sale_order_id') invoice_count = fields.Integer("Number of invoices", related='sale_order_id.invoice_count') @@ -164,7 +166,7 @@ class ProjectTask(models.Model): task.partner_id = sale_order_id.partner_id task.sale_order_id = sale_order_id - @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id') + @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id') def _compute_sale_line(self): for task in self: if not task.sale_line_id: diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index a84243d4d5d..9bfa52a269e 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -29,7 +29,7 @@ class SaleOrder(models.Model): "product lead time. Otherwise, it will be based on the shortest.") warehouse_id = fields.Many2one( 'stock.warehouse', string='Warehouse', required=True, - compute='_compute_warehouse_id', store=True, readonly=False, pre_compute=True, + compute='_compute_warehouse_id', store=True, readonly=False, precompute=True, states={'sale': [('readonly', True)], 'done': [('readonly', False)], 'cancel': [('readonly', False)]}, check_company=True) picking_ids = fields.One2many('stock.picking', 'sale_id', string='Transfers') @@ -268,7 +268,7 @@ class SaleOrderLine(models.Model): is_mto = fields.Boolean(compute='_compute_is_mto') display_qty_widget = fields.Boolean(compute='_compute_qty_to_deliver') customer_lead = fields.Float( - compute='_compute_customer_lead', store=True, readonly=False, pre_compute=True, + compute='_compute_customer_lead', store=True, readonly=False, precompute=True, inverse='_inverse_customer_lead') @api.depends('product_type', 'product_uom_qty', 'qty_delivered', 'state', 'move_ids', 'product_uom') diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 4db3957ccc5..3e32189b82b 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -25,7 +25,9 @@ class AccountAnalyticLine(models.Model): compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True) commercial_partner_id = fields.Many2one('res.partner', compute="_compute_commercial_partner") timesheet_invoice_id = fields.Many2one('account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet") - so_line = fields.Many2one(compute="_compute_so_line", store=True, readonly=False, domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', 'child_of', commercial_partner_id)]") + so_line = fields.Many2one( + compute="_compute_so_line", store=True, readonly=False, precompute=True, + domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', 'child_of', commercial_partner_id)]") # we needed to store it only in order to be able to groupby in the portal order_id = fields.Many2one(related='so_line.order_id', store=True, readonly=False) is_so_line_edited = fields.Boolean("Is Sales Order Item Manually Edited") diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index be04cc55c0d..bd774364fa8 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -53,7 +53,8 @@ class Project(models.Model): compute="_compute_timesheet_product_id", store=True, readonly=False, default=_default_timesheet_product_id) warning_employee_rate = fields.Boolean(compute='_compute_warning_employee_rate') - partner_id = fields.Many2one(compute='_compute_partner_id', store=True, readonly=False) + partner_id = fields.Many2one( + compute='_compute_partner_id', store=True, readonly=False, precompute=True) @api.depends('sale_line_id', 'sale_line_employee_ids', 'allow_billable') def _compute_pricing_type(self): @@ -482,7 +483,7 @@ class ProjectTask(models.Model): super(ProjectTask, billable_tasks)._compute_sale_order_id() (self - billable_tasks).sale_order_id = False - @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id.commercial_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'allow_billable') + @api.depends('commercial_partner_id', 'sale_line_id.order_partner_id', 'parent_id.sale_line_id', 'project_id.sale_line_id', 'allow_billable') def _compute_sale_line(self): billable_tasks = self.filtered('allow_billable') (self - billable_tasks).update({'sale_line_id': False}) diff --git a/addons/sale_timesheet/models/project_sale_line_employee_map.py b/addons/sale_timesheet/models/project_sale_line_employee_map.py index 0ca804b41a5..f5c732b649b 100644 --- a/addons/sale_timesheet/models/project_sale_line_employee_map.py +++ b/addons/sale_timesheet/models/project_sale_line_employee_map.py @@ -10,7 +10,9 @@ class ProjectProductEmployeeMap(models.Model): project_id = fields.Many2one('project.project', "Project", required=True) employee_id = fields.Many2one('hr.employee', "Employee", required=True) - sale_line_id = fields.Many2one('sale.order.line', "Sales Order Item", compute="_compute_sale_line_id", store=True, readonly=False, + sale_line_id = fields.Many2one( + 'sale.order.line', "Sales Order Item", + compute="_compute_sale_line_id", store=True, readonly=False, precompute=True, domain="""[ ('is_service', '=', True), ('is_expense', '=', False), diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 4e6f51f9a0c..6d1576ea9af 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -116,7 +116,8 @@ class StockMove(models.Model): "The other possibility allows you to directly create a procurement on the source location (and thus ignore " "its current stock) to gather products. If we want to chain moves and have this one to wait for the previous, " "this second option should be chosen.") - scrapped = fields.Boolean('Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True) + scrapped = fields.Boolean( + 'Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True, precompute=True) scrap_ids = fields.One2many('stock.scrap', 'move_id') group_id = fields.Many2one('procurement.group', 'Procurement Group', default=_default_group_id) rule_id = fields.Many2one( 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') From 9d8b88e86d8a9e5792e5a3ef79ac2c90eed10617 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Thu, 25 Nov 2021 12:29:27 +0100 Subject: [PATCH 55/65] [FIX] sale_timesheet: Avoid pricelist side effect with other modules --- addons/sale_timesheet/tests/common_reporting.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/addons/sale_timesheet/tests/common_reporting.py b/addons/sale_timesheet/tests/common_reporting.py index 03814923b24..ebbdcc0dd2d 100644 --- a/addons/sale_timesheet/tests/common_reporting.py +++ b/addons/sale_timesheet/tests/common_reporting.py @@ -9,6 +9,16 @@ class TestCommonReporting(TestCommonSaleTimesheet): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) + # Note: According to the installed module, a default pricelist is set + # on the created company (see create override on product module) + # Force the pricelist on the company to avoid having a random one + # with advanced discount configuration, etc... + cls.env['ir.property']._set_default( + 'property_product_pricelist', 'res.partner', + cls.company_data['default_pricelist'], + cls.company_data['company'], + ) + # expense product cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({ 'name': "Expense service", From 0f2edc94bf12d6728ba2f5ec9d9bdd490a1d4077 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 29 Nov 2021 17:57:52 +0100 Subject: [PATCH 56/65] [IMP] sale: Use a cache to compute fiscal positions --- addons/sale/models/sale_order.py | 2 +- addons/sale/models/sale_order_line.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index f87a9758eed..f72bd184524 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -403,7 +403,7 @@ class SaleOrder(models.Model): if not order.partner_id: order.fiscal_position_id = False continue - key = (order.company_id, order.partner_id, order.partner_shipping_id) + 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 diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index da4cb582fd0..dbf4fa13536 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -139,9 +139,12 @@ class SaleOrderLine(models.Model): @api.depends('product_id') def _compute_tax_id(self): + cached_fpos = {} 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) + if line.order_id not in cached_fpos: + cached_fpos[line.order_id] = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_id) + fpos = cached_fpos[line.order_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) From 79dedf5024985e134d2a58950260aee18faba7f5 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 29 Nov 2021 18:03:18 +0100 Subject: [PATCH 57/65] [IMP] sale: Don't fetch fiscal position if nothing to map --- addons/sale/models/sale_order_line.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index dbf4fa13536..a649a1a284f 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -142,11 +142,15 @@ class SaleOrderLine(models.Model): cached_fpos = {} for line in self: line = line.with_company(line.company_id) + taxes = line.product_id.taxes_id.filtered(lambda t: t.company_id == line.env.company) + if not line.product_id or not taxes: + # Nothing to map + line.tax_id = [(5, 0, 0)] + continue if line.order_id not in cached_fpos: cached_fpos[line.order_id] = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_id) fpos = cached_fpos[line.order_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_create_multi From 2541fe2067173a67673643e7c8f4bb67ae09fd0f Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 29 Nov 2021 18:16:51 +0100 Subject: [PATCH 58/65] [IMP] sale: Use a cache to avoid computing several time the same sales team --- addons/sale/models/sale_order.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index f72bd184524..3961191c3a2 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -452,13 +452,18 @@ class SaleOrder(models.Model): @api.depends('partner_id', 'user_id') def _compute_team_id(self): + cached_teams = {} for order in self: - order.team_id = self.env['crm.team'].with_context( - default_team_id=(order.team_id or order.partner_id.team_id).id - )._get_default_team_id( - user_id=self.user_id.id, - domain=[('company_id', 'in', [self.company_id.id, False])] - ) + 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): From c574c76bc199c8f219f11c3fc3ed3c5cb6b953c2 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Tue, 30 Nov 2021 11:58:32 +0100 Subject: [PATCH 59/65] [FIX] sale: sale.order.line population product template model is not a populate dependency of sale, do not try to fetch the ids of templates if the model wasn't populated. --- addons/sale/populate/sale_order.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 From b8a0fe79f4fd27c1d74db02e361c5a99034b1cc1 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Tue, 30 Nov 2021 12:33:29 +0100 Subject: [PATCH 60/65] [IMP] sale: remove fpos fallback in SOL compute --- addons/sale/models/sale_order_line.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index a649a1a284f..ad433aef278 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -139,19 +139,15 @@ class SaleOrderLine(models.Model): @api.depends('product_id') def _compute_tax_id(self): - cached_fpos = {} for line in self: line = line.with_company(line.company_id) taxes = line.product_id.taxes_id.filtered(lambda t: t.company_id == line.env.company) if not line.product_id or not taxes: # Nothing to map - line.tax_id = [(5, 0, 0)] + line.tax_id = False continue - if line.order_id not in cached_fpos: - cached_fpos[line.order_id] = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_id) - fpos = cached_fpos[line.order_id] # If company_id is set, always filter taxes by the company - line.tax_id = fpos.map_tax(taxes) + line.tax_id = line.order_id.fiscal_position_id.map_tax(taxes) @api.model_create_multi def create(self, vals_list): From 69dc4bba42abd54491b89905d5c3ab7380575076 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Tue, 30 Nov 2021 17:50:58 +0100 Subject: [PATCH 61/65] [IMP] core,sale: enforce batch creation support for sale order (lines) --- addons/sale/tests/__init__.py | 1 + addons/sale/tests/test_performance.py | 15 +++++++++++++++ odoo/addons/base/tests/common.py | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 addons/sale/tests/test_performance.py diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index f86f2e9323f..b75fb04a21b 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -5,6 +5,7 @@ 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_sale_flow from . import test_sale_order 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/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): From f36334d97a1ed6c567e8f050e4c126d5019864ab Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Tue, 30 Nov 2021 18:01:11 +0100 Subject: [PATCH 62/65] [IMP] sale: onchange tests can now be triggered post_install --- addons/sale/tests/test_onchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/sale/tests/test_onchange.py b/addons/sale/tests/test_onchange.py index 25a0cd2f1e4..8845e5f7f3f 100644 --- a/addons/sale/tests/test_onchange.py +++ b/addons/sale/tests/test_onchange.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests.common import TransactionCase +from odoo.tests import tagged, TransactionCase +@tagged('post_install', '-at_install') class TestSaleOnchanges(TransactionCase): def test_sale_warnings(self): From 31c3894b80bbe2502cd9dc454f6d07617c5f9f8d Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 1 Dec 2021 14:34:41 +0100 Subject: [PATCH 63/65] [REV] sale: do not precompute fields depending on x2m and do not force precomputation on side models, it would reduce their performance. --- addons/hr_timesheet/models/hr_timesheet.py | 4 ++-- addons/project/models/project.py | 4 ++-- addons/sale/models/sale_order_line.py | 12 ++++++------ addons/sale_project/models/project.py | 4 ++-- addons/sale_timesheet/models/account.py | 4 +--- addons/sale_timesheet/models/project.py | 2 +- .../models/project_sale_line_employee_map.py | 2 +- addons/stock/models/stock_move.py | 2 +- 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index c1390dc4fd0..7423a4eca29 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -40,11 +40,11 @@ class AccountAnalyticLine(models.Model): task_id = fields.Many2one( 'project.task', 'Task', index=True, - compute='_compute_task_id', store=True, readonly=False, precompute=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', domain=_domain_project_id, - compute='_compute_project_id', store=True, readonly=False, precompute=True) + 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/project/models/project.py b/addons/project/models/project.py index 8b4a008d350..659290d0eef 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -937,7 +937,7 @@ class Task(models.Model): copy=False, readonly=True) project_id = fields.Many2one('project.project', string='Project', recursive=True, - compute='_compute_project_id', store=True, readonly=False, precompute=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 @@ -968,7 +968,7 @@ class Task(models.Model): help="The current user's personal task stage.") partner_id = fields.Many2one('res.partner', string='Customer', recursive=True, tracking=True, - compute='_compute_partner_id', store=True, readonly=False, precompute=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/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index ad433aef278..8572c0b488c 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -224,7 +224,7 @@ class SaleOrderLine(models.Model): ('invoiced', 'Fully Invoiced'), ('to invoice', 'To Invoice'), ('no', 'Nothing to Invoice') - ], string='Invoice Status', compute='_compute_invoice_status', store=True, precompute=True) + ], 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) @@ -287,21 +287,21 @@ class SaleOrderLine(models.Model): " - Stock Moves: the quantity comes from confirmed pickings\n") qty_delivered = fields.Float( 'Delivered Quantity', copy=False, - compute='_compute_qty_delivered', store=True, readonly=False, precompute=True, + compute='_compute_qty_delivered', store=True, readonly=False, digits='Product Unit of Measure') qty_to_invoice = fields.Float( - compute='_compute_qty_to_invoice', string='To Invoice Quantity', store=True, precompute=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, precompute=True, + 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, precompute=True) + 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, precompute=True) + compute='_compute_untaxed_amount_to_invoice', store=True) analytic_tag_ids = fields.Many2many( 'account.analytic.tag', string='Analytic Tags', diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 6910bc29ed3..50c696c5229 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -12,7 +12,7 @@ class Project(models.Model): sale_line_id = fields.Many2one( 'sale.order.line', 'Sales Order Item', copy=False, - compute="_compute_sale_line_id", store=True, readonly=False, index=True, precompute=True, + compute="_compute_sale_line_id", store=True, readonly=False, index=True, domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', '=?', partner_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Sales order item to which the project is linked. Link the timesheet entry to the sales order item defined on the project. " "Only applies on tasks without sale order item defined, and if the employee is not in the 'Employee/Sales Order Item Mapping' of the project.") @@ -141,7 +141,7 @@ class ProjectTask(models.Model): sale_line_id = fields.Many2one( 'sale.order.line', 'Sales Order Item', copy=False, tracking=True, index=True, recursive=True, - compute='_compute_sale_line', store=True, readonly=False, precompute=True, + compute='_compute_sale_line', store=True, readonly=False, domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]", help="Sales Order Item to which the time spent on this task will be added, in order to be invoiced to your customer.") project_sale_order_id = fields.Many2one('sale.order', string="Project's sale order", related='project_id.sale_order_id') diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 3e32189b82b..4db3957ccc5 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -25,9 +25,7 @@ class AccountAnalyticLine(models.Model): compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True) commercial_partner_id = fields.Many2one('res.partner', compute="_compute_commercial_partner") timesheet_invoice_id = fields.Many2one('account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet") - so_line = fields.Many2one( - compute="_compute_so_line", store=True, readonly=False, precompute=True, - domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', 'child_of', commercial_partner_id)]") + so_line = fields.Many2one(compute="_compute_so_line", store=True, readonly=False, domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', 'child_of', commercial_partner_id)]") # we needed to store it only in order to be able to groupby in the portal order_id = fields.Many2one(related='so_line.order_id', store=True, readonly=False) is_so_line_edited = fields.Boolean("Is Sales Order Item Manually Edited") diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index bd774364fa8..ddee7e27bbd 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -54,7 +54,7 @@ class Project(models.Model): default=_default_timesheet_product_id) warning_employee_rate = fields.Boolean(compute='_compute_warning_employee_rate') partner_id = fields.Many2one( - compute='_compute_partner_id', store=True, readonly=False, precompute=True) + compute='_compute_partner_id', store=True, readonly=False) @api.depends('sale_line_id', 'sale_line_employee_ids', 'allow_billable') def _compute_pricing_type(self): diff --git a/addons/sale_timesheet/models/project_sale_line_employee_map.py b/addons/sale_timesheet/models/project_sale_line_employee_map.py index f5c732b649b..6ca913e2017 100644 --- a/addons/sale_timesheet/models/project_sale_line_employee_map.py +++ b/addons/sale_timesheet/models/project_sale_line_employee_map.py @@ -12,7 +12,7 @@ class ProjectProductEmployeeMap(models.Model): employee_id = fields.Many2one('hr.employee', "Employee", required=True) sale_line_id = fields.Many2one( 'sale.order.line', "Sales Order Item", - compute="_compute_sale_line_id", store=True, readonly=False, precompute=True, + compute="_compute_sale_line_id", store=True, readonly=False, domain="""[ ('is_service', '=', True), ('is_expense', '=', False), diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 6d1576ea9af..f1eb1115bc5 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -117,7 +117,7 @@ class StockMove(models.Model): "its current stock) to gather products. If we want to chain moves and have this one to wait for the previous, " "this second option should be chosen.") scrapped = fields.Boolean( - 'Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True, precompute=True) + 'Scrapped', related='location_dest_id.scrap_location', readonly=True, store=True) scrap_ids = fields.One2many('stock.scrap', 'move_id') group_id = fields.Many2one('procurement.group', 'Procurement Group', default=_default_group_id) rule_id = fields.Many2one( From 6e122d8ccb20da4ad5e3994285d7231c5fd8e4ce Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Wed, 1 Dec 2021 15:15:10 +0100 Subject: [PATCH 64/65] [IMP] sale: currency rate computation performance * should not be triggered when pricelist is changed, only if currency is modified * cache results to reduce sql queries --- addons/sale/models/sale_order.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index 3961191c3a2..feac5f926be 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -220,7 +220,10 @@ class SaleOrder(models.Model): 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 @@ -292,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() From 45eb2f869fe98dad129fd4dc7689178bb7df0be3 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Tue, 30 Nov 2021 16:56:57 +0100 Subject: [PATCH 65/65] [IMP] sale_stock: Update SO batch creation query counter --- addons/sale_stock/tests/test_create_perf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_stock/tests/test_create_perf.py b/addons/sale_stock/tests/test_create_perf.py index 962cc585c53..c6bc9d0f4e6 100644 --- a/addons/sale_stock/tests/test_create_perf.py +++ b/addons/sale_stock/tests/test_create_perf.py @@ -47,7 +47,7 @@ class TestPERF(common.TransactionCase): ], } for i in range(self.ENTITIES)] - with self.assertQueryCount(admin=2753): + with self.assertQueryCount(admin=1100): t0 = time.time() self.env["sale.order"].create(vals_list) t1 = time.time()