first commit
This commit is contained in:
283
modules/product_kit/product.py
Normal file
283
modules/product_kit/product.py
Normal file
@@ -0,0 +1,283 @@
|
||||
# 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:]),
|
||||
]
|
||||
Reference in New Issue
Block a user