From 53dc0afa8551c24d6ac89eaf0edded7e6b4564eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Thu, 2 Apr 2026 17:25:24 +0200 Subject: [PATCH] Do not simply copy One2Many from product linked by template and product Closes #14739 (grafted from a15addd781e0cac818b6a7d2a969ee30d68932ba) --HG-- branch : 7.0 --- modules/product/__init__.py | 6 ++- modules/product/product.py | 70 +++++++++++++++++++++++++++++++- modules/product_image/product.py | 17 +++++++- modules/sale_point/product.py | 3 +- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/modules/product/__init__.py b/modules/product/__init__.py index 7c35129030..a6fc0b4afa 100644 --- a/modules/product/__init__.py +++ b/modules/product/__init__.py @@ -4,10 +4,12 @@ from trytond.pool import Pool from . import category, configuration, ir, product, uom -from .product import price_digits, round_price +from .product import ( + copy_product_filtered, copy_template_filtered, price_digits, round_price) from .uom import uom_conversion_digits -__all__ = [price_digits, round_price, uom_conversion_digits] +__all__ = [price_digits, round_price, uom_conversion_digits, + copy_template_filtered, copy_product_filtered] def register(): diff --git a/modules/product/product.py b/modules/product/product.py index cd1a2c1ead..a0f5cfefef 100644 --- a/modules/product/product.py +++ b/modules/product/product.py @@ -30,7 +30,9 @@ except ImportError: 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 = [ @@ -54,6 +56,72 @@ def round_price(value, rounding=None): 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): "Product Template" diff --git a/modules/product_image/product.py b/modules/product_image/product.py index e309e3a1b7..252c7ea8a0 100644 --- a/modules/product_image/product.py +++ b/modules/product_image/product.py @@ -9,6 +9,15 @@ import PIL.Image 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 Pool, PoolMeta from trytond.pyson import Bool, Eval, If from trytond.tools import slugify @@ -76,13 +85,17 @@ class ImageURLMixin: 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( diff --git a/modules/sale_point/product.py b/modules/sale_point/product.py index 6974447e1c..e1eff2c06d 100644 --- a/modules/sale_point/product.py +++ b/modules/sale_point/product.py @@ -7,7 +7,8 @@ from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval -class Template(metaclass=PoolMeta): +class Template( + metaclass=PoolMeta): __name__ = 'product.template' gross_price = fields.MultiValue(fields.Numeric(