details:   https://code.tryton.org/tryton/commit/532e2b8635e5
branch:    default
user:      Cédric Krier <[email protected]>
date:      Thu Apr 02 17:26:12 2026 +0200
description:
        Use mixin to copy of One2Many from product linked by template and 
product
diffstat:

 modules/product_kit/product.py                  |  70 +++---------------------
 modules/purchase/product.py                     |  63 ++--------------------
 modules/sale_product_customer/product.py        |  65 ++--------------------
 modules/sale_rental/product.py                  |  55 +------------------
 modules/stock_product_location/product.py       |  61 +--------------------
 modules/stock_product_location_place/product.py |  53 +-----------------
 6 files changed, 37 insertions(+), 330 deletions(-)

diffs (539 lines):

diff -r a15addd781e0 -r 532e2b8635e5 modules/product_kit/product.py
--- a/modules/product_kit/product.py    Thu Apr 02 17:25:24 2026 +0200
+++ b/modules/product_kit/product.py    Thu Apr 02 17:26:12 2026 +0200
@@ -4,12 +4,16 @@
 
 from trytond.model import (
     ModelSQL, ModelStorage, ModelView, fields, sequence_ordered)
-from trytond.modules.product import round_price
+from trytond.modules.product import (
+    copy_product_filtered, copy_template_filtered, round_price)
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, If
 
 
-class Template(metaclass=PoolMeta):
+class Template(
+        copy_template_filtered(
+            'components', 'parent_template', 'parent_product'),
+        metaclass=PoolMeta):
     __name__ = "product.template"
 
     components = fields.One2Many(
@@ -32,34 +36,11 @@
         if self.type == 'kit':
             self.cost_price_method = 'fixed'
 
-    @classmethod
-    def copy(cls, templates, default=None):
-        pool = Pool()
-        Component = pool.get('product.component')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
 
-        copy_components = 'components' not in default
-        default.setdefault('components', None)
-        new_templates = super().copy(templates, default)
-        if copy_components:
-            old2new = {}
-            to_copy = []
-            for template, new_template in zip(templates, new_templates):
-                to_copy.extend(
-                    c for c in template.components if not c.parent_product)
-                old2new[template.id] = new_template.id
-            if to_copy:
-                Component.copy(to_copy, {
-                        'parent_template': (lambda d:
-                            old2new[d['parent_template']]),
-                        })
-        return new_templates
-
-
-class Product(metaclass=PoolMeta):
+class Product(
+        copy_product_filtered(
+            'components', 'parent_template', 'parent_product'),
+        metaclass=PoolMeta):
     __name__ = "product.product"
 
     components = fields.One2Many(
@@ -108,37 +89,6 @@
             quantities[kit.id] = kit.default_uom.floor(min(qties, default=0))
         return quantities
 
-    @classmethod
-    def copy(cls, products, default=None):
-        pool = Pool()
-        Component = pool.get('product.component')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
-
-        copy_components = 'components' not in default
-        if 'template' in default:
-            default.setdefault('components', None)
-        new_products = super().copy(products, default)
-        if 'template' in default and copy_components:
-            template2new = {}
-            product2new = {}
-            to_copy = []
-            for product, new_product in zip(products, new_products):
-                if product.components:
-                    to_copy.extend(product.components)
-                    template2new[product.template.id] = new_product.template.id
-                    product2new[product.id] = new_product.id
-            if to_copy:
-                Component.copy(to_copy, {
-                        'parent_product': (lambda d:
-                            product2new[d['parent_product']]),
-                        'parent_template': (lambda d:
-                            template2new[d['parent_template']]),
-                        })
-        return new_products
-
 
 class ComponentMixin(sequence_ordered(), ModelStorage):
 
diff -r a15addd781e0 -r 532e2b8635e5 modules/purchase/product.py
--- a/modules/purchase/product.py       Thu Apr 02 17:25:24 2026 +0200
+++ b/modules/purchase/product.py       Thu Apr 02 17:26:12 2026 +0200
@@ -10,7 +10,8 @@
     Index, MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
 from trytond.modules.currency.fields import Monetary
 from trytond.modules.product import (
-    ProductDeactivatableMixin, price_digits, round_price)
+    ProductDeactivatableMixin, copy_product_filtered, copy_template_filtered,
+    price_digits, round_price)
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Bool, Eval, If, TimeDelta
 from trytond.tools import is_full_text, lstrip_wildcard
@@ -19,7 +20,8 @@
 from .exceptions import PurchaseUOMWarning
 
 
-class Template(metaclass=PoolMeta):
+class Template(
+        copy_template_filtered('product_suppliers'), metaclass=PoolMeta):
     __name__ = "product.template"
     purchasable = fields.Boolean("Purchasable")
     product_suppliers = fields.One2Many(
@@ -84,33 +86,9 @@
                         raise PurchaseUOMWarning(
                             name, gettext('purchase.msg_change_purchase_uom'))
 
-    @classmethod
-    def copy(cls, templates, default=None):
-        pool = Pool()
-        ProductSupplier = pool.get('purchase.product_supplier')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
 
-        copy_suppliers = 'product_suppliers' not in default
-        default.setdefault('product_suppliers', None)
-        new_templates = super().copy(templates, default)
-        if copy_suppliers:
-            old2new = {}
-            to_copy = []
-            for template, new_template in zip(templates, new_templates):
-                to_copy.extend(
-                    ps for ps in template.product_suppliers if not ps.product)
-                old2new[template.id] = new_template.id
-            if to_copy:
-                ProductSupplier.copy(to_copy, {
-                        'template': lambda d: old2new[d['template']],
-                        })
-        return new_templates
-
-
-class Product(metaclass=PoolMeta):
+class Product(
+        copy_product_filtered('product_suppliers'), metaclass=PoolMeta):
     __name__ = 'product.product'
 
     product_suppliers = fields.One2Many(
@@ -274,35 +252,6 @@
             prices[product.id] = unit_price
         return prices
 
-    @classmethod
-    def copy(cls, products, default=None):
-        pool = Pool()
-        ProductSupplier = pool.get('purchase.product_supplier')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
-
-        copy_suppliers = 'product_suppliers' not in default
-        if 'template' in default:
-            default.setdefault('product_suppliers', None)
-        new_products = super().copy(products, default)
-        if 'template' in default and copy_suppliers:
-            template2new = {}
-            product2new = {}
-            to_copy = []
-            for product, new_product in zip(products, new_products):
-                if product.product_suppliers:
-                    to_copy.extend(product.product_suppliers)
-                    template2new[product.template.id] = new_product.template.id
-                    product2new[product.id] = new_product.id
-            if to_copy:
-                ProductSupplier.copy(to_copy, {
-                        'product': lambda d: product2new[d['product']],
-                        'template': lambda d: template2new[d['template']],
-                        })
-        return new_products
-
 
 class ProductSupplier(
         sequence_ordered(), ProductDeactivatableMixin, MatchMixin,
diff -r a15addd781e0 -r 532e2b8635e5 modules/sale_product_customer/product.py
--- a/modules/sale_product_customer/product.py  Thu Apr 02 17:25:24 2026 +0200
+++ b/modules/sale_product_customer/product.py  Thu Apr 02 17:26:12 2026 +0200
@@ -3,8 +3,9 @@
 
 from trytond.model import (
     MatchMixin, ModelSQL, ModelView, fields, sequence_ordered)
-from trytond.modules.product import ProductDeactivatableMixin
-from trytond.pool import Pool, PoolMeta
+from trytond.modules.product import (
+    ProductDeactivatableMixin, copy_product_filtered, copy_template_filtered)
+from trytond.pool import PoolMeta
 from trytond.pyson import Bool, Eval, If
 from trytond.tools import is_full_text, lstrip_wildcard
 
@@ -85,7 +86,8 @@
             ]
 
 
-class Template(metaclass=PoolMeta):
+class Template(
+        copy_template_filtered('product_customers'), metaclass=PoolMeta):
     __name__ = 'product.template'
     product_customers = fields.One2Many(
         'sale.product_customer', 'template', "Customers",
@@ -98,33 +100,9 @@
             if product_customer.match(pattern):
                 yield product_customer
 
-    @classmethod
-    def copy(cls, templates, default=None):
-        pool = Pool()
-        ProductCustomer = pool.get('sale.product_customer')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
 
-        copy_customers = 'product_customers' not in default
-        default.setdefault('product_customers', None)
-        new_templates = super().copy(templates, default)
-        if copy_customers:
-            old2new = {}
-            to_copy = []
-            for template, new_template in zip(templates, new_templates):
-                to_copy.extend(
-                    pc for pc in template.product_customers if not pc.product)
-                old2new[template.id] = new_template.id
-            if to_copy:
-                ProductCustomer.copy(to_copy, {
-                        'template': lambda d: old2new[d['template']],
-                        })
-        return new_templates
-
-
-class Product(metaclass=PoolMeta):
+class Product(
+        copy_product_filtered('product_customers'), metaclass=PoolMeta):
     __name__ = 'product.product'
     product_customers = fields.One2Many(
         'sale.product_customer', 'product', "Customers",
@@ -141,32 +119,3 @@
                 yield product_customer
         pattern['product'] = None
         yield from self.template.product_customer_used(**pattern)
-
-    @classmethod
-    def copy(cls, products, default=None):
-        pool = Pool()
-        ProductCustomer = pool.get('sale.product_customer')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
-
-        copy_customers = 'product_customers' not in default
-        if 'template' in default:
-            default.setdefault('product_customers', None)
-        new_products = super().copy(products, default)
-        if 'template' in default and copy_customers:
-            template2new = {}
-            product2new = {}
-            to_copy = []
-            for product, new_product in zip(products, new_products):
-                if product.product_customers:
-                    to_copy.extend(product.product_customers)
-                    template2new[product.template.id] = new_product.template.id
-                    product2new[product.id] = new_product.id
-            if to_copy:
-                ProductCustomer.copy(to_copy, {
-                        'product': lambda d: product2new[d['product']],
-                        'template': lambda d: template2new[d['template']],
-                        })
-        return new_products
diff -r a15addd781e0 -r 532e2b8635e5 modules/sale_rental/product.py
--- a/modules/sale_rental/product.py    Thu Apr 02 17:25:24 2026 +0200
+++ b/modules/sale_rental/product.py    Thu Apr 02 17:26:12 2026 +0200
@@ -12,7 +12,8 @@
 from trytond.modules.account_product.product import (
     account_used, template_property)
 from trytond.modules.currency.fields import Monetary
-from trytond.modules.product import price_digits, round_price
+from trytond.modules.product import (
+    copy_product_filtered, copy_template_filtered, price_digits, round_price)
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Eval, Id, If, TimeDelta
 from trytond.transaction import Transaction
@@ -97,7 +98,7 @@
         cls.__access__.add('tax')
 
 
-class Template(metaclass=PoolMeta):
+class Template(copy_template_filtered('rental_prices'), metaclass=PoolMeta):
     __name__ = 'product.template'
 
     rentable = fields.Boolean(
@@ -160,30 +161,8 @@
                     'invisible': ~Eval('rentable', False),
                     })]
 
-    @classmethod
-    def copy(cls, templates, default=None):
-        pool = Pool()
-        RentalPrice = pool.get('product.rental.price')
-        default = default.copy() if default is not None else {}
 
-        copy_rental_prices = 'rental_prices' not in default
-        default.setdefault('rental_prices', None)
-        new_templates = super().copy(templates, default=default)
-        if copy_rental_prices:
-            old2new = {}
-            to_copy = []
-            for template, new_template in zip(templates, new_templates):
-                to_copy.extend(
-                    rp for rp in template.rental_prices if not rp.product)
-                old2new[template.id] = new_template.id
-            if to_copy:
-                RentalPrice.copy(to_copy, {
-                        'template': lambda d: old2new[d['template']],
-                        })
-        return new_templates
-
-
-class Product(metaclass=PoolMeta):
+class Product(copy_product_filtered('rental_prices'), metaclass=PoolMeta):
     __name__ = 'product.product'
 
     rental_prices = fields.One2Many(
@@ -261,32 +240,6 @@
             if price.match(quantity, duration, pattern):
                 return price.get_unit_price(self.rental_unit)
 
-    @classmethod
-    def copy(cls, products, default=None):
-        pool = Pool()
-        RentalPrice = pool.get('product.rental.price')
-        default = default.copy() if default is not None else {}
-
-        copy_rental_prices = 'rental_prices' not in default
-        if 'template' in default:
-            default.setdefault('rental_prices', None)
-        new_products = super().copy(products, default=default)
-        if 'template' in default and copy_rental_prices:
-            template2new = {}
-            product2new = {}
-            to_copy = []
-            for product, new_product in zip(products, new_products):
-                if product.rental_prices:
-                    to_copy.extend(product.rental_prices)
-                    template2new[product.template.id] = new_product.template.id
-                    product2new[product.id] = new_product.id
-            if to_copy:
-                RentalPrice.copy(to_copy, {
-                        'product': lambda d: product2new[d['product']],
-                        'template': lambda d: template2new[d['template']],
-                        })
-        return new_products
-
 
 class RentalPrice(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
     __name__ = 'product.rental.price'
diff -r a15addd781e0 -r 532e2b8635e5 modules/stock_product_location/product.py
--- a/modules/stock_product_location/product.py Thu Apr 02 17:25:24 2026 +0200
+++ b/modules/stock_product_location/product.py Thu Apr 02 17:26:12 2026 +0200
@@ -1,11 +1,13 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
 from trytond.model import fields
-from trytond.pool import Pool, PoolMeta
+from trytond.modules.product import (
+    copy_product_filtered, copy_template_filtered)
+from trytond.pool import PoolMeta
 from trytond.pyson import Eval
 
 
-class Template(metaclass=PoolMeta):
+class Template(copy_template_filtered('locations'), metaclass=PoolMeta):
     __name__ = 'product.template'
     locations = fields.One2Many('stock.product.location', 'template',
         "Default Locations",
@@ -13,32 +15,8 @@
             'invisible': ~Eval('type').in_(['goods', 'assets']),
             })
 
-    @classmethod
-    def copy(cls, templates, default=None):
-        pool = Pool()
-        ProductLocation = pool.get('stock.product.location')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
 
-        copy_locations = 'locations' not in default
-        default.setdefault('locations', None)
-        new_templates = super().copy(templates, default)
-        if copy_locations:
-            old2new = {}
-            to_copy = []
-            for template, new_template in zip(templates, new_templates):
-                to_copy.extend(l for l in template.locations if not l.product)
-                old2new[template.id] = new_template.id
-            if to_copy:
-                ProductLocation.copy(to_copy, {
-                        'template': lambda d: old2new[d['template']],
-                        })
-        return new_templates
-
-
-class Product(metaclass=PoolMeta):
+class Product(copy_product_filtered('locations'), metaclass=PoolMeta):
     __name__ = 'product.product'
     locations = fields.One2Many('stock.product.location', 'product',
         "Default Locations",
@@ -48,32 +26,3 @@
         states={
             'invisible': ~Eval('type').in_(['goods', 'assets']),
             })
-
-    @classmethod
-    def copy(cls, products, default=None):
-        pool = Pool()
-        ProductLocation = pool.get('stock.product.location')
-        if default is None:
-            default = {}
-        else:
-            default = default.copy()
-
-        copy_locations = 'locations' not in default
-        if 'template' in default:
-            default.setdefault('locations', None)
-        new_products = super().copy(products, default)
-        if 'template' in default and copy_locations:
-            template2new = {}
-            product2new = {}
-            to_copy = []
-            for product, new_product in zip(products, new_products):
-                if product.locations:
-                    to_copy.extend(product.locations)
-                    template2new[product.template.id] = new_product.template.id
-                    product2new[product.id] = new_product.id
-            if to_copy:
-                ProductLocation.copy(to_copy, {
-                        'product': lambda d: product2new[d['product']],
-                        'template': lambda d: template2new[d['template']],
-                        })
-        return new_products
diff -r a15addd781e0 -r 532e2b8635e5 
modules/stock_product_location_place/product.py
--- a/modules/stock_product_location_place/product.py   Thu Apr 02 17:25:24 
2026 +0200
+++ b/modules/stock_product_location_place/product.py   Thu Apr 02 17:26:12 
2026 +0200
@@ -2,11 +2,13 @@
 # this repository contains the full copyright notices and license terms.
 
 from trytond.model import fields
-from trytond.pool import Pool, PoolMeta
+from trytond.modules.product import (
+    copy_product_filtered, copy_template_filtered)
+from trytond.pool import PoolMeta
 from trytond.pyson import Eval
 
 
-class Template(metaclass=PoolMeta):
+class Template(copy_template_filtered('location_places'), metaclass=PoolMeta):
     __name__ = 'product.template'
 
     location_places = fields.One2Many(
@@ -22,29 +24,8 @@
                     and place.location == location):
                 return place
 
-    @classmethod
-    def copy(cls, templates, default=None):
-        pool = Pool()
-        ProductLocationPlace = pool.get('stock.product.location.place')
-        default = default.copy() if default is not None else {}
 
-        copy_location_places = 'location_places' not in default
-        default.setdefault('location_places', None)
-        new_templates = super().copy(templates, default=default)
-        if copy_location_places:
-            old2new, to_copy = {}, []
-            for template, new_template in zip(templates, new_templates):
-                to_copy.extend(
-                    p for p in template.location_places if not p.product)
-                old2new[template.id] = new_template.id
-            if to_copy:
-                ProductLocationPlace.copy(to_copy, {
-                        'template': lambda d: old2new[d['template']],
-                        })
-        return new_templates
-
-
-class Product(metaclass=PoolMeta):
+class Product(copy_product_filtered('location_places'), metaclass=PoolMeta):
     __name__ = 'product.product'
 
     location_places = fields.One2Many(
@@ -59,27 +40,3 @@
             if place.location == location:
                 return place
         return self.template.get_place(location)
-
-    @classmethod
-    def copy(cls, products, default=None):
-        pool = Pool()
-        ProductLocationPlace = pool.get('stock.product.location.place')
-        default = default.copy() if default is not None else {}
-
-        copy_location_places = 'location_places' not in default
-        if 'template' in default:
-            default.setdefault('location_places', None)
-        new_products = super().copy(products, default=default)
-        if 'template' in default and copy_location_places:
-            template2new, product2new, to_copy = {}, {}, []
-            for product, new_product in zip(products, new_products):
-                if product.location_places:
-                    to_copy.extend(product.location_places)
-                    template2new[product.template.id] = new_product.template.id
-                    product2new[product.id] = new_product.id
-            if to_copy:
-                ProductLocationPlace.copy(to_copy, {
-                        'product': lambda d: product2new[d['product']],
-                        'template': lambda d: template2new[d['template']],
-                        })
-        return new_products

Reply via email to