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