From 57ced812f099bec67bade265fd592160ad5690a2 Mon Sep 17 00:00:00 2001 From: Victor Feyens Date: Mon, 8 Nov 2021 11:21:58 +0100 Subject: [PATCH] [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. --- addons/mrp_repair/models/repair.py | 2 +- addons/point_of_sale/models/pos_order.py | 4 +- addons/point_of_sale/models/product.py | 2 +- addons/point_of_sale/static/src/js/models.js | 2 +- addons/point_of_sale/tests/common.py | 2 +- .../tests/test_pos_other_currency_config.py | 14 +- addons/point_of_sale/tests/test_pos_setup.py | 2 +- addons/product/models/product.py | 11 +- addons/product/models/product_pricelist.py | 212 ++++++++++-------- addons/product/models/product_template.py | 11 +- .../report/product_pricelist_report.py | 2 +- addons/product/tests/test_pricelist.py | 10 +- addons/repair/models/repair.py | 6 +- addons/sale/models/sale_order.py | 1 + addons/sale/models/sale_order_line.py | 138 ++++++------ addons/sale_management/models/sale_order.py | 20 +- addons/website_sale/models/sale_order.py | 74 +++--- odoo/api.py | 2 +- 18 files changed, 261 insertions(+), 254 deletions(-) diff --git a/addons/mrp_repair/models/repair.py b/addons/mrp_repair/models/repair.py index 5ae591fba41..1c39c0edc3b 100644 --- a/addons/mrp_repair/models/repair.py +++ b/addons/mrp_repair/models/repair.py @@ -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 diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 3fafb06b35e..ff495959aad 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -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) diff --git a/addons/point_of_sale/models/product.py b/addons/point_of_sale/models/product.py index d4fe5288b7d..f6627037b5d 100644 --- a/addons/point_of_sale/models/product.py +++ b/addons/point_of_sale/models/product.py @@ -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 diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index e3419ba0818..204d029fa56 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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 diff --git a/addons/point_of_sale/tests/common.py b/addons/point_of_sale/tests/common.py index 4068c2a9ff0..2c16284419f 100644 --- a/addons/point_of_sale/tests/common.py +++ b/addons/point_of_sale/tests/common.py @@ -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) diff --git a/addons/point_of_sale/tests/test_pos_other_currency_config.py b/addons/point_of_sale/tests/test_pos_other_currency_config.py index c7e0a1aaedb..4c0765427dd 100644 --- a/addons/point_of_sale/tests/test_pos_other_currency_config.py +++ b/addons/point_of_sale/tests/test_pos_other_currency_config.py @@ -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 diff --git a/addons/point_of_sale/tests/test_pos_setup.py b/addons/point_of_sale/tests/test_pos_setup.py index 22fa68b3f9b..9e44d9bdea4 100644 --- a/addons/point_of_sale/tests/test_pos_setup.py +++ b/addons/point_of_sale/tests/test_pos_setup.py @@ -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 diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 62d5827f2c6..7f0c1919fbc 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -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) diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 8d41359ee70..049b6fd33db 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -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 diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 93c3792e65b..0551750195a 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -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 diff --git a/addons/product/report/product_pricelist_report.py b/addons/product/report/product_pricelist_report.py index 44463dd5f77..f4dee8f0ab0 100644 --- a/addons/product/report/product_pricelist_report.py +++ b/addons/product/report/product_pricelist_report.py @@ -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'] = [ diff --git a/addons/product/tests/test_pricelist.py b/addons/product/tests/test_pricelist.py index 610865f5af1..e097532a7e5 100644 --- a/addons/product/tests/test_pricelist.py +++ b/addons/product/tests/test_pricelist.py @@ -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! diff --git a/addons/repair/models/repair.py b/addons/repair/models/repair.py index 395407d2f37..8de9cdd5f77 100644 --- a/addons/repair/models/repair.py +++ b/addons/repair/models/repair.py @@ -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.'), diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py index f668729c711..5e4be166c0e 100644 --- a/addons/sale/models/sale_order.py +++ b/addons/sale/models/sale_order.py @@ -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, diff --git a/addons/sale/models/sale_order_line.py b/addons/sale/models/sale_order_line.py index 8572c0b488c..3043fb7dd99 100644 --- a/addons/sale/models/sale_order_line.py +++ b/addons/sale/models/sale_order_line.py @@ -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: diff --git a/addons/sale_management/models/sale_order.py b/addons/sale_management/models/sale_order.py index a9c5a097232..0c6c533b56f 100644 --- a/addons/sale_management/models/sale_order.py +++ b/addons/sale_management/models/sale_order.py @@ -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() diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index fb53371f80a..95f63361122 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -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) diff --git a/odoo/api.py b/odoo/api.py index b0fca49f9da..841cf1cdf58 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -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: