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. -->
+
+
+
+
+
diff --git a/modules/product/view/product_tree.xml b/modules/product/view/product_tree.xml
index 14ea7b5726..57dca47055 100644
--- a/modules/product/view/product_tree.xml
+++ b/modules/product/view/product_tree.xml
@@ -4,4 +4,7 @@ this repository contains the full copyright notices and license terms. -->
+
+
+