diff --git a/__init__.py b/__init__.py index 29648d0..7b9e273 100644 --- a/__init__.py +++ b/__init__.py @@ -18,6 +18,7 @@ def register(): production.Production, production.ProductionDetailedStart, production.ProcessProductionAsyncStart, + production.ProductionCost, account.Move, stock.Move, ir.Cron, diff --git a/bom.py b/bom.py index 34db442..b76dfca 100644 --- a/bom.py +++ b/bom.py @@ -5,14 +5,13 @@ from datetime import datetime, date from trytond.pool import Pool, PoolMeta from trytond.modules.product import round_price -from trytond.transaction import Transaction from trytond.model import fields, ModelSQL, ModelView class BOM(metaclass=PoolMeta): __name__ = 'production.bom' - direct_costs = fields.One2Many('production.bom.direct_cost', - 'bom', 'Direct Costs') + direct_costs = fields.One2Many('production.bom.direct_cost', 'bom', + 'Direct Costs') @classmethod def calc_cost_ldm(cls): @@ -65,12 +64,12 @@ class BOMDirectCost(ModelSQL, ModelView): product = fields.Many2One('product.product', 'Product', required=True, domain=[('type', '!=', 'active')]) uom = fields.Many2One('product.uom', 'UoM') - quantity = fields.Float('Quantity', required=True, digits=(16,2)) + quantity = fields.Float('Quantity', required=True, digits=(16, 2)) notes = fields.Char('Notes') kind = fields.Selection([ ('labour', 'Labour'), - ('imc', 'IMC'), - ('', ''), + ('indirect', 'Indirect'), + ('service', 'Service'), ], 'Kind') @staticmethod diff --git a/production.py b/production.py index 1c21666..7b4635f 100644 --- a/production.py +++ b/production.py @@ -3,15 +3,15 @@ from decimal import Decimal from trytond.pool import PoolMeta, Pool -from trytond.model import fields, ModelView +from trytond.model import fields, ModelView, ModelSQL from trytond.pyson import Eval from trytond.transaction import Transaction -from trytond.wizard import Wizard, StateTransition, StateReport, StateView, Button +from trytond.wizard import ( + Wizard, StateTransition, StateReport, StateView, Button) from trytond.modules.company import CompanyReport from trytond.i18n import gettext from trytond.exceptions import UserError from trytond.report import Report -from trytond.modules.product import price_digits, round_price ZERO = Decimal(0) @@ -24,6 +24,14 @@ def round_dec(number): class Production(metaclass=PoolMeta): __name__ = 'production' + _analytic_dom = [ + ('type', 'in', ['normal', 'distribution']), + ('company', '=', Eval('context', {}).get('company', -1)), + ('parent', '=', Eval('analytic_account')), + ] + _states = { + 'readonly': ~Eval('state').in_(['draft', 'request']), + } in_account_move = fields.Many2One('account.move', 'In Account Move', states={'readonly': True}) out_account_move = fields.Many2One('account.move', 'Out Account Move', @@ -33,19 +41,32 @@ class Production(metaclass=PoolMeta): warehouse_target = fields.Many2One('stock.location', 'Warehouse Target', domain=[('type', '=', 'warehouse')]) warehouse_moves = fields.One2Many('stock.move', 'origin', 'Warehouse Moves') + costs = fields.One2Many('production.cost', 'production', 'Costs') material_costs = fields.Numeric('Material Costs', digits=(16, 2), - readonly=True) - labour_costs = fields.Numeric('Labour Costs', digits=(16, 2), readonly=True) - imc_costs = fields.Numeric('IMC Costs', digits=(16, 2), readonly=True) - total_cost = fields.Numeric('Total Cost', digits=(16, 2), readonly=True) - perfomance = fields.Function(fields.Numeric('Perfomance', digits=(16, 2)), 'get_perfomance') + states={'readonly': True}) + labour_costs = fields.Numeric('Labour Costs', digits=(16, 2), + states={'readonly': True}) + indirect_costs = fields.Numeric('Indirect Costs', digits=(16, 2), + states={'readonly': True}) + services_costs = fields.Numeric('Services Costs', digits=(16, 2), + states={'readonly': True}) + total_cost = fields.Function(fields.Numeric('Total Cost', digits=(16, 2)), + 'get_total_cost') + perfomance = fields.Function(fields.Numeric('Perfomance', digits=(16, 2)), + 'get_perfomance') analytic_account = fields.Many2One('analytic_account.account', - 'Analytic Account', domain=[ - ('type', 'in', ['normal', 'distribution']), + 'Analytic Account Main', domain=[ + ('type', '=', 'view'), ('company', '=', Eval('context', {}).get('company', -1)) - ], states={ - 'readonly': ~Eval('state').in_(['draft', 'request']), - }) + ], states=_states) + analytic_account_materials = fields.Many2One('analytic_account.account', + 'Analytic Account Materials', domain=_analytic_dom) + analytic_account_labour = fields.Many2One('analytic_account.account', + 'Analytic Account Labour', domain=_analytic_dom) + analytic_account_indirect = fields.Many2One('analytic_account.account', + 'Analytic Account Indirect', domain=_analytic_dom) + analytic_account_services = fields.Many2One('analytic_account.account', + 'Analytic Account Services', domain=_analytic_dom) @staticmethod def default_warehouse_origin(): @@ -64,17 +85,18 @@ class Production(metaclass=PoolMeta): @classmethod def wait(cls, records): super(Production, cls).wait(records) - # for rec in records: - # cls.create_account_move(rec, 'wait', 'in_account_move') + for rec in records: + # cls.create_account_move(rec, 'wait', 'in_account_move') # FIXME # rec.create_stock_move('in') + pass @classmethod def done(cls, records): Move = Pool().get('stock.move') super(Production, cls).done(records) for rec in records: - # cls.create_account_move(rec, 'done', 'out_account_move') + cls.create_account_move(rec, 'done', 'out_account_move') for output in rec.outputs: unit_price = rec._compute_unit_cost() Move.write([output], {'unit_price': unit_price}) @@ -90,10 +112,18 @@ class Production(metaclass=PoolMeta): new_cost_price = round_dec(total_cost / Decimal(self.quantity)) return new_cost_price + def get_total_cost(self, name=None): + return sum([ + self.material_costs or 0, + self.indirect_costs or 0, + self.services_costs or 0, + self.labour_costs or 0, + ]) + def get_perfomance(self, name=None): res = Decimal(0) if self.bom: - origin = sum(p.quantity for p in self.bom.outputs ) * self.quantity + origin = sum(p.quantity for p in self.bom.outputs) * self.quantity result = sum(p.quantity for p in self.outputs) if result != 0 and origin != 0: res = Decimal(str(round(result*100/origin, 2))) @@ -108,56 +138,51 @@ class Production(metaclass=PoolMeta): ProductCost.write(products, {'cost_price': new_cost}) @classmethod - def get_production_lines(cls, bom, materials_cost, date_, factor, args=None): + def get_production_lines(cls, rec, date_, factor, args=None): """ Get Production Account Move Lines """ - lines = [] + cost_finished_production = [] values = {} - output = bom.outputs[0] + output = rec.bom.outputs[0] + account_expense = output.product.account_expense_used + analytic_materials = args.get('analytic_materials', None) materials_line = { 'description': '', 'account': account_expense.id, 'debit': 0, - 'credit': materials_cost, + 'credit': rec.material_costs, } - analytic = args.get('analytic', None) - cls.set_analytic_lines(materials_line, date_, analytic) + cls.set_analytic_lines(materials_line, date_, analytic_materials) lines.append(materials_line) - cost_finished_production.append(materials_cost) + cost_finished_production.append(rec.materials_cost) - values = { - 'total_labour': [], - 'total_imc': [], - } - - for dc in bom.direct_costs: - account_id = dc.product.account_expense_used.id - amount = Decimal(dc.quantity * factor) * dc.product.cost_price - amount = Decimal(round(amount, 2)) - line_ = { - 'description': '', - 'account': account_id, - 'debit': 0, - 'credit': amount, - } - - cls.set_analytic_lines(line_, date_, analytic) + account_id = None + lines = [] + for cost in rec.costs: + amount = rec.amount + if cost.kind == 'labour': + account_id = cost.product.account_expense_used.id + values['total_labour'].append(rec.labour_costs) + line_ = { + 'description': '', + 'account': account_id, + 'debit': 0, + 'credit': rec.amount, + } + cls.set_analytic_lines(line_, date_, analytic_work_force) + lines.append(line_) lines.append(line_) cost_finished_production.append(amount) - if dc.kind == 'labour': - values['total_labour'].append(amount) - elif dc.kind == 'imc': - values['total_imc'].append(amount) - - account_stock = output.product.account_stock_used - lines.append({ - 'description': '', - 'account': account_stock.id, - 'debit': sum(cost_finished_production), - 'credit': 0, - }) - return lines, values + # + # account_stock = output.product.account_stock_used + # lines.append({ + # 'description': '', + # 'account': account_stock.id, + # 'debit': sum(cost_finished_production), + # 'credit': 0, + # }) + # return lines, values @classmethod def get_consumption_lines(cls, inputs, factor, date_, args=None): @@ -227,32 +252,39 @@ class Production(metaclass=PoolMeta): raise UserError( gettext('production_accounting.msg_planned_date_required') ) - analytic_ctx = {'analytic': rec.analytic_account} + analytic_ctx = { + 'analytic_materials': rec.analytic_account_materials, + 'analytic_labour': rec.analytic_account_labour, + 'analytic_indirect': rec.analytic_account_indirect, + 'analytic_services': rec.analytic_account_services, + } to_update = {} output = rec.outputs[0] factor = rec.quantity / output.quantity - if kind == 'wait': - date_ = rec.planned_date - lines, values = cls.get_consumption_lines( - rec.inputs, factor, date_, analytic_ctx - ) - to_update['material_costs'] = values['material_costs'] + # if kind == 'wait': + # date_ = rec.planned_date + # lines, values = cls.get_consumption_lines( + # rec.inputs, factor, date_, analytic_ctx + # ) + # to_update['material_costs'] = values['material_costs'] if kind == 'done': date_ = rec.effective_date lines, values = cls.get_production_lines( - rec.bom, - rec.material_costs, + rec, date_, factor, analytic_ctx ) - total_labour = sum(values['total_labour']) - total_imc = sum(values['total_imc']) - to_update['material_costs'] = round_dec(rec.cost) - to_update['labour_costs'] = round_dec(total_labour) - to_update['imc_costs'] = round_dec(total_imc) - to_update['total_cost'] = round_dec(total_imc + total_labour + rec.cost) + # total_labour = sum(values['total_labour']) + # total_indirect = sum(values['total_indirect']) + # total_services = sum(values['total_services']) + # to_update['material_costs'] = round_dec(rec.cost) + # to_update['labour_costs'] = round_dec(total_labour) + # to_update['indirect_costs'] = round_dec(total_indirect) + # to_update['services_costs'] = round_dec(total_services) + # to_update['total_cost'] = round_dec( + # total_indirect + total_labour + rec.cost) period_id = Period.find(rec.company.id, date=date_) @@ -267,8 +299,8 @@ class Production(metaclass=PoolMeta): }]) Move.post([move]) - to_update[field] = move.id - cls.write([rec], to_update) + # to_update[field] = move.id + # cls.write([rec], to_update) def create_stock_move(self, kind, field=None): pool = Pool() @@ -321,6 +353,67 @@ class Production(metaclass=PoolMeta): lines.append(analytic_line) line['analytic_lines'] = [('create', lines)] + @fields.depends('inputs', 'material_costs') + def on_change_inputs(self, name=None): + res = [] + for _input in self.inputs: + res.append(Decimal(_input.quantity) * _input.product.cost_price) + self.material_costs = sum(res) + + @fields.depends('costs', 'indirect_costs', 'services_costs', 'labour_costs') + def on_change_costs(self, name=None): + indirect_costs = [] + services_costs = [] + labour_costs = [] + for cost in self.costs: + if cost.kind == 'indirect': + indirect_costs.append(cost.amount) + elif cost.kind == 'labour': + labour_costs.append(cost.amount) + elif cost.kind == 'service': + services_costs.append(cost.amount) + + self.indirect_costs = sum(indirect_costs) + self.services_costs = sum(services_costs) + self.labour_costs = sum(labour_costs) + + +class ProductionCost(ModelSQL, ModelView): + "Production Cost" + __name__ = "production.cost" + production = fields.Many2One('production', 'Production', required=True, + ondelete='CASCADE') + product = fields.Many2One('product.product', 'Product', required=True, + domain=[('type', '!=', 'active')]) + unit = fields.Many2One('product.uom', 'UoM') + effective_date = fields.Date('Effective Date') + quantity = fields.Float('Quantity', required=True, digits=(16, 2)) + unit_price = fields.Numeric('Unit Price', required=True, digits=(16, 2)) + amount = fields.Numeric('Amount', digits=(16, 2), states={'readonly': True}) + analytic_account = fields.Many2One('analytic_account.account', + 'Analytic Account', domain=[ + ('type', 'in', ['normal', 'distribution']), + ('company', '=', Eval('context', {}).get('company', -1)), + ('parent', '=', Eval('_parent_production', {}).get('analytic_account')), + ]) + kind = fields.Selection([ + ('labour', 'Labour'), + ('indirect', 'Indirect'), + ('service', 'Service'), + ], 'Kind', required=True) + notes = fields.Text('Notes') + + @fields.depends('product', 'unit', 'unit_price') + def on_change_product(self, name=None): + if self.product: + self.unit = self.product.default_uom.id + self.unit_price = self.product.cost_price + + @fields.depends('unit_price', 'amount', 'quantity') + def on_change_with_amount(self, name=None): + if self.unit_price and self.quantity: + return round(self.unit_price * Decimal(self.quantity), 2) + class ProductionReport(CompanyReport): 'Production Report' diff --git a/production.xml b/production.xml index efea57d..796ae82 100644 --- a/production.xml +++ b/production.xml @@ -72,5 +72,15 @@ this repository contains the full copyright notices and license terms. --> sequence="2" id="menu_process_production_async" action="wizard_process_production_async"/> + + production.cost + form + production_cost_form + + + production.cost + tree + production_cost_tree + diff --git a/view/production_cost_form.xml b/view/production_cost_form.xml new file mode 100644 index 0000000..aed2c35 --- /dev/null +++ b/view/production_cost_form.xml @@ -0,0 +1,21 @@ + +
+