details: https://code.tryton.org/tryton/commit/8714930676b1
branch: 7.6
user: Cédric Krier <[email protected]>
date: Thu Apr 02 17:25:24 2026 +0200
description:
Do not simply copy One2Many from product linked by template and product
Closes #14739
(grafted from a15addd781e0cac818b6a7d2a969ee30d68932ba)
diffstat:
modules/product/__init__.py | 5 +-
modules/product/product.py | 70 +++++++++++++++++++++++++++++++++++++++-
modules/product_image/product.py | 17 ++++++++-
3 files changed, 87 insertions(+), 5 deletions(-)
diffs (144 lines):
diff -r f18ca405a525 -r 8714930676b1 modules/product/__init__.py
--- a/modules/product/__init__.py Thu Apr 02 15:33:36 2026 +0200
+++ b/modules/product/__init__.py Thu Apr 02 17:25:24 2026 +0200
@@ -6,11 +6,12 @@
from . import category, configuration, ir, product, uom
from .product import (
ProductDeactivatableMixin, TemplateDeactivatableMixin, price_digits,
- round_price)
+ round_price, copy_template_filtered, copy_product_filtered)
from .uom import uom_conversion_digits
__all__ = [price_digits, round_price, uom_conversion_digits,
- ProductDeactivatableMixin, TemplateDeactivatableMixin]
+ ProductDeactivatableMixin, TemplateDeactivatableMixin,
+ copy_template_filtered, copy_product_filtered]
def register():
diff -r f18ca405a525 -r 8714930676b1 modules/product/product.py
--- a/modules/product/product.py Thu Apr 02 15:33:36 2026 +0200
+++ b/modules/product/product.py Thu Apr 02 17:25:24 2026 +0200
@@ -32,7 +32,9 @@
from .exceptions import InvalidIdentifierCode
from .ir import price_decimal
-__all__ = ['price_digits', 'round_price', 'TemplateFunction']
+__all__ = [
+ 'price_digits', 'round_price', 'TemplateFunction',
+ 'copy_template_filtered', 'copy_product_filtered']
logger = logging.getLogger(__name__)
TYPES = [
@@ -56,6 +58,72 @@
Decimal(1) / 10 ** price_digits[1], rounding=rounding)
+def copy_template_filtered(
+ field_name, template_name='template', product_name='product'):
+ class TemplateCopyFiltered:
+ __slots__ = ()
+
+ @classmethod
+ def copy(cls, templates, default=None):
+ Target = getattr(cls, field_name).get_target()
+ assert (getattr(Target, template_name).model_name
+ == 'product.template')
+ assert (getattr(Target, product_name).model_name
+ == 'product.product')
+ default = default.copy() if default is not None else {}
+ copy_field = field_name not in default
+ default.setdefault(field_name)
+ new_templates = super().copy(templates, default=default)
+ if copy_field:
+ old2new, to_copy = {}, []
+ for template, new_template in zip(templates, new_templates):
+ to_copy.extend(
+ l for l in getattr(template, field_name)
+ if not getattr(l, product_name))
+ old2new[template.id] = new_template.id
+ if to_copy:
+ Target.copy(to_copy, {
+ template_name: lambda d: old2new[d[template_name]],
+ })
+ return new_templates
+ return TemplateCopyFiltered
+
+
+def copy_product_filtered(
+ field_name, template_name='template', product_name='product'):
+ class ProductCopyFiltered:
+ __slots__ = ()
+
+ @classmethod
+ def copy(cls, products, default=None):
+ Target = getattr(cls, field_name).get_target()
+ assert (getattr(Target, template_name).model_name
+ == 'product.template')
+ assert (getattr(Target, product_name).model_name
+ == 'product.product')
+ default = default.copy() if default is not None else {}
+ copy_field = field_name not in default
+ default.setdefault(field_name)
+ new_products = super().copy(products, default=default)
+ if copy_field:
+ template2new, product2new, to_copy = {}, {}, []
+ for product, new_product in zip(products, new_products):
+ if targets := getattr(product, field_name):
+ to_copy.extend(targets)
+ template2new[product.template.id] = (
+ new_product.template.id)
+ product2new[product.id] = new_product.id
+ if to_copy:
+ Target.copy(to_copy, {
+ product_name: lambda d: product2new[
+ d[product_name]],
+ template_name: lambda d: template2new[
+ d[template_name]],
+ })
+ return new_products
+ return ProductCopyFiltered
+
+
class Template(
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
__name__ = "product.template"
diff -r f18ca405a525 -r 8714930676b1 modules/product_image/product.py
--- a/modules/product_image/product.py Thu Apr 02 15:33:36 2026 +0200
+++ b/modules/product_image/product.py Thu Apr 02 17:25:24 2026 +0200
@@ -9,6 +9,15 @@
from trytond.config import config
from trytond.model import (
MatchMixin, ModelSQL, ModelView, Unique, fields, sequence_ordered)
+try:
+ from trytond.modules.product import (
+ copy_product_filtered, copy_template_filtered)
+except ImportError:
+ class copy_product_filtered:
+ pass
+
+ class copy_template_filtered:
+ pass
from trytond.pool import PoolMeta
from trytond.pyson import Bool, Eval, If
from trytond.tools import slugify
@@ -82,13 +91,17 @@
yield image
-class Template(ImageURLMixin, metaclass=PoolMeta):
+class Template(
+ copy_template_filtered('images'),
+ ImageURLMixin, metaclass=PoolMeta):
__name__ = 'product.template'
__image_url__ = '/product/image'
images = fields.One2Many('product.image', 'template', "Images")
-class Product(ImageURLMixin, metaclass=PoolMeta):
+class Product(
+ copy_product_filtered('images'),
+ ImageURLMixin, metaclass=PoolMeta):
__name__ = 'product.product'
__image_url__ = '/product/variant/image'
images = fields.One2Many(