diff --git a/addons/event_sale/models/event_ticket.py b/addons/event_sale/models/event_ticket.py index f684e8b50a1..99995aab551 100644 --- a/addons/event_sale/models/event_ticket.py +++ b/addons/event_sale/models/event_ticket.py @@ -53,6 +53,9 @@ class EventTemplateTicket(models.Model): def _compute_price_reduce(self): for ticket in self: product = ticket.product_id + # TODO drop price field usage + # and ideally remove this whole price_reduce logic + # seems strange to not apply pricelist logic but still use pricelist discount... discount = (product.lst_price - product.price) / product.lst_price if product.lst_price else 0.0 ticket.price_reduce = (1.0 - discount) * ticket.price diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 7f0c1919fbc..14206ca31f8 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -679,7 +679,7 @@ class ProductProduct(models.Model): if not company: company = self.env.company - date = date or fields.Date.today() + date = date or fields.Date.context_today(self) prices = dict.fromkeys(self.ids, 0.0) for product in products: diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index fbe3261d977..91af22df74e 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -108,12 +108,23 @@ class Pricelist(models.Model): Note: self.ensure_one() - :returns: (product unit price, applied pricelist rule) - :rtype: tuple + :returns: (product unit price, applied pricelist rule id) + :rtype: tuple(float, int) """ self.ensure_one() return self._compute_price_rule(product, quantity, uom=uom, date=date)[product.id] + def _get_product_rule(self, product, quantity, uom=None, date=False): + """Compute the pricelist price & rule for the specified product, qty & uom. + + Note: self.ensure_one() + + :returns: applied pricelist rule id + :rtype: int or False + """ + self.ensure_one() + return self._compute_price_rule(product, quantity, uom=uom, date=date)[product.id][1] + def _compute_price_rule(self, products, qty, uom=None, date=False): """ Low-level method - Mono pricelist, multi products Returns: dict{product_id: (price, suitable_rule) for the given pricelist} @@ -182,6 +193,7 @@ class Pricelist(models.Model): suitable_rule = rule break + # TODO VFE provide a way for lazy computation of price ? if suitable_rule: price = suitable_rule._compute_price(product, qty, target_uom, date) else: diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 0551750195a..07a05bea113 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -99,6 +99,7 @@ class ProductTemplate(models.Model): sale_ok = fields.Boolean('Can be Sold', default=True) purchase_ok = fields.Boolean('Can be Purchased', default=True) + # TODO VFE remove this search magic & the price field? pricelist_id = fields.Many2one( 'product.pricelist', 'Pricelist', store=False, help='Technical field. Used for searching on pricelists, not stored in database.') @@ -624,7 +625,7 @@ class ProductTemplate(models.Model): templates = self.with_company(company).sudo() if not company: company = self.env.company - date = date or fields.Date.today() + date = date or fields.Date.context_today(self) prices = dict.fromkeys(self.ids, 0.0) for template in templates: diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index f7cd41a088c..f1b0ab375bf 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -61,6 +61,13 @@ class SaleOrderLine(models.Model): Compute the amounts of the SO line. """ for line in self: + if line.display_type: + line.update({ + 'price_tax': 0.0, + 'price_total': 0.0, + 'price_subtotal': 0.0, + }) + continue price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) 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({ @@ -228,6 +235,9 @@ class SaleOrderLine(models.Model): price_unit = fields.Float( 'Unit Price', required=True, digits='Product Price', compute='_compute_price_unit', store=True, readonly=False, precompute=True) + pricelist_item_id = fields.Many2one( + 'product.pricelist.item', compute='_compute_pricelist_item_id', + help="Tech field caching pricelist rule used for price & discount computation") price_reduce = fields.Float( string='Price Reduce', digits='Product Price', @@ -653,18 +663,46 @@ class SaleOrderLine(models.Model): } } - @api.depends('product_id', 'product_uom', 'product_uom_qty', 'tax_id') + @api.depends('product_id', 'product_uom', 'product_uom_qty') + def _compute_pricelist_item_id(self): + for line in self: + if not line.product_id or line.display_type or not line.order_id.pricelist_id: + line.pricelist_item_id = False + else: + line.pricelist_item_id = line.order_id.pricelist_id._get_product_rule( + line.product_id, line.product_uom_qty or 1.0, line.product_uom, line.order_id.date_order) + + def _get_price_rule_id(self, **product_context): + self.ensure_one() + + pricelist_rule = self.pricelist_item_id + order_date = self.order_id.date_order or fields.Date.today() + product = self.product_id.with_context(**product_context) + qty = self.product_uom_qty or 1.0 + + if pricelist_rule: + price = pricelist_rule._compute_price( + product, qty, self.product_uom, order_date) + else: + # fall back on Sales Price if no rule is found + price = product.price_compute('list_price', uom=self.product_uom, date=order_date)[product.id] + + # Note: we do not rely on the currency parameter of price_compute + # to avoid rounding the resulting price value to the currency decimal precision + if product.currency_id != self.currency_id: + price = product.currency_id._convert( + price, self.currency_id, self.env.company, order_date, round=False) + return price, pricelist_rule.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: + if not line.product_uom or not line.product_id or not line.order_id.pricelist_id: line.price_unit = 0.0 - continue - if line.order_id.pricelist_id: + else: price = line._get_display_price() line.price_unit = self.env['account.tax']._fix_tax_included_price_company( price, line.product_id.taxes_id, line.tax_id, line.company_id) - else: - line.price_unit = 0.0 def _get_display_price(self): self.ensure_one() @@ -672,6 +710,8 @@ class SaleOrderLine(models.Model): product = self.product_id + # NOTE VFE: no_variant attributes are not considered while computing discount + # Is it expected or unexpected behavior ? # it is possible that a no_variant attribute is still in a variant if # the type of the attribute has been changed after creation. no_variant_attributes_price_extra = [ @@ -682,24 +722,23 @@ class SaleOrderLine(models.Model): ) ] if no_variant_attributes_price_extra: - product = product.with_context( + final_price, rule_id = self._get_price_rule_id( no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra) ) + else: + final_price, rule_id = self._get_price_rule_id() if self.order_id.pricelist_id.discount_policy == 'with_discount': - return self.order_id.pricelist_id._get_product_price( - product, self.product_uom_qty or 1.0, self.product_uom, self.order_id.date_order) + return final_price - final_price, rule_id = self.order_id.pricelist_id._get_product_price_rule( - product, self.product_uom_qty or 1.0, self.product_uom, self.order_id.date_order) base_price, currency = self._get_real_price_currency( product, rule_id, self.product_uom_qty, self.product_uom, date=self.order_id.date_order) - if currency != self.order_id.pricelist_id.currency_id: + if currency != self.currency_id: base_price = currency._convert( base_price, - self.order_id.pricelist_id.currency_id, - self.order_id.company_id or self.env.company, + self.currency_id, + self.company_id or self.env.company, self.order_id.date_order or fields.Date.today()) # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) @@ -794,7 +833,7 @@ class SaleOrderLine(models.Model): 'tax_id', 'analytic_tag_ids' ] - @api.depends('product_id', 'price_unit', 'product_uom', 'product_uom_qty', 'tax_id') + @api.depends('product_id', 'product_uom', 'product_uom_qty') 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. @@ -814,9 +853,8 @@ class SaleOrderLine(models.Model): line.discount = 0.0 - price, rule_id = line.order_id.pricelist_id._get_product_price_rule( - line.product_id, line.product_uom_qty, line.product_uom, line.order_id.date_order) - new_list_price, currency = line.with_company(self.company_id)._get_real_price_currency( + price, rule_id = line._get_price_rule_id() + new_list_price, currency = line.with_company(line.company_id)._get_real_price_currency( line.product_id, rule_id, line.product_uom_qty, line.product_uom, date=line.order_id.date_order) if new_list_price != 0: diff --git a/addons/sale/tests/test_sale_pricelist.py b/addons/sale/tests/test_sale_pricelist.py index 398fa2d0448..eb7950083c2 100644 --- a/addons/sale/tests/test_sale_pricelist.py +++ b/addons/sale/tests/test_sale_pricelist.py @@ -354,6 +354,8 @@ class TestSaleOrder(TestSaleCommon): def test_sale_change_of_pricelists_excluded_value_discount(self): """ Test SO with the pricelist 'discount displayed' and check displayed percentage value after multiple changes of pricelist """ + self.env.user.groups_id += self.env.ref('product.group_discount_per_so_line') + # Create a pricelist without discount policy: percentage on all products pricelist_discount_excl_global = self.env['product.pricelist'].create({ 'name': 'Pricelist C', diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index 93a02e196e9..5ceed5dfdaa 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -88,6 +88,7 @@ class SaleOrder(models.Model): def update_prices(self): self.ensure_one() res = super().update_prices() + # TODO env.add_to_compute after onchange conversion to computes for line in self.sale_order_option_ids: line.price_unit = self.pricelist_id._get_product_price(line.product_id, line.quantity, uom=line.uom_id) return res @@ -220,15 +221,15 @@ class SaleOrderOption(models.Model): return [('line_id', '=', False)] return [('line_id', '!=', False)] + # TODO convert to compute (+ precompute) @api.onchange('product_id', 'uom_id', 'quantity') def _onchange_product_id(self): if not self.product_id: return - product = self.product_id - product_lang = product.with_context(lang=self.order_id.partner_id.lang) + product_lang = self.product_id.with_context(lang=self.order_id.partner_id.lang) self.name = product_lang.get_product_multiline_description_sale() - self.uom_id = self.uom_id or product.uom_id + self.uom_id = self.uom_id or self.product_id.uom_id # 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) diff --git a/addons/sale_stock/tests/test_create_perf.py b/addons/sale_stock/tests/test_create_perf.py index e22910847eb..3a55a529432 100644 --- a/addons/sale_stock/tests/test_create_perf.py +++ b/addons/sale_stock/tests/test_create_perf.py @@ -59,7 +59,7 @@ class TestPERF(common.TransactionCase): def test_dummy_sales_orders_batch_creation_perf(self): """ Dummy SOlines (notes/sections) should not add any custom queries other than their insert""" # + 4 SOL insert - with self.assertQueryCount(admin=45): + with self.assertQueryCount(admin=46): self.env['sale.order'].create([{ 'partner_id': self.partners[0].id, 'user_id': self.salesmans[0].id, @@ -73,7 +73,7 @@ class TestPERF(common.TransactionCase): @warmup def test_light_sales_orders_batch_creation_perf_without_taxes(self): self.products[0].taxes_id = [Command.set([])] - with self.assertQueryCount(admin=57): + with self.assertQueryCount(admin=59): self.env['sale.order'].create([{ 'partner_id': self.partners[0].id, 'user_id': self.salesmans[0].id, @@ -87,7 +87,7 @@ class TestPERF(common.TransactionCase): @users('admin') @warmup def test_light_sales_orders_batch_creation_perf(self): - with self.assertQueryCount(admin=68): # 67 locally, 68 in nightly runbot + with self.assertQueryCount(admin=69): # 68 locally, 69 in nightly runbot self.env['sale.order'].create([{ 'partner_id': self.partners[0].id, 'user_id': self.salesmans[0].id, @@ -101,9 +101,10 @@ class TestPERF(common.TransactionCase): @users('admin') @warmup def test_complex_sales_orders_batch_creation_perf(self): - # NOTE: sometimes 1101 on runbot, do not change without verifying in multi-builds + # NOTE: sometimes more queries on runbot, + # do not change without verifying in multi-builds # (Seems to be a time-based problem, everytime happening around 10PM) - self._test_complex_sales_orders_batch_creation_perf(1101) + self._test_complex_sales_orders_batch_creation_perf(1550) @users('admin') @warmup @@ -111,8 +112,9 @@ class TestPERF(common.TransactionCase): """Cover the "complex" logic triggered inside the `_compute_discount`""" self.env['product.pricelist'].search([]).discount_policy = 'without_discount' self.env.user.groups_id += self.env.ref('product.group_discount_per_so_line') - # 2093 locally, 2094 in nightly runbot - self._test_complex_sales_orders_batch_creation_perf(2094) + + # Verify any modification to this count on nightly runbot builds + self._test_complex_sales_orders_batch_creation_perf(1594) def _test_complex_sales_orders_batch_creation_perf(self, query_count): MSG = "Model %s, %i records, %s, time %.2f" diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 95f63361122..bf1e23aafac 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -113,6 +113,7 @@ class SaleOrder(models.Model): pricelist = order.pricelist_id pricelist_price, pricelist_rule_id = pricelist._get_product_price_rule(product, qty, date=order_date) + # TODO VFE factorize discount computation or delegate discount computation to sale computes if pricelist.discount_policy == 'without_discount': # This part is pretty much a copy-paste of the method '_onchange_discount' of # 'sale.order.line'.