Files
tradon/modules/product_kit/product.py
2026-03-14 09:42:12 +00:00

284 lines
9.9 KiB
Python

# 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 decimal import Decimal
from trytond.model import (
ModelSQL, ModelStorage, ModelView, fields, sequence_ordered)
from trytond.modules.product import round_price
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, If
class Template(metaclass=PoolMeta):
__name__ = "product.template"
components = fields.One2Many(
'product.component', 'parent_template', "Components")
@classmethod
def __setup__(cls):
super().__setup__()
cls.type.selection.append(('kit', "Kit"))
@classmethod
def _cost_price_method_domain_per_type(cls):
types_cost_method = super()._cost_price_method_domain_per_type()
types_cost_method['kit'] = [('cost_price_method', '=', 'fixed')]
return types_cost_method
@fields.depends('type', 'cost_price_method')
def on_change_type(self):
super().on_change_type()
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):
__name__ = "product.product"
components = fields.One2Many(
'product.component', 'parent_product', "Components")
def get_multivalue(self, name, **pattern):
pool = Pool()
Uom = pool.get('product.uom')
value = super().get_multivalue(name, **pattern)
if name == 'cost_price' and self.type == 'kit':
value = Decimal(0)
for component in self.components_used:
cost_price = component.product.get_multivalue(
'cost_price', **pattern)
cost_price = Uom.compute_price(
component.product.default_uom, cost_price, component.unit)
value += cost_price * Decimal(str(component.quantity))
value = round_price(value)
return value
@property
def components_used(self):
if self.components:
yield from self.components
else:
for component in self.template.components:
if not component.parent_product:
yield component
@classmethod
def get_quantity(cls, products, name):
pool = Pool()
Uom = pool.get('product.uom')
kits = [p for p in products if p.type == 'kit']
quantities = super().get_quantity(products, name)
for kit in kits:
qties = []
for component in kit.components_used:
component_qty = Uom.compute_qty(
component.product.default_uom,
getattr(component.product, name),
component.unit, round=False)
if not component.fixed:
component_qty /= component.quantity
qties.append(component_qty)
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):
parent_type = fields.Function(fields.Selection(
'get_product_types', "Parent Type"), 'on_change_with_parent_type')
product = fields.Many2One(
'product.product', "Product", required=True,
domain=[
('components', '=', None),
('template.components', '=', None),
If(Eval('parent_type') == 'kit',
('type', '=', 'goods'),
()),
])
product_unit_category = fields.Function(
fields.Many2One('product.uom.category', "Product Unit Category"),
'on_change_with_product_unit_category')
quantity = fields.Float("Quantity", digits='unit', required=True)
unit = fields.Many2One('product.uom', "Unit", required=True,
domain=[
If(Bool(Eval('product_unit_category')),
('category', '=', Eval('product_unit_category')),
('category', '!=', -1)),
],
depends={'product'})
fixed = fields.Boolean("Fixed",
help="Check to make the quantity of the component independent "
"of the kit quantity.")
@classmethod
def get_product_types(cls):
pool = Pool()
Product = pool.get('product.product')
return Product.fields_get(['type'])['type']['selection']
def on_change_with_parent_type(self, name):
raise NotImplementedError
@property
def parent_uom(self):
raise NotImplementedError
@fields.depends('product', 'unit', 'quantity',
methods=['on_change_with_product_unit_category'])
def on_change_product(self):
if self.product:
self.product_unit_category = (
self.on_change_with_product_unit_category())
if (not self.unit
or self.unit.category != self.product_unit_category):
self.unit = self.product.default_uom
@fields.depends('product')
def on_change_with_product_unit_category(self, name=None):
return self.product.default_uom.category if self.product else None
def get_line(self, Line, quantity, unit, **values):
pool = Pool()
Uom = pool.get('product.uom')
line = Line(product=self.product, **values)
line.unit = self.unit
if self.fixed:
line.quantity = self.quantity
else:
quantity = Uom.compute_qty(
unit, quantity, self.parent_uom, round=False)
line.quantity = self.unit.round(quantity * self.quantity)
return line
def get_rec_name(self, name):
pool = Pool()
Lang = pool.get('ir.lang')
lang = Lang.get()
return (lang.format_number_symbol(
self.quantity, self.unit, digits=self.unit.digits)
+ ' %s' % self.product.rec_name)
@classmethod
def search_rec_name(cls, name, clause):
return [
('product.rec_name', *clause[1:]),
]
class Component(ComponentMixin, ModelSQL, ModelView):
__name__ = "product.component"
parent_template = fields.Many2One(
'product.template', "Parent Product",
required=True, ondelete='CASCADE',
domain=[
If(Bool(Eval('parent_product')),
('products', '=', Eval('parent_product')),
()),
])
parent_product = fields.Many2One(
'product.product', "Parent Variant", ondelete='CASCADE',
domain=[
If(Bool(Eval('parent_template')),
('template', '=', Eval('parent_template')),
()),
])
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('parent_template')
@fields.depends(
'parent_product', '_parent_parent_product.template')
def on_change_parent_product(self):
if self.parent_product:
self.parent_template = self.parent_product.template
@fields.depends(
'parent_template', '_parent_parent_template.type',
'parent_product', '_parent_parent_product.type')
def on_change_with_parent_type(self, name=None):
if self.parent_product:
return self.parent_product.type
elif self.parent_template:
return self.parent_template.type
@property
def parent_uom(self):
if self.parent_product:
return self.parent_product.default_uom
elif self.parent_template:
return self.parent_template.default_uom
def get_rec_name(self, name):
return super().get_rec_name(name) + (
' @ %s' % (
self.parent_product.rec_name if self.parent_product
else self.parent_template.rec_name))
@classmethod
def search_rec_name(cls, name, clause):
return super().search_rec_name(name, clause) + [
('parent_product.rec_name',) + tuple(clause[1:]),
('parent_template.rec_name',) + tuple(clause[1:]),
]