[REF] product: clean pricelist API

* Do not rely on context, everything should be cleary specified through
parameters
  Catch context keys in an unique targeted place to improve code clarity
* Drop strange old API
  * do not provide unused partner parameter anymore
  * do not provide products, qty as a list of tuple, we only request the
same qty for all products anyway
* Clear methods, add/adapt comments and docstrings
* Reduce potential side-effects of context content.
This commit is contained in:
Victor Feyens
2021-12-23 15:30:23 +01:00
parent 6b80cce173
commit 57ced812f0
18 changed files with 261 additions and 254 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ class RepairLine(models.Model):
product = bom_line.product_id
uom = bom_line.product_uom_id
partner = self.repair_id.partner_id
price = self.repair_id.pricelist_id.get_product_price(product, qty, partner, uom_id=uom.id)
price = self.repair_id.pricelist_id._get_product_price(product, qty, uom=uom)
tax = self.env['account.tax']
if partner:
partner_invoice = self.repair_id.partner_invoice_id or partner
+2 -2
View File
@@ -982,8 +982,8 @@ class PosOrderLine(models.Model):
raise UserError(
_('You have to select a pricelist in the sale form !\n'
'Please set one before choosing a product.'))
price = self.order_id.pricelist_id.get_product_price(
self.product_id, self.qty or 1.0, self.order_id.partner_id)
price = self.order_id.pricelist_id._get_product_price(
self.product_id, self.qty or 1.0)
self.tax_ids = self.product_id.taxes_id.filtered(lambda r: not self.company_id or r.company_id == self.company_id)
tax_ids_after_fiscal_position = self.order_id.fiscal_position_id.map_tax(self.tax_ids)
self.price_unit = self.env['account.tax']._fix_tax_included_price_company(price, self.tax_ids, tax_ids_after_fiscal_position, self.company_id)
+1 -1
View File
@@ -59,7 +59,7 @@ class ProductProduct(models.Model):
pricelists = config.available_pricelist_ids
else:
pricelists = config.pricelist_id
price_per_pricelist_id = pricelists.price_get(self.id, quantity)
price_per_pricelist_id = pricelists._price_get(self.id, quantity)
pricelist_list = [{'name': pl.name, 'price': price_per_pricelist_id[pl.id]} for pl in pricelists]
# Warehouses
+1 -1
View File
@@ -1707,7 +1707,7 @@ exports.Product = Backbone.Model.extend({
}
return this.pos.units_by_id[unit_id];
},
// Port of get_product_price on product.pricelist.
// Port of _get_product_price on product.pricelist.
//
// Anything related to UOM can be ignored, the POS will always use
// the default UOM set on the product and the user cannot change
+1 -1
View File
@@ -406,7 +406,7 @@ class TestPoSCommon(ValuationReconciliationTestCommon):
fiscal_position = customer.property_account_position_id if customer else default_fiscal_position
def create_order_line(product, quantity):
price_unit = self.pricelist.get_product_price(product, quantity, False)
price_unit = self.pricelist._get_product_price(product, quantity)
tax_ids = fiscal_position.map_tax(product.taxes_id)
tax_values = (
tax_ids.compute_all(price_unit, self.currency, quantity)
@@ -42,13 +42,13 @@ class TestPoSOtherCurrencyConfig(TestPoSCommon):
# (see `self._create_other_currency_config` method)
# Except for product2 where the price is specified in the pricelist.
self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product1, 1, self.customer), 5.00)
self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product2, 1, self.customer), 12.99)
self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product3, 1, self.customer), 15.00)
self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product4, 1, self.customer), 50)
self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product5, 1, self.customer), 100)
self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product6, 1, self.customer), 22.65)
self.assertAlmostEqual(self.config.pricelist_id.get_product_price(self.product7, 1, self.customer), 3.50)
self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product1, 1), 5.00)
self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product2, 1), 12.99)
self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product3, 1), 15.00)
self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product4, 1), 50)
self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product5, 1), 100)
self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product6, 1), 22.65)
self.assertAlmostEqual(self.config.pricelist_id._get_product_price(self.product7, 1), 3.50)
def test_02_orders_without_invoice(self):
""" orders without invoice
+1 -1
View File
@@ -45,7 +45,7 @@ class TestPoSSetup(TestPoSCommon):
def test_product_price(self):
def get_price(pricelist, product):
return pricelist.get_product_price(product, 1, self.customer)
return pricelist._get_product_price(product, 1)
# check usd pricelist
+5 -6
View File
@@ -234,14 +234,12 @@ class ProductProduct(models.Model):
def _compute_is_product_variant(self):
self.is_product_variant = True
@api.depends_context('pricelist', 'partner', 'quantity', 'uom', 'date', 'no_variant_attributes_price_extra')
@api.depends_context('pricelist', 'quantity', 'uom', 'date', 'no_variant_attributes_price_extra')
def _compute_product_price(self):
prices = {}
pricelist_id_or_name = self._context.get('pricelist')
if pricelist_id_or_name:
pricelist = None
partner = self.env.context.get('partner', False)
quantity = self.env.context.get('quantity', 1.0)
# Support context pricelists specified as list, display_name or ID for compatibility
if isinstance(pricelist_id_or_name, list):
@@ -254,9 +252,10 @@ class ProductProduct(models.Model):
pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)
if pricelist:
quantities = [quantity] * len(self)
partners = [partner] * len(self)
prices = pricelist.get_products_price(self, quantities, partners)
quantity = self.env.context.get('quantity', 1.0)
uom = self.env['uom.uom'].browse(self.env.context.get('uom'))
date = self.env.context.get('date')
prices = pricelist._get_products_price(self, quantity, uom=uom, date=date)
for product in self:
product.price = prices.get(product.id, 0.0)
+121 -91
View File
@@ -73,7 +73,7 @@ class Pricelist(models.Model):
return pricelist_ids
return super()._name_search(name, args, operator=operator, limit=limit, name_get_uid=name_get_uid)
def _compute_price_rule_multi(self, products_qty_partner, date=False, uom_id=False):
def _compute_price_rule_multi(self, products, qty, uom=None, date=False):
""" Low-level method - Multi pricelist, multi products
Returns: dict{product_id: dict{pricelist_id: (price, suitable_rule)} }"""
if not self.ids:
@@ -82,13 +82,13 @@ class Pricelist(models.Model):
pricelists = self
results = {}
for pricelist in pricelists:
subres = pricelist._compute_price_rule(products_qty_partner, date=date, uom_id=uom_id)
subres = pricelist._compute_price_rule(products, qty, uom=uom, date=date)
for product_id, price in subres.items():
results.setdefault(product_id, {})
results[product_id][pricelist.id] = price
return results
def _compute_price_rule_get_items(self, products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids):
def _compute_price_rule_get_items(self, date, prod_tmpl_ids, prod_ids, categ_ids):
self.ensure_one()
# Load all rules
self.env['product.pricelist.item'].flush(['price', 'currency_id', 'company_id'])
@@ -116,31 +116,29 @@ class Pricelist(models.Model):
item_ids = [x[0] for x in self.env.cr.fetchall()]
return self.env['product.pricelist.item'].browse(item_ids)
def _compute_price_rule(self, products_qty_partner, date=False, uom_id=False):
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}
Date in context can be a date, datetime, ...
:param products: recordset of product (product.product/product.template)
:param float qty: quantity of products requested (in given uom)
:param uom: unit of measure (uom.uom record)
If not specified, prices returned are expressed in product uoms
:param date: date to use for price computation and currency conversions
:type date: date or datetime
:param products_qty_partner: list of typles products, quantity, partner
:param datetime date: validity date
:param ID uom_id: intermediate unit of measure
:returns: product_id: (price, pricelist_rule)
:rtype: dict
"""
self.ensure_one()
if not date:
date = self._context.get('date') or fields.Datetime.now()
if not uom_id and self._context.get('uom'):
uom_id = self._context['uom']
if uom_id:
# rebrowse with uom if given
products = [item[0].with_context(uom=uom_id) for item in products_qty_partner]
products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)]
else:
products = [item[0] for item in products_qty_partner]
if not products:
return {}
if not date:
# Used to fetch pricelist rules and currency rates
date = fields.Datetime.now()
categ_ids = {}
for p in products:
categ = p.categ_id
@@ -151,40 +149,36 @@ class Pricelist(models.Model):
is_product_template = products[0]._name == "product.template"
if is_product_template:
prod_tmpl_ids = [tmpl.id for tmpl in products]
prod_tmpl_ids = products.ids
# all variants of all products
prod_ids = [p.id for p in
list(chain.from_iterable([t.product_variant_ids for t in products]))]
prod_ids = products.product_variant_ids.ids
else:
prod_ids = [product.id for product in products]
prod_tmpl_ids = [product.product_tmpl_id.id for product in products]
prod_ids = products.ids
prod_tmpl_ids = products.product_tmpl_id.ids
items = self._compute_price_rule_get_items(products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids)
# Fetch all rules potentially matching specified products and date
rules = self._compute_price_rule_get_items(date, prod_tmpl_ids, prod_ids, categ_ids)
if 'currency' in products.env.context:
# Remove currency from product context to avoid any
# side-effect currency conversion in price_compute calls
# since we manage currency conversion without the context fallbacks
product = products.with_context(currency=False)
results = {}
for product, qty, partner in products_qty_partner:
results[product.id] = 0.0
suitable_rule = False
for product in products:
suitable_rule = self.env['product.pricelist.item']
# Final unit price is computed according to `qty` in the `qty_uom_id` UoM.
# An intermediary unit price may be computed according to a different UoM, in
# which case the price_uom_id contains that UoM.
# The final price will be converted to match `qty_uom_id`.
qty_uom_id = self._context.get('uom') or product.uom_id.id
qty_in_product_uom = qty
if qty_uom_id != product.uom_id.id:
try:
qty_in_product_uom = self.env['uom.uom'].browse([self._context['uom']])._compute_quantity(qty, product.uom_id)
except UserError:
# Ignored - incompatible UoM in context, use default product UoM
pass
# Compute quantity in product uom because pricelist rules are specified
# w.r.t product default UoM (min_quantity, price_surchage, ...)
product_uom = product.uom_id
target_uom = uom or product_uom # If no uom is specified, fall back on the product uom
if target_uom != product_uom:
qty_in_product_uom = target_uom._compute_quantity(qty, product_uom, raise_if_failure=False)
else:
qty_in_product_uom = qty
# if Public user try to access standard price from website sale, need to call price_compute.
# TDE SURPRISE: product can actually be a template
price = product.price_compute('list_price')[product.id]
price_uom = self.env['uom.uom'].browse([qty_uom_id])
for rule in items:
for rule in rules:
if rule.min_quantity and qty_in_product_uom < rule.min_quantity:
continue
if is_product_template:
@@ -208,61 +202,52 @@ class Pricelist(models.Model):
if not cat:
continue
if rule.base == 'pricelist' and rule.base_pricelist_id:
price_tmp = rule.base_pricelist_id._compute_price_rule([(product, qty, partner)], date, uom_id)[product.id][0] # TDE: 0 = price, 1 = rule
price = rule.base_pricelist_id.currency_id._convert(price_tmp, self.currency_id, self.env.company, date, round=False)
else:
# if base option is public price take sale price else cost price of product
# price_compute returns the price in the context UoM, i.e. qty_uom_id
price = product.price_compute(rule.base)[product.id]
if price is not False:
price = rule._compute_price(price, price_uom, product, quantity=qty, partner=partner)
suitable_rule = rule
suitable_rule = rule
break
# Final price conversion into pricelist currency
if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist':
if suitable_rule.base == 'standard_price':
cur = product.cost_currency_id
else:
cur = product.currency_id
price = cur._convert(price, self.currency_id, self.env.company, date, round=False)
if not suitable_rule:
cur = product.currency_id
price = cur._convert(price, self.currency_id, self.env.company, date, round=False)
if suitable_rule:
price = suitable_rule._compute_price(product, qty, target_uom, date)
else:
# fall back on Sales Price if no rule is found
price = product.price_compute('list_price', uom=target_uom, date=date)[product.id]
results[product.id] = (price, suitable_rule and suitable_rule.id or False)
if product.currency_id != self.currency_id:
price = product.currency_id._convert(price, self.currency_id, self.env.company, date, round=False)
results[product.id] = (price, suitable_rule.id)
return results
# New methods: product based
def get_products_price(self, products, quantities, partners, date=False, uom_id=False):
def _get_products_price(self, products, quantity, uom=None, date=False):
""" For a given pricelist, return price for products
Returns: dict{product_id: product price}, in the given pricelist """
self.ensure_one()
return {
product_id: res_tuple[0]
for product_id, res_tuple in self._compute_price_rule(
list(zip(products, quantities, partners)),
products,
quantity,
uom=uom,
date=date,
uom_id=uom_id
).items()
}
def get_product_price(self, product, quantity, partner, date=False, uom_id=False):
def _get_product_price(self, product, quantity, uom=None, date=False):
""" For a given pricelist, return price for a given product """
self.ensure_one()
return self._compute_price_rule([(product, quantity, partner)], date=date, uom_id=uom_id)[product.id][0]
return self._compute_price_rule(product, quantity, uom=uom, date=date)[product.id][0]
def get_product_price_rule(self, product, quantity, partner, date=False, uom_id=False):
def _get_product_price_rule(self, product, quantity, uom=None, date=False):
""" For a given pricelist, return price and rule for a given product """
self.ensure_one()
return self._compute_price_rule([(product, quantity, partner)], date=date, uom_id=uom_id)[product.id]
return self._compute_price_rule(product, quantity, uom=uom, date=date)[product.id]
def price_get(self, prod_id, qty, partner=None):
def _price_get(self, product, qty):
""" Multi pricelist, mono product - returns price per pricelist """
return {key: price[0] for key, price in self.price_rule_get(prod_id, qty, partner=partner).items()}
return {
key: price[0]
for key, price in self._compute_price_rule_multi(product, qty)[product.id].items()}
def _get_partner_pricelist_multi_search_domain_hook(self, company_id):
return [
@@ -598,32 +583,77 @@ class PricelistItem(models.Model):
self.invalidate_cache()
return res
def _compute_price(self, price, price_uom, product, quantity=1.0, partner=False):
def _compute_price(self, product, quantity, uom, date):
"""Compute the unit price of a product in the context of a pricelist application.
The unused parameters are there to make the full context available for overrides.
:param product: recordset of product (product.product/product.template)
:param float qty: quantity of products requested (in given uom)
:param uom: unit of measure (uom.uom record)
:param datetime date: date to use for price computation and currency conversions
:returns: price according to pricelist rule, expressed in pricelist currency
:rtype: float
"""
self.ensure_one()
convert_to_price_uom = (lambda price: product.uom_id._compute_price(price, price_uom))
if self.compute_price == 'fixed':
price = convert_to_price_uom(self.fixed_price)
elif self.compute_price == 'percentage':
price = (price - (price * (self.percent_price / 100))) or 0.0
product.ensure_one()
uom.ensure_one()
# Pricelist specific values are specified according to product UoM
# and must be multiplied according to the factor between uoms
product_uom = product.uom_id
if product_uom != uom:
convert = lambda p: product_uom._compute_price(p, uom)
else:
convert = lambda p: p
if self.compute_price == 'fixed':
price = convert(self.fixed_price)
elif self.compute_price == 'percentage':
base_price = self._compute_base_price(product, quantity, uom=uom, date=date)
price = (base_price - (base_price * (self.percent_price / 100))) or 0.0
else:
base_price = self._compute_base_price(product, quantity, uom=uom, date=date)
# complete formula
price_limit = price
price = (price - (price * (self.price_discount / 100))) or 0.0
price_limit = base_price
price = (base_price - (base_price * (self.price_discount / 100))) or 0.0
if self.price_round:
price = tools.float_round(price, precision_rounding=self.price_round)
if self.price_surcharge:
price_surcharge = convert_to_price_uom(self.price_surcharge)
price += price_surcharge
price += convert(self.price_surcharge)
if self.price_min_margin:
price_min_margin = convert_to_price_uom(self.price_min_margin)
price = max(price, price_limit + price_min_margin)
price = max(price, price_limit + convert(self.price_min_margin))
if self.price_max_margin:
price_max_margin = convert_to_price_uom(self.price_max_margin)
price = min(price, price_limit + price_max_margin)
price = min(price, price_limit + convert(self.price_max_margin))
return price
def _compute_base_price(self, product, quantity, uom, date):
""" Compute the base price for a given rule
:param product: recordset of product (product.product/product.template)
:param float qty: quantity of products requested (in given uom)
:param uom: unit of measure (uom.uom record)
:param datetime date: date to use for price computation and currency conversions
:returns: base price, expressed in pricelist currency
:rtype: float
"""
rule_base = self.base
if rule_base == 'pricelist' and self.base_pricelist_id:
price = self.base_pricelist_id._get_product_price(
product, quantity, uom, date)
src_currency = self.base_pricelist_id.currency_id
elif rule_base == "standard_price":
src_currency = product.cost_currency_id
price = product.price_compute(rule_base, uom=uom, date=date)[product.id]
else: # lst_price
src_currency = product.currency_id
price = product.price_compute(rule_base, uom=uom, date=date)[product.id]
target_currency = self.pricelist_id.currency_id
if src_currency != target_currency:
price = src_currency._convert(price, target_currency, self.env.company, date, round=False)
return price
+5 -6
View File
@@ -70,7 +70,7 @@ class ProductTemplate(models.Model):
'res.currency', 'Cost Currency', compute='_compute_cost_currency_id')
# price fields
# price: total template price, context dependent (partner, pricelist, quantity)
# price: total template price, context dependent (pricelist, quantity)
price = fields.Float(
'Price', compute='_compute_template_price', inverse='_set_template_price',
digits='Product Price')
@@ -202,8 +202,6 @@ class ProductTemplate(models.Model):
pricelist_id_or_name = self._context.get('pricelist')
if pricelist_id_or_name:
pricelist = None
partner = self.env.context.get('partner')
quantity = self.env.context.get('quantity', 1.0)
# Support context pricelists specified as list, display_name or ID for compatibility
if isinstance(pricelist_id_or_name, list):
@@ -216,9 +214,10 @@ class ProductTemplate(models.Model):
pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)
if pricelist:
quantities = [quantity] * len(self)
partners = [partner] * len(self)
prices = pricelist.get_products_price(self, quantities, partners)
quantity = self.env.context.get('quantity', 1.0)
uom = self.env['uom.uom'].browse(self.env.context.get('uom'))
date = self.env.context.get('date')
prices = pricelist._get_products_price(self, quantity, uom=uom, date=date)
return prices
@@ -52,7 +52,7 @@ class ProductPricelistReport(models.AbstractModel):
'uom': product.uom_id.name,
}
for qty in quantities:
data['price'][qty] = pricelist.get_product_price(product, qty, False)
data['price'][qty] = pricelist._get_product_price(product, qty)
if is_product_tmpl and product.product_variant_count > 1:
data['variants'] = [
+5 -5
View File
@@ -73,7 +73,7 @@ class TestPricelist(TransactionCase):
# make sure 'tonne' resolves down to 1 'kg'.
self.uom_ton.write({'rounding': 0.001})
# setup product stored in 'tonnes', with a discounted pricelist for qty > 3 tonnes
spam_id = self.env['product.product'].create({
spam = self.env['product.product'].create({
'name': '1 tonne of spam',
'uom_id': self.uom_ton.id,
'uom_po_id': self.uom_ton.id,
@@ -88,13 +88,13 @@ class TestPricelist(TransactionCase):
'base': 'list_price', # based on public price
'min_quantity': 3, # min = 3 tonnes
'price_surcharge': -10, # -10 EUR / tonne
'product_id': spam_id.id
'product_id': spam.id
})
pricelist = self.public_pricelist
def test_unit_price(qty, uom, expected_unit_price):
spam = spam_id.with_context({'uom': uom})
unit_price = pricelist.with_context({'uom': uom}).get_product_price(spam, qty, False)
def test_unit_price(qty, uom_id, expected_unit_price):
uom = self.env['uom.uom'].browse(uom_id)
unit_price = pricelist._get_product_price(spam, qty, uom=uom)
self.assertAlmostEqual(unit_price, expected_unit_price, msg='Computed unit price is wrong')
# Test prices - they are *per unit*, the quantity is only here to match the pricelist rules!
+2 -4
View File
@@ -734,10 +734,9 @@ class RepairLine(models.Model):
@api.onchange('product_uom')
def _onchange_product_uom(self):
partner = self.repair_id.partner_id
pricelist = self.repair_id.pricelist_id
if pricelist and self.product_id and self.type != 'remove':
price = pricelist.get_product_price(self.product_id, self.product_uom_qty, partner, uom_id=self.product_uom.id)
price = pricelist._get_product_price(self.product_id, self.product_uom_qty, uom=self.product_uom)
if price is False:
warning = {
'title': _('No valid pricelist line found.'),
@@ -822,10 +821,9 @@ class RepairFee(models.Model):
@api.onchange('product_uom')
def _onchange_product_uom(self):
partner = self.repair_id.partner_id
pricelist = self.repair_id.pricelist_id
if pricelist and self.product_id:
price = pricelist.get_product_price(self.product_id, self.product_uom_qty, partner, uom_id=self.product_uom.id)
price = pricelist._get_product_price(self.product_id, self.product_uom_qty, uom=self.product_uom)
if price is False:
warning = {
'title': _('No valid pricelist line found.'),
+1
View File
@@ -526,6 +526,7 @@ class SaleOrder(models.Model):
self.ensure_one()
lines_to_update = []
for line in self.order_line.filtered(lambda line: not line.display_type):
# TODO VFE remove price field usage
product = line.product_id.with_context(
partner=self.partner_id,
quantity=line.product_uom_qty,
+67 -71
View File
@@ -606,9 +606,12 @@ class SaleOrderLine(models.Model):
return {}
def _get_display_price(self, product):
# TODO: drop product param, enforce product=self.product_id for environment consistency (company, context, ...)
# TO DO: move me in master/saas-16 on sale.order
# awa: don't know if it's still the case since we need the "product_no_variant_attribute_value_ids" field now
# to be able to compute the full price
self.ensure_one()
product.ensure_one()
# it is possible that a no_variant attribute is still in a variant if
# the type of the attribute has been changed after creation.
@@ -625,15 +628,20 @@ class SaleOrderLine(models.Model):
)
if self.order_id.pricelist_id.discount_policy == 'with_discount':
return product.with_context(pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id).price
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)
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)
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)
final_price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(product or self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id)
base_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)
if currency != self.order_id.pricelist_id.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.order_id.date_order or fields.Date.today())
base_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())
# negative discounts (= surcharge) are included in the display price
return max(base_price, final_price)
@@ -685,23 +693,16 @@ class SaleOrderLine(models.Model):
}
}
@api.depends('product_id', 'product_uom', 'product_uom_qty')
@api.depends('product_id', 'product_uom', 'product_uom_qty', 'tax_id')
def _compute_price_unit(self):
for line in self:
if not line.product_uom or not line.product_id:
line.price_unit = 0.0
continue
if line.order_id.pricelist_id and line.order_partner_id:
product = line.product_id.with_context(
partner=line.order_partner_id,
quantity=line.product_uom_qty,
date=line.order_id.date_order,
pricelist=line.order_id.pricelist_id.id,
uom=line.product_uom.id,
fiscal_position=self.env.context.get('fiscal_position')
)
if line.order_id.pricelist_id:
price = line._get_display_price(line.product_id)
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)
price, line.product_id.taxes_id, line.tax_id, line.company_id)
else:
line.price_unit = 0.0
@@ -739,50 +740,55 @@ class SaleOrderLine(models.Model):
if self._check_line_unlink():
raise UserError(_('You can not remove an order line once the sales order is confirmed.\nYou should rather set the quantity to 0.'))
def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id):
def _get_real_price_currency(self, product, rule_id, qty, uom=None, date=False):
"""Retrieve the price before applying the pricelist
:param obj product: object of current product record
:parem float qty: total quentity of product
:param tuple price_and_rule: tuple(price, suitable_rule) coming from pricelist computation
:param obj uom: unit of measure of current order line
:param integer pricelist_id: pricelist id of sales order"""
PricelistItem = self.env['product.pricelist.item']
field_name = 'lst_price'
currency_id = None
product_currency = product.currency_id
WARNING: make sure this method is called with the right company in the environment.
:param recordset product: object of current product record
:param int rule_id: suitable rule found, as a `product.pricelist.item` id
:param float qty: total quentity of product
:param recordset uom: unit of measure of current order line
:param int pricelist_id: pricelist id of sales order
"""
product.ensure_one()
if rule_id:
PricelistItem = self.env['product.pricelist.item']
pricelist_item = PricelistItem.browse(rule_id)
if pricelist_item.pricelist_id.discount_policy == 'without_discount':
while pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id and pricelist_item.base_pricelist_id.discount_policy == 'without_discount':
_price, rule_id = pricelist_item.base_pricelist_id.with_context(uom=uom.id).get_product_price_rule(product, qty, self.order_id.partner_id)
_price, rule_id = pricelist_item.base_pricelist_id._get_product_price_rule(
product, qty, uom=uom, date=date)
pricelist_item = PricelistItem.browse(rule_id)
if pricelist_item.base == 'standard_price':
field_name = 'standard_price'
product_currency = product.cost_currency_id
price = product.standard_price
currency = product.cost_currency_id
elif pricelist_item.base == 'pricelist' and pricelist_item.base_pricelist_id:
field_name = 'price'
product = product.with_context(pricelist=pricelist_item.base_pricelist_id.id)
product_currency = pricelist_item.base_pricelist_id.currency_id
currency_id = pricelist_item.pricelist_id.currency_id
if not currency_id:
currency_id = product_currency
cur_factor = 1.0
else:
if currency_id.id == product_currency.id:
cur_factor = 1.0
price = pricelist_item.base_pricelist_id._get_product_price(
product, qty, uom=uom, date=date)
currency = pricelist_item.base_pricelist_id.currency_id
else:
cur_factor = currency_id._get_conversion_rate(product_currency, currency_id, self.company_id or self.env.company, self.order_id.date_order or fields.Date.today())
price = product.lst_price
currency = product.currency_id
target_currency = pricelist_item.pricelist_id.currency_id
product_uom = self.env.context.get('uom') or product.uom_id.id
if uom and uom.id != product_uom:
# the unit price is in a different uom
uom_factor = uom._compute_price(1.0, product.uom_id)
else:
uom_factor = 1.0
price = product.lst_price
currency = product.currency_id
return product[field_name] * uom_factor * cur_factor, currency_id
target_currency = currency
product_uom = product.uom_id
if uom and uom != product_uom:
price = product_uom._compute_price(price, uom)
if currency != target_currency:
price = currency._convert(price, target_currency, self.env.company, date)
return price, target_currency
def _get_protected_fields(self):
return [
@@ -797,33 +803,23 @@ class SaleOrderLine(models.Model):
return
for line in self:
if not (line.product_id and line.product_uom and
line.order_partner_id and line.order_id.pricelist_id and
line.order_id.pricelist_id.discount_policy == 'without_discount'):
if not line.product_id or line.display_type:
line.discount = 0.0
if not (
line.product_uom
and line.order_partner_id
and line.order_id.pricelist_id
and line.order_id.pricelist_id.discount_policy == 'without_discount'
):
continue
line.discount = 0.0
product = line.product_id.with_context(
partner=line.order_partner_id,
quantity=line.product_uom_qty,
date=line.order_id.date_order,
pricelist=line.order_id.pricelist_id.id,
uom=line.product_uom.id,
fiscal_position=self.env.context.get('fiscal_position'))
product_context = dict(
self.env.context,
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_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)
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(
line.product_id, rule_id, line.product_uom_qty, line.product_uom, date=line.order_id.date_order)
if new_list_price != 0:
if line.order_id.pricelist_id.currency_id != currency:
+7 -13
View File
@@ -69,7 +69,7 @@ class SaleOrder(models.Model):
discount = 0
if self.pricelist_id:
pricelist_price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
pricelist_price = self.pricelist_id._get_product_price(option.product_id, 1, uom=option.uom_id)
if self.pricelist_id.discount_policy == 'without_discount' and price:
discount = max(0, (price - pricelist_price) * 100 / price)
@@ -89,7 +89,7 @@ class SaleOrder(models.Model):
self.ensure_one()
res = super().update_prices()
for line in self.sale_order_option_ids:
line.price_unit = self.pricelist_id.get_product_price(line.product_id, line.quantity, self.partner_id, uom_id=line.uom_id.id)
line.price_unit = self.pricelist_id._get_product_price(line.product_id, line.quantity, uom=line.uom_id)
return res
@api.onchange('sale_order_template_id')
@@ -112,7 +112,7 @@ class SaleOrder(models.Model):
discount = 0
if self.pricelist_id:
pricelist_price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False)
pricelist_price = self.pricelist_id._get_product_price(line.product_id, 1, uom=line.product_uom_id)
if self.pricelist_id.discount_policy == 'without_discount' and price:
discount = max(0, (price - pricelist_price) * 100 / price)
@@ -224,16 +224,10 @@ class SaleOrderOption(models.Model):
def _onchange_product_id(self):
if not self.product_id:
return
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id,
quantity=self.quantity,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.uom_id.id,
fiscal_position=self.env.context.get('fiscal_position')
)
self.name = product.get_product_multiline_description_sale()
product = self.product_id
product_lang = product.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
# To compute the discount a so line is created in cache
values = self._get_values_to_add_to_order()
+32 -42
View File
@@ -104,55 +104,51 @@ class SaleOrder(models.Model):
def _website_product_id_change(self, order_id, product_id, qty=0, **kwargs):
order = self.sudo().browse(order_id)
product_context = dict(self.env.context)
product_context.setdefault('lang', order.partner_id.lang)
product_context.update({
'partner': order.partner_id,
'quantity': qty,
'date': order.date_order,
'pricelist': order.pricelist_id.id,
})
product = self.env['product.product'].with_context(product_context).with_company(order.company_id.id).browse(product_id)
discount = 0
order = order.with_company(order.company_id)
product = order.env['product.product'].browse(product_id)
if order.pricelist_id.discount_policy == 'without_discount':
# This part is pretty much a copy-paste of the method '_compute_discount' of
discount = 0
order_date = order.date_order or fields.Date.today()
qty = qty or 1.0
pricelist = order.pricelist_id
pricelist_price, pricelist_rule_id = pricelist._get_product_price_rule(product, qty, date=order_date)
if pricelist.discount_policy == 'without_discount':
# This part is pretty much a copy-paste of the method '_onchange_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)
if order.pricelist_id and order.partner_id:
order_line = order._cart_find_product_line(product.id)
if order_line:
price = self.env['account.tax']._fix_tax_included_price_company(price, product.taxes_id, order_line[0].tax_id, self.company_id)
pu = self.env['account.tax']._fix_tax_included_price_company(pu, product.taxes_id, order_line[0].tax_id, self.company_id)
if pu != 0:
if order.pricelist_id.currency_id != currency:
pricelist_base_price, currency = request.env['sale.order.line']._get_real_price_currency(
product, pricelist_rule_id, qty, product.uom_id, date=order_date)
if pricelist_base_price != 0:
if order.currency_id != currency:
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
date = order.date_order or fields.Date.today()
pu = currency._convert(pu, order.pricelist_id.currency_id, order.company_id, date)
discount = (pu - price) / pu * 100
pricelist_base_price = currency._convert(
pricelist_base_price, order.currency_id, order.company_id, order_date)
price = pricelist_base_price
discount = (pricelist_base_price - pricelist_price) / pricelist_base_price * 100
if discount < 0:
# In case the discount is negative, we don't want to show it to the customer,
# but we still want to use the price defined on the pricelist
discount = 0
pu = price
price = pricelist_price
else:
# In case the price_unit equal 0 and therefore not able to calculate the discount,
# we fallback on the price defined on the pricelist.
pu = price
price = pricelist_price
else:
pu = product.price
if order.pricelist_id and order.partner_id:
order_line = order._cart_find_product_line(product.id)
if order_line:
pu = self.env['account.tax']._fix_tax_included_price_company(pu, product.taxes_id, order_line[0].tax_id, self.company_id)
price = pricelist_price
if order.pricelist_id and order.partner_id:
order_line = order._cart_find_product_line(product.id)
if order_line:
price = self.env['account.tax']._fix_tax_included_price_company(
price, product.taxes_id, order_line[0].tax_id, order.company_id)
return {
'product_id': product_id,
'product_uom_qty': qty,
'order_id': order_id,
'product_uom': product.uom_id.id,
'price_unit': pu,
'price_unit': price,
'discount': discount,
}
@@ -266,15 +262,9 @@ class SaleOrder(models.Model):
no_variant_attributes_price_extra = [ptav.price_extra for ptav in order_line.product_no_variant_attribute_value_ids]
values = self.with_context(no_variant_attributes_price_extra=tuple(no_variant_attributes_price_extra))._website_product_id_change(self.id, product_id, qty=quantity, **kwargs)
order = self.sudo().browse(self.id)
if self.pricelist_id.discount_policy == 'with_discount' and not self.env.context.get('fixed_price'):
product_context.update({
'partner': order.partner_id,
'quantity': quantity,
'date': order.date_order,
'pricelist': order.pricelist_id.id,
})
product_with_context = self.env['product.product'].with_context(product_context).with_company(order.company_id.id)
product = product_with_context.browse(product_id)
product = product.with_company(order.company_id)
product_with_context = product_with_context.with_company(order.company_id)
order_line.write(values)
+1 -1
View File
@@ -283,7 +283,7 @@ def depends_context(*args):
pricelist = self.env['product.pricelist'].browse(product.env.context['pricelist'])
else:
pricelist = self.env['product.pricelist'].get_default_pricelist()
product.price = pricelist.get_products_price(product).get(product.id, 0.0)
product.price = pricelist._get_products_price(product).get(product.id, 0.0)
All dependencies must be hashable. The following keys have special
support: