From 6ba56ce0c87433d62ef30caabf958c68e7309759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 7 Oct 2025 16:23:45 +0200 Subject: [PATCH] Allow a different list price to be set for each variant Closes #14154 --- modules/product/CHANGELOG | 1 + modules/product/doc/design.rst | 3 + modules/product/doc/usage.rst | 2 + modules/product/product.py | 60 ++++++++++++++++--- .../tests/scenario_product_variant.rst | 60 ++++++++++++++----- modules/product/view/product_form_simple.xml | 5 ++ modules/product/view/product_tree.xml | 3 + 7 files changed, 112 insertions(+), 22 deletions(-) diff --git a/modules/product/CHANGELOG b/modules/product/CHANGELOG index 7ac1f1ed13..fed8f5b83f 100644 --- a/modules/product/CHANGELOG +++ b/modules/product/CHANGELOG @@ -1,3 +1,4 @@ +* Allow a different list price to be set for each variant Version 7.6.0 - 2025-04-28 -------------------------- diff --git a/modules/product/doc/design.rst b/modules/product/doc/design.rst index e47fabc0bb..4ecf7f0080 100644 --- a/modules/product/doc/design.rst +++ b/modules/product/doc/design.rst @@ -60,6 +60,9 @@ It does, however, have some properties that are specific to each variant such as the description, cost price, and suffix code which is important to distinguish between different variants. +Instead of using the template's list price, a list price can be set on the +variant. + The identifiers that are used to refer to a product are also specific to a product variant. A product identifier is made up from a type and a code. diff --git a/modules/product/doc/usage.rst b/modules/product/doc/usage.rst index 10910424d8..3741cd6452 100644 --- a/modules/product/doc/usage.rst +++ b/modules/product/doc/usage.rst @@ -20,6 +20,8 @@ properties of the shirt, such as the name, unit of measurement, categories, and so on, would be the same. So in this case you would create a single *Product* to represent the shirt, and a *Variant* for each of the different sizes. +Then, you can either define a different list price for each variant or use the +list price from the product only. Structuring your products like this can help you manage and update them. .. tip:: diff --git a/modules/product/product.py b/modules/product/product.py index 8cc2560c9d..f76b2b3d76 100644 --- a/modules/product/product.py +++ b/modules/product/product.py @@ -149,6 +149,23 @@ class Template( return pool.get('product.cost_price_method') return super().multivalue_model(field) + def multivalue_records(self, field): + records = super().multivalue_records(field) + if field == 'list_price': + # Sort to get record with empty product first + records = sorted(records, key=lambda r: r.product is not None) + return records + + def get_multivalue(self, name, **pattern): + if name == 'list_price': + pattern.setdefault('product', None) + return super().get_multivalue(name, **pattern) + + def set_multivalue(self, name, value, save=True, **pattern): + if name == 'list_price': + pattern.setdefault('product', None) + return super().set_multivalue(name, value, save=save, **pattern) + @classmethod def order_code(cls, tables): table, _ = tables[None] @@ -401,6 +418,19 @@ class Product( identifiers = fields.One2Many( 'product.identifier', 'product', "Identifiers", help="Other identifiers associated with the variant.") + list_price = fields.MultiValue(fields.Numeric( + "List Price", digits=price_digits, + states={ + 'readonly': ~Eval('context', {}).get('company'), + }, + help="The standard price the variant is sold at.\n" + "Leave empty to use the list price of the product.")) + list_prices = fields.One2Many( + 'product.list_price', 'product', "List Prices") + list_price_used = fields.Function(fields.Numeric( + "List Price", digits=price_digits, + help="The standard price the variant is sold at."), + 'get_list_price_used') cost_price = fields.MultiValue(fields.Numeric( "Cost Price", digits=price_digits, states={ @@ -553,7 +583,9 @@ class Product( @classmethod def multivalue_model(cls, field): pool = Pool() - if field == 'cost_price': + if field == 'list_price': + return pool.get('product.list_price') + elif field == 'cost_price': return pool.get('product.cost_price') return super().multivalue_model(field) @@ -562,12 +594,16 @@ class Product( if name in {'cost_price', 'list_price'} and not value: if not pattern.get('company', context.get('company')): return [] + if name == 'list_price': + pattern.setdefault('template', self.template.id) return super().set_multivalue(name, value, save=save, **pattern) def get_multivalue(self, name, **pattern): if isinstance(self._fields[name], TemplateFunction): return self.template.get_multivalue(name, **pattern) else: + if name == 'list_price': + pattern.setdefault('template', self.template.id) return super().get_multivalue(name, **pattern) @classmethod @@ -693,12 +729,11 @@ class Product( default.setdefault('replacing') return super().copy(products, default=default) - @property - def list_price_used(self): - transaction = Transaction() - with transaction.reset_context(), \ - transaction.set_context(self._context): - return self.template.get_multivalue('list_price') + def get_list_price_used(self, name): + list_price = self.get_multivalue('list_price') + if list_price is None: + list_price = self.template.get_multivalue('list_price') + return list_price @classmethod def sync_code(cls, products): @@ -747,7 +782,16 @@ class Product( class ProductListPrice(ModelSQL, CompanyValueMixin): __name__ = 'product.list_price' template = fields.Many2One( - 'product.template', "Template", ondelete='CASCADE', + 'product.template', "Template", ondelete='CASCADE', required=True, + context={ + 'company': Eval('company', -1), + }, + depends={'company'}) + product = fields.Many2One( + 'product.product', "Product", ondelete='CASCADE', + domain=[ + ('template', '=', Eval('template', -1)), + ], context={ 'company': Eval('company', -1), }, diff --git a/modules/product/tests/scenario_product_variant.rst b/modules/product/tests/scenario_product_variant.rst index 92add587c8..ce7efcde3b 100644 --- a/modules/product/tests/scenario_product_variant.rst +++ b/modules/product/tests/scenario_product_variant.rst @@ -27,30 +27,62 @@ Create a template:: >>> template.save() >>> len(template.products) 1 - >>> product, = template.products - >>> product.code + >>> product1, = template.products + >>> product1.code 'PROD' - >>> product.suffix_code = "001" - >>> product.save() - >>> product.code + >>> product1.suffix_code = "001" + >>> product1.save() + >>> product1.code 'PROD001' Create a variant:: >>> Product = Model.get('product.product') - >>> product = Product() - >>> product.template = template - >>> product.name + >>> product2 = Product() + >>> product2.template = template + >>> product2.name 'Product' - >>> product.list_price + >>> product2.suffix_code = "002" + >>> product2.save() + >>> product2.list_price_used Decimal('42.0000') - >>> product.suffix_code = "002" - >>> product.save() - >>> product.list_price - Decimal('42.0000') - >>> product.code + >>> product2.code 'PROD002' +Change variant list price:: + + >>> product2.list_price = Decimal('50.0000') + >>> product2.save() + >>> product2.list_price + Decimal('50.0000') + >>> product2.list_price_used + Decimal('50.0000') + + >>> product1.reload() + >>> product1.list_price + >>> product1.list_price_used + Decimal('42.0000') + + >>> template.reload() + >>> template.list_price + Decimal('42.0000') + +Change product list price:: + + >>> template.list_price = Decimal('40.0000') + >>> template.save() + + >>> product2.reload() + >>> product2.list_price + Decimal('50.0000') + >>> product2.list_price_used + Decimal('50.0000') + + >>> product1.reload() + >>> product1.list_price + >>> product1.list_price_used + Decimal('40.0000') + Change template code:: >>> template.code = "PRD" diff --git a/modules/product/view/product_form_simple.xml b/modules/product/view/product_form_simple.xml index 78f438776d..fd3d565f4f 100644 --- a/modules/product/view/product_form_simple.xml +++ b/modules/product/view/product_form_simple.xml @@ -12,6 +12,11 @@ this repository contains the full copyright notices and license terms. -->