2015-01-19 19:16:09 +01:00
|
|
|
# The COPYRIGHT file at the top level of this repository contains the full
|
|
|
|
# copyright notices and license terms.
|
2013-10-17 10:59:15 +02:00
|
|
|
from decimal import Decimal
|
2015-01-19 19:16:09 +01:00
|
|
|
|
|
|
|
from trytond.config import config
|
2014-04-09 19:59:12 +02:00
|
|
|
from trytond.model import ModelSQL, ModelView, fields
|
2013-10-17 10:59:15 +02:00
|
|
|
from trytond.pool import Pool
|
2014-02-18 19:04:03 +01:00
|
|
|
from trytond.pyson import Eval, Bool, If
|
2014-02-03 18:06:06 +01:00
|
|
|
from trytond.transaction import Transaction
|
2014-03-25 23:43:47 +01:00
|
|
|
from trytond.wizard import Wizard, StateView, StateAction, Button
|
2015-01-19 19:16:09 +01:00
|
|
|
|
2014-11-05 09:28:18 +01:00
|
|
|
DIGITS = int(config.get('digits', 'unit_price_digits', 4))
|
2013-10-17 10:59:15 +02:00
|
|
|
|
2014-03-25 23:43:47 +01:00
|
|
|
__all__ = ['PlanCostType', 'Plan', 'PlanBOM', 'PlanProductLine', 'PlanCost',
|
|
|
|
'CreateBomStart', 'CreateBom']
|
2014-02-03 18:06:06 +01:00
|
|
|
|
|
|
|
|
|
|
|
class PlanCostType(ModelSQL, ModelView):
|
|
|
|
'Plan Cost Type'
|
|
|
|
__name__ = 'product.cost.plan.cost.type'
|
|
|
|
name = fields.Char('Name', required=True, translate=True)
|
2015-01-19 19:16:09 +01:00
|
|
|
system = fields.Boolean('System Managed', readonly=True)
|
|
|
|
plan_field_name = fields.Char('Plan Field Name', readonly=True)
|
2013-10-17 10:59:15 +02:00
|
|
|
|
|
|
|
|
2014-04-09 19:59:12 +02:00
|
|
|
class Plan(ModelSQL, ModelView):
|
2013-10-17 10:59:15 +02:00
|
|
|
'Product Cost Plan'
|
|
|
|
__name__ = 'product.cost.plan'
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2014-03-16 04:07:48 +01:00
|
|
|
number = fields.Char('Number', select=True, readonly=True)
|
2014-04-14 12:43:23 +02:00
|
|
|
name = fields.Char('Name', select=True)
|
2014-03-16 02:49:45 +01:00
|
|
|
active = fields.Boolean('Active')
|
2014-05-27 10:47:44 +02:00
|
|
|
product = fields.Many2One('product.product', 'Product')
|
2015-01-19 19:16:09 +01:00
|
|
|
product_uom_category = fields.Function(
|
|
|
|
fields.Many2One('product.uom.category', 'Product UoM Category'),
|
|
|
|
'on_change_with_product_uom_category')
|
2014-09-02 17:22:07 +02:00
|
|
|
quantity = fields.Float('Quantity', digits=(16, Eval('uom_digits', 2)),
|
|
|
|
required=True, depends=['uom_digits'])
|
|
|
|
uom = fields.Many2One('product.uom', 'UoM', required=True, domain=[
|
2015-01-19 19:16:09 +01:00
|
|
|
If(Bool(Eval('product')),
|
2014-03-18 16:24:50 +01:00
|
|
|
('category', '=', Eval('product_uom_category')),
|
2014-06-11 01:59:24 +02:00
|
|
|
('id', '!=', -1),
|
2014-03-18 16:24:50 +01:00
|
|
|
)],
|
|
|
|
states={
|
|
|
|
'readonly': Bool(Eval('product')),
|
2015-01-19 19:16:09 +01:00
|
|
|
}, depends=['product', 'product_uom_category'])
|
2014-09-02 17:22:07 +02:00
|
|
|
uom_digits = fields.Function(fields.Integer('UoM Digits'),
|
|
|
|
'on_change_with_uom_digits')
|
2014-05-27 10:47:44 +02:00
|
|
|
bom = fields.Many2One('production.bom', 'BOM',
|
2014-04-09 19:59:12 +02:00
|
|
|
depends=['product'], domain=[
|
2014-01-04 17:53:24 +01:00
|
|
|
('output_products', '=', Eval('product', 0)),
|
2014-01-19 20:49:59 +01:00
|
|
|
])
|
2014-05-27 10:47:44 +02:00
|
|
|
boms = fields.One2Many('product.cost.plan.bom_line', 'plan', 'BOMs')
|
2013-10-17 10:59:15 +02:00
|
|
|
products = fields.One2Many('product.cost.plan.product_line', 'plan',
|
2014-05-27 10:47:44 +02:00
|
|
|
'Products')
|
2014-03-24 18:48:34 +01:00
|
|
|
products_tree = fields.Function(
|
2014-06-05 14:07:29 +02:00
|
|
|
fields.One2Many('product.cost.plan.product_line', 'plan', 'Products',
|
|
|
|
domain=[
|
|
|
|
('parent', '=', None),
|
2014-07-01 14:47:53 +02:00
|
|
|
],
|
|
|
|
states={
|
|
|
|
'readonly': ~Bool(Eval('costs', [0])),
|
|
|
|
},
|
|
|
|
depends=['costs']),
|
2014-04-09 19:59:12 +02:00
|
|
|
'get_products_tree', setter='set_products_tree')
|
2015-01-19 19:16:09 +01:00
|
|
|
products_cost = fields.Function(fields.Numeric('Products Cost',
|
2014-05-27 10:47:44 +02:00
|
|
|
digits=(16, DIGITS)),
|
2015-01-19 19:16:09 +01:00
|
|
|
'get_products_cost')
|
2014-02-03 18:06:06 +01:00
|
|
|
costs = fields.One2Many('product.cost.plan.cost', 'plan', 'Costs')
|
2015-02-06 09:52:05 +01:00
|
|
|
product_cost_price = fields.Function(fields.Numeric('Product Cost Price',
|
|
|
|
digits=(16, DIGITS)),
|
|
|
|
'get_product_cost_price')
|
2014-03-16 02:42:22 +01:00
|
|
|
cost_price = fields.Function(fields.Numeric('Unit Cost Price',
|
2014-05-27 10:47:44 +02:00
|
|
|
digits=(16, DIGITS)),
|
2015-01-19 19:16:09 +01:00
|
|
|
'get_cost_price')
|
2014-03-18 16:45:07 +01:00
|
|
|
notes = fields.Text('Notes')
|
2013-10-17 10:59:15 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def __setup__(cls):
|
|
|
|
super(Plan, cls).__setup__()
|
|
|
|
cls._buttons.update({
|
2014-01-04 17:53:24 +01:00
|
|
|
'compute': {
|
2014-04-09 19:59:12 +02:00
|
|
|
'icon': 'tryton-spreadsheet',
|
2013-10-17 10:59:15 +02:00
|
|
|
},
|
2015-02-06 09:52:05 +01:00
|
|
|
'update_product_cost_price': {
|
2015-01-19 19:16:09 +01:00
|
|
|
'icon': 'tryton-refresh',
|
|
|
|
},
|
2013-10-17 10:59:15 +02:00
|
|
|
})
|
2014-04-09 02:00:05 +02:00
|
|
|
cls._error_messages.update({
|
2015-01-19 19:16:09 +01:00
|
|
|
'product_lines_will_be_removed': (
|
|
|
|
'It will remove the existing Product Lines in this plan.'),
|
2015-01-27 09:52:03 +01:00
|
|
|
'lacks_the_product': 'The Cost Plan "%s" lacks the product.',
|
2015-01-19 19:16:09 +01:00
|
|
|
'bom_already_exists': (
|
|
|
|
'A bom already exists for cost plan "%s".'),
|
2014-04-10 00:42:24 +02:00
|
|
|
'cannot_mix_input_uoms': ('Product "%(product)s" in Cost Plan '
|
|
|
|
'"%(plan)s" has different units of measure.'),
|
2015-01-19 19:16:09 +01:00
|
|
|
'product_already_has_bom': (
|
|
|
|
'Product "%s" already has a BOM assigned.'),
|
2014-04-09 02:00:05 +02:00
|
|
|
})
|
|
|
|
|
2014-03-16 02:49:45 +01:00
|
|
|
@staticmethod
|
|
|
|
def default_active():
|
|
|
|
return True
|
|
|
|
|
2013-10-17 10:59:15 +02:00
|
|
|
@staticmethod
|
|
|
|
def default_state():
|
|
|
|
return 'draft'
|
|
|
|
|
2014-05-13 18:12:37 +02:00
|
|
|
def get_rec_name(self, name):
|
|
|
|
res = '[%s]' % self.number
|
|
|
|
if self.name:
|
|
|
|
res += ' ' + self.name
|
2015-01-27 09:52:03 +01:00
|
|
|
elif self.product:
|
|
|
|
res += ' ' + self.product.rec_name
|
2014-05-13 18:12:37 +02:00
|
|
|
return res
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def search_rec_name(cls, name, clause):
|
|
|
|
return ['OR',
|
|
|
|
('number',) + tuple(clause[1:]),
|
|
|
|
('name',) + tuple(clause[1:]),
|
2015-01-27 09:52:03 +01:00
|
|
|
('product',) + tuple(clause[1:]),
|
2014-05-13 18:12:37 +02:00
|
|
|
]
|
|
|
|
|
2015-02-06 09:19:29 +01:00
|
|
|
@fields.depends('product', 'bom', 'boms', 'name')
|
2014-01-04 17:53:24 +01:00
|
|
|
def on_change_product(self):
|
2015-02-06 09:19:29 +01:00
|
|
|
res = {
|
|
|
|
'bom': None,
|
|
|
|
}
|
|
|
|
if not self.name:
|
|
|
|
res['name'] = self.product.rec_name
|
2014-01-04 17:53:24 +01:00
|
|
|
bom = self.on_change_with_bom()
|
|
|
|
self.bom = bom
|
|
|
|
res['boms'] = self.on_change_with_boms()
|
2014-03-18 16:24:50 +01:00
|
|
|
if self.product:
|
|
|
|
res['uom'] = self.product.default_uom.id
|
2014-01-04 17:53:24 +01:00
|
|
|
return res
|
|
|
|
|
2014-09-02 17:22:07 +02:00
|
|
|
@fields.depends('uom')
|
|
|
|
def on_change_with_uom_digits(self, name=None):
|
|
|
|
return self.uom.digits if self.uom else 2
|
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
@fields.depends('product', 'uom')
|
2014-08-28 14:48:18 +02:00
|
|
|
def on_change_with_product_uom_category(self, name=None):
|
|
|
|
if self.product:
|
|
|
|
return self.product.default_uom_category.id
|
2015-01-19 19:16:09 +01:00
|
|
|
if self.uom:
|
|
|
|
return self.uom.category.id
|
2014-08-28 14:48:18 +02:00
|
|
|
|
2014-05-27 10:47:44 +02:00
|
|
|
@fields.depends('product')
|
2013-10-17 10:59:15 +02:00
|
|
|
def on_change_with_bom(self):
|
|
|
|
BOM = Pool().get('production.bom')
|
2014-01-04 17:53:24 +01:00
|
|
|
if not self.product:
|
|
|
|
return
|
|
|
|
boms = BOM.search([('output_products', '=', self.product.id)])
|
2013-10-17 10:59:15 +02:00
|
|
|
if boms:
|
|
|
|
return boms[0].id
|
|
|
|
|
2014-05-27 10:47:44 +02:00
|
|
|
@fields.depends('bom', 'boms', 'product')
|
2013-10-17 10:59:15 +02:00
|
|
|
def on_change_with_boms(self):
|
2014-01-04 17:53:24 +01:00
|
|
|
boms = {
|
|
|
|
'remove': [x.id for x in self.boms],
|
|
|
|
'add': [],
|
|
|
|
}
|
|
|
|
if not self.bom:
|
|
|
|
return boms
|
|
|
|
|
|
|
|
def find_boms(inputs):
|
|
|
|
res = []
|
|
|
|
for input_ in inputs:
|
|
|
|
if input_.product.boms:
|
2014-03-18 16:40:53 +01:00
|
|
|
product_bom = input_.product.boms[0].bom
|
|
|
|
res.append((input_.product.id, product_bom.id))
|
|
|
|
res += find_boms(product_bom.inputs)
|
2014-01-04 17:53:24 +01:00
|
|
|
return res
|
|
|
|
|
|
|
|
products = set(find_boms(self.bom.inputs))
|
2014-06-03 09:54:45 +02:00
|
|
|
for index, (product_id, bom_id) in enumerate(products):
|
|
|
|
boms['add'].append((index, {
|
|
|
|
'product': product_id,
|
|
|
|
'bom': None,
|
|
|
|
}))
|
2014-01-04 17:53:24 +01:00
|
|
|
return boms
|
2013-10-17 10:59:15 +02:00
|
|
|
|
2014-08-28 14:48:18 +02:00
|
|
|
def get_products_tree(self, name):
|
|
|
|
return [x.id for x in self.products if not x.parent]
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def set_products_tree(cls, lines, name, value):
|
|
|
|
cls.write(lines, {
|
|
|
|
'products': value,
|
|
|
|
})
|
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
def get_products_cost(self, name):
|
2014-08-28 14:48:18 +02:00
|
|
|
if not self.quantity:
|
|
|
|
return Decimal('0.0')
|
2015-01-19 19:16:09 +01:00
|
|
|
cost = sum(p.get_total_cost(None, round=False) for p in self.products)
|
2014-08-28 14:48:18 +02:00
|
|
|
cost /= Decimal(str(self.quantity))
|
2015-01-19 19:16:09 +01:00
|
|
|
digits = self.__class__.products_cost.digits[1]
|
2014-08-28 14:48:18 +02:00
|
|
|
return cost.quantize(Decimal(str(10 ** -digits)))
|
|
|
|
|
2015-02-06 09:52:05 +01:00
|
|
|
def get_product_cost_price(self, name):
|
|
|
|
return self.product.cost_price if self.product else None
|
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
def get_cost_price(self, name):
|
2014-03-24 18:48:34 +01:00
|
|
|
return sum(c.cost for c in self.costs if c.cost)
|
2014-01-19 20:49:59 +01:00
|
|
|
|
2013-10-17 10:59:15 +02:00
|
|
|
@classmethod
|
2015-04-17 15:38:46 +02:00
|
|
|
def clean(cls, plans):
|
2014-01-04 17:53:24 +01:00
|
|
|
pool = Pool()
|
2014-03-19 02:00:28 +01:00
|
|
|
ProductLine = pool.get('product.cost.plan.product_line')
|
|
|
|
CostLine = pool.get('product.cost.plan.cost')
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
product_lines = ProductLine.search([
|
|
|
|
('plan', 'in', [p.id for p in plans]),
|
|
|
|
])
|
|
|
|
if product_lines:
|
|
|
|
cls.raise_user_warning('remove_product_lines',
|
|
|
|
'product_lines_will_be_removed')
|
|
|
|
ProductLine.delete(product_lines)
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
with Transaction().set_context(reset_costs=True):
|
|
|
|
CostLine.delete(CostLine.search([
|
|
|
|
('plan', 'in', [p.id for p in plans]),
|
|
|
|
('system', '=', True),
|
|
|
|
]))
|
2014-01-04 17:53:24 +01:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@ModelView.button
|
2013-10-17 10:59:15 +02:00
|
|
|
def compute(cls, plans):
|
2014-01-04 17:53:24 +01:00
|
|
|
pool = Pool()
|
2014-03-19 02:00:28 +01:00
|
|
|
ProductLine = pool.get('product.cost.plan.product_line')
|
|
|
|
CostLine = pool.get('product.cost.plan.cost')
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2015-04-17 15:38:46 +02:00
|
|
|
cls.clean(plans)
|
|
|
|
|
2014-01-04 17:53:24 +01:00
|
|
|
to_create = []
|
|
|
|
for plan in plans:
|
2014-02-18 19:04:03 +01:00
|
|
|
if plan.product and plan.bom:
|
|
|
|
to_create.extend(plan.explode_bom(plan.product, plan.bom,
|
2014-09-02 17:22:07 +02:00
|
|
|
plan.quantity, plan.uom))
|
2014-01-04 17:53:24 +01:00
|
|
|
if to_create:
|
2014-03-19 02:00:28 +01:00
|
|
|
ProductLine.create(to_create)
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2014-04-09 20:39:51 +02:00
|
|
|
to_create = []
|
2014-02-03 18:06:06 +01:00
|
|
|
for plan in plans:
|
2014-04-09 20:39:51 +02:00
|
|
|
to_create.extend(plan.get_costs())
|
|
|
|
if to_create:
|
|
|
|
CostLine.create(to_create)
|
2014-02-03 18:06:06 +01:00
|
|
|
|
2014-01-04 17:53:24 +01:00
|
|
|
def explode_bom(self, product, bom, quantity, uom):
|
2014-03-16 02:42:22 +01:00
|
|
|
"Returns products for the especified products"
|
2014-01-04 17:53:24 +01:00
|
|
|
pool = Pool()
|
|
|
|
Input = pool.get('production.bom.input')
|
|
|
|
res = []
|
|
|
|
|
|
|
|
plan_boms = {}
|
|
|
|
for plan_bom in self.boms:
|
|
|
|
if plan_bom.bom:
|
|
|
|
plan_boms[plan_bom.product.id] = plan_bom.bom
|
|
|
|
|
|
|
|
factor = bom.compute_factor(product, quantity, uom)
|
|
|
|
|
|
|
|
for input_ in bom.inputs:
|
|
|
|
product = input_.product
|
|
|
|
if product.id in plan_boms:
|
|
|
|
quantity = Input.compute_quantity(input_, factor)
|
|
|
|
res.extend(self.explode_bom(product, plan_boms[product.id],
|
|
|
|
quantity, input_.uom))
|
|
|
|
else:
|
|
|
|
line = self.get_product_line(input_, factor)
|
|
|
|
if line:
|
|
|
|
line['plan'] = self.id
|
|
|
|
res.append(line)
|
|
|
|
return res
|
|
|
|
|
|
|
|
def get_product_line(self, input_, factor):
|
|
|
|
"""
|
|
|
|
Returns a dict with values of the new line to create
|
|
|
|
params:
|
|
|
|
*input_*: Production.bom.input record for the product
|
|
|
|
*factor*: The factor to calculate the quantity
|
|
|
|
"""
|
|
|
|
pool = Pool()
|
2014-09-02 17:22:07 +02:00
|
|
|
UoM = pool.get('product.uom')
|
2014-01-04 17:53:24 +01:00
|
|
|
Input = pool.get('production.bom.input')
|
2014-03-24 16:02:19 +01:00
|
|
|
ProductLine = pool.get('product.cost.plan.product_line')
|
2014-01-04 17:53:24 +01:00
|
|
|
quantity = Input.compute_quantity(input_, factor)
|
2014-09-02 17:22:07 +02:00
|
|
|
cost_factor = Decimal(UoM.compute_qty(input_.product.default_uom, 1,
|
2014-03-24 16:02:19 +01:00
|
|
|
input_.uom))
|
|
|
|
digits = ProductLine.product_cost_price.digits[1]
|
2015-01-19 19:16:09 +01:00
|
|
|
if cost_factor == Decimal('0.0'):
|
|
|
|
product_cost_price = Decimal('0.0')
|
|
|
|
cost_price = Decimal('0.0')
|
|
|
|
else:
|
|
|
|
product_cost_price = (input_.product.cost_price /
|
|
|
|
cost_factor).quantize(Decimal(str(10 ** -digits)))
|
|
|
|
digits = ProductLine.cost_price.digits[1]
|
|
|
|
cost_price = (input_.product.cost_price /
|
|
|
|
cost_factor).quantize(Decimal(str(10 ** -digits)))
|
2014-03-24 16:02:19 +01:00
|
|
|
|
2014-01-04 17:53:24 +01:00
|
|
|
return {
|
2014-02-19 12:16:21 +01:00
|
|
|
'name': input_.product.rec_name,
|
2014-01-04 17:53:24 +01:00
|
|
|
'product': input_.product.id,
|
|
|
|
'quantity': quantity,
|
|
|
|
'uom': input_.uom.id,
|
2014-03-24 16:02:19 +01:00
|
|
|
'product_cost_price': product_cost_price,
|
|
|
|
'cost_price': cost_price,
|
2014-02-19 12:16:21 +01:00
|
|
|
}
|
2013-10-17 10:59:15 +02:00
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
def get_costs(self):
|
|
|
|
"Returns the cost lines to be created on compute"
|
|
|
|
pool = Pool()
|
|
|
|
CostType = pool.get('product.cost.plan.cost.type')
|
|
|
|
|
|
|
|
ret = []
|
|
|
|
system_cost_types = CostType.search([
|
|
|
|
('system', '=', True),
|
|
|
|
])
|
|
|
|
for cost_type in system_cost_types:
|
|
|
|
ret.append(self._get_cost_line(cost_type))
|
|
|
|
return ret
|
|
|
|
|
|
|
|
def _get_cost_line(self, cost_type):
|
|
|
|
return {
|
|
|
|
'plan': self.id,
|
|
|
|
'type': cost_type.id,
|
2015-04-17 15:38:46 +02:00
|
|
|
'system': cost_type.system,
|
|
|
|
'internal_cost': Decimal('0'),
|
2015-01-19 19:16:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
@ModelView.button
|
2015-02-06 09:52:05 +01:00
|
|
|
def update_product_cost_price(cls, plans):
|
2015-01-19 19:16:09 +01:00
|
|
|
for plan in plans:
|
|
|
|
if not plan.product:
|
|
|
|
continue
|
2015-02-06 09:52:05 +01:00
|
|
|
plan._update_product_cost_price()
|
2015-01-19 19:16:09 +01:00
|
|
|
plan.product.save()
|
|
|
|
plan.product.template.save()
|
|
|
|
|
2015-02-06 09:52:05 +01:00
|
|
|
def _update_product_cost_price(self):
|
2015-01-19 19:16:09 +01:00
|
|
|
pool = Pool()
|
|
|
|
Uom = pool.get('product.uom')
|
|
|
|
|
|
|
|
assert self.product
|
|
|
|
cost_price = Uom.compute_price(self.uom, self.cost_price,
|
|
|
|
self.product.default_uom)
|
|
|
|
if hasattr(self.product.__class__, 'cost_price'):
|
|
|
|
digits = self.product.__class__.cost_price.digits[1]
|
|
|
|
cost_price = cost_price.quantize(Decimal(str(10 ** -digits)))
|
|
|
|
self.product.cost_price = cost_price
|
|
|
|
else:
|
|
|
|
digits = self.product.template.__class__.cost_price.digits[1]
|
|
|
|
cost_price = cost_price.quantize(Decimal(str(10 ** -digits)))
|
|
|
|
self.product.template.cost_price = cost_price
|
|
|
|
|
2014-04-09 02:00:05 +02:00
|
|
|
def create_bom(self, name):
|
2014-04-10 00:18:25 +02:00
|
|
|
pool = Pool()
|
|
|
|
BOM = pool.get('production.bom')
|
|
|
|
ProductBOM = pool.get('product.product-production.bom')
|
2015-01-27 09:52:03 +01:00
|
|
|
|
|
|
|
if not self.product:
|
|
|
|
self.raise_user_error('lacks_the_product', self.rec_name)
|
2014-04-09 02:00:05 +02:00
|
|
|
if self.bom:
|
2015-02-06 09:19:29 +01:00
|
|
|
self.raise_user_error('bom_already_exists%s' % self.id,
|
|
|
|
'bom_already_exists', self.rec_name)
|
2015-01-27 09:52:03 +01:00
|
|
|
|
2014-04-09 02:00:05 +02:00
|
|
|
bom = BOM()
|
|
|
|
bom.name = name
|
|
|
|
bom.inputs = self._get_bom_inputs()
|
|
|
|
bom.outputs = self._get_bom_outputs()
|
|
|
|
bom.save()
|
|
|
|
self.bom = bom
|
|
|
|
self.save()
|
2014-04-10 00:18:25 +02:00
|
|
|
|
|
|
|
if self.product.boms:
|
2015-02-06 09:52:05 +01:00
|
|
|
# TODO: create new bom to allow diferent "versions"?
|
2014-04-10 00:18:25 +02:00
|
|
|
product_bom = self.product.boms[0]
|
2014-04-10 00:42:24 +02:00
|
|
|
if product_bom.bom:
|
2015-02-06 09:19:29 +01:00
|
|
|
self.raise_user_warning('product_already_has_bom%s' % self.id,
|
|
|
|
'product_already_has_bom',
|
2014-04-10 00:42:24 +02:00
|
|
|
self.product.rec_name)
|
2014-04-10 00:18:25 +02:00
|
|
|
else:
|
|
|
|
product_bom = ProductBOM()
|
|
|
|
product_bom.product = self.product
|
|
|
|
product_bom.bom = bom
|
|
|
|
product_bom.save()
|
2014-04-09 02:00:05 +02:00
|
|
|
return bom
|
|
|
|
|
|
|
|
def _get_bom_outputs(self):
|
|
|
|
BOMOutput = Pool().get('production.bom.output')
|
|
|
|
outputs = []
|
|
|
|
if self.product:
|
|
|
|
output = BOMOutput()
|
|
|
|
output.product = self.product
|
2015-01-19 19:16:09 +01:00
|
|
|
output.uom = self.uom
|
2014-04-09 02:00:05 +02:00
|
|
|
output.quantity = self.quantity
|
|
|
|
outputs.append(output)
|
|
|
|
return outputs
|
|
|
|
|
|
|
|
def _get_bom_inputs(self):
|
2014-04-10 00:42:24 +02:00
|
|
|
inputs = {}
|
2014-04-09 02:00:05 +02:00
|
|
|
for line in self.products:
|
|
|
|
if not line.product:
|
|
|
|
continue
|
2014-04-10 00:42:24 +02:00
|
|
|
input_ = self._get_input_line(line)
|
|
|
|
if input_.product.id not in inputs:
|
|
|
|
inputs[input_.product.id] = input_
|
|
|
|
continue
|
|
|
|
existing = inputs[input_.product.id]
|
|
|
|
if existing.uom != input_.uom:
|
|
|
|
self.raise_user_error('cannot_mix_input_uoms', {
|
|
|
|
'plan': self.rec_name,
|
|
|
|
'product': existing.product.rec_name,
|
|
|
|
})
|
|
|
|
existing.quantity += input_.quantity
|
|
|
|
return inputs.values()
|
2014-04-09 02:00:05 +02:00
|
|
|
|
|
|
|
def _get_input_line(self, line):
|
|
|
|
'Return the BOM Input line for a product line'
|
|
|
|
BOMInput = Pool().get('production.bom.input')
|
|
|
|
input_ = BOMInput()
|
|
|
|
input_.product = line.product
|
|
|
|
input_.uom = line.uom
|
|
|
|
input_.quantity = line.quantity
|
2014-09-02 17:22:07 +02:00
|
|
|
parent_line = line.parent
|
|
|
|
while parent_line:
|
|
|
|
input_.quantity *= parent_line.quantity
|
|
|
|
parent_line = parent_line.parent
|
2014-04-09 02:00:05 +02:00
|
|
|
return input_
|
|
|
|
|
2014-08-28 14:48:18 +02:00
|
|
|
@classmethod
|
|
|
|
def create(cls, vlist):
|
|
|
|
Sequence = Pool().get('ir.sequence')
|
|
|
|
Config = Pool().get('production.configuration')
|
|
|
|
|
|
|
|
vlist = [x.copy() for x in vlist]
|
|
|
|
config = Config(1)
|
|
|
|
for values in vlist:
|
|
|
|
values['number'] = Sequence.get_id(
|
|
|
|
config.product_cost_plan_sequence.id)
|
|
|
|
return super(Plan, cls).create(vlist)
|
|
|
|
|
2014-09-02 17:22:07 +02:00
|
|
|
@classmethod
|
|
|
|
def copy(cls, plans, default=None):
|
|
|
|
if default is None:
|
|
|
|
default = {}
|
|
|
|
else:
|
|
|
|
default = default.copy()
|
|
|
|
default['products'] = None
|
|
|
|
default['products_tree'] = None
|
2015-02-06 09:19:29 +01:00
|
|
|
default['bom'] = None
|
2014-09-02 17:22:07 +02:00
|
|
|
|
|
|
|
new_plans = []
|
|
|
|
for plan in plans:
|
|
|
|
new_plans.append(plan._copy_plan(default))
|
|
|
|
return new_plans
|
|
|
|
|
|
|
|
def _copy_plan(self, default):
|
|
|
|
ProductLine = Pool().get('product.cost.plan.product_line')
|
|
|
|
|
|
|
|
new_plan, = super(Plan, self).copy([self], default=default)
|
|
|
|
ProductLine.copy(self.products_tree, default={
|
|
|
|
'plan': new_plan.id,
|
|
|
|
'children': None,
|
|
|
|
})
|
|
|
|
return new_plan
|
|
|
|
|
2014-08-28 14:48:18 +02:00
|
|
|
@classmethod
|
|
|
|
def delete(cls, plans):
|
|
|
|
CostLine = Pool().get('product.cost.plan.cost')
|
|
|
|
to_delete = []
|
|
|
|
for plan in plans:
|
|
|
|
to_delete += plan.costs
|
|
|
|
with Transaction().set_context(reset_costs=True):
|
|
|
|
CostLine.delete(to_delete)
|
|
|
|
super(Plan, cls).delete(plans)
|
|
|
|
|
2013-10-17 10:59:15 +02:00
|
|
|
|
|
|
|
class PlanBOM(ModelSQL, ModelView):
|
|
|
|
'Product Cost Plan BOM'
|
2013-11-16 00:39:12 +01:00
|
|
|
__name__ = 'product.cost.plan.bom_line'
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2014-02-18 19:04:03 +01:00
|
|
|
plan = fields.Many2One('product.cost.plan', 'Plan', required=True,
|
|
|
|
ondelete='CASCADE')
|
2013-10-17 10:59:15 +02:00
|
|
|
product = fields.Many2One('product.product', 'Product', required=True)
|
2014-01-04 17:53:24 +01:00
|
|
|
bom = fields.Many2One('production.bom', 'BOM', domain=[
|
|
|
|
('output_products', '=', Eval('product', 0)),
|
|
|
|
], depends=['product'])
|
2013-10-17 10:59:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
class PlanProductLine(ModelSQL, ModelView):
|
|
|
|
'Product Cost Plan Product Line'
|
|
|
|
__name__ = 'product.cost.plan.product_line'
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2014-04-09 00:55:39 +02:00
|
|
|
name = fields.Char('Name')
|
2014-03-18 16:24:50 +01:00
|
|
|
sequence = fields.Integer('Sequence')
|
2014-03-24 18:48:34 +01:00
|
|
|
parent = fields.Many2One('product.cost.plan.product_line', 'Parent')
|
|
|
|
children = fields.One2Many('product.cost.plan.product_line', 'parent',
|
|
|
|
'Children')
|
2014-02-18 19:04:03 +01:00
|
|
|
plan = fields.Many2One('product.cost.plan', 'Plan', required=True,
|
|
|
|
ondelete='CASCADE')
|
2015-01-19 19:16:09 +01:00
|
|
|
product = fields.Many2One('product.product', 'Product', domain=[
|
2014-01-04 17:53:24 +01:00
|
|
|
('type', '!=', 'service'),
|
2015-01-19 19:16:09 +01:00
|
|
|
If(Bool(Eval('children')),
|
|
|
|
('default_uom.category', '=', Eval('uom_category')),
|
|
|
|
()),
|
|
|
|
], depends=['children', 'uom_category'])
|
2014-03-16 02:17:36 +01:00
|
|
|
quantity = fields.Float('Quantity', required=True,
|
|
|
|
digits=(16, Eval('uom_digits', 2)), depends=['uom_digits'])
|
2014-09-02 17:22:07 +02:00
|
|
|
uom_category = fields.Function(fields.Many2One('product.uom.category',
|
|
|
|
'UoM Category'),
|
|
|
|
'on_change_with_uom_category')
|
2015-01-19 19:16:09 +01:00
|
|
|
uom = fields.Many2One('product.uom', 'UoM', required=True, domain=[
|
|
|
|
If(Bool(Eval('children')) | Bool(Eval('product')),
|
2014-09-02 17:22:07 +02:00
|
|
|
('category', '=', Eval('uom_category')),
|
2015-01-19 19:16:09 +01:00
|
|
|
()),
|
|
|
|
], depends=['children', 'product', 'uom_category'])
|
2014-09-02 17:22:07 +02:00
|
|
|
uom_digits = fields.Function(fields.Integer('UoM Digits'),
|
2014-05-27 10:47:44 +02:00
|
|
|
'on_change_with_uom_digits')
|
2014-05-14 10:10:42 +02:00
|
|
|
product_cost_price = fields.Numeric('Product Cost Price',
|
|
|
|
digits=(16, DIGITS),
|
2014-01-15 14:38:36 +01:00
|
|
|
states={
|
2014-02-18 19:04:03 +01:00
|
|
|
'readonly': True,
|
2014-05-27 10:47:44 +02:00
|
|
|
}, depends=['product'])
|
2014-05-14 10:10:42 +02:00
|
|
|
cost_price = fields.Numeric('Cost Price', required=True,
|
|
|
|
digits=(16, DIGITS))
|
2015-01-19 19:16:09 +01:00
|
|
|
unit_cost = fields.Function(fields.Numeric('Unit Cost',
|
|
|
|
digits=(16, DIGITS),
|
|
|
|
help="The cost of this product for each unit of plan's product."),
|
|
|
|
'get_unit_cost')
|
|
|
|
total_cost = fields.Function(fields.Numeric('Total Cost',
|
|
|
|
digits=(16, DIGITS),
|
|
|
|
help="The cost of this product for total plan's quantity."),
|
|
|
|
'get_total_cost')
|
2014-01-04 17:53:24 +01:00
|
|
|
|
2014-03-18 16:24:50 +01:00
|
|
|
@classmethod
|
|
|
|
def __setup__(cls):
|
|
|
|
super(PlanProductLine, cls).__setup__()
|
|
|
|
cls._order.insert(0, ('sequence', 'ASC'))
|
|
|
|
|
2015-01-28 14:36:19 +01:00
|
|
|
@classmethod
|
|
|
|
def validate(cls, lines):
|
2015-04-17 15:38:46 +02:00
|
|
|
super(PlanProductLine, cls).validate(lines)
|
|
|
|
cls.check_recursion(lines)
|
2015-01-28 14:36:19 +01:00
|
|
|
|
2014-03-18 16:24:50 +01:00
|
|
|
@staticmethod
|
|
|
|
def order_sequence(tables):
|
|
|
|
table, _ = tables[None]
|
|
|
|
return [table.sequence == None, table.sequence]
|
|
|
|
|
2014-05-27 10:47:44 +02:00
|
|
|
@fields.depends('product', 'uom')
|
2014-01-04 17:53:24 +01:00
|
|
|
def on_change_product(self):
|
|
|
|
res = {}
|
|
|
|
if self.product:
|
|
|
|
uoms = self.product.default_uom.category.uoms
|
|
|
|
if (not self.uom or self.uom not in uoms):
|
2014-02-19 12:16:21 +01:00
|
|
|
res['name'] = self.product.rec_name
|
2014-01-04 17:53:24 +01:00
|
|
|
res['uom'] = self.product.default_uom.id
|
|
|
|
res['uom.rec_name'] = self.product.default_uom.rec_name
|
2014-01-15 14:38:36 +01:00
|
|
|
res['product_cost_price'] = self.product.cost_price
|
2014-03-24 18:48:34 +01:00
|
|
|
res['cost_price'] = self.product.cost_price
|
2014-01-04 17:53:24 +01:00
|
|
|
else:
|
2014-02-19 12:16:21 +01:00
|
|
|
res['name'] = None
|
2014-01-04 17:53:24 +01:00
|
|
|
res['uom'] = None
|
|
|
|
res['uom.rec_name'] = ''
|
2014-01-15 14:38:36 +01:00
|
|
|
res['product_cost_price'] = None
|
2014-01-04 17:53:24 +01:00
|
|
|
return res
|
|
|
|
|
2015-04-13 13:25:01 +02:00
|
|
|
@fields.depends('children', '_parent_plan.uom', 'product', 'uom', 'plan')
|
2014-01-04 17:53:24 +01:00
|
|
|
def on_change_with_uom_category(self, name=None):
|
2015-01-19 19:16:09 +01:00
|
|
|
if self.children:
|
|
|
|
# If product line has children, it must be have computable
|
|
|
|
# quantities of plan product
|
2015-04-13 10:17:53 +02:00
|
|
|
if self.plan and self.plan.uom:
|
|
|
|
return self.plan.uom.category.id
|
2014-01-04 17:53:24 +01:00
|
|
|
if self.product:
|
|
|
|
return self.product.default_uom.category.id
|
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
@fields.depends('uom')
|
|
|
|
def on_change_with_uom_digits(self, name=None):
|
|
|
|
if self.uom:
|
|
|
|
return self.uom.digits
|
|
|
|
return 2
|
|
|
|
|
|
|
|
@fields.depends('product', 'uom', 'cost_price')
|
|
|
|
def on_change_with_cost_price(self):
|
|
|
|
UoM = Pool().get('product.uom')
|
|
|
|
|
|
|
|
if (not self.product or not self.uom
|
2015-04-15 19:44:59 +02:00
|
|
|
or (self.cost_price != None
|
2015-01-19 19:16:09 +01:00
|
|
|
and self.cost_price != self.product.cost_price)):
|
|
|
|
cost = self.cost_price
|
|
|
|
else:
|
|
|
|
cost = UoM.compute_price(self.product.default_uom,
|
|
|
|
self.product.cost_price, self.uom)
|
|
|
|
if cost:
|
|
|
|
digits = self.__class__.cost_price.digits[1]
|
|
|
|
return cost.quantize(Decimal(str(10 ** -digits)))
|
|
|
|
return cost
|
|
|
|
|
2014-05-27 10:47:44 +02:00
|
|
|
@fields.depends('product', 'uom')
|
2014-03-18 18:30:33 +01:00
|
|
|
def on_change_with_product_cost_price(self):
|
2014-09-02 17:22:07 +02:00
|
|
|
UoM = Pool().get('product.uom')
|
2015-01-19 19:16:09 +01:00
|
|
|
if not self.product:
|
2014-03-18 18:30:33 +01:00
|
|
|
return
|
2015-01-19 19:16:09 +01:00
|
|
|
if not self.uom:
|
|
|
|
cost = self.product.cost_price
|
|
|
|
else:
|
|
|
|
cost = UoM.compute_price(self.product.default_uom,
|
|
|
|
self.product.cost_price, self.uom)
|
2014-03-18 18:30:33 +01:00
|
|
|
digits = self.__class__.product_cost_price.digits[1]
|
|
|
|
return cost.quantize(Decimal(str(10 ** -digits)))
|
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
def get_unit_cost(self, name):
|
|
|
|
unit_cost = self.total_cost
|
|
|
|
if unit_cost and self.plan and self.plan.quantity:
|
|
|
|
unit_cost /= Decimal(str(self.plan.quantity))
|
|
|
|
digits = self.__class__.unit_cost.digits[1]
|
|
|
|
return unit_cost.quantize(Decimal(str(10 ** -digits)))
|
|
|
|
|
|
|
|
def get_total_cost(self, name, round=True):
|
|
|
|
if not self.cost_price:
|
|
|
|
return Decimal('0.0')
|
|
|
|
# Quantity is the quantity of this line for all plan's quantity
|
2014-03-24 18:48:34 +01:00
|
|
|
quantity = self.quantity
|
2015-01-19 19:16:09 +01:00
|
|
|
line = self
|
|
|
|
while quantity and line.parent:
|
|
|
|
quantity *= line.parent.quantity
|
|
|
|
line = line.parent
|
2014-02-18 19:04:03 +01:00
|
|
|
if not quantity:
|
2014-01-15 14:38:36 +01:00
|
|
|
return Decimal('0.0')
|
2014-04-09 22:48:55 +02:00
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
total_cost = Decimal(str(quantity)) * self.cost_price
|
|
|
|
if not round:
|
|
|
|
return total_cost
|
|
|
|
digits = self.__class__.total_cost.digits[1]
|
|
|
|
return total_cost.quantize(Decimal(str(10 ** -digits)))
|
2014-03-16 02:17:36 +01:00
|
|
|
|
2014-09-02 17:22:07 +02:00
|
|
|
@classmethod
|
|
|
|
def copy(cls, lines, default=None):
|
|
|
|
if default is None:
|
|
|
|
default = {}
|
|
|
|
else:
|
|
|
|
default = default.copy()
|
|
|
|
default['children'] = None
|
|
|
|
|
|
|
|
new_lines = []
|
|
|
|
for line in lines:
|
|
|
|
new_line, = super(PlanProductLine, cls).copy([line],
|
|
|
|
default=default)
|
|
|
|
new_lines.append(new_line)
|
|
|
|
|
|
|
|
new_default = default.copy()
|
|
|
|
new_default['parent'] = new_line.id
|
|
|
|
cls.copy(line.children, default=new_default)
|
|
|
|
return new_lines
|
|
|
|
|
2014-03-16 02:17:36 +01:00
|
|
|
|
2014-02-03 18:06:06 +01:00
|
|
|
STATES = {
|
|
|
|
'readonly': Eval('system', False),
|
|
|
|
}
|
|
|
|
DEPENDS = ['system']
|
|
|
|
|
|
|
|
|
|
|
|
class PlanCost(ModelSQL, ModelView):
|
|
|
|
'Plan Cost'
|
|
|
|
__name__ = 'product.cost.plan.cost'
|
|
|
|
|
2014-02-18 19:04:03 +01:00
|
|
|
plan = fields.Many2One('product.cost.plan', 'Plan', required=True,
|
|
|
|
ondelete='CASCADE')
|
2014-04-14 09:20:34 +02:00
|
|
|
sequence = fields.Integer('Sequence')
|
2015-01-19 19:16:09 +01:00
|
|
|
type = fields.Many2One('product.cost.plan.cost.type', 'Type', domain=[
|
|
|
|
('system', '=', Eval('system')),
|
|
|
|
],
|
2014-02-03 18:06:06 +01:00
|
|
|
required=True, states=STATES, depends=DEPENDS)
|
2015-01-19 19:16:09 +01:00
|
|
|
internal_cost = fields.Numeric('Cost (Internal Use)', digits=(16, DIGITS),
|
|
|
|
readonly=True)
|
|
|
|
cost = fields.Function(fields.Numeric('Cost', digits=(16, DIGITS),
|
|
|
|
required=True, states=STATES, depends=DEPENDS),
|
|
|
|
'get_cost', setter='set_cost')
|
2014-02-03 18:06:06 +01:00
|
|
|
system = fields.Boolean('System Managed', readonly=True)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def __setup__(cls):
|
|
|
|
super(PlanCost, cls).__setup__()
|
2014-04-14 09:20:34 +02:00
|
|
|
cls._order.insert(0, ('sequence', 'ASC'))
|
2014-02-03 18:06:06 +01:00
|
|
|
cls._error_messages.update({
|
2014-03-19 02:00:28 +01:00
|
|
|
'delete_system_cost': ('You can not delete cost "%(cost)s" '
|
|
|
|
'from plan "%(plan)s" because it\'s managed by system.'),
|
2014-02-03 18:06:06 +01:00
|
|
|
})
|
|
|
|
|
2014-04-14 09:20:34 +02:00
|
|
|
@staticmethod
|
|
|
|
def order_sequence(tables):
|
|
|
|
table, _ = tables[None]
|
|
|
|
return [table.sequence == None, table.sequence]
|
|
|
|
|
2014-02-03 18:06:06 +01:00
|
|
|
def get_rec_name(self, name):
|
|
|
|
return self.type.rec_name
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def search_rec_name(cls, name, clause):
|
|
|
|
return [('type.name',) + tuple(clause[1:])]
|
|
|
|
|
2015-01-19 19:16:09 +01:00
|
|
|
@staticmethod
|
|
|
|
def default_system():
|
|
|
|
return False
|
|
|
|
|
|
|
|
def get_cost(self, name):
|
|
|
|
if self.system:
|
|
|
|
cost = getattr(self.plan, self.type.plan_field_name)
|
|
|
|
else:
|
|
|
|
cost = self.internal_cost
|
|
|
|
digits = self.__class__.cost.digits[1]
|
|
|
|
return cost.quantize(Decimal(str(10 ** -digits)))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def set_cost(cls, records, name, value):
|
|
|
|
records_todo = [r for r in records if not r.system]
|
2015-04-17 15:38:46 +02:00
|
|
|
if records_todo:
|
|
|
|
cls.write(records_todo, {
|
|
|
|
'internal_cost': value,
|
|
|
|
})
|
2015-01-19 19:16:09 +01:00
|
|
|
|
2014-02-03 18:06:06 +01:00
|
|
|
@classmethod
|
|
|
|
def delete(cls, costs):
|
|
|
|
if not Transaction().context.get('reset_costs', False):
|
|
|
|
for cost in costs:
|
|
|
|
if cost.system:
|
2014-03-19 02:00:28 +01:00
|
|
|
cls.raise_user_error('delete_system_cost', {
|
|
|
|
'cost': cost.rec_name,
|
|
|
|
'plan': cost.plan.rec_name,
|
|
|
|
})
|
2014-02-03 18:06:06 +01:00
|
|
|
super(PlanCost, cls).delete(costs)
|
|
|
|
|
2014-03-25 23:43:47 +01:00
|
|
|
|
|
|
|
class CreateBomStart(ModelView):
|
|
|
|
'Create BOM Start'
|
|
|
|
__name__ = 'product.cost.plan.create_bom.start'
|
|
|
|
|
2014-04-09 00:54:26 +02:00
|
|
|
name = fields.Char('Name', required=True)
|
2014-03-25 23:43:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
class CreateBom(Wizard):
|
|
|
|
'Create BOM'
|
|
|
|
__name__ = 'product.cost.plan.create_bom'
|
|
|
|
|
|
|
|
start = StateView('product.cost.plan.create_bom.start',
|
|
|
|
'product_cost_plan.create_bom_start_view_form', [
|
|
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
|
|
Button('Ok', 'bom', 'tryton-ok', True),
|
|
|
|
])
|
|
|
|
bom = StateAction('production.act_bom_list')
|
|
|
|
|
|
|
|
def default_start(self, fields):
|
2014-04-09 00:53:57 +02:00
|
|
|
CostPlan = Pool().get('product.cost.plan')
|
2014-03-25 23:43:47 +01:00
|
|
|
plan = CostPlan(Transaction().context.get('active_id'))
|
2014-04-09 02:00:05 +02:00
|
|
|
return {
|
2015-01-27 09:52:03 +01:00
|
|
|
'name': plan.rec_name,
|
2014-04-09 02:00:05 +02:00
|
|
|
}
|
2014-03-25 23:43:47 +01:00
|
|
|
|
|
|
|
def do_bom(self, action):
|
2014-04-09 02:00:05 +02:00
|
|
|
CostPlan = Pool().get('product.cost.plan')
|
2014-04-09 00:53:57 +02:00
|
|
|
plan = CostPlan(Transaction().context.get('active_id'))
|
2014-04-09 02:00:05 +02:00
|
|
|
bom = plan.create_bom(self.start.name)
|
|
|
|
data = {
|
|
|
|
'res_id': [bom.id]
|
|
|
|
}
|
2014-03-25 23:43:47 +01:00
|
|
|
action['views'].reverse()
|
|
|
|
return action, data
|