[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:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'.
|
||||
|
||||
Reference in New Issue
Block a user