Do not simply copy One2Many from product linked by template and product
Closes #14739
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
|
||||
__all__ = [
|
||||
'price_digits', 'round_price', 'uom_conversion_digits',
|
||||
'ProductDeactivatableMixin', 'TemplateDeactivatableMixin']
|
||||
'ProductDeactivatableMixin', 'TemplateDeactivatableMixin',
|
||||
'copy_template_filtered', 'copy_product_filtered']
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -12,7 +13,8 @@ def __getattr__(name):
|
||||
return uom_conversion_digits
|
||||
elif name in {
|
||||
'price_digits', 'round_price',
|
||||
'ProductDeactivatableMixin', 'TemplateDeactivatableMixin'}:
|
||||
'ProductDeactivatableMixin', 'TemplateDeactivatableMixin',
|
||||
'copy_template_filtered', 'copy_product_filtered'}:
|
||||
from . import product
|
||||
return getattr(product, name)
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -33,7 +33,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 = [
|
||||
@@ -57,7 +59,74 @@ 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(
|
||||
copy_template_filtered('list_prices'),
|
||||
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
|
||||
__name__ = "product.template"
|
||||
name = fields.Char(
|
||||
@@ -407,6 +476,7 @@ class _One2ManyWithInactive(fields.One2Many):
|
||||
|
||||
|
||||
class Product(
|
||||
copy_product_filtered('list_prices'),
|
||||
TemplateDeactivatableMixin, tree('replaced_by'), ModelSQL, ModelView,
|
||||
CompanyMultiValueMixin):
|
||||
__name__ = "product.product"
|
||||
|
||||
@@ -12,6 +12,8 @@ import trytond.config as config
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
MatchMixin, ModelSQL, ModelView, Unique, fields, sequence_ordered)
|
||||
from trytond.modules.product import (
|
||||
copy_product_filtered, copy_template_filtered)
|
||||
from trytond.pool import PoolMeta
|
||||
from trytond.pyson import Bool, Eval, If
|
||||
from trytond.tools import slugify
|
||||
@@ -96,13 +98,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(
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from trytond.model import ModelSQL, fields
|
||||
from trytond.modules.company.model import CompanyValueMixin
|
||||
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
|
||||
|
||||
@@ -26,7 +27,9 @@ class _GrossPriceMixin:
|
||||
today))
|
||||
|
||||
|
||||
class Template(_GrossPriceMixin, metaclass=PoolMeta):
|
||||
class Template(
|
||||
copy_template_filtered('gross_prices'),
|
||||
_GrossPriceMixin, metaclass=PoolMeta):
|
||||
__name__ = 'product.template'
|
||||
|
||||
gross_price = fields.MultiValue(fields.Numeric(
|
||||
@@ -46,7 +49,9 @@ class Template(_GrossPriceMixin, metaclass=PoolMeta):
|
||||
return super().multivalue_model(field)
|
||||
|
||||
|
||||
class Product(_GrossPriceMixin, metaclass=PoolMeta):
|
||||
class Product(
|
||||
copy_product_filtered('gross_prices'),
|
||||
_GrossPriceMixin, metaclass=PoolMeta):
|
||||
__name__ = 'product.product'
|
||||
|
||||
gross_price = fields.MultiValue(fields.Numeric(
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from trytond.model import ModelSQL, ModelView, fields, sequence_ordered, tree
|
||||
from trytond.modules.product import (
|
||||
copy_product_filtered, copy_template_filtered)
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Bool, Eval, If
|
||||
|
||||
|
||||
class Template(metaclass=PoolMeta):
|
||||
class Template(copy_template_filtered('tasks'), metaclass=PoolMeta):
|
||||
__name__ = 'product.template'
|
||||
|
||||
taskable = fields.Boolean(
|
||||
@@ -29,7 +31,7 @@ class Template(metaclass=PoolMeta):
|
||||
])
|
||||
|
||||
|
||||
class Product(metaclass=PoolMeta):
|
||||
class Product(copy_product_filtered('tasks'), metaclass=PoolMeta):
|
||||
__name__ = 'product.product'
|
||||
|
||||
tasks = fields.One2Many(
|
||||
|
||||
Reference in New Issue
Block a user