From 77c9e76e82bbdace7ad0e3bcba4630027b7865af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 15 Jul 2016 13:09:02 +0200 Subject: [PATCH] [REF] product_pricelist, *: clean the price and rule computation methods A new API is introduced for price computing using pricelists. It is simplier to use in most use cases, which is computing the product price in a given pricelist. Old methods return dictionnaries and tuples, which are rarely used in the code. --- addons/mrp_repair/models/mrp_repair.py | 4 +- addons/point_of_sale/models/pos_order.py | 4 +- addons/product/models/product.py | 5 +- addons/product/models/product_pricelist.py | 108 +++++++++++++----- addons/product/models/product_template.py | 5 +- addons/product/report/product_pricelist.py | 7 +- addons/product/tests/test_pricelist.py | 8 +- .../models/sale_order.py | 14 +-- addons/website_quote/models/order.py | 6 +- 9 files changed, 105 insertions(+), 56 deletions(-) diff --git a/addons/mrp_repair/models/mrp_repair.py b/addons/mrp_repair/models/mrp_repair.py index c3970a71d52..9f99e97adf1 100644 --- a/addons/mrp_repair/models/mrp_repair.py +++ b/addons/mrp_repair/models/mrp_repair.py @@ -492,7 +492,7 @@ class RepairLine(models.Model): 'message': _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.')} else: - price = pricelist.price_get(self.product_id.id, self.product_uom_qty, partner.id)[pricelist.id] + price = pricelist.get_product_price(self.product_id, self.product_uom_qty, partner) if price is False: warning = { 'title': _('No valid pricelist line found !'), @@ -554,7 +554,7 @@ class RepairFee(models.Model): 'message': _('You have to select a pricelist in the Repair form !\n Please set one before choosing a product.')} else: - price = pricelist.price_get(self.product_id.id, self.product_uom_qty, partner.id)[pricelist.id] + price = pricelist.get_product_price(self.product_id, self.product_uom_qty, partner) if price is False: warning = { 'title': _('No valid pricelist line found !'), diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index f8f31dee81b..8ba58432a97 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -755,8 +755,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.price_get( - self.product_id.id, self.qty or 1.0, self.order_id.partner_id.id)[self.order_id.pricelist_id.id] + price = self.order_id.pricelist_id.get_product_price( + self.product_id, self.qty or 1.0, self.order_id.partner_id) self._onchange_qty() self.price_unit = price self.tax_ids = self.product_id.taxes_id diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 61299c41873..02eb87c4c02 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -162,9 +162,10 @@ class product_product(osv.osv): if isinstance(pricelist, (int, long)): products = self.browse(cr, uid, ids, context=context) - qtys = map(lambda x: (x, quantity, partner), products) + quantities = [quantity] * len(products) + partners = [partner] * len(products) pl = plobj.browse(cr, uid, pricelist, context=context) - price = plobj._price_get_multi(cr,uid, pl, qtys, context=context) + price = plobj.get_products_price(cr, uid, [pl.id], products, quantities, partners, context=context) for id in ids: res[id] = price.get(id, 0.0) for id in ids: diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py index 43cc0bebc92..c8ef2993d1c 100644 --- a/addons/product/models/product_pricelist.py +++ b/addons/product/models/product_pricelist.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from collections import namedtuple, defaultdict from itertools import chain import time @@ -98,32 +99,44 @@ class product_pricelist(osv.osv): 'item_ids': _get_item_ids, } - def price_rule_get_multi(self, cr, uid, ids, products_by_qty_by_partner, context=None): - """multi products 'price_get'. - @param ids: - @param products_by_qty: - @param partner: - @param context: { - 'date': Date of the pricelist (%Y-%m-%d),} - @return: a dict of dict with product_id as key and a dict 'price by pricelist' as value - """ + def _compute_price_rule_multi(self, cr, uid, ids, products_qty_partner, date=False, uom_id=False, context=None): + """ Low-level method - Multi pricelist, multi products + Returns: dict{product_id: dict{pricelist_id: (price, suitable_rule)} }""" if not ids: ids = self.pool.get('product.pricelist').search(cr, uid, [], context=context) results = {} for pricelist in self.browse(cr, uid, ids, context=context): - subres = self._price_rule_get_multi(cr, uid, pricelist, products_by_qty_by_partner, context=context) + subres = self._compute_price_rule(cr, uid, [pricelist.id], products_qty_partner, date=date, uom_id=uom_id, context=context) for product_id, price in subres.items(): results.setdefault(product_id, {}) results[product_id][pricelist.id] = price return results - def _price_get_multi(self, cr, uid, pricelist, products_by_qty_by_partner, context=None): - return dict((key, price[0]) for key, price in self._price_rule_get_multi(cr, uid, pricelist, products_by_qty_by_partner, context=context).items()) + def _compute_price_rule(self, cr, uid, ids, products_qty_partner, date=False, uom_id=False, context=None): + """ Low-level method - Mono pricelist, multi products + Returns: dict{product_id: (price, suitable_rule) for the given pricelist} - def _price_rule_get_multi(self, cr, uid, pricelist, products_by_qty_by_partner, context=None): - context = context or {} - date = context.get('date') and context['date'][0:10] or time.strftime(DEFAULT_SERVER_DATE_FORMAT) - products = map(lambda x: x[0], products_by_qty_by_partner) + If date in context: Date of the pricelist (%Y-%m-%d) + + :param products_qty_partner: list of typles products, quantity, partner + :param datetime date: validity date + :param ID uom_id: intermediate unit of measure + """ + context = context if context is not None else {} + if not date: + date = context.get('date', time.strftime(DEFAULT_SERVER_DATE_FORMAT)) + if not uom_id and context.get('uom'): + uom_id = context['uom'] + if uom_id: + # rebrowse with uom if given + product_ids = [item[0].id for item in products_qty_partner] + products = self.pool['product.product'].browse(cr, uid, product_ids, context=dict(context, uom=uom_id)) + 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] + + pricelist = self.browse(cr, uid, ids[0], context=context) + context = context if context is not None else {} product_uom_obj = self.pool.get('product.uom') if not products: @@ -149,22 +162,23 @@ class product_pricelist(osv.osv): # Load all rules cr.execute( - 'SELECT i.id ' - 'FROM product_pricelist_item AS i ' - 'LEFT JOIN product_category AS c ' - 'ON i.categ_id = c.id ' - 'WHERE (product_tmpl_id IS NULL OR product_tmpl_id = any(%s))' - 'AND (product_id IS NULL OR product_id = any(%s))' - 'AND (categ_id IS NULL OR categ_id = any(%s)) ' - 'AND (pricelist_id = %s) ' - 'AND ((i.date_start IS NULL OR i.date_start<=%s) AND (i.date_end IS NULL OR i.date_end>=%s))' - 'ORDER BY applied_on, min_quantity desc, c.parent_left desc', + 'SELECT item.id ' + 'FROM product_pricelist_item AS item ' + 'LEFT JOIN product_category AS categ ' + 'ON item.categ_id = categ.id ' + 'WHERE (item.product_tmpl_id IS NULL OR item.product_tmpl_id = any(%s))' + 'AND (item.product_id IS NULL OR item.product_id = any(%s))' + 'AND (item.categ_id IS NULL OR item.categ_id = any(%s)) ' + 'AND (item.pricelist_id = %s) ' + 'AND (item.date_start IS NULL OR item.date_start<=%s) ' + 'AND (item.date_end IS NULL OR item.date_end>=%s)' + 'ORDER BY item.applied_on, item.min_quantity desc, categ.parent_left desc', (prod_tmpl_ids, prod_ids, categ_ids, pricelist.id, date, date)) item_ids = [x[0] for x in cr.fetchall()] items = self.pool.get('product.pricelist.item').browse(cr, uid, item_ids, context=context) results = {} - for product, qty, partner in products_by_qty_by_partner: + for product, qty, partner in products_qty_partner: results[product.id] = 0.0 suitable_rule = False @@ -213,7 +227,7 @@ class product_pricelist(osv.osv): continue if rule.base == 'pricelist' and rule.base_pricelist_id: - price_tmp = self._price_get_multi(cr, uid, rule.base_pricelist_id, [(product, qty, partner)], context=context)[product.id] + price_tmp = self._compute_price_rule(cr, uid, [rule.base_pricelist_id.id], [(product, qty, partner)], context=context)[product.id][0] # TDE: 0 = price, 1 = rule ptype_src = rule.base_pricelist_id.currency_id.id price = self.pool['res.currency'].compute(cr, uid, ptype_src, pricelist.currency_id.id, price_tmp, round=False, context=context) else: @@ -255,16 +269,48 @@ class product_pricelist(osv.osv): price = self.pool['res.currency'].compute(cr, uid, product.currency_id.id, pricelist.currency_id.id, price, round=False, context=context) results[product.id] = (price, suitable_rule and suitable_rule.id or False) + return results + # New methods: product based + def get_products_price(self, cr, uid, ids, products, quantities, partners, date=False, uom_id=False, context=None): + """ For a given pricelist, return price for products + Returns: dict{product_id: product price}, in the given pricelist """ + pricelist = self.browse(cr, uid, ids[0], context=context) + return dict((product_id, res_tuple[0]) for product_id, res_tuple in pricelist._compute_price_rule(zip(products, quantities, partners), date=date, uom_id=uom_id).iteritems()) + + def get_product_price(self, cr, uid, ids, product, quantity, partner, date=False, uom_id=False, context=None): + """ For a given pricelist, return price for a given product """ + pricelist = self.browse(cr, uid, ids[0], context=context) + return pricelist._compute_price_rule([(product, quantity, partner)], date=date, uom_id=uom_id)[product.id][0] + + def get_product_price_rule(self, cr, uid, ids, product, quantity, partner, date=False, uom_id=False, context=None): + """ For a given pricelist, return price and rule for a given product """ + pricelist = self.browse(cr, uid, ids[0], context=context) + return pricelist._compute_price_rule([(product, quantity, partner)], date=date, uom_id=uom_id)[product.id] + + # Compatibility to remove after v10 - DEPRECATED + def _price_rule_get_multi(self, cr, uid, pricelist, products_by_qty_by_partner, context=None): + """ Low level method computing the result tuple for a given pricelist and multi products - return tuple """ + return self._compute_price_rule(cr, uid, [pricelist.id], products_by_qty_by_partner, context=context) + def price_get(self, cr, uid, ids, prod_id, qty, partner=None, context=None): + """ Multi pricelist, mono product - returns price per pricelist """ return dict((key, price[0]) for key, price in self.price_rule_get(cr, uid, ids, prod_id, qty, partner=partner, context=context).items()) + def price_rule_get_multi(self, cr, uid, ids, products_by_qty_by_partner, context=None): + """ Multi pricelist, multi product - return tuple """ + return self._compute_price_rule_multi(cr, uid, ids, products_by_qty_by_partner, context=context) + def price_rule_get(self, cr, uid, ids, prod_id, qty, partner=None, context=None): + """ Multi pricelist, mono product - return tuple """ product = self.pool.get('product.product').browse(cr, uid, prod_id, context=context) - res_multi = self.price_rule_get_multi(cr, uid, ids, products_by_qty_by_partner=[(product, qty, partner)], context=context) - res = res_multi[prod_id] - return res + return self._compute_price_rule_multi(cr, uid, ids, [(product, qty, partner)], context=context)[prod_id] + + def _price_get_multi(self, cr, uid, pricelist, products_by_qty_by_partner, context=None): + """ Mono pricelist, multi product - return price per product """ + return self.get_products_price(cr, uid, [pricelist.id], zip(**products_by_qty_by_partner), context=context) + class product_pricelist_item(osv.osv): _name = "product.pricelist.item" diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 57003af1c93..f3f17b575ee 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -44,9 +44,10 @@ class product_template(osv.osv): if isinstance(pricelist, (int, long)): products = self.browse(cr, uid, ids, context=context) - qtys = map(lambda x: (x, quantity, partner), products) + quantities = [quantity] * len(products) + partners = [partner] * len(products) pl = plobj.browse(cr, uid, pricelist, context=context) - price = plobj._price_get_multi(cr, uid, pl, qtys, context=context) + price = plobj.get_products_price(cr, uid, [pl.id], products, quantities, partners, context=context) for id in ids: res[id] = price.get(id, 0.0) for id in ids: diff --git a/addons/product/report/product_pricelist.py b/addons/product/report/product_pricelist.py index 28003d84bf9..b2b8e856965 100644 --- a/addons/product/report/product_pricelist.py +++ b/addons/product/report/product_pricelist.py @@ -87,9 +87,10 @@ class product_pricelist(report_sxw.rml_parse): def _get_price(self, pricelist_id, product_id, qty): sale_price_digits = self.get_digits(dp='Product Price') pricelist = self.pool.get('product.pricelist').browse(self.cr, self.uid, [pricelist_id], context=self.localcontext)[0] - price_dict = self.pool.get('product.pricelist').price_get(self.cr, self.uid, [pricelist_id], product_id, qty, context=self.localcontext) - if price_dict[pricelist_id]: - price = float_round(price_dict[pricelist_id], precision_digits=sale_price_digits) + product = self.pool['product.product'].browse(self.cr, self.uid, product_id, context=self.localcontext) + price = self.pool.get('product.pricelist').get_product_price(self.cr, self.uid, [pricelist_id], product, qty, False, context=self.localcontext) + if price: + price = float_round(price, precision_digits=sale_price_digits) else: res = self.pool.get('product.product').read(self.cr, self.uid, [product_id], ['list_price']) price = float_round(res[0]['list_price'], precision_digits=sale_price_digits) diff --git a/addons/product/tests/test_pricelist.py b/addons/product/tests/test_pricelist.py index 063b71124b6..8639e3914b1 100644 --- a/addons/product/tests/test_pricelist.py +++ b/addons/product/tests/test_pricelist.py @@ -86,6 +86,7 @@ class TestPricelist(TransactionCase): 'list_price': tonne_price, 'type': 'consu', }) + self.registry('product.pricelist.item').create(cr, uid, { 'pricelist_id': self.public_pricelist_id, 'sequence': 10, @@ -98,9 +99,10 @@ class TestPricelist(TransactionCase): 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] + spam = self.product_product.browse(cr, uid, spam_id, {'uom': uom}) + unit_price = self.registry('product.pricelist').get_product_price(cr, uid, [pricelist_id], + spam, qty, False, + context={'uom': uom}) self.assertAlmostEqual(unit_price, expected_unit_price) # Test prices - they are *per unit*, the quantity is only here to match the pricelist rules! diff --git a/addons/product_visible_discount/models/sale_order.py b/addons/product_visible_discount/models/sale_order.py index 7b6a0b52c7e..6a27e82bd78 100644 --- a/addons/product_visible_discount/models/sale_order.py +++ b/addons/product_visible_discount/models/sale_order.py @@ -7,17 +7,16 @@ from openerp import api, fields, models class SaleOrderLine(models.Model): _inherit = "sale.order.line" - def _get_real_price_currency(self, product, pricelist_dict, qty, uom, pricelist_id): + def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id): """Retrieve the price before applying the pricelist :param obj product: object of current product record :parem float qty: total quentity of product - :param dict pricelist_dict: dict contain pricelist id and rule id + :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 sale order""" PricelistItem = self.env['product.pricelist.item'] field_name = 'lst_price' currency_id = None - rule_id = pricelist_dict[pricelist_id][1] if pricelist_dict.get(pricelist_id) else False if rule_id: pricelist_item = PricelistItem.browse(rule_id) if pricelist_item.base == 'standard_price': @@ -55,17 +54,16 @@ class SaleOrderLine(models.Model): context_partner = dict(self.env.context, partner_id=self.order_id.partner_id.id) pricelist_context = dict(context_partner, uom=self.product_uom.id, date=self.order_id.date_order) - list_price = self.order_id.pricelist_id.with_context(pricelist_context).price_rule_get(self.product_id.id, self.product_uom_qty or 1.0, self.order_id.partner_id) - new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, list_price, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) + price, rule_id = self.order_id.pricelist_id.with_context(pricelist_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id) + new_list_price, currency_id = self.with_context(context_partner)._get_real_price_currency(self.product_id, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id) new_list_price = self.env['account.tax']._fix_tax_included_price(new_list_price, self.product_id.taxes_id, self.tax_id) - list_price = list_price[self.order_id.pricelist_id.id][0] - if list_price != 0 and new_list_price != 0: + if price != 0 and new_list_price != 0: if self.product_id.company_id and self.order_id.pricelist_id.currency_id != self.product_id.company_id.currency_id: # new_list_price is in company's currency while price in pricelist currency ctx = dict(context_partner, date=self.order_id.date_order) new_list_price = self.env['res.currency'].browse(currency_id).with_context(ctx).compute(new_list_price, self.order_id.pricelist_id.currency_id) - discount = (new_list_price - list_price) / new_list_price * 100 + discount = (new_list_price - price) / new_list_price * 100 if discount > 0: self.discount = discount diff --git a/addons/website_quote/models/order.py b/addons/website_quote/models/order.py index 11d37f2e302..25f47f54d47 100644 --- a/addons/website_quote/models/order.py +++ b/addons/website_quote/models/order.py @@ -213,7 +213,7 @@ class sale_order(osv.osv): if pricelist_id: uom_context = context.copy() uom_context['uom'] = line.product_uom_id.id - price = pricelist_obj.price_get(cr, uid, [pricelist_id], line.product_id.id, 1, context=uom_context)[pricelist_id] + price = pricelist_obj.get_product_price(cr, uid, [pricelist_id], line.product_id, 1, False, context=uom_context) else: price = line.price_unit @@ -241,7 +241,7 @@ class sale_order(osv.osv): if pricelist_id: uom_context = context.copy() uom_context['uom'] = option.uom_id.id - price = pricelist_obj.price_get(cr, uid, [pricelist_id], option.product_id.id, 1, context=uom_context)[pricelist_id] + price = pricelist_obj.get_product_price(cr, uid, [pricelist_id], option.product_id, 1, False, context=uom_context) else: price = option.price_unit options.append((0, 0, { @@ -461,7 +461,7 @@ class sale_order_option(osv.osv): pricelist = self.order_id.pricelist_id if pricelist and product: partner_id = self.order_id.partner_id.id - self.price_unit = pricelist.with_context(uom=self.uom_id.id).price_get(product.id, self.quantity, partner_id)[pricelist.id] + self.price_unit = pricelist.with_context(uom=self.uom_id.id).get_product_price(product, self.quantity, partner_id) domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]} return {'domain': domain}