From 920b5fa91305b51ce1225d0e668201df5dc05918 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Wed, 8 Jan 2025 13:48:17 -0500 Subject: [PATCH] [MIG] sale_pricelist_global_rule: Migration to version 17.0 - Reused Odoo's base code for computing pricelists, adapted it to new functions, and simplified the logic. - Renamed selection options for the applied_on field: 4_global_product_template to 3_1_global_product_template and 5_global_product_category to 3_2_global_product_category to ensure the correct order in pricelist items. - Fixed an assertion in the tests. Previously, changing the UoM did not update the price, but this behavior was introduced in PR OCA/sale-workflow#2473. - Added a new assertion for the pricelist_item_id field. --- sale_pricelist_global_rule/__manifest__.py | 2 +- sale_pricelist_global_rule/models/__init__.py | 1 + .../models/product_pricelist.py | 209 +++--------------- .../models/sale_order.py | 113 ++++------ .../models/sale_order_line.py | 22 ++ .../tests/test_pricelist_global.py | 206 ++++++++++++++++- .../views/product_pricelist_item_views.xml | 6 +- .../views/sale_order_views.xml | 4 +- 8 files changed, 307 insertions(+), 256 deletions(-) create mode 100644 sale_pricelist_global_rule/models/sale_order_line.py diff --git a/sale_pricelist_global_rule/__manifest__.py b/sale_pricelist_global_rule/__manifest__.py index 4335f479d53..8dd2d6293f2 100644 --- a/sale_pricelist_global_rule/__manifest__.py +++ b/sale_pricelist_global_rule/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Sale pricelist global rule", - "version": "15.0.1.0.3", + "version": "17.0.1.0.0", "summary": "Apply a global rule to all sale order", "author": "Tecnativa, Odoo Community Association (OCA)", "category": "Sales Management", diff --git a/sale_pricelist_global_rule/models/__init__.py b/sale_pricelist_global_rule/models/__init__.py index d9cbc1df214..afb6ccc5fac 100644 --- a/sale_pricelist_global_rule/models/__init__.py +++ b/sale_pricelist_global_rule/models/__init__.py @@ -1,2 +1,3 @@ from . import sale_order from . import product_pricelist +from . import sale_order_line diff --git a/sale_pricelist_global_rule/models/product_pricelist.py b/sale_pricelist_global_rule/models/product_pricelist.py index 3654bc9eb42..5631023bd6e 100644 --- a/sale_pricelist_global_rule/models/product_pricelist.py +++ b/sale_pricelist_global_rule/models/product_pricelist.py @@ -2,175 +2,17 @@ from odoo.exceptions import ValidationError -class ProductPricelist(models.Model): - _inherit = "product.pricelist" - - def _compute_price_rule_get_items( - self, products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids - ): - items = super()._compute_price_rule_get_items( - products_qty_partner, date, uom_id, prod_tmpl_ids, prod_ids, categ_ids - ) - # ignore new global rules on Odoo standard - return items.filtered( - lambda item: item.applied_on - not in ["4_global_product_template", "5_global_product_category"] - ) - - def _compute_price_rule_get_items_globals(self, date, prod_tmpl_ids, categ_ids): - self.ensure_one() - # Load all global rules - # inspired by _compute_price_rule_get_items - # but only for global rules - self.env["product.pricelist.item"].flush( - ["price", "currency_id", "company_id", "active", "date_start", "date_end"] - ) - self.env.cr.execute( - """ - SELECT - item.id - FROM - product_pricelist_item AS item - LEFT JOIN product_category AS categ - ON item.global_categ_id = categ.id - WHERE - (item.global_product_tmpl_id IS NULL OR item.global_product_tmpl_id = any(%s)) - AND (item.global_categ_id IS NULL OR item.global_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) - AND (item.active = TRUE) - AND item.applied_on IN ( - '4_global_product_template', - '5_global_product_category' - ) - ORDER BY - item.applied_on, item.min_quantity desc, categ.complete_name desc, item.id desc - """, - (prod_tmpl_ids, categ_ids, self.id, date, date), - ) - # NOTE: if you change `order by` on that query, make sure it matches - # _order from model to avoid inconstencies and undeterministic issues. - - item_ids = [x[0] for x in self.env.cr.fetchall()] - return self.env["product.pricelist.item"].browse(item_ids) - - def _extract_products_and_categs_from_sale(self, sale): - """ - Extract unique product templates and categories (including their parents) - :param sale: browse_record(sale.order) - :returns: tuple(product_template_ids , product_category_ids) - """ - categ_ids = set() - prod_tmpl_ids = set() - for line in sale.order_line.filtered(lambda x: not x.display_type): - prod_tmpl_ids.add(line.product_id.product_tmpl_id.id) - categ = line.product_id.categ_id - while categ: - categ_ids.add(categ.id) - categ = categ.parent_id - return list(prod_tmpl_ids), list(categ_ids) - - def _compute_price_rule_global(self, sale): - """Compute the price for the given sale order - :param sale: browse_record(sale.order) - :returns: dict{sale_order_line_id: (price, suitable_rule) for the given pricelist} - """ - self.ensure_one() - date = sale.date_order - qty_data = { - "by_template": {}, - "by_categ": {}, - } - for line in sale.order_line.filtered(lambda x: not x.display_type): - qty_in_product_uom = line.product_uom_qty - # Final unit price is computed according to `qty` in the default `uom_id`. - if line.product_uom != line.product_id.uom_id: - qty_in_product_uom = line.product_uom._compute_quantity( - qty_in_product_uom, line.product_id.uom_id - ) - key_template = line.product_id.product_tmpl_id - key_categ = line.product_id.categ_id - qty_data["by_template"].setdefault(key_template, 0.0) - qty_data["by_template"][key_template] += qty_in_product_uom - qty_data["by_categ"].setdefault(key_categ, 0.0) - qty_data["by_categ"][key_categ] += qty_in_product_uom - - prod_tmpl_ids, categ_ids = self._extract_products_and_categs_from_sale(sale) - - items = self._compute_price_rule_get_items_globals( - date, prod_tmpl_ids, categ_ids - ) - results = {} - for line in sale.order_line.filtered(lambda x: not x.display_type): - product = line.product_id - results[line.id] = 0.0 - suitable_rule = False - - # if Public user try to access standard price from website sale, - # need to call price_compute. - price = product.price_compute("list_price")[product.id] - - price_uom = product.uom_id - for rule in items: - if not rule._is_applicable_for_sale(product.product_tmpl_id, qty_data): - continue - if rule.base == "pricelist" and rule.base_pricelist_id: - # first, try compute the price for global rule - # otherwise, fallback to regular computation - # with qty from line instead of accumulated qty - ( - price, - rule_applied, - ) = rule.base_pricelist_id._compute_price_rule_global(sale)[line.id] - if not rule_applied: - price = rule.base_pricelist_id._compute_price_rule( - [(product, line.product_uom_qty, sale.partner_id)], - date, - line.product_uom.id, - )[product.id][0] - src_currency = rule.base_pricelist_id.currency_id - 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 rule.base == "standard_price": - src_currency = product.cost_currency_id - else: - src_currency = product.currency_id - - if src_currency != self.currency_id: - price = src_currency._convert( - price, self.currency_id, self.env.company, date, round=False - ) - - if price is not False: - price = rule._compute_price(price, price_uom, product) - suitable_rule = rule - break - - if not suitable_rule: - cur = product.currency_id - price = cur._convert( - price, self.currency_id, self.env.company, date, round=False - ) - - results[line.id] = (price, suitable_rule and suitable_rule.id or False) - - return results - - class ProductPricelistItem(models.Model): _inherit = "product.pricelist.item" applied_on = fields.Selection( selection_add=[ - ("4_global_product_template", "Global - Product template"), - ("5_global_product_category", "Global - Product category"), + ("3_1_global_product_template", "Global - Product template"), + ("3_2_global_product_category", "Global - Product category"), ], ondelete={ - "4_global_product_template": "set default", - "5_global_product_category": "set default", + "3_1_global_product_template": "set default", + "3_2_global_product_category": "set default", }, ) global_product_tmpl_id = fields.Many2one( @@ -196,7 +38,7 @@ def _check_product_consistency(self): res = super()._check_product_consistency() for item in self: if ( - item.applied_on == "5_global_product_category" + item.applied_on == "3_2_global_product_category" and not item.global_categ_id ): raise ValidationError( @@ -206,7 +48,7 @@ def _check_product_consistency(self): ) ) elif ( - item.applied_on == "4_global_product_template" + item.applied_on == "3_1_global_product_template" and not item.global_product_tmpl_id ): raise ValidationError( @@ -231,16 +73,19 @@ def _check_product_consistency(self): "price_discount", "price_surcharge", ) - def _get_pricelist_item_name_price(self): - res = super()._get_pricelist_item_name_price() + def _compute_name_and_price(self): + res = super()._compute_name_and_price() for item in self: - if item.global_categ_id and item.applied_on == "5_global_product_category": + if ( + item.global_categ_id + and item.applied_on == "3_2_global_product_category" + ): item.name = _("Global category: %s") % ( item.global_categ_id.display_name ) elif ( item.global_product_tmpl_id - and item.applied_on == "4_global_product_template" + and item.applied_on == "3_1_global_product_template" ): item.name = _("Global product: %s") % ( item.global_product_tmpl_id.display_name @@ -253,7 +98,7 @@ def create(self, vals_list): if values.get("applied_on", False): # Ensure item consistency for later searches. applied_on = values["applied_on"] - if applied_on == "5_global_product_category": + if applied_on == "3_2_global_product_category": values.update( { "product_id": None, @@ -262,7 +107,7 @@ def create(self, vals_list): "global_product_tmpl_id": None, } ) - elif applied_on == "4_global_product_template": + elif applied_on == "3_1_global_product_template": values.update( { "product_id": None, @@ -277,7 +122,7 @@ def write(self, values): if values.get("applied_on", False): # Ensure item consistency for later searches. applied_on = values["applied_on"] - if applied_on == "5_global_product_category": + if applied_on == "3_2_global_product_category": values.update( { "product_id": None, @@ -286,7 +131,7 @@ def write(self, values): "global_product_tmpl_id": None, } ) - elif applied_on == "4_global_product_template": + elif applied_on == "3_1_global_product_template": values.update( { "product_id": None, @@ -297,7 +142,7 @@ def write(self, values): ) return super().write(values) - def _is_applicable_for_sale(self, product_template, qty_data): + def _is_applicable_for(self, product, qty_in_product_uom): """Check whether the current rule is valid for the given sale order and cummulated quantity. :param product_template: browse_record(product.template) @@ -310,18 +155,24 @@ def _is_applicable_for_sale(self, product_template, qty_data): :rtype: bool """ self.ensure_one() + qty_data = self.env.context.get("pricelist_global_cummulative_quantity", {}) + if not qty_data or self.applied_on not in [ + "3_1_global_product_template", + "3_2_global_product_category", + ]: + return super()._is_applicable_for(product, qty_in_product_uom) is_applicable = True - if self.applied_on == "4_global_product_template": - total_qty = qty_data["by_template"].get(product_template, 0.0) + if self.applied_on == "3_1_global_product_template": + total_qty = qty_data["by_template"].get(product.product_tmpl_id, 0.0) if self.min_quantity and total_qty < self.min_quantity: is_applicable = False - elif self.global_product_tmpl_id != product_template: + elif self.global_product_tmpl_id != product.product_tmpl_id: is_applicable = False - elif self.applied_on == "5_global_product_category": - total_qty = qty_data["by_categ"].get(product_template.categ_id, 0.0) + elif self.applied_on == "3_2_global_product_category": + total_qty = qty_data["by_categ"].get(product.categ_id, 0.0) if self.min_quantity and total_qty < self.min_quantity: is_applicable = False - elif not product_template.categ_id.parent_path.startswith( + elif not product.categ_id.parent_path.startswith( self.global_categ_id.parent_path ): is_applicable = False diff --git a/sale_pricelist_global_rule/models/sale_order.py b/sale_pricelist_global_rule/models/sale_order.py index d41605a9dcd..58f68f25ae6 100644 --- a/sale_pricelist_global_rule/models/sale_order.py +++ b/sale_pricelist_global_rule/models/sale_order.py @@ -1,5 +1,4 @@ from odoo import api, fields, models -from odoo.tools import float_compare class SaleOrder(models.Model): @@ -8,20 +7,55 @@ class SaleOrder(models.Model): need_recompute_pricelist_global = fields.Boolean() has_pricelist_global = fields.Boolean(compute="_compute_has_pricelist_global") + def _get_cummulative_quantity(self): + """Compute the cummulative quantity of products in the sale order. + :returns: dict{ + by_template: {product.template: qty}, + by_categ: {product.category: qty}} + } + """ + self.ensure_one() + qty_data = { + "by_template": {}, + "by_categ": {}, + } + for line in self.order_line.filtered("product_id"): + qty_in_product_uom = line.product_uom_qty + # Final unit price is computed + # according to `qty` in the default `uom_id`. + if line.product_uom != line.product_id.uom_id: + qty_in_product_uom = line.product_uom._compute_quantity( + qty_in_product_uom, line.product_id.uom_id + ) + key_template = line.product_id.product_tmpl_id + key_categ = line.product_id.categ_id + qty_data["by_template"].setdefault(key_template, 0.0) + qty_data["by_template"][key_template] += qty_in_product_uom + qty_data["by_categ"].setdefault(key_categ, 0.0) + qty_data["by_categ"][key_categ] += qty_in_product_uom + return qty_data + @api.depends("pricelist_id") def _compute_has_pricelist_global(self): for sale in self: if not sale.pricelist_id: sale.has_pricelist_global = False continue - ( - prod_tmpl_ids, - categ_ids, - ) = sale.pricelist_id._extract_products_and_categs_from_sale(sale) - items = sale.pricelist_id._compute_price_rule_get_items_globals( - sale.date_order, prod_tmpl_ids, categ_ids + qty_data = self._get_cummulative_quantity() + pricelist = sale.pricelist_id.with_context( + pricelist_global_cummulative_quantity=qty_data ) - sale.has_pricelist_global = bool(items) + suitable_rule = self.env["product.pricelist.item"] + for line in sale.order_line: + suitable_rule = pricelist._get_product_rule( + line.product_id, + quantity=line.product_uom_qty or 1.0, + uom=line.product_uom, + date=line.order_id.date_order, + ) + if suitable_rule: + break + sale.has_pricelist_global = bool(suitable_rule) @api.onchange("order_line") def _onchange_need_recompute_pricelist_global(self): @@ -29,60 +63,11 @@ def _onchange_need_recompute_pricelist_global(self): def button_compute_pricelist_global_rule(self): self.ensure_one() - prices_data = self.pricelist_id._compute_price_rule_global(self) - digits = self.pricelist_id.currency_id.decimal_places - is_discount_visible = ( - self.pricelist_id.discount_policy == "without_discount" - and self.env.user.has_group("product.group_discount_per_so_line") - ) - for line in self.order_line.filtered(lambda x: not x.display_type): - vals_to_write = {"discount": 0.0} - product = line.product_id.with_context( - lang=self.partner_id.lang, - partner=self.partner_id, - quantity=line.product_uom_qty, - date=self.date_order, - pricelist=self.pricelist_id.id, - uom=line.product_uom.id, - fiscal_position=self.env.context.get("fiscal_position"), - ) - price, suitable_rule = prices_data[line.id] - if is_discount_visible: - product_context = dict( - self.env.context, - partner_id=self.partner_id.id, - date=self.date_order, - uom=line.product_uom.id, - ) - - base_price, currency = line.with_context( - **product_context - )._get_real_price_currency( - product, - suitable_rule, - line.product_uom_qty, - line.product_uom, - self.pricelist_id.id, - ) - if base_price != 0: - if self.pricelist_id.currency_id != currency: - # we need new_list_price in the same currency as price, - # which is in the SO's pricelist's currency - base_price = currency._convert( - base_price, - self.pricelist_id.currency_id, - self.company_id or self.env.company, - self.date_order or fields.Date.context_today(self), - ) - discount = (base_price - price) / base_price * 100 - if (discount > 0 and base_price > 0) or ( - discount < 0 and base_price < 0 - ): - vals_to_write["discount"] = discount - price = max(base_price, price) - - if float_compare(price, line.price_unit, precision_digits=digits) != 0: - vals_to_write["price_unit"] = price - if vals_to_write: - line.write(vals_to_write) + # Clear existing discounts before recomputing. + self.order_line.write({"discount": 0.0}) + qty_data = self._get_cummulative_quantity() + sale_order = self.with_context(pricelist_global_cummulative_quantity=qty_data) + sale_order.order_line._compute_pricelist_item_id() + sale_order.order_line._compute_price_unit() + sale_order.order_line._compute_discount() self.need_recompute_pricelist_global = False diff --git a/sale_pricelist_global_rule/models/sale_order_line.py b/sale_pricelist_global_rule/models/sale_order_line.py new file mode 100644 index 00000000000..b7a73f17a94 --- /dev/null +++ b/sale_pricelist_global_rule/models/sale_order_line.py @@ -0,0 +1,22 @@ +from odoo import models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _compute_pricelist_item_id(self): + # Compute the cumulative quantity of products in the sale order + # for each line to ensure quantities are not mixed between different orders. + # Store the data in a dictionary to avoid redundant computations + # for the same order multiple times. + sale_data = {} + res = None + for line in self: + if line.order_id not in sale_data: + sale_data[line.order_id] = line.order_id._get_cummulative_quantity() + qty_data = sale_data[line.order_id] + res = super( + SaleOrderLine, + line.with_context(pricelist_global_cummulative_quantity=qty_data), + )._compute_pricelist_item_id() + return res diff --git a/sale_pricelist_global_rule/tests/test_pricelist_global.py b/sale_pricelist_global_rule/tests/test_pricelist_global.py index 31a5c1adad5..c3ab7b82d92 100644 --- a/sale_pricelist_global_rule/tests/test_pricelist_global.py +++ b/sale_pricelist_global_rule/tests/test_pricelist_global.py @@ -94,7 +94,7 @@ def setUpClass(cls): cls.pricelist_item_by_product = cls.PricelistItem.create( { "pricelist_id": cls.pricelist_global.id, - "applied_on": "4_global_product_template", + "applied_on": "3_1_global_product_template", "global_product_tmpl_id": cls.t_shirt.id, "compute_price": "percentage", "percent_price": 10, @@ -104,7 +104,7 @@ def setUpClass(cls): cls.pricelist_item_by_categ = cls.PricelistItem.create( { "pricelist_id": cls.pricelist_global.id, - "applied_on": "5_global_product_category", + "applied_on": "3_2_global_product_category", "global_categ_id": cls.categ_1.id, "compute_price": "percentage", "percent_price": 10, @@ -180,6 +180,10 @@ def test_01_by_product_less_min_quantity(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_02_by_product_fixed_price(self): """ @@ -203,6 +207,14 @@ def test_02_by_product_fixed_price(self): self.assertEqual(self.sale_line_m_black.price_unit, 50) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_03_by_product_discount(self): """ @@ -220,6 +232,14 @@ def test_03_by_product_discount(self): self.assertEqual(self.sale_line_m_black.price_unit, 90) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_04_by_product_formula(self): """ @@ -257,6 +277,14 @@ def test_04_by_product_formula(self): self.assertEqual(self.sale_line_m_black.price_unit, 75) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_05_by_product_base_other_pricelist_normal(self): """ @@ -278,7 +306,8 @@ def test_05_by_product_base_other_pricelist_normal(self): Case 3: - Total qty=16 - Base pricelist: - - Applies to both sale_line_m_red and sale_line_m_black (both with quantity=8) + - Applies to both sale_line_m_red and + sale_line_m_black (both with quantity=8) - Global pricelist: - Base price = 100 * 20% discount (from base pricelist=80) - Final price = 80 * 10% discount = 72 @@ -297,6 +326,8 @@ def test_05_by_product_base_other_pricelist_normal(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) # case 2 self.sale_line_m_red.product_uom_qty = 4 self.sale_line_m_black.product_uom_qty = 11 @@ -305,6 +336,12 @@ def test_05_by_product_base_other_pricelist_normal(self): self.assertEqual(self.sale_line_m_black.price_unit, 72) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) # case 3 self.sale_line_m_red.product_uom_qty = 8 self.sale_line_m_black.product_uom_qty = 8 @@ -313,6 +350,12 @@ def test_05_by_product_base_other_pricelist_normal(self): self.assertEqual(self.sale_line_m_black.price_unit, 72) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) def test_06_by_product_base_other_pricelist_global(self): """ @@ -329,7 +372,7 @@ def test_06_by_product_base_other_pricelist_global(self): """ self.pricelist_item_base.write( { - "applied_on": "4_global_product_template", + "applied_on": "3_1_global_product_template", "global_product_tmpl_id": self.t_shirt.id, } ) @@ -347,6 +390,8 @@ def test_06_by_product_base_other_pricelist_global(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) # case 2 self.sale_line_m_red.product_uom_qty = 8 self.sale_line_m_black.product_uom_qty = 8 @@ -355,6 +400,12 @@ def test_06_by_product_base_other_pricelist_global(self): self.assertEqual(self.sale_line_m_black.price_unit, 72) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) def test_11_by_categ_less_min_quantity(self): """ @@ -373,6 +424,8 @@ def test_11_by_categ_less_min_quantity(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) def test_12_by_categ_fixed_price(self): """ @@ -398,6 +451,12 @@ def test_12_by_categ_fixed_price(self): self.assertEqual(self.sale_line_m_black.price_unit, 50) self.assertEqual(self.sale_line_2.price_unit, 50) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_categ + ) def test_13_by_categ_discount(self): """ @@ -417,6 +476,12 @@ def test_13_by_categ_discount(self): self.assertEqual(self.sale_line_m_black.price_unit, 90) self.assertEqual(self.sale_line_2.price_unit, 180) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_categ + ) def test_14_by_categ_formula(self): """ @@ -456,6 +521,12 @@ def test_14_by_categ_formula(self): self.assertEqual(self.sale_line_m_black.price_unit, 75) self.assertEqual(self.sale_line_2.price_unit, 155) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_categ + ) def test_15_by_categ_base_other_pricelist_normal(self): """ @@ -501,6 +572,10 @@ def test_15_by_categ_base_other_pricelist_normal(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 2 self.sale_line_m_red.product_uom_qty = 4 self.sale_line_m_black.product_uom_qty = 8 @@ -511,6 +586,16 @@ def test_15_by_categ_base_other_pricelist_normal(self): self.assertEqual(self.sale_line_m_black.price_unit, 72) self.assertEqual(self.sale_line_2.price_unit, 180) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_2.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 3 self.sale_line_m_red.product_uom_qty = 6 self.sale_line_m_black.product_uom_qty = 8 @@ -520,6 +605,16 @@ def test_15_by_categ_base_other_pricelist_normal(self): self.assertEqual(self.sale_line_m_black.price_unit, 72) self.assertEqual(self.sale_line_2.price_unit, 180) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_2.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_16_by_categ_base_other_pricelist_global(self): """ @@ -532,7 +627,8 @@ def test_16_by_categ_base_other_pricelist_global(self): Case 2: - Total qty=21 - Base pricelist: - - Applicable on sale_line_m_red and sale_line_m_black (both with quantity=7) + - Applicable on sale_line_m_red + and sale_line_m_black (both with quantity=7) - Applicable on sale_line_2 (quantity=7) - Global pricelist: - Applicable on sale_line_m_red and sale_line_m_black @@ -544,7 +640,7 @@ def test_16_by_categ_base_other_pricelist_global(self): """ self.pricelist_item_base.write( { - "applied_on": "5_global_product_category", + "applied_on": "3_2_global_product_category", "global_categ_id": self.categ_1.id, } ) @@ -564,6 +660,10 @@ def test_16_by_categ_base_other_pricelist_global(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 2 self.sale_line_m_red.product_uom_qty = 7 self.sale_line_m_black.product_uom_qty = 7 @@ -574,6 +674,16 @@ def test_16_by_categ_base_other_pricelist_global(self): self.assertEqual(self.sale_line_m_black.price_unit, 72) self.assertEqual(self.sale_line_2.price_unit, 144) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertEqual( + self.sale_line_2.pricelist_item_id, self.pricelist_item_by_categ + ) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_pricelist_by_dates(self): """ @@ -596,6 +706,10 @@ def test_pricelist_by_dates(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 2 self.sale_order1.date_order = "2025-01-01 00:00:00" self.sale_order1.button_compute_pricelist_global_rule() @@ -603,6 +717,10 @@ def test_pricelist_by_dates(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 3 self.sale_order1.date_order = "2024-12-31 00:00:00" self.sale_order1.button_compute_pricelist_global_rule() @@ -610,6 +728,14 @@ def test_pricelist_by_dates(self): self.assertEqual(self.sale_line_m_black.price_unit, 90) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_pricelist_by_uom(self): """ @@ -635,24 +761,40 @@ def test_pricelist_by_uom(self): self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 2 self.sale_line_m_red.product_uom_qty = 1 self.sale_line_m_red.product_uom = self.env.ref("uom.product_uom_dozen") self.sale_line_m_black.product_uom_qty = 1 self.sale_order1.button_compute_pricelist_global_rule() - self.assertEqual(self.sale_line_m_red.price_unit, 100) + self.assertEqual(self.sale_line_m_red.price_unit, 1200) self.assertEqual(self.sale_line_m_black.price_unit, 100) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertFalse(self.sale_line_m_red.pricelist_item_id) + self.assertFalse(self.sale_line_m_black.pricelist_item_id) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 3 self.sale_line_m_red.product_uom_qty = 1 self.sale_line_m_red.product_uom = self.env.ref("uom.product_uom_dozen") self.sale_line_m_black.product_uom_qty = 6 self.sale_order1.button_compute_pricelist_global_rule() - self.assertEqual(self.sale_line_m_red.price_unit, 90) + self.assertEqual(self.sale_line_m_red.price_unit, 1080) self.assertEqual(self.sale_line_m_black.price_unit, 90) self.assertEqual(self.sale_line_2.price_unit, 200) self.assertEqual(self.sale_line_3.price_unit, 300) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) def test_pricelist_visible_discount(self): """ @@ -711,6 +853,14 @@ def test_pricelist_visible_discount(self): self.assertEqual(self.sale_line_2.discount, 0) self.assertEqual(self.sale_line_3.price_unit, 300) self.assertEqual(self.sale_line_3.discount, 0) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 2 self.pricelist_global.write({"discount_policy": "without_discount"}) self.sale_order1.button_compute_pricelist_global_rule() @@ -722,6 +872,14 @@ def test_pricelist_visible_discount(self): self.assertEqual(self.sale_line_2.discount, 0) self.assertEqual(self.sale_line_3.price_unit, 300) self.assertEqual(self.sale_line_3.discount, 0) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 3 self.pricelist_item_by_product.write( { @@ -740,6 +898,14 @@ def test_pricelist_visible_discount(self): self.assertEqual(self.sale_line_2.discount, 0) self.assertEqual(self.sale_line_3.price_unit, 300) self.assertEqual(self.sale_line_3.discount, 0) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 4 self.pricelist_global.write({"discount_policy": "without_discount"}) self.pricelist_base.write({"discount_policy": "with_discount"}) @@ -752,6 +918,14 @@ def test_pricelist_visible_discount(self): self.assertEqual(self.sale_line_2.discount, 0) self.assertEqual(self.sale_line_3.price_unit, 300) self.assertEqual(self.sale_line_3.discount, 0) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 5 self.pricelist_global.write({"discount_policy": "with_discount"}) self.pricelist_base.write({"discount_policy": "without_discount"}) @@ -764,6 +938,14 @@ def test_pricelist_visible_discount(self): self.assertEqual(self.sale_line_2.discount, 0) self.assertEqual(self.sale_line_3.price_unit, 300) self.assertEqual(self.sale_line_3.discount, 0) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) # case 6 self.pricelist_global.write({"discount_policy": "without_discount"}) self.pricelist_base.write({"discount_policy": "without_discount"}) @@ -776,3 +958,11 @@ def test_pricelist_visible_discount(self): self.assertEqual(self.sale_line_2.discount, 0) self.assertEqual(self.sale_line_3.price_unit, 300) self.assertEqual(self.sale_line_3.discount, 0) + self.assertEqual( + self.sale_line_m_red.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertEqual( + self.sale_line_m_black.pricelist_item_id, self.pricelist_item_by_product + ) + self.assertFalse(self.sale_line_2.pricelist_item_id) + self.assertFalse(self.sale_line_3.pricelist_item_id) diff --git a/sale_pricelist_global_rule/views/product_pricelist_item_views.xml b/sale_pricelist_global_rule/views/product_pricelist_item_views.xml index 216efd4c3f6..dbe41013987 100644 --- a/sale_pricelist_global_rule/views/product_pricelist_item_views.xml +++ b/sale_pricelist_global_rule/views/product_pricelist_item_views.xml @@ -10,12 +10,14 @@ diff --git a/sale_pricelist_global_rule/views/sale_order_views.xml b/sale_pricelist_global_rule/views/sale_order_views.xml index 930eed6fed2..8687d32a430 100644 --- a/sale_pricelist_global_rule/views/sale_order_views.xml +++ b/sale_pricelist_global_rule/views/sale_order_views.xml @@ -11,14 +11,14 @@ string="Recompute pricelist global" name="button_compute_pricelist_global_rule" type="object" - attrs="{'invisible': ['|', '|', ('state', 'not in', ('draft', 'sent')), ('has_pricelist_global', '=', False), ('need_recompute_pricelist_global', '=', False)]}" + invisible="state not in ('draft', 'sent') or not has_pricelist_global or not need_recompute_pricelist_global" class="oe_highlight" />