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(

Reply via email to