mirror of
https://github.com/NaN-tic/trytond-product_dynamic_configurator.git
synced 2023-12-14 03:22:58 +01:00
1971 lines
76 KiB
Python
1971 lines
76 KiB
Python
from decimal import Decimal
|
|
from trytond.model import (Workflow, ModelView, ModelSQL,
|
|
DeactivableMixin, fields, sequence_ordered, tree)
|
|
from trytond.pyson import Eval, Not, If, Bool
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.config import config
|
|
from trytond.modules.company.model import employee_field
|
|
from trytond.transaction import Transaction
|
|
from trytond.i18n import gettext
|
|
from trytond.exceptions import UserError
|
|
from jinja2 import Template as Jinja2Template
|
|
from jinja2.exceptions import UndefinedError as Jinja2UndefinedError
|
|
from collections import OrderedDict
|
|
from copy import copy
|
|
import math
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
price_digits = (16, config.getint('product', 'price_decimal', default=4))
|
|
_ZERO = Decimal(0)
|
|
_ROUND = Decimal('.0001')
|
|
|
|
TYPE = [
|
|
(None, ''),
|
|
('bom', 'BoM'),
|
|
('product', 'Product'),
|
|
('purchase_product', 'Purchase Product'),
|
|
('group', 'Group'),
|
|
('match', 'Match'),
|
|
# ('operation', 'Operation'),
|
|
('options', 'Options'),
|
|
('number', 'Number'),
|
|
('text', 'Text'),
|
|
('function', 'Function'),
|
|
('attribute', 'Product Attribute'),
|
|
]
|
|
|
|
|
|
class PriceCategory(ModelSQL, ModelView):
|
|
""" Price Category """
|
|
__name__ = 'configurator.property.price_category'
|
|
|
|
name = fields.Char('Name')
|
|
|
|
|
|
class QuotationCategory(ModelSQL, ModelView):
|
|
""" Price Category """
|
|
__name__ = 'configurator.property.quotation_category'
|
|
|
|
name = fields.Char('Name')
|
|
type_ = fields.Selection([('goods', 'Goods'),
|
|
('works', 'Works'), ('other', 'Other')], 'Type')
|
|
party = fields.Many2One('party.party', 'Default Supplier')
|
|
|
|
|
|
class Template(metaclass=PoolMeta):
|
|
__name__ = 'product.template'
|
|
|
|
def get_purchase_context(self):
|
|
return {}
|
|
|
|
|
|
class Property(DeactivableMixin, tree(separator=' / '), sequence_ordered(),
|
|
ModelSQL, ModelView):
|
|
""" Property """
|
|
__name__ = 'configurator.property'
|
|
# Code may not contain spaces or special characters (usable in formulas)
|
|
code = fields.Char('Code', required=True)
|
|
name = fields.Char('Name', required=True, translate=True)
|
|
type = fields.Selection(TYPE, 'Type', required=True)
|
|
user_input = fields.Boolean('User Input',
|
|
states={
|
|
'invisible': Not(Eval('type').in_(['number', 'options', 'text'])),
|
|
})
|
|
template = fields.Boolean('Template',
|
|
states={
|
|
'invisible': Not(Eval('type').in_(['bom', 'purchase_product']))
|
|
})
|
|
product = fields.Many2One('product.product', 'Product',
|
|
states={
|
|
'invisible': Eval('type') != 'product',
|
|
}
|
|
)
|
|
uom = fields.Many2One('product.uom', 'UoM', states={
|
|
'invisible': Eval('type').in_(['function', 'text', 'number',
|
|
'options', 'group', 'attribute']),
|
|
'required': Eval('type').in_(['bom', 'product', 'purchase_product'])
|
|
},
|
|
domain=[If(Bool(Eval('product_uom_category')),
|
|
('category', '=', Eval('product_uom_category', -1)), ())],
|
|
depends=['product_uom_category', 'type'])
|
|
childs = fields.One2Many('configurator.property', 'parent', 'childs',
|
|
states={
|
|
'invisible': Not(Eval('type').in_(['bom', 'purchase_product',
|
|
'options', 'group', 'match']))
|
|
})
|
|
parent = fields.Many2One('configurator.property', 'Parent', select=True,
|
|
ondelete='CASCADE')
|
|
quantity = fields.Char('Quantity', states={
|
|
'invisible': Not(Eval('type').in_(['purchase_product',
|
|
'bom', 'product', 'function', 'match'])),
|
|
'required': Eval('type').in_(['bom', 'product', 'purchase_product'])
|
|
}, depends=['type'])
|
|
bom_quantity = fields.Char('Bom Quantity', states={
|
|
'invisible': Not(Eval('type').in_(['purchase_product',
|
|
'bom', 'product', 'function', 'match'])),
|
|
'required': Eval('type').in_(['bom', 'product', 'purchase_product'])
|
|
}, depends=['type'])
|
|
price_category = fields.Many2One('configurator.property.price_category',
|
|
'Price Category',
|
|
states={
|
|
'invisible': Eval('type').in_(['function', 'text', 'number',
|
|
'attribute', 'group']),
|
|
},)
|
|
object_expression = fields.Char('Object Expression',
|
|
states={
|
|
'invisible': Not(Eval('type').in_(['purchase_product', 'bom']))
|
|
}
|
|
)
|
|
work_center_category = fields.Many2One('production.work_center.category',
|
|
'Work Center Category', states={
|
|
'invisible': Eval('type') != 'operation',
|
|
'required': Eval('type').in_(['operation'])
|
|
}, depends=['type'])
|
|
operation_type = fields.Many2One('production.operation.type',
|
|
'Operation Type', states={
|
|
'invisible': Eval('type') != 'operation',
|
|
'required': Eval('type').in_(['operation'])
|
|
}, depends=['type'])
|
|
|
|
attribute_set = fields.Many2One('product.attribute.set', 'Attribute Set',
|
|
states={
|
|
'invisible': Eval('type') != 'attribute',
|
|
'required': Eval('type') == 'attribute',
|
|
})
|
|
product_attribute = fields.Many2One('product.attribute',
|
|
'Product Attribute',
|
|
domain=[('sets', '=', Eval('attribute_set'))],
|
|
depends=['attribute_set'],
|
|
states={
|
|
'invisible': Eval('type') != 'attribute',
|
|
'required': Eval('type') == 'attribute',
|
|
})
|
|
attribute_search_op = fields.Char('Operator', states={
|
|
'invisible': Eval('type') != 'attribute',
|
|
})
|
|
product_attribute_value = fields.Char('Product Attribute Value', states={
|
|
'invisible': Eval('type') != 'attribute',
|
|
'required': Eval('type') == 'attribute',
|
|
})
|
|
product_template = fields.Many2One('product.template', 'Product Template',
|
|
domain=[('configurator_template', '=', True)],
|
|
states={
|
|
'invisible': Not(Eval('type').in_(['purchase_product', 'bom'])),
|
|
'required': Eval('type').in_(['purchase_product', 'bom'])
|
|
}, depends=['type']
|
|
)
|
|
product_uom_category = fields.Function(
|
|
fields.Many2One('product.uom.category', 'Product Uom Category'),
|
|
'on_change_with_product_uom_category')
|
|
|
|
quotation_category = fields.Many2One(
|
|
'configurator.property.quotation_category', 'Quoatation Category',
|
|
states={
|
|
'invisible': Eval('type').in_(['function', 'text', 'number',
|
|
'attribute', 'group']),
|
|
},)
|
|
code_jinja = fields.Many2One('configurator.jinja_template',
|
|
'Code Template',
|
|
states={
|
|
'invisible': Not(Eval('type').in_(['purchase_product', 'bom']))
|
|
})
|
|
name_jinja = fields.Many2One('configurator.jinja_template',
|
|
'Name Template',
|
|
states={
|
|
'invisible': Not(Eval('type').in_(['purchase_product', 'bom']))
|
|
})
|
|
option_default = fields.Many2One('configurator.property',
|
|
'Default Option', domain=[
|
|
('id', '!=', Eval('id', -1)),
|
|
('parent', 'child_of', Eval('id', -1))
|
|
],
|
|
depends=['id', 'parent', 'type'],
|
|
states={
|
|
'invisible': Eval('type').in_(['option'])
|
|
})
|
|
childrens = fields.Function(fields.One2Many('configurator.property', None,
|
|
'Childrens'), 'get_childrens', searcher='search_childrens')
|
|
|
|
option_price_property = fields.Many2One('configurator.property',
|
|
'Option Price Property',
|
|
states={
|
|
'invisible': Eval('type') != 'purchase_product'
|
|
},
|
|
domain = [('id', 'in', Eval('childrens', []))],
|
|
depends=['type'],
|
|
help='Price for option when purchase_product is selected')
|
|
|
|
|
|
@staticmethod
|
|
def default_sequence():
|
|
return 99
|
|
|
|
@classmethod
|
|
def search_childrens(cls, name, clause):
|
|
childrens = cls.search([('parent', '!=', None),
|
|
('active', '=', True)])
|
|
|
|
return [('id', 'in', [x.id for x in childrens]),
|
|
['OR', ('name', clause[-2], clause[-1]),
|
|
('code', clause[-2], clause[-1])] ]
|
|
|
|
def get_childrens(self, name):
|
|
Property = Pool().get('configurator.property')
|
|
parent = self
|
|
if self.type != 'bom':
|
|
parent = self.get_parent()
|
|
childrens = Property.search([('parent', 'child_of', [parent.id])])
|
|
childrens = [x for x in childrens if x.type in ('bom', 'product',
|
|
'purchase_product', 'match')]
|
|
childs = []
|
|
for children in childrens:
|
|
p = children.get_parent()
|
|
if children.type == 'bom':
|
|
p = children.parent and children.parent.get_parent()
|
|
if p == parent:
|
|
childs.append(children.id)
|
|
return childs
|
|
|
|
def get_rec_name(self, name):
|
|
res = ''
|
|
parent = self.get_parent()
|
|
if self.code:
|
|
res = '[%s] ' % self.code
|
|
if self.code and parent and parent.parent:
|
|
res = '[%s/%s] ' % (parent.code, self.code)
|
|
res += self.name
|
|
return res
|
|
|
|
def get_full_code(self):
|
|
res = ''
|
|
parent = self.get_parent()
|
|
if self.code:
|
|
res = '%s' % self.code
|
|
if self.code and parent and parent.parent:
|
|
res = '%s_%s' % (parent.code, self.code)
|
|
return res
|
|
|
|
def render_expression_record(self, expression, record):
|
|
template = Jinja2Template(expression, trim_blocks=True)
|
|
try:
|
|
res = template.render(record)
|
|
except TypeError as e:
|
|
raise UserError(gettext(
|
|
'product_dynamic_configurator.msg_expression_error',
|
|
property=self.rec_name,
|
|
expression=expression[:25]+('...' if len(expression) > 25 else ''),
|
|
invalid=str(e)))
|
|
except Jinja2UndefinedError as e:
|
|
raise UserError(gettext(
|
|
'product_dynamic_configurator.msg_expression_error',
|
|
property=self.rec_name,
|
|
expression=expression[:25]+('...' if len(expression) > 25 else ''),
|
|
invalid=str(e)))
|
|
if res:
|
|
res = res.replace('\t', '').replace('\n', '').strip()
|
|
return res
|
|
|
|
@classmethod
|
|
def copy(cls, properties, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
|
|
option_default = (dict(
|
|
(x.get_full_code(), x.option_default.get_full_code())
|
|
for x in properties if x.option_default))
|
|
|
|
option_price = (dict(
|
|
(x.get_full_code(), x.option_price_property.get_full_code())
|
|
for x in properties if x.option_price_property))
|
|
|
|
default.setdefault('option_price_property', None)
|
|
default.setdefault('option_default', None)
|
|
|
|
new_properties = super().copy(properties, default=default)
|
|
|
|
to_save = []
|
|
for prop in new_properties:
|
|
code = prop.get_full_code()
|
|
if code not in option_price and code not in option_default:
|
|
continue
|
|
|
|
parent = prop.get_parent()
|
|
childs = parent.childrens
|
|
codes = dict((x.get_full_code(),x) for x in childs)
|
|
|
|
if code in option_price:
|
|
price_code = option_price.get(code)
|
|
prop.option_price_property = codes.get(price_code)
|
|
|
|
if code in option_default:
|
|
default_code = option_default.get(code)
|
|
prop.option_default = codes.get(default_code)
|
|
|
|
to_save.append(prop)
|
|
|
|
cls.save(to_save)
|
|
return to_save
|
|
|
|
|
|
|
|
@fields.depends('user_input', 'quantity', 'uom', 'template', 'product',
|
|
'price_category', 'object_expression', 'attribute_set',
|
|
'work_center_category', 'operation_type', 'product_attribute',
|
|
'product_attribute_value', 'product_template')
|
|
def on_change_type(self):
|
|
self.quantity = None
|
|
self.uom = None
|
|
self.template = None
|
|
self.product = None
|
|
self.price_category = None
|
|
self.object_expression = None
|
|
self.attribute_set = None
|
|
self.work_center_category = None
|
|
self.operation_type = None
|
|
self.product_attribute = None
|
|
self.product_attribute_value = None
|
|
self.product_template = None
|
|
|
|
@fields.depends('product')
|
|
def on_change_product(self):
|
|
if self.product:
|
|
self.uom = self.product.default_uom
|
|
|
|
@fields.depends('product_template')
|
|
def on_change_product_template(self):
|
|
if self.product_template:
|
|
self.uom = self.product_template.default_uom
|
|
|
|
@fields.depends('product', 'product_template')
|
|
def on_change_with_product_uom_category(self, name=None):
|
|
if self.product:
|
|
return self.product.default_uom_category.id
|
|
if self.product_template:
|
|
return self.product_template.default_uom_category.id
|
|
|
|
def get_parent(self):
|
|
if self.type == 'bom':
|
|
return self
|
|
if not self.parent:
|
|
return self
|
|
return self.parent.get_parent()
|
|
|
|
def get_root_parent(self):
|
|
if self.parent:
|
|
return self.get_root_parent(self.parent)
|
|
return self
|
|
|
|
def compute_attributes(self, design, attributes=None):
|
|
Attribute = Pool().get('configurator.design.attribute')
|
|
if not attributes:
|
|
attributes = {}
|
|
res = []
|
|
attribute = attributes.get(self.code)
|
|
if self.user_input:
|
|
if not attribute:
|
|
attribute = Attribute()
|
|
attribute.design = design
|
|
attribute.property = self
|
|
attribute.property_type = \
|
|
attribute.on_change_with_property_type()
|
|
if attribute.property_type == 'options':
|
|
attribute.property_options = \
|
|
attribute.on_change_with_property_options()
|
|
if attribute.property.option_default:
|
|
attribute.option = attribute.property.option_default.id
|
|
attributes[self.code] = attribute
|
|
res.append(attribute)
|
|
else:
|
|
for child in self.childs:
|
|
res += child.compute_attributes(design, attributes)
|
|
return res
|
|
|
|
@staticmethod
|
|
def _create_objects(objects):
|
|
pool = Pool()
|
|
design = Transaction().context.get('design')
|
|
if not design:
|
|
return
|
|
|
|
CreatedObject = pool.get('configurator.object')
|
|
to_create = [design.create_object(obj) for obj in objects]
|
|
if to_create:
|
|
CreatedObject.save(to_create)
|
|
|
|
def evaluate(self, expression, values, design):
|
|
custom_locals = copy(locals())
|
|
custom_locals['math'] = math
|
|
keys = [x for x in values.keys()]
|
|
keys.sort(key=lambda x: ( str(x.parent and x.parent.sequence or 0).zfill(5) +
|
|
str(x.sequence).zfill(6)))
|
|
|
|
for prop in keys:
|
|
attr = values[prop]
|
|
if isinstance(attr, dict):
|
|
continue
|
|
if prop.type == 'function':
|
|
custom_locals[prop.code] = attr
|
|
else:
|
|
custom_locals[prop.code] = attr.number or attr.option
|
|
try:
|
|
code = compile(expression, "<string>", "eval")
|
|
res = eval(code, custom_locals)
|
|
# if isinstance(res, str):
|
|
# code = compile(res, "<string>", "eval")
|
|
# res = eval(code, custom_locals)
|
|
return res
|
|
except BaseException as e:
|
|
logger.error(str(e))
|
|
pass
|
|
# raise UserError(gettext(
|
|
# 'product_dynamic_configurator.msg_expression_error',
|
|
# property=self.name, expression=self.quantity,
|
|
# invalid=str(e)))
|
|
|
|
def create_prices(self, design, values):
|
|
created_obj = {}
|
|
if self.type not in ('match'):
|
|
for prop in self.childs:
|
|
if self.type == 'options' and prop.type != 'purchase_product':
|
|
continue
|
|
parent = prop.get_parent()
|
|
val = values
|
|
if parent in values:
|
|
val = values[parent]
|
|
res = prop.create_prices(design, val)
|
|
if res is None:
|
|
continue
|
|
created_obj.update(res)
|
|
parent = self.get_parent()
|
|
val = values
|
|
if parent in values:
|
|
val = values[parent]
|
|
res = getattr(self, 'get_%s' % self.type)(
|
|
design, val, created_obj)
|
|
if res is None:
|
|
return created_obj
|
|
created_obj.update(res)
|
|
return created_obj
|
|
|
|
def get_match_domain(self, design):
|
|
return []
|
|
|
|
def get_match(self, design, values, created_obj):
|
|
pool = Pool()
|
|
Product = pool.get('product.product')
|
|
BomInput = pool.get('production.bom.input')
|
|
Uom = pool.get('product.uom')
|
|
domain = []
|
|
|
|
suppliers = dict((x.category, x.supplier) for x in design.suppliers)
|
|
if self.quotation_category and self.quotation_category in suppliers:
|
|
domain += [('product_suppliers.party', '=',
|
|
suppliers[self.quotation_category].id)]
|
|
|
|
for child in self.childs:
|
|
attribute = child.product_attribute
|
|
value = self.evaluate(child.product_attribute_value, values, design)
|
|
|
|
if isinstance(value, Product):
|
|
val = None
|
|
for attr in value.attributes:
|
|
if attr.attribute == attribute:
|
|
val = attr.value
|
|
break
|
|
if not val:
|
|
return {self: (None, [])}
|
|
value = val
|
|
op = child.attribute_search_op or '='
|
|
type_ = attribute.type_
|
|
domain += [
|
|
('type', '=', 'service'),
|
|
('template.attribute_set', '=', child.attribute_set.id),
|
|
('attributes.attribute.id', '=', attribute.id),
|
|
('attributes.value_%s' % type_, op, value),
|
|
]
|
|
domain += self.get_match_domain(design)
|
|
products = Product.search(domain)
|
|
if not products:
|
|
return {self: (None, [])}
|
|
|
|
product = None
|
|
for prod in products:
|
|
if len(prod.attributes) > len(self.childs):
|
|
continue
|
|
product = prod
|
|
|
|
if not product:
|
|
return {self: (None, [])}
|
|
|
|
context = Transaction().context
|
|
quantity = self.bom_quantity or self.quantity
|
|
if context.get('prices', False):
|
|
quantity = self.quantity
|
|
|
|
quantity = self.evaluate(quantity, values, design)
|
|
quantity = Uom.compute_qty(self.uom, quantity,
|
|
product.default_uom)
|
|
bom_input = BomInput()
|
|
bom_input.product = product
|
|
bom_input.on_change_product()
|
|
bom_input.quantity = quantity
|
|
return {self: (bom_input, [])}
|
|
|
|
def get_number(self, design, values, created_obj):
|
|
pass
|
|
|
|
def get_text(self, design, values, created_obj):
|
|
pass
|
|
|
|
def get_function(self, design, values, created_obj):
|
|
context = Transaction().context
|
|
if context.get('prices', False):
|
|
value = self.evaluate(self.quantity, values, design)
|
|
else:
|
|
value = self.evaluate(self.bom_quantity, values, design)
|
|
|
|
return {self: (value, [])}
|
|
|
|
def get_options(self, design, values, created_obj):
|
|
attribute = values.get(self)
|
|
if attribute and attribute.option is not None:
|
|
res = attribute.option.create_prices(design, values)
|
|
option = res.get(attribute.option, None)
|
|
if option:
|
|
res.update({self: (option[0], [])})
|
|
return res
|
|
return {self: (None, [])}
|
|
|
|
def get_attribute(self, design, values, created_obj):
|
|
pool = Pool()
|
|
ProductAttribute = pool.get('product.product.attribute')
|
|
product_attribute = ProductAttribute()
|
|
product_attribute.attribute = self.product_attribute
|
|
product_attribute.value = self.product_attribute_value
|
|
return {self: (product_attribute, [])}
|
|
|
|
def get_operation(self, design, values, created_obj):
|
|
pool = Pool()
|
|
Uom = pool.get('product.uom')
|
|
Operation = pool.get('production.route.operation')
|
|
operation = Operation()
|
|
operation.work_center_category = self.work_center_category
|
|
operation.operation_type = self.operation_type
|
|
operation.time = 1 # TODO: hardcoded
|
|
operation.time_uom = 9 # TODO: hardcoded
|
|
operation.quantity_uom = self.uom
|
|
|
|
context = Transaction().context
|
|
quantity = self.bom_quantity or self.quantity
|
|
if context.get('prices', False):
|
|
quantity = self.quantity
|
|
|
|
operation.quantity = Uom.compute_qty(self.uom,
|
|
self.evaluate(quantity, values, design), self.uom)
|
|
operation.calculation = 'standard'
|
|
return {self: (operation, [])}
|
|
|
|
def get_product_attribute_typed(self, prop, value):
|
|
if prop.product_attribute.type_ == 'boolean':
|
|
return ('bool', bool(value))
|
|
elif prop.product_attribute.type_ == 'integer':
|
|
return('integer', int(value))
|
|
elif prop.product_attribute.type_ == 'char':
|
|
return ('char', str(value))
|
|
elif prop.product_attribute.type_ == 'numeric':
|
|
return ('numeric', Decimal(str(value)))
|
|
elif prop.product_attribute.type_ == 'date':
|
|
raise 'Not Implemented'
|
|
elif prop.product_attribute.type_ == 'datetime':
|
|
raise 'Not Implemented'
|
|
elif prop.product_attribute.type_ == 'selection':
|
|
raise 'Not Implemented'
|
|
|
|
def get_product_template_object_copy(self, template):
|
|
Template = Pool().get('product.template')
|
|
ntemplate = Template()
|
|
ntemplate.attribute_set = None
|
|
ntemplate.attributes = tuple()
|
|
for f in template._fields:
|
|
setattr(ntemplate, f, getattr(template, f))
|
|
ntemplate.configurator_template = False
|
|
ntemplate.categories_all = None
|
|
ntemplate.products = []
|
|
ntemplate.product_suppliers = []
|
|
return ntemplate
|
|
|
|
def get_product_product_object_copy(self, product):
|
|
Product = Pool().get('product.product')
|
|
nproduct = Product()
|
|
for f in product._fields:
|
|
if not hasattr(nproduct, f):
|
|
continue
|
|
setattr(nproduct, f, getattr(product, f))
|
|
nproduct.attributes = tuple()
|
|
nproduct.account_category = None
|
|
nproduct.template = None
|
|
nproduct.product_suppliers = []
|
|
|
|
return nproduct
|
|
|
|
def get_field_name_from_attributes(self, attribute_set, name, record):
|
|
BomInput = Pool().get('production.bom.input')
|
|
values = {}
|
|
for key, val in record.items():
|
|
if isinstance(val, BomInput):
|
|
values[key] = val.product.name
|
|
else:
|
|
values[key] = val
|
|
name_field = getattr(attribute_set, name)
|
|
return attribute_set.render_expression_record(name_field, values)
|
|
|
|
def get_property_code(self, design, custom_locals):
|
|
code = design.render_field(self, 'code_jinja', custom_locals)
|
|
code = code and code.strip()
|
|
return code
|
|
|
|
def get_property_name(self, design, custom_locals):
|
|
code = design.render_field(self, 'name_jinja', custom_locals)
|
|
code = code and code.strip() or '' + self.name.strip()
|
|
return code
|
|
|
|
def get_purchase_product(self, design, values, created_obj):
|
|
pool = Pool()
|
|
BomInput = pool.get('production.bom.input')
|
|
Uom = pool.get('product.uom')
|
|
Product = pool.get('product.product')
|
|
ProductCostPrice = pool.get('product.cost_price')
|
|
Attribute = pool.get('product.product.attribute')
|
|
DesignAttribute = pool.get('configurator.design.attribute')
|
|
exists = False
|
|
|
|
if self.parent and self.parent.type == 'options':
|
|
attr = DesignAttribute.search([('property', '=', self.parent.id),
|
|
('design', '=', design.id)])
|
|
if not attr or attr and attr[0] .option is None:
|
|
return
|
|
|
|
if not self.product_template:
|
|
return
|
|
|
|
custom_locals = design.design_full_dict()
|
|
template = self.get_product_template_object_copy(self.product_template)
|
|
template.name = self.name + "(" + design.name + ")"
|
|
template.list_price = 0
|
|
template.info_ratio = Decimal('1.0')
|
|
product = self.get_product_product_object_copy(
|
|
self.product_template.products[0])
|
|
product.template = template
|
|
product.default_uom = self.uom
|
|
|
|
# Generate code
|
|
template.code = self.get_property_code(design, custom_locals)
|
|
if not template.code:
|
|
template.code = "purchase (%s)" % (template.code or str(design.id))
|
|
template_name = self.get_property_name(design, custom_locals)
|
|
if template_name:
|
|
template.name = template_name
|
|
|
|
if not hasattr(product, 'attributes'):
|
|
product.attributes = tuple()
|
|
for prop, child_res in created_obj.items():
|
|
child_res = child_res[0]
|
|
if prop.type == 'attribute':
|
|
template.attribute_set = prop.attribute_set
|
|
type_, value = self.get_product_attribute_typed(prop,
|
|
self.evaluate(prop.product_attribute_value, values, design))
|
|
setattr(child_res, 'value_' + type_, value)
|
|
setattr(child_res, 'value', value)
|
|
product.attributes += (child_res,)
|
|
elif prop.type == 'match' and child_res:
|
|
for attribute in child_res.product.attributes:
|
|
attr = Attribute()
|
|
attr.template = template
|
|
attr.attribute = attribute.attribute
|
|
attr.attribute_type = attribute.attribute_type
|
|
setattr(attr, 'value_' + attribute.attribute_type,
|
|
attribute.value)
|
|
product.attributes += (attr,)
|
|
|
|
if self.object_expression:
|
|
expressions = eval(self.object_expression)
|
|
for key, value in expressions.items():
|
|
val = self.evaluate(value, values, design)
|
|
quantize = Decimal(str(10.0 ** -4))
|
|
try:
|
|
val = float(Decimal(val or 0).quantize(quantize))
|
|
except:
|
|
pass
|
|
setattr(template, key, val)
|
|
|
|
template.products = (product,)
|
|
if template.attribute_set:
|
|
template._update_attributes_values()
|
|
template.products = None
|
|
|
|
exists_product = Product.search(
|
|
[('template.code', '=', template.code),
|
|
('template.default_uom', '=', template.default_uom)])
|
|
|
|
if exists_product:
|
|
product = exists_product[0]
|
|
template = product.template
|
|
exists = True
|
|
|
|
template = self.update_product_values(template, design, values, created_obj, exists)
|
|
product = self.update_variant_values(product, values, None, design)
|
|
template = self.template_update(template, None, design)
|
|
|
|
bom_input = BomInput()
|
|
bom_input.product = product
|
|
bom_input.on_change_product()
|
|
context = Transaction().context
|
|
quantity = self.bom_quantity or self.quantity
|
|
if context.get('prices', False):
|
|
quantity = self.quantity
|
|
|
|
bom_input.quantity = Uom.compute_qty(self.uom,
|
|
self.evaluate(quantity, values, design), product.default_uom)
|
|
|
|
# Calculate cost_price for purchase_product
|
|
cost_price = 0
|
|
suppliers = dict((x.category, x.supplier) for x in design.suppliers)
|
|
goods_supplier = None
|
|
for category, supplier in suppliers.items():
|
|
if (self.option_price_property and
|
|
self.quotation_category == category):
|
|
goods_supplier = supplier
|
|
break
|
|
if not self.option_price_property and category.type_ == 'goods':
|
|
goods_supplier = supplier
|
|
break
|
|
|
|
if not goods_supplier:
|
|
return {self: (bom_input, [])}
|
|
|
|
ProductSupplier = pool.get('purchase.product_supplier')
|
|
Price = pool.get('purchase.product_supplier.price')
|
|
if hasattr(product, 'product_suppliers') and product.product_suppliers:
|
|
for supplier in product.product_suppliers:
|
|
supplier.active = False
|
|
else:
|
|
product.product_suppliers = []
|
|
|
|
product_supplier = ProductSupplier()
|
|
product_supplier.template = template
|
|
# product_supplier.product = product
|
|
product_supplier.party = goods_supplier
|
|
product_supplier.on_change_party()
|
|
product_supplier.prices = ()
|
|
|
|
design_qty = self.evaluate(design.template.quantity, values, design)
|
|
if hasattr(product, 'product_suppliers') and product.product_suppliers:
|
|
for supplier in product.product_suppliers:
|
|
supplier.active = False
|
|
else:
|
|
product.product_suppliers = []
|
|
|
|
for quote in design.prices:
|
|
if hasattr(quote, 'state') and quote.state != 'confirmed':
|
|
continue
|
|
|
|
cost_price = 0
|
|
uom = None
|
|
qty = Uom.compute_qty(design.template.uom, design_qty,
|
|
design.quotation_uom)
|
|
for prop, v in created_obj.items():
|
|
v = v[0]
|
|
if not v or prop.type not in ('product', 'match', 'bom'):
|
|
continue
|
|
for line in quote.prices:
|
|
if line.property != prop:
|
|
continue
|
|
cost_price += line.unit_price
|
|
uom = line.uom
|
|
|
|
if self.option_price_property:
|
|
for line in quote.prices:
|
|
if line.property != self.option_price_property:
|
|
continue
|
|
cost_price = line.manual_unit_price or line.unit_price
|
|
|
|
if cost_price:
|
|
price = Price()
|
|
price.uom = template.purchase_uom
|
|
price.info_unit = template.info_unit
|
|
qty = ((quote.quantity / qty) * bom_input.quantity)
|
|
price.quantity = Uom.compute_qty(self.uom, qty,
|
|
self.product_template.purchase_uom)
|
|
|
|
if uom != self.uom:
|
|
cost_price = Uom.compute_price(self.uom, cost_price,
|
|
self.product_template.default_uom)
|
|
cost_price_sup = Uom.compute_price(self.uom, cost_price,
|
|
self.product_template.purchase_uom)
|
|
supplier_unit_price = cost_price
|
|
product_cost_price = cost_price_sup
|
|
if template.use_info_unit:
|
|
product_cost_price = template.get_unit_price(cost_price)
|
|
supplier_unit_price = template.get_unit_price(cost_price_sup)
|
|
else:
|
|
product_cost_price = Uom.compute_price(self.uom, cost_price,
|
|
self.product_template.default_uom)
|
|
supplier_unit_price = cost_price
|
|
|
|
digits = ProductCostPrice.cost_price.digits[1]
|
|
product.cost_price = Decimal(product_cost_price).quantize(Decimal(
|
|
str(10.0 ** -digits)))
|
|
|
|
digits = Price.unit_price.digits[1]
|
|
price.unit_price = Decimal(supplier_unit_price).quantize(Decimal(
|
|
str(10.0 ** -digits)))
|
|
|
|
product_supplier.prices += (price,)
|
|
price.product = product
|
|
if getattr(template, 'use_info_unit'):
|
|
price.info_unit_price = (
|
|
price.on_change_with_info_unit_price())
|
|
price.info_quantity = price.on_change_with_info_quantity()
|
|
product.product_suppliers += (product_supplier,)
|
|
return {self: (bom_input, [])}
|
|
|
|
def get_group(self, design, values, created_obj):
|
|
res = {}
|
|
for child in self.childs:
|
|
r = getattr(child, 'get_%s' % child.type)(
|
|
design, values, created_obj)
|
|
if not r:
|
|
continue
|
|
res.update(r)
|
|
return res
|
|
|
|
def get_product(self, design, values, created_obj):
|
|
pool = Pool()
|
|
BomInput = pool.get('production.bom.input')
|
|
Uom = pool.get('product.uom')
|
|
|
|
if not self.product:
|
|
return
|
|
|
|
quantity = self.bom_quantity or self.quantity
|
|
context = Transaction().context
|
|
if context.get('prices', False):
|
|
quantity = self.quantity
|
|
|
|
attribute = None
|
|
if self.parent and self.parent.type == 'options':
|
|
attribute = values.get(self.parent)
|
|
product = self.product
|
|
if attribute and attribute.number:
|
|
quantity = attribute.number
|
|
else:
|
|
quantity = self.evaluate(quantity, values, design)
|
|
|
|
quantity = Uom.compute_qty(self.uom, quantity, product.default_uom)
|
|
bom_input = BomInput()
|
|
bom_input.product = product
|
|
bom_input.on_change_product()
|
|
bom_input.quantity = quantity
|
|
return {self: (bom_input, [])}
|
|
|
|
def get_bom(self, design, values, created_obj):
|
|
pool = Pool()
|
|
Product = pool.get('product.product')
|
|
Bom = pool.get('production.bom')
|
|
BomInput = pool.get('production.bom.input')
|
|
BomOutput = pool.get('production.bom.output')
|
|
ProductBom = pool.get('product.product-production.bom')
|
|
Operation = pool.get('production.route.operation')
|
|
Route = pool.get('production.route')
|
|
Uom = pool.get('product.uom')
|
|
Attribute = pool.get('product.product.attribute')
|
|
|
|
def create_bom_input(property_):
|
|
if property_.type == 'purchase_product':
|
|
return False
|
|
if property_.parent:
|
|
return create_bom_input(property_.parent)
|
|
return True
|
|
|
|
bom, = Bom.search([('name', '=', self.name)]) or [None]
|
|
if bom:
|
|
return {self: bom}
|
|
|
|
res_obj = []
|
|
bom = Bom()
|
|
bom.name = "%s (%s)" % (self.name, (design.code or str(design.id)))
|
|
bom.active = True
|
|
bom.inputs = ()
|
|
bom.outputs = ()
|
|
operations_route = ()
|
|
for child, child_res in created_obj.items():
|
|
if child.type == 'bom' and child.parent:
|
|
parent = child.parent.get_parent()
|
|
else:
|
|
parent = child.get_parent()
|
|
|
|
if parent != self and child.parent:
|
|
continue
|
|
child_res, _ = child_res
|
|
if isinstance(child_res, BomInput):
|
|
# TODO: needed to make purchase_product a composite of products
|
|
# then we need to avoid on bom.
|
|
if child_res.product.template.type == 'service':
|
|
continue
|
|
if child_res in bom.inputs:
|
|
continue
|
|
if (child.type == 'product' and child.parent
|
|
and child.parent.type != 'options'):
|
|
bom.inputs += (child_res,)
|
|
elif child.type == 'purchase_product':
|
|
bom.inputs += (child_res,)
|
|
elif child.type == 'options':
|
|
bom.inputs += (child_res,)
|
|
|
|
elif isinstance(child_res, Bom):
|
|
bom_input = BomInput()
|
|
child_output, = child_res.outputs
|
|
bom_input.product = child_output.product
|
|
bom_input.on_change_product()
|
|
bom_input.quantity = child_output.quantity
|
|
bom_input.uom = child_output.uom
|
|
bom.inputs += (bom_input,)
|
|
elif isinstance(child_res, Operation):
|
|
operations_route += (child_res,)
|
|
|
|
route = None
|
|
if operations_route:
|
|
route = Route()
|
|
route.uom = operations_route[0].quantity_uom
|
|
route.name = design.name + "(" + design.code + ")"
|
|
route.operations = operations_route
|
|
res_obj.append(route)
|
|
|
|
template = None
|
|
if not self.product_template:
|
|
return
|
|
template = self.get_product_template_object_copy(self.product_template)
|
|
template.name = "%s (%s)" % (self.name, 'Id' + str(design.id))
|
|
|
|
product = self.get_product_product_object_copy(
|
|
self.product_template.products[0])
|
|
|
|
product.template = template
|
|
product.default_uom = self.uom
|
|
product.list_price = 0
|
|
|
|
# Generate code
|
|
custom_locals = design.design_full_dict()
|
|
# Generate code
|
|
template.code = self.get_property_code(design, custom_locals)
|
|
if not template.code:
|
|
template.code = "%s (%s)" % (self.code, 'Id' + str(design.id))
|
|
|
|
template_name = self.get_property_name(design, custom_locals)
|
|
if template_name:
|
|
template.name = template_name
|
|
|
|
template = self.update_product_values(template, design, values, created_obj, bom=bom)
|
|
template = self.template_update(template, bom, design)
|
|
product = self.update_variant_values(product, values, bom, design)
|
|
|
|
bom.name = template.code
|
|
|
|
# Copy attributes
|
|
if not hasattr(template, 'attributes'):
|
|
template.attributes = tuple()
|
|
for prop, child_res in created_obj.items():
|
|
child_res = child_res[0]
|
|
if prop.type == 'attribute':
|
|
template.attribute_set = prop.attribute_sets
|
|
type_, value = self.get_product_attribute_typed(prop,
|
|
self.evaluate(prop.product_attribute_value, values, design))
|
|
setattr(child_res, 'value_' + type_, value)
|
|
setattr(child_res, 'value', value)
|
|
template.attributes += (child_res,)
|
|
elif prop.type == 'match' and child_res:
|
|
for attribute in child_res.product.attributes:
|
|
attr = Attribute()
|
|
attr.template = template
|
|
attr.attribute = attribute.attribute
|
|
attr.attribute_type = attribute.attribute_type
|
|
setattr(attr, 'value_' + attribute.attribute_type,
|
|
attribute.value)
|
|
product.attributes += (attr,)
|
|
|
|
template._update_attributes_values()
|
|
|
|
if self.object_expression:
|
|
expressions = eval(self.object_expression)
|
|
for key, value in expressions.items():
|
|
val = self.evaluate(value, values, design)
|
|
quantize = Decimal(str(10.0 ** -4))
|
|
try:
|
|
val = float(Decimal(val or 0).quantize(quantize))
|
|
except:
|
|
pass
|
|
setattr(template, key, val)
|
|
|
|
exists_product = Product.search([
|
|
('template.code', '=', template.code)])
|
|
if exists_product:
|
|
product = exists_product[0]
|
|
|
|
quantity = self.bom_quantity or self.quantity
|
|
context = Transaction().context
|
|
if context.get('prices', False):
|
|
quantity = self.quantity
|
|
|
|
output = BomOutput()
|
|
output.bom = bom
|
|
output.product = product
|
|
output.uom = product.default_uom
|
|
output.quantity = Uom.compute_qty(self.uom,
|
|
self.evaluate(quantity, values, design), product.default_uom)
|
|
bom.outputs += (output,)
|
|
bom.name = "(%s) %s" % (template.code, template.name)
|
|
product_bom = ProductBom()
|
|
product_bom.product = product
|
|
product_bom.bom = bom
|
|
if 'phantom' in Product._fields:
|
|
product.phantom = True
|
|
if route:
|
|
product_bom.route = route
|
|
res_obj.append(product_bom)
|
|
return {self: (bom, res_obj)}
|
|
|
|
def template_update(self, template, bom, design):
|
|
return template
|
|
|
|
|
|
def get_ratio_for_prices(self, values, ratio, design):
|
|
quantity = self.bom_quantity or self.quantity
|
|
context = Transaction().context
|
|
if context.get('prices', False):
|
|
quantity = self.quantity
|
|
|
|
if not self.parent:
|
|
r = self.evaluate(quantity or '1.0', values, design) or 1.0
|
|
return ratio / r
|
|
|
|
parent = self.parent
|
|
parent_quantity = parent.bom_quantity or parent.quantity
|
|
if context.get('prices', False):
|
|
parent_quantity = parent.quantity
|
|
|
|
ratio_parent = self.evaluate(parent_quantity or '1.0', values, design) or 1.0
|
|
return parent.get_ratio_for_prices(values,
|
|
(ratio or 1.0) * ratio_parent, design)
|
|
|
|
def create_design_line(self, quantity, uom, unit_price, quote):
|
|
DesignLine = Pool().get('configurator.design.line')
|
|
Uom = Pool().get('product.uom')
|
|
|
|
quantity = Uom.compute_qty(self.uom, quantity, uom, round=False)
|
|
dl = DesignLine()
|
|
dl.quotation = quote
|
|
dl.quantity = quantity
|
|
dl.uom = uom
|
|
dl.unit_price = unit_price
|
|
return dl
|
|
|
|
def get_quotation_categories(self):
|
|
categories = []
|
|
if not self.childs:
|
|
if self.quotation_category:
|
|
return [self.quotation_category]
|
|
return []
|
|
|
|
for child in self.childs:
|
|
if child.quotation_category:
|
|
categories += [child.quotation_category]
|
|
categories += child.get_quotation_categories()
|
|
return categories
|
|
|
|
|
|
class CreatedObject(ModelSQL, ModelView):
|
|
""" Created Object """
|
|
__name__ = 'configurator.object'
|
|
|
|
design = fields.Many2One('configurator.design', 'Design', required=True,
|
|
ondelete='CASCADE')
|
|
object = fields.Reference('Object', selection='get_object', required=True)
|
|
|
|
@classmethod
|
|
def _get_created_object_type(cls):
|
|
"""Return list of Model names for object Reference"""
|
|
return [
|
|
'production.bom',
|
|
'production.bom.input',
|
|
'production.bom.output',
|
|
'product.product',
|
|
'production.route',
|
|
'product.product-production.bom',
|
|
]
|
|
|
|
@classmethod
|
|
def get_object(cls):
|
|
IrModel = Pool().get('ir.model')
|
|
models = cls._get_created_object_type()
|
|
models = IrModel.search([
|
|
('model', 'in', models),
|
|
])
|
|
return [(None, '')] + [(m.model, m.name) for m in models]
|
|
|
|
def get_rec_name(self, name):
|
|
return self.object and self.object.rec_name
|
|
|
|
|
|
READONLY_STATE = {
|
|
'readonly': (Eval('state') != 'draft'),
|
|
}
|
|
|
|
STATES = [('draft', 'draft'), ('done', 'Done'), ('cancel', 'Cancel')]
|
|
|
|
|
|
class Design(Workflow, ModelSQL, ModelView):
|
|
'Design'
|
|
__name__ = 'configurator.design'
|
|
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
states={
|
|
'readonly': (Eval('state') != 'draft'),
|
|
},
|
|
domain=[
|
|
('id', If(Eval('context', {}).contains('company'), '=', '!='),
|
|
Eval('context', {}).get('company', -1)),
|
|
],
|
|
depends=['state'], select=True)
|
|
code = fields.Char('Code', states=READONLY_STATE, depends=['state'])
|
|
name = fields.Char('Name', states=READONLY_STATE, depends=['state'],
|
|
translate=True)
|
|
manual_code = fields.Char('Manual Code',
|
|
states={
|
|
'readonly': ~Eval('attributes', [0]) & READONLY_STATE,
|
|
},
|
|
depends=['state', 'attributes'])
|
|
manual_name = fields.Char('Manual Name',
|
|
states={
|
|
'readonly': ~Eval('attributes', [0]) & READONLY_STATE,
|
|
},
|
|
depends=['state', 'attributes'], translate=True)
|
|
party = fields.Many2One('party.party', 'Party', states=READONLY_STATE,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
}, depends=['state', 'company'])
|
|
template = fields.Many2One('configurator.property', 'Template',
|
|
domain=[('template', '=', True)],
|
|
states={
|
|
'readonly': Eval('attributes', [0]) | (Eval('state') != 'draft'),
|
|
}, depends=['state', 'template', 'attributes'])
|
|
design_date = fields.Date('Design Date', states=READONLY_STATE,
|
|
depends=['state'])
|
|
currency = fields.Many2One('currency.currency', 'Currency',
|
|
states=READONLY_STATE, depends=['state'])
|
|
attributes = fields.One2Many('configurator.design.attribute', 'design',
|
|
'Attributes', states=READONLY_STATE, depends=['state'])
|
|
prices = fields.One2Many('configurator.quotation.line', 'design',
|
|
'Quotations', states=READONLY_STATE, depends=['state'])
|
|
objects = fields.One2Many('configurator.object', 'design', 'Objects',
|
|
readonly=True)
|
|
state = fields.Selection(STATES, 'State', readonly=True, required=True)
|
|
product = fields.Many2One('product.product', 'Product Designed',
|
|
readonly=True, context={
|
|
'company': Eval('company', -1),
|
|
}, depends=['company'])
|
|
suppliers = fields.One2Many('configurator.quotation.supplier', 'design',
|
|
'Suppliers', order=[('category', 'ASC')])
|
|
quotation_uom = fields.Many2One('product.uom', 'Quotation Uom',
|
|
states={
|
|
'readonly': Bool(Eval('prices', [0])),
|
|
'required': True,
|
|
},
|
|
domain=[
|
|
If(Bool(Eval('product_uom_category')),
|
|
('category', '=', Eval('product_uom_category')),
|
|
('category', '!=', -1)),
|
|
],
|
|
depends=['prices', 'product_uom_category', 'state'])
|
|
sale_uom = fields.Many2One('product.uom', 'Sale Uom',
|
|
states={
|
|
'readonly': Bool(Eval('prices', [0])),
|
|
'required': True,
|
|
},
|
|
domain=[
|
|
If(Bool(Eval('product_uom_category')),
|
|
('category', '=', Eval('product_uom_category')),
|
|
('category', '!=', -1)),
|
|
],
|
|
depends=['prices', 'product_uom_category', 'state'])
|
|
product_exists = fields.Function(fields.Many2One('product.product',
|
|
'Searched Product', context={
|
|
'company': Eval('company', -1),
|
|
}, depends=['company']), 'get_product_exist')
|
|
quotation_date = fields.Date('Quotation Date', readonly=True)
|
|
quoted_by = employee_field("Quoted By")
|
|
process_date = fields.Date('Quotation Date', readonly=True)
|
|
process_by = employee_field("Processed By")
|
|
product_uom_category = fields.Function(
|
|
fields.Many2One('product.uom.category', 'Product Uom Category'),
|
|
'on_change_with_product_uom_category')
|
|
product_codes = fields.Text('Product Codes', readonly=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Design, cls).__setup__()
|
|
cls._transitions |= set((
|
|
('draft', 'done'),
|
|
('draft', 'cancel'),
|
|
))
|
|
cls._buttons.update({
|
|
'cancel': {
|
|
'invisible': ~Eval('state').in_(['draft']),
|
|
'depends': ['state'],
|
|
},
|
|
'update': {
|
|
'invisible': (~Eval('state').in_(['draft'])
|
|
| Eval('attributes', [-1])),
|
|
'depends': ['state', 'attributes'],
|
|
},
|
|
'process': {
|
|
'invisible': Eval('state').in_(['done', 'cancel']),
|
|
'depends': ['state'],
|
|
},
|
|
'create_prices': {
|
|
'invisible': ~Eval('state').in_(['draft']),
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
|
|
@staticmethod
|
|
def default_currency():
|
|
Company = Pool().get('company.company')
|
|
if Transaction().context.get('company'):
|
|
company = Company(Transaction().context['company'])
|
|
return company.currency.id
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@staticmethod
|
|
def default_design_date():
|
|
Date = Pool().get('ir.date')
|
|
return Date.today()
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@fields.depends('template', 'code', 'product_exists', methods=[
|
|
'design_full_dict', 'get_product_exist'])
|
|
def on_change_manual_code(self):
|
|
if not self.template:
|
|
return
|
|
custom_locals = self.design_full_dict()
|
|
self.code = self.template.render_expression_record(
|
|
self.template.code_jinja and self.template.code_jinja.full_content
|
|
or '', custom_locals) or self.code
|
|
self.product_exists = self.get_product_exist()
|
|
|
|
@fields.depends('template', 'name', methods=['design_full_dict'])
|
|
def on_change_manual_name(self):
|
|
if not self.template:
|
|
return
|
|
custom_locals = self.design_full_dict()
|
|
self.name = self.template.render_expression_record(
|
|
self.template.name_jinja and self.template.name_jinja.full_content
|
|
or '', custom_locals)
|
|
|
|
@fields.depends('template')
|
|
def on_change_with_product_uom_category(self, name=None):
|
|
if self.template:
|
|
return self.template.product_template.default_uom_category.id
|
|
|
|
@fields.depends('template')
|
|
def on_change_template(self):
|
|
if not self.template:
|
|
return
|
|
template = self.template.product_template
|
|
self.quotation_uom = (template.quotation_uom
|
|
and template.quotation_uom.id or template.default_uom.id)
|
|
self.sale_uom = self.template.product_template.sale_uom.id
|
|
|
|
@classmethod
|
|
def copy(cls, designs, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
|
|
default.setdefault('design_date', None)
|
|
default.setdefault('process_by', None)
|
|
default.setdefault('process_date', None)
|
|
default.setdefault('quoted_by', None)
|
|
default.setdefault('quotion_date', None)
|
|
default.setdefault('objects', None)
|
|
default.setdefault('product', None)
|
|
default.setdefault('product_codes', None)
|
|
default.setdefault('purchase_uom_category', None)
|
|
return super(Design, cls).copy(designs, default=default)
|
|
|
|
def get_product_exist(self, name=None):
|
|
if self.product:
|
|
return self.product.id
|
|
Product = Pool().get('product.product')
|
|
with Transaction().set_context(active_test=False):
|
|
products = Product.search([
|
|
('code', '=', self.code)], limit=1)
|
|
if not products:
|
|
return None
|
|
product, = products
|
|
return product.id
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancel')
|
|
def cancel(cls, designs):
|
|
pass
|
|
|
|
def as_dict(self):
|
|
Function = Pool().get('configurator.property')
|
|
functions = Function.search([('type', '=', 'function'),
|
|
('parent', 'child_of', [self.template.id]) ])
|
|
|
|
res = {}
|
|
for attribute in self.attributes:
|
|
parent = attribute.property.get_parent()
|
|
if parent not in res:
|
|
res[parent] = {}
|
|
res[parent][attribute.property] = attribute
|
|
|
|
functions.sort(key=lambda x: (
|
|
str(x.parent and x.parent.sequence or 0).zfill(5) +
|
|
str(x.sequence).zfill(6)))
|
|
|
|
for function_ in functions:
|
|
parent = function_.get_parent()
|
|
if res.get(parent):
|
|
res[parent][function_] = function_.evaluate(function_.quantity,
|
|
res[parent], self)
|
|
return res
|
|
|
|
def create_object(self, object):
|
|
pool = Pool()
|
|
CreatedObject = pool.get('configurator.object')
|
|
created = CreatedObject()
|
|
created.object = object
|
|
created.design = self
|
|
return created
|
|
|
|
def get_attributes(self):
|
|
values = {}
|
|
if not self.template:
|
|
return []
|
|
for attribute in self.attributes:
|
|
values[attribute.property.code] = attribute
|
|
return self.template.compute_attributes(self, values)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def update(cls, designs):
|
|
QuotationSupplier = Pool().get(
|
|
'configurator.quotation.supplier')
|
|
for design in designs:
|
|
design.attributes = design.get_attributes()
|
|
categories = design.template.get_quotation_categories()
|
|
suppliers = []
|
|
for category in set(categories):
|
|
q = QuotationSupplier(category=category)
|
|
q.supplier = category.party
|
|
suppliers.append(q)
|
|
design.suppliers = suppliers
|
|
|
|
cls.save(designs)
|
|
|
|
def custom_operations(self, res):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def create_prices(cls, designs):
|
|
pool = Pool()
|
|
User = pool.get('res.user')
|
|
DesignLine = pool.get('configurator.design.line')
|
|
Lang = pool.get('ir.lang')
|
|
remove_lines = []
|
|
BomInput = pool.get('production.bom.input')
|
|
Product = pool.get('product.product')
|
|
Uom = pool.get('product.uom')
|
|
Date = Pool().get('ir.date')
|
|
to_save = []
|
|
|
|
context = Transaction().context.copy()
|
|
context['prices'] = True
|
|
|
|
for design in designs:
|
|
prices = {}
|
|
remove_lines = []
|
|
for price in design.prices:
|
|
remove_lines += price.prices
|
|
product_codes = []
|
|
design.quotation_date = Date.today()
|
|
design.quoted_by = User(Transaction().user).employee
|
|
design.product_codes = ''
|
|
values = design.as_dict()
|
|
if not values:
|
|
continue
|
|
|
|
with Transaction().set_context(context):
|
|
res = design.template.create_prices(design, values)
|
|
|
|
design.custom_operations(res)
|
|
suppliers = dict((x.category, x.supplier)
|
|
for x in design.suppliers)
|
|
product_codes = []
|
|
for quote in design.prices:
|
|
quote_quantity = Uom.compute_qty(design.quotation_uom,
|
|
quote.quantity, design.template.uom, round=False)
|
|
bom_quantity = Uom.compute_qty(design.template.uom,
|
|
eval(design.template.quantity), design.template.uom,
|
|
round=False)
|
|
quote_ratio = quote_quantity / bom_quantity
|
|
for prop, v in res.items():
|
|
v = v[0]
|
|
key = (prop.price_category or prop.id, quote)
|
|
dl = prices.get(key)
|
|
quantity = 0
|
|
cost_price = None
|
|
product = None
|
|
if prop.type == 'bom':
|
|
for output in v.outputs:
|
|
code = '%s - %s' % (
|
|
output.product.template.code,
|
|
output.product.template.name)
|
|
if code not in product_codes:
|
|
product_codes += [code]
|
|
|
|
if prop.type == 'purchase_product':
|
|
code = '%s - %s' % (
|
|
v.product.template.code,
|
|
v.product.template.name)
|
|
if code not in product_codes:
|
|
product_codes += [code]
|
|
|
|
if prop.type not in ('product', 'match'):
|
|
continue
|
|
if prop.type in ('product', 'match'):
|
|
if isinstance(v, BomInput):
|
|
quantity = (v.quantity or 0) * quote_ratio
|
|
product = v.product
|
|
elif isinstance(v, Product):
|
|
parent = prop.get_parent()
|
|
quantity = prop.evaluate(prop.quantity,
|
|
design.as_dict()[parent], design)
|
|
quantity = quantity * quote_ratio
|
|
product = v
|
|
# if prop.type == 'operation':
|
|
# quantity = prop.evaluate(prop.quantity,
|
|
# design.as_dict())
|
|
# quantity = quantity * quote.quantity
|
|
# quantity = v.compute_time(quantity, v.time_uom)
|
|
# cost_price = prop.work_center_category.cost_price
|
|
dl = prices.get(key)
|
|
if quantity == 0:
|
|
continue
|
|
if not dl:
|
|
supplier = None
|
|
if prop.quotation_category:
|
|
supplier = suppliers.get(prop.quotation_category)
|
|
parent = prop.get_parent()
|
|
with Transaction().set_context(context):
|
|
qty_ratio = prop.get_ratio_for_prices(
|
|
values.get(parent, {}), 1, design)
|
|
if not product:
|
|
continue
|
|
cost_price = quote.get_unit_price(product,
|
|
quantity * qty_ratio, prop.uom, supplier)
|
|
dl = prop.create_design_line(quantity * qty_ratio,
|
|
prop.uom, cost_price, quote)
|
|
dl.qty_ratio = qty_ratio
|
|
dl.debug_quantity = quantity
|
|
if not prop.price_category:
|
|
dl.property = prop
|
|
prices[key] = dl
|
|
else:
|
|
parent = prop.get_parent()
|
|
with Transaction().set_context(context):
|
|
qty_ratio = prop.get_ratio_for_prices(
|
|
values.get(parent, {}), 1, design)
|
|
cost_price = (Decimal(qty_ratio * quantity) / (
|
|
dl.unit_price
|
|
+ Decimal(qty_ratio * quantity) * cost_price))
|
|
cost_price = Decimal(cost_price).quantize(Decimal(
|
|
str(10.0 ** -price_digits[1])))
|
|
dl.quantity += quantity * qty_ratio
|
|
dl.debug_quantity = quantity
|
|
dl.unit_price = cost_price
|
|
|
|
DesignLine.delete(remove_lines)
|
|
to_save = prices.values()
|
|
DesignLine.save(to_save)
|
|
|
|
custom_locals = design.design_full_dict()
|
|
code = design.render_field(design.template, 'code_jinja',
|
|
custom_locals)
|
|
design.code = code
|
|
design.product_codes = "\n".join(product_codes)
|
|
design.save()
|
|
|
|
langs = Lang.search([('active', '=', True),
|
|
('translatable', '=', True)])
|
|
for lang in langs:
|
|
design.render_design_fields(lang)
|
|
|
|
def design_full_dict(self):
|
|
record = self.as_dict()
|
|
custom_locals = OrderedDict()
|
|
all = {}
|
|
Property = Pool().get('configurator.property')
|
|
properties = Property.search([
|
|
('parent', 'child_of', [self.template.id])], order=[('sequence','ASC')])
|
|
custom_locals['design'] = self
|
|
properties.sort(key=lambda x: (
|
|
str(x.parent and x.parent.sequence or 0).zfill(5) +
|
|
str(x.sequence).zfill(6)))
|
|
|
|
for property in properties:
|
|
custom_locals[property.get_full_code()] = property
|
|
parent = property.get_parent()
|
|
if parent:
|
|
if parent.code not in all:
|
|
all[parent.code] = {}
|
|
all[parent.code][property.code] = property
|
|
else:
|
|
all[property.code] = property
|
|
|
|
for parent_prop, attributes in record.items():
|
|
for prop, attr in attributes.items():
|
|
custom_locals[prop.get_full_code()] = attr
|
|
parent = prop.get_parent()
|
|
if parent:
|
|
if parent.code not in all:
|
|
all[parent.code] = {}
|
|
all[parent.code][prop.code] = attr
|
|
else:
|
|
all[prop.code] = attr
|
|
|
|
custom_locals['tree'] = all
|
|
return custom_locals
|
|
|
|
def get_design_render_fields(self):
|
|
return [('name_jinja', 'name')]
|
|
|
|
def render_design_fields(self, lang):
|
|
pool = Pool()
|
|
Design = pool.get('configurator.design')
|
|
JinjaField = pool.get('configurator.jinja_template')
|
|
design_fields = self.get_design_render_fields()
|
|
|
|
with Transaction().set_context(language=lang.code):
|
|
design = Design(self.id)
|
|
custom_locals = design.design_full_dict()
|
|
ptemplate = design.template
|
|
for tmpl_field, field in design_fields:
|
|
f = getattr(ptemplate, tmpl_field)
|
|
if not f or f is None or f == '':
|
|
continue
|
|
if isinstance(f, JinjaField):
|
|
f = f.full_content
|
|
val = ptemplate.render_expression_record(f, custom_locals)
|
|
val = val.replace('\n', '').replace('\t', '')
|
|
setattr(design, field, val)
|
|
design.save()
|
|
|
|
|
|
def render_product_fields(self, lang, product, pproperty=None):
|
|
pool = Pool()
|
|
Design = pool.get('configurator.design')
|
|
Product = pool.get('product.product')
|
|
product_fields = self.get_product_render_fields()
|
|
|
|
with Transaction().set_context(language=lang.code):
|
|
design = Design(self.id)
|
|
product = Product(product.id)
|
|
custom_locals = design.design_full_dict()
|
|
template = product.template
|
|
property = pproperty or design.template
|
|
for tmpl_field, field in product_fields:
|
|
val = ''
|
|
if tmpl_field == 'name' and pproperty:
|
|
parent = pproperty.get_parent()
|
|
if parent and parent != pproperty:
|
|
val = (self.render_field(parent, tmpl_field,
|
|
custom_locals) or '').strip()
|
|
|
|
val = val + (self.render_field(property, tmpl_field,
|
|
custom_locals) or '').strip()
|
|
if val:
|
|
if tmpl_field != 'name':
|
|
setattr(product, field, val)
|
|
else:
|
|
setattr(template, field, val)
|
|
template.save()
|
|
product.save()
|
|
|
|
def render_field(self, property, field, custom_locals):
|
|
pool = Pool()
|
|
JinjaField = pool.get('configurator.jinja_template')
|
|
f = getattr(property, field)
|
|
if not f:
|
|
return ''
|
|
if isinstance(f, JinjaField):
|
|
f = f.full_content
|
|
|
|
res = property.render_expression_record(f, custom_locals)
|
|
return res or ''
|
|
|
|
def get_product_render_fields(self):
|
|
return [('name_jinja', 'name')]
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
def process(cls, designs):
|
|
pool = Pool()
|
|
CreatedObject = pool.get('configurator.object')
|
|
Lang = pool.get('ir.lang')
|
|
langs = Lang.search([('active', '=', True),
|
|
('translatable', '=', True)])
|
|
to_delete = []
|
|
for design in designs:
|
|
custom_locals = design.design_full_dict()
|
|
design.code = design.render_field(design.template, 'code_jinja',
|
|
custom_locals)
|
|
to_delete += [x for x in design.objects]
|
|
res = design.template.create_prices(design, design.as_dict())
|
|
for prop, objs in res.items():
|
|
obj, additional = objs
|
|
if prop.type == 'bom':
|
|
obj.save()
|
|
ref = design.create_object(obj)
|
|
ref.save()
|
|
for input_ in obj.inputs:
|
|
ref = design.create_object(input_)
|
|
ref.save()
|
|
for output_ in obj.outputs:
|
|
ref = design.create_object(output_)
|
|
ref.save()
|
|
product = output_.product
|
|
if prop.parent is None:
|
|
design.product = product
|
|
design.save()
|
|
for lang in langs:
|
|
design.render_product_fields(lang, product, prop)
|
|
if prop.type == 'purchase_product':
|
|
product = obj.product
|
|
product.save()
|
|
if prop.parent is None:
|
|
design.product = product
|
|
design.save()
|
|
for lang in langs:
|
|
design.render_product_fields(lang, product, prop)
|
|
|
|
for obj in additional:
|
|
obj.save()
|
|
ref = design.create_object(obj)
|
|
if ref:
|
|
ref.save()
|
|
|
|
product = design.product
|
|
template = product.template
|
|
template.code = design.code
|
|
template.product_customer_only = True
|
|
template.save()
|
|
|
|
create_product_customer = True
|
|
if product.product_customers:
|
|
parties = [x.party for x in product.product_customers]
|
|
if design.party in parties:
|
|
create_product_customer = False
|
|
|
|
if create_product_customer:
|
|
ProductCustomer = pool.get('sale.product_customer')
|
|
product_customer = ProductCustomer()
|
|
product_customer.product = product
|
|
product_customer.on_change_product()
|
|
product_customer.party = design.party
|
|
product_customer.name = design.name
|
|
product_customer.code = design.code
|
|
product_customer.save()
|
|
|
|
CreatedObject.delete(to_delete)
|
|
|
|
|
|
class QuotationSupplier(ModelSQL, ModelView):
|
|
""" Quotation Supplier """
|
|
__name__ = 'configurator.quotation.supplier'
|
|
|
|
design = fields.Many2One('configurator.design', 'Design')
|
|
category = fields.Many2One('configurator.property.quotation_category',
|
|
'Category')
|
|
supplier = fields.Many2One('party.party', 'Supplier')
|
|
|
|
|
|
class QuotationLine(ModelSQL, ModelView):
|
|
""" Quotation Line """
|
|
__name__ = 'configurator.quotation.line'
|
|
|
|
design = fields.Many2One('configurator.design', 'Design', required=True,
|
|
ondelete='CASCADE')
|
|
quantity = fields.Float('Quantity', digits=(16, 4),
|
|
states={
|
|
'required': True,
|
|
'readonly': Bool(Eval('unit_price'))
|
|
}, depends=['unit_price'])
|
|
prices = fields.One2Many('configurator.design.line', 'quotation', 'Prices')
|
|
global_margin = fields.Float('Global Margin', digits=(16, 4),
|
|
states={'readonly': Eval('design_state') != 'draft'},
|
|
depends=['design_state'])
|
|
cost_price = fields.Function(fields.Numeric('Cost Price',
|
|
digits=price_digits), 'get_prices')
|
|
list_price = fields.Function(fields.Numeric('Total Price',
|
|
digits=price_digits), 'get_prices')
|
|
manual_list_price = fields.Numeric('Manual List Price',
|
|
digits=price_digits, states={'readonly': Eval('design_state') != 'draft'},
|
|
depends=['design_state'])
|
|
margin = fields.Function(fields.Numeric('Margin', digits=(16, 4)),
|
|
'get_prices')
|
|
margin_w_manual = fields.Function(fields.Numeric('Margin', digits=(16, 4)),
|
|
'get_prices')
|
|
unit_price = fields.Function(fields.Numeric('Unit Price',
|
|
digits=price_digits), 'get_prices')
|
|
product_uom_category = fields.Function(
|
|
fields.Many2One('product.uom.category', 'Product Uom Category'),
|
|
'get_product_uom_category')
|
|
design_state = fields.Function(fields.Selection(STATES, 'Design State'),
|
|
'on_change_with_design_state')
|
|
material_cost_price = fields.Function(fields.Numeric('Cost Material',
|
|
digits=(16, 4)), 'get_prices')
|
|
margin_material = fields.Function(fields.Numeric('Margin Material',
|
|
digits=(16, 4)), 'get_prices')
|
|
cost_price_no_manual = fields.Function(fields.Numeric(
|
|
'Cost Price No Manual', digits=price_digits), 'get_prices')
|
|
unit_price_per_mil = fields.Function(fields.Numeric('Cost/Qty',
|
|
digits=price_digits), 'get_prices')
|
|
unit_price_no_manual_per_mil = fields.Function(fields.Numeric(
|
|
'Cost(NoM)/Qty', digits=price_digits), 'get_prices')
|
|
manual_list_price_on_sale_uom = fields.Function(
|
|
fields.Numeric('Manual List Price On Sale Uome',
|
|
digits=price_digits), 'get_prices')
|
|
|
|
def get_rec_name(self, name):
|
|
return '%s - %s' % (str(self.quantity),
|
|
self.design.name)
|
|
|
|
@fields.depends('design', '_parent_design.state')
|
|
def on_change_with_design_state(self, name=None):
|
|
if self.design:
|
|
return self.design.state
|
|
|
|
def get_product_uom_category(self, name=None):
|
|
if self.design and self.design.template:
|
|
return self.design.template.uom.category.id
|
|
|
|
def _get_context_purchase_price(self, uom=None):
|
|
context = {}
|
|
context['currency'] = self.design.currency and self.design.currency.id
|
|
context['purchase_date'] = self.design.design_date
|
|
if uom:
|
|
context['uom'] = uom and uom.id
|
|
return context
|
|
|
|
def get_unit_price(self, product, quantity, uom, supplier):
|
|
pool = Pool()
|
|
Product = pool.get('product.product')
|
|
|
|
context = self._get_context_purchase_price()
|
|
context.update(product.template.get_purchase_context())
|
|
if supplier:
|
|
context['supplier'] = supplier.id
|
|
elif context.get('supplier'):
|
|
del context['supplier']
|
|
|
|
context[uom] = uom and uom.id
|
|
with Transaction().set_context(context):
|
|
unit_price = Product.get_purchase_price(
|
|
[product], abs(quantity or 0))[product.id]
|
|
if unit_price:
|
|
unit_price = unit_price.quantize(
|
|
Decimal(1) / 10 ** price_digits[1])
|
|
return unit_price
|
|
|
|
@classmethod
|
|
def get_prices(cls, quotations, names):
|
|
pool = Pool()
|
|
Uom = pool.get('product.uom')
|
|
res = {}
|
|
quantize = Decimal(str(10.0 ** -price_digits[1]))
|
|
for name in {'cost_price', 'list_price', 'margin', 'unit_price',
|
|
'material_cost_price', 'margin_material',
|
|
'cost_price_no_manual', 'margin_w_manual',
|
|
'manual_list_price_on_sale_uom',
|
|
'unit_price_per_mil', 'unit_price_no_manual_per_mil'}:
|
|
res[name] = {}.fromkeys([x.id for x in quotations], Decimal(0))
|
|
|
|
for quote in quotations:
|
|
cost_price = 0
|
|
list_price = 0
|
|
cost_price_noman = 0
|
|
material_cost_price = 0
|
|
quote_quantity = Uom.compute_qty(quote.design.quotation_uom,
|
|
quote.quantity, quote.design.template.uom, round=False)
|
|
unit_price_uom = quote.design.template.product_template.default_uom
|
|
quote_quantity2 = Uom.compute_qty(quote.design.template.uom,
|
|
quote_quantity, unit_price_uom, round=True)
|
|
|
|
for line in quote.prices:
|
|
cost_price += (Decimal(line.quantity or 0)
|
|
* (line.manual_unit_price or line.unit_price))
|
|
cost_price_noman += Decimal(line.quantity) * line.unit_price
|
|
list_price += line.amount
|
|
material_cost_price += Decimal(line.property.quotation_category
|
|
and line.property.quotation_category.type_ == 'goods'
|
|
and line.quantity or 0) * (line.manual_unit_price
|
|
or line.unit_price)
|
|
list_price = (list_price
|
|
* Decimal(1 + ((quote.global_margin or 0)) or 0
|
|
)).quantize(quantize)
|
|
|
|
unit_price = 0
|
|
if quote_quantity2:
|
|
unit_price = Decimal(float(list_price) / quote_quantity2
|
|
).quantize(quantize)
|
|
|
|
res['list_price'][quote.id] = Decimal(quote_quantity) * (
|
|
quote.manual_list_price or unit_price)
|
|
res['cost_price'][quote.id] = cost_price
|
|
res['cost_price_no_manual'][quote.id] = cost_price_noman
|
|
res['margin'][quote.id] = (res['list_price'][quote.id]
|
|
- cost_price_noman)
|
|
res['unit_price'][quote.id] = unit_price
|
|
res['material_cost_price'][quote.id] = material_cost_price
|
|
res['margin_material'][quote.id] = (res['list_price'][quote.id]
|
|
- material_cost_price)
|
|
res['margin_w_manual'][quote.id] = (res['list_price'][quote.id]
|
|
- cost_price)
|
|
res['unit_price_per_mil'][quote.id] = (
|
|
res['cost_price'][quote.id] / Decimal(quote_quantity2))
|
|
res['unit_price_no_manual_per_mil'][quote.id] = (
|
|
res['cost_price_no_manual'][quote.id] / Decimal(quote_quantity2))
|
|
res['manual_list_price_on_sale_uom'][quote.id] = (
|
|
Uom.compute_price(unit_price_uom, quote.manual_list_price,
|
|
quote.design.sale_uom))
|
|
return res
|
|
|
|
|
|
class DesignLine(sequence_ordered(), ModelSQL, ModelView):
|
|
'Design Line'
|
|
__name__ = 'configurator.design.line'
|
|
quotation = fields.Many2One('configurator.quotation.line', 'Quotation',
|
|
readonly=True, required=True,
|
|
ondelete='CASCADE')
|
|
category = fields.Many2One('configurator.property.price_category',
|
|
'Category', readonly=True)
|
|
property = fields.Many2One('configurator.property', 'Property',
|
|
readonly=True)
|
|
quantity = fields.Float('Quantity', readonly=True)
|
|
uom = fields.Many2One('product.uom', 'UoM', readonly=True)
|
|
unit_price = fields.Numeric('Unit Price', digits=price_digits,
|
|
readonly=True)
|
|
manual_unit_price = fields.Numeric('Manual Unit Price',
|
|
digits=price_digits)
|
|
margin = fields.Float('Margin', digits=(16, 4), )
|
|
amount = fields.Function(fields.Numeric('Amount', digits=price_digits),
|
|
'on_change_with_amount')
|
|
currency = fields.Function(fields.Many2One('currency.currency',
|
|
'Currency'),
|
|
'get_currency')
|
|
qty_ratio = fields.Float('Quantity Ratio', readonly=True)
|
|
debug_quantity = fields.Float('Debug Qty', readonly=True) # TODO: remove
|
|
debug_amount = fields.Function(fields.Numeric('Debug Amount',
|
|
digits=price_digits), 'on_change_with_debug_amount') # TODO: remove
|
|
|
|
@fields.depends('quantity', 'unit_price', 'manual_unit_price')
|
|
def on_change_with_amount(self, name=None):
|
|
if not self.quantity or not (self.unit_price
|
|
or self.manual_unit_price):
|
|
return _ZERO
|
|
price = self.manual_unit_price or self.unit_price
|
|
return Decimal(str(self.quantity)) * price * Decimal(
|
|
self.margin and 1 + self.margin or 1)
|
|
|
|
@fields.depends('debug_quantity', 'unit_price', 'manual_unit_price')
|
|
def on_change_with_debug_amount(self, name=None):
|
|
if not self.debug_quantity or not (self.unit_price
|
|
or self.manual_unit_price):
|
|
return _ZERO
|
|
price = self.manual_unit_price or self.unit_price
|
|
return Decimal(str(self.debug_quantity)) * price * Decimal(
|
|
self.margin and 1 + self.margin or 1)
|
|
|
|
def get_currency(self, name=None):
|
|
return self.quotation.design.currency
|
|
|
|
|
|
class DesignAttribute(sequence_ordered(), ModelSQL, ModelView):
|
|
'Design Attribute'
|
|
__name__ = 'configurator.design.attribute'
|
|
design = fields.Many2One('configurator.design', 'Design', required=True,
|
|
ondelete='CASCADE')
|
|
property = fields.Many2One('configurator.property',
|
|
'Property', required=True, readonly=True)
|
|
property_type = fields.Function(fields.Selection(TYPE, 'Type'),
|
|
'on_change_with_property_type')
|
|
use_property = fields.Boolean('Add',
|
|
states={
|
|
'invisible': Eval('property_type') != 'bom',
|
|
},
|
|
)
|
|
property_options = fields.Function(fields.Many2Many(
|
|
'configurator.property', None, None, 'Options'),
|
|
'on_change_with_property_options')
|
|
option = fields.Many2One('configurator.property', 'Option', domain=[
|
|
('id', 'in', Eval('property_options')),
|
|
], states={
|
|
'invisible': Eval('property_type') != 'options',
|
|
'readonly': Eval('design_state') != 'draft',
|
|
}, depends=['property_type', 'property_options', 'design_state'])
|
|
number = fields.Float('Number', states={
|
|
'invisible': Eval('property_type') != 'number',
|
|
'readonly': Eval('design_state') != 'draft',
|
|
}, depends=['property_type', 'design_state'])
|
|
text = fields.Char('Text', states={
|
|
'invisible': Eval('property_type') != 'text',
|
|
'readonly': Eval('design_state') != 'draft',
|
|
}, depends=['property_type', 'design_state'])
|
|
design_state = fields.Function(fields.Selection(STATES, 'Design State'),
|
|
'on_change_with_design_state')
|
|
|
|
@fields.depends('design', '_parent_design.state')
|
|
def on_change_with_design_state(self, name=None):
|
|
if self.design:
|
|
return self.design.state
|
|
|
|
@fields.depends('property')
|
|
def on_change_with_property_type(self, name=None):
|
|
if not self.property:
|
|
return
|
|
return self.property.type
|
|
|
|
@fields.depends('property')
|
|
def on_change_with_property_options(self, name=None):
|
|
if not self.property:
|
|
return []
|
|
res = [x.id for x in self.property.childs]
|
|
return res
|