From da12f2b809d74ef5637153b1fe67c9fc9b741fa4 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 9 Dec 2014 15:28:37 +0100 Subject: [PATCH] [FIX] product: pricelist: multiple corrections in UoM-related conversions Pricelist computations need to consider 2 different Units of Measure: - The default product UoM (product.uom_id), used as reference for the various quantities and amounts specified in each pricelist rules. - The `context UoM` is the UoM in which the result is requested, that is the list price UoM. For example the 'price_min_margin' amount is meant for the unit price of 1 x default UoM. When the context UoM is not the default product UoM, it can be any UoM of the same UoM Category, and the various quantities and amounts specified on the rule need to be adapted accordingly: - min_quantity (expressed in terms of the default UoM) - price_surcharge (specified for 1 x default UoM) - price_min_margin (specified for 1 x default UoM) - price_max_margin (specified for 1 x default UoM) The UoM corrections were not done consistently and resulted in wrong prices when computing the price using a non-default UoM. The cases were a conversion was needed or not were not properly identified within the _price_rule_get_multi(). After this commit, the various code branches in _price_rule_get_multi always ensures that: - price requested for: `qty` of `qty_uom_id` - `qty_in_product_uom` is the requested `qty` converted to default UoM - current (intermediary) price: `price` for `price_uom_id` Therefore `price` and `price_uom_id` are always in sync, and `price_uom_id` can always be compared with `qty_uom_id' in order to know whether a conversion is still needed. This patch also corrects and extends the regression tests introduced at revision 79ebe10. --- addons/product/pricelist.py | 78 +++++++++++++++----------- addons/product/product.py | 3 +- addons/product/tests/test_pricelist.py | 47 +++++++++++++++- 3 files changed, 93 insertions(+), 35 deletions(-) diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py index 49b518a8078..5881099a13c 100644 --- a/addons/product/pricelist.py +++ b/addons/product/pricelist.py @@ -247,19 +247,27 @@ class product_pricelist(osv.osv): results = {} for product, qty, partner in products_by_qty_by_partner: - uom_price_already_computed = False results[product.id] = 0.0 - price = False rule_id = False + price = False + + # 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 = context.get('uom') or product.uom_id.id + price_uom_id = product.uom_id.id + qty_in_product_uom = qty + if qty_uom_id != product.uom_id.id: + try: + qty_in_product_uom = product_uom_obj._compute_qty( + cr, uid, context['uom'], qty, product.uom_id.id or product.uos_id.id) + except except_orm: + # Ignored - incompatible UoM in context, use default product UoM + pass + for rule in items: - if 'uom' in context and product.uom_id and context['uom'] != product.uom_id.id: - try: - qty_in_product_uom = product_uom_obj._compute_qty(cr, uid, context['uom'], qty, product.uom_id.id, dict(context.items() + [('raise-exception', False)])) - except except_orm: - qty_in_product_uom = qty - else: - qty_in_product_uom = qty - if rule.min_quantity and qty_in_product_uom 3 tonnes + spam_id = self.product_product.copy(cr, uid, self.usb_adapter_id, + { 'name': '1 tonne of spam', + 'uom_id': self.tonne_id, + 'uos_id': self.tonne_id, + 'uom_po_id': self.tonne_id, + 'list_price': tonne_price, + }) + pricelist_version_id = self.ir_model_data.xmlid_to_res_id(cr, uid, 'product.ver0') + self.registry('product.pricelist.item').create(cr, uid, + { 'price_version_id': pricelist_version_id, + 'sequence': 10, + 'name': '3+ tonnes: -10 EUR discount/t', + 'base': 1, # based on public price + 'min_quantity': 3, # min = 3 tonnes + 'price_surcharge': -10, # -10 EUR / tonne + 'product_id': spam_id, + }) + pricelist_id = self.public_pricelist_id + + def test_unit_price(qty, uom, expected_unit_price): + unit_price = self.registry('product.pricelist').price_get(cr, uid, [pricelist_id], + spam_id, qty, + context={'uom': uom})[pricelist_id] + 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! + test_unit_price(2, kg, tonne_price / 1000.0) + test_unit_price(2000, kg, tonne_price / 1000.0) + test_unit_price(3500, kg, (tonne_price - 10) / 1000.0) + test_unit_price(2, tonne, tonne_price) + test_unit_price(3, tonne, tonne_price - 10)