[IMP] sale: Use a 'cached' pricelist rule mechanism

Cache the pricelist rule used for price computation in a dedicated non
stored field, since it is used both for the price_unit and the discount
computation.

This divides by 2 the number of pricelist requests/queries when
discounts computation is enabled.
This commit is contained in:
Victor Feyens
2021-12-23 15:30:23 +01:00
parent 3295fa4733
commit 0acd96c340
9 changed files with 92 additions and 32 deletions
+3
View File
@@ -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
+1 -1
View File
@@ -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:
+14 -2
View File
@@ -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:
+2 -1
View File
@@ -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:
+56 -18
View File
@@ -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:
+2
View File
@@ -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',
+4 -3
View File
@@ -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)
+9 -7
View File
@@ -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"
+1
View File
@@ -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'.