646 lines
22 KiB
Python
646 lines
22 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
from decimal import Decimal
|
|
|
|
from trytond.pool import PoolMeta, Pool
|
|
from trytond.model import fields, ModelView
|
|
from trytond.pyson import Eval
|
|
from trytond.transaction import Transaction
|
|
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)
|
|
|
|
|
|
def round_dec(number):
|
|
if not isinstance(number, Decimal):
|
|
number = Decimal(number)
|
|
return Decimal(number.quantize(Decimal('.01')))
|
|
|
|
|
|
class Production(metaclass=PoolMeta):
|
|
__name__ = 'production'
|
|
in_account_move = fields.Many2One('account.move', 'In Account Move',
|
|
states={'readonly': True})
|
|
out_account_move = fields.Many2One('account.move', 'Out Account Move',
|
|
states={'readonly': True})
|
|
warehouse_origin = fields.Many2One('stock.location', 'Warehouse Origin',
|
|
domain=[('type', '=', 'warehouse')])
|
|
warehouse_target = fields.Many2One('stock.location', 'Warehouse Target',
|
|
domain=[('type', '=', 'warehouse')])
|
|
warehouse_moves = fields.One2Many('stock.move', 'origin', 'Warehouse Moves')
|
|
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')
|
|
analytic_account = fields.Many2One('analytic_account.account',
|
|
'Analytic Account', domain=[
|
|
('type', 'in', ['normal', 'distribution']),
|
|
('company', '=', Eval('context', {}).get('company', -1))
|
|
], states={
|
|
'readonly': ~Eval('state').in_(['draft', 'request']),
|
|
})
|
|
|
|
@staticmethod
|
|
def default_warehouse_origin():
|
|
config = Pool().get('production.configuration')(1)
|
|
if config.warehouse_origin:
|
|
return config.warehouse_origin.id
|
|
return
|
|
|
|
@staticmethod
|
|
def default_warehouse_target():
|
|
config = Pool().get('production.configuration')(1)
|
|
if config.warehouse_origin:
|
|
return config.warehouse_target.id
|
|
return
|
|
|
|
@classmethod
|
|
def wait(cls, records):
|
|
super(Production, cls).wait(records)
|
|
# for rec in records:
|
|
# cls.create_account_move(rec, 'wait', 'in_account_move')
|
|
# FIXME
|
|
# rec.create_stock_move('in')
|
|
|
|
@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')
|
|
for output in rec.outputs:
|
|
unit_price = rec._compute_unit_cost()
|
|
Move.write([output], {'unit_price': unit_price})
|
|
rec.update_product_cost(output.product)
|
|
#rec.create_stock_move('out')
|
|
|
|
def _compute_unit_cost(self):
|
|
total_cost = self.total_cost
|
|
if not total_cost and self.cost:
|
|
total_cost = self.cost
|
|
|
|
ouput = self.outputs[0]
|
|
new_cost_price = round_dec(total_cost / Decimal(self.quantity))
|
|
return new_cost_price
|
|
|
|
def get_perfomance(self, name=None):
|
|
res = Decimal(0)
|
|
if self.bom:
|
|
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)))
|
|
return res
|
|
|
|
def update_product_cost(self, product):
|
|
ProductCost = Pool().get('product.cost_price')
|
|
products = ProductCost.search([
|
|
('product', '=', product.id),
|
|
])
|
|
new_cost = self._compute_unit_cost()
|
|
ProductCost.write(products, {'cost_price': new_cost})
|
|
|
|
@classmethod
|
|
def get_production_lines(cls, bom, materials_cost, date_, factor, args=None):
|
|
""" Get Production Account Move Lines """
|
|
lines = []
|
|
cost_finished_production = []
|
|
values = {}
|
|
output = bom.outputs[0]
|
|
account_expense = output.product.account_expense_used
|
|
materials_line = {
|
|
'description': '',
|
|
'account': account_expense.id,
|
|
'debit': 0,
|
|
'credit': materials_cost,
|
|
}
|
|
analytic = args.get('analytic', None)
|
|
cls.set_analytic_lines(materials_line, date_, analytic)
|
|
lines.append(materials_line)
|
|
cost_finished_production.append(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)
|
|
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
|
|
|
|
@classmethod
|
|
def get_consumption_lines(cls, inputs, factor, date_, args=None):
|
|
""" Get Consumption Account Move Lines """
|
|
lines = []
|
|
# balance = []
|
|
costs_lines = {}
|
|
for _in in inputs:
|
|
product = _in.product
|
|
if product.cost_price == 0:
|
|
continue
|
|
category = product.template.account_category
|
|
if not category or not category.account_stock:
|
|
raise UserError(gettext(
|
|
'production_accounting.msg_category_account_stock',
|
|
product=product.rec_name
|
|
))
|
|
account_stock_id = category.account_stock.id
|
|
credit = round_dec(product.cost_price * Decimal(_in.quantity) * Decimal(factor))
|
|
credit = round(credit, 0)
|
|
_line = {
|
|
'description': product.template.name,
|
|
'account': account_stock_id,
|
|
'debit': 0,
|
|
'credit': credit
|
|
}
|
|
lines.append(_line)
|
|
# balance.append(credit)
|
|
account_expense_id = category.account_expense.id
|
|
try:
|
|
costs_lines[account_expense_id].append(credit)
|
|
except Exception as e:
|
|
costs_lines[account_expense_id] = [credit]
|
|
|
|
material_costs = []
|
|
for account_id, ldebit in costs_lines.items():
|
|
debit = sum(ldebit)
|
|
material_costs.append(debit)
|
|
line_ = {
|
|
'description': '',
|
|
'account': account_id,
|
|
'debit': debit,
|
|
'credit': 0,
|
|
}
|
|
analytic = args.get('analytic', None)
|
|
cls.set_analytic_lines(line_, date_, analytic)
|
|
lines.append(line_)
|
|
|
|
values = {'material_costs': round_dec(sum(material_costs))}
|
|
return lines, values
|
|
|
|
@classmethod
|
|
def create_account_move(cls, rec, kind, field=None):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
Journal = pool.get('account.journal')
|
|
Period = pool.get('account.period')
|
|
|
|
journals = Journal.search([
|
|
('code', '=', 'STO')
|
|
])
|
|
|
|
if journals:
|
|
journal = journals[0]
|
|
|
|
if not rec.planned_date:
|
|
raise UserError(
|
|
gettext('production_accounting.msg_planned_date_required')
|
|
)
|
|
analytic_ctx = {'analytic': rec.analytic_account}
|
|
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 == 'done':
|
|
date_ = rec.effective_date
|
|
lines, values = cls.get_production_lines(
|
|
rec.bom,
|
|
rec.material_costs,
|
|
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)
|
|
|
|
period_id = Period.find(rec.company.id, date=date_)
|
|
|
|
move, = Move.create([{
|
|
'journal': journal.id,
|
|
'period': period_id,
|
|
'date': date_,
|
|
'state': 'draft',
|
|
'lines': [('create', lines)],
|
|
'origin': str(rec),
|
|
'description': rec.number,
|
|
}])
|
|
|
|
Move.post([move])
|
|
to_update[field] = move.id
|
|
cls.write([rec], to_update)
|
|
|
|
def create_stock_move(self, kind, field=None):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
to_create = []
|
|
warehouse_pdc = self.warehouse.storage_location.id
|
|
if kind == 'in':
|
|
records = self.inputs
|
|
from_warehouse = self.warehouse_origin
|
|
to_warehouse = self.warehouse
|
|
date_ = self.planned_date
|
|
elif kind == 'out':
|
|
records = self.outputs
|
|
from_warehouse = self.warehouse
|
|
to_warehouse = self.warehouse_target
|
|
date_ = self.effective_date
|
|
|
|
from_location_id = from_warehouse.storage_location.id
|
|
to_location_id = to_warehouse.storage_location.id
|
|
for rec in records:
|
|
to_create.append({
|
|
'product': rec.product,
|
|
'quantity': rec.quantity,
|
|
'from_location': from_location_id,
|
|
'to_location': to_location_id,
|
|
'origin': str(self),
|
|
'effective_date': date_,
|
|
'unit_price': rec.unit_price,
|
|
'uom': rec.uom.id,
|
|
'state': 'draft',
|
|
})
|
|
|
|
moves = Move.create(to_create)
|
|
Move.do(moves)
|
|
|
|
@classmethod
|
|
def set_analytic_lines(cls, line, date, analytic_account):
|
|
"Yield analytic lines for the accounting line and the date"
|
|
if not analytic_account:
|
|
return
|
|
|
|
lines = []
|
|
amount = line['debit'] or line['credit']
|
|
for account, amount in analytic_account.distribute(amount):
|
|
analytic_line = {}
|
|
analytic_line['debit'] = amount if line['debit'] else Decimal(0)
|
|
analytic_line['credit'] = amount if line['credit'] else Decimal(0)
|
|
analytic_line['account'] = account
|
|
analytic_line['date'] = date
|
|
lines.append(analytic_line)
|
|
line['analytic_lines'] = [('create', lines)]
|
|
|
|
|
|
class ProductionReport(CompanyReport):
|
|
'Production Report'
|
|
__name__ = 'production.report'
|
|
|
|
|
|
class DoneProductions(Wizard):
|
|
'Done Productions'
|
|
__name__ = 'production.done_productions'
|
|
start_state = 'done_productions'
|
|
done_productions = StateTransition()
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(DoneProductions, cls).__setup__()
|
|
|
|
def transition_done_productions(self):
|
|
Production = Pool().get('production')
|
|
ids = Transaction().context['active_ids']
|
|
if not ids:
|
|
return 'end'
|
|
|
|
productions = Production.browse(ids)
|
|
for prd in productions:
|
|
if prd.state != 'running':
|
|
continue
|
|
Production.done([prd])
|
|
|
|
return 'end'
|
|
|
|
|
|
class ProcessProductionAsyncStart(ModelView):
|
|
'Process Production Async Start'
|
|
__name__ = 'production.process_production_async.start'
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
shop = fields.Many2One('sale.shop', 'Shop', required=True)
|
|
date = fields.Date('Start Date', required=True)
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
|
|
class ProcessProductionAsync(Wizard):
|
|
'Process Production Async'
|
|
__name__ = 'production.process_production_async'
|
|
start = StateView('production.process_production_async.start',
|
|
'production_accounting.process_production_async_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Ok', 'accept', 'tryton-ok', default=True),
|
|
])
|
|
accept = StateTransition()
|
|
|
|
def create_move(self, lines, journal, period_id):
|
|
Move = Pool().get('account.move')
|
|
move, = Move.create([{
|
|
'journal': journal.id,
|
|
'period': period_id,
|
|
'date': self.start.date,
|
|
'state': 'draft',
|
|
'lines': [('create', lines)],
|
|
'description': '',
|
|
# 'origin': ,
|
|
}])
|
|
Move.post([move])
|
|
print('Asiento creado No ', move.number)
|
|
|
|
def create_stock_moves(self, producibles):
|
|
Move = Pool().get('stock.move')
|
|
date_ = self.start.date
|
|
in_moves = []
|
|
out_moves = []
|
|
from_location_id = self.start.shop.warehouse.storage_location.id
|
|
to_location_id = self.start.shop.warehouse.production_location.id
|
|
for product, values in producibles.items():
|
|
output = values['bom'].outputs[0]
|
|
quantity = sum(values['quantity'])
|
|
factor = quantity / output.quantity
|
|
for input in values['bom'].inputs:
|
|
_product = input.product
|
|
qty = round(
|
|
input.quantity * factor,
|
|
_product.template.default_uom.digits
|
|
)
|
|
in_moves.append({
|
|
'product': _product.id,
|
|
'uom': _product.template.default_uom.id,
|
|
'effective_date': date_,
|
|
'quantity': qty,
|
|
'from_location': from_location_id,
|
|
'to_location': to_location_id,
|
|
})
|
|
out_moves.append({
|
|
'product': product.id,
|
|
'uom': product.template.default_uom.id,
|
|
'effective_date': date_,
|
|
'quantity': quantity,
|
|
'unit_price': product.cost_price,
|
|
'from_location': to_location_id,
|
|
'to_location': from_location_id,
|
|
})
|
|
res1 = Move.create(in_moves)
|
|
res2 = Move.create(out_moves)
|
|
Move.do(res1 + res2)
|
|
|
|
def create_production_moves(self, producibles, journal, period_id):
|
|
Production = Pool().get('production')
|
|
accounts = {}
|
|
lines = []
|
|
analytic = self.start.shop.analytic_account
|
|
date_ = self.start.date
|
|
|
|
for product, values in producibles.items():
|
|
quantity = sum(values['quantity'])
|
|
bom = values['bom']
|
|
output = bom.outputs[0]
|
|
inputs = bom.inputs
|
|
factor = quantity / output.quantity
|
|
for input in inputs:
|
|
_product = input.product
|
|
account_expense = _product.account_expense_used
|
|
account_stock = _product.account_stock_used
|
|
input_amount = Decimal(
|
|
input.quantity * float(_product.cost_price) * factor
|
|
)
|
|
try:
|
|
accounts[account_stock].append(input_amount)
|
|
except:
|
|
accounts[account_stock] = [input_amount]
|
|
|
|
material_costs = []
|
|
for acc, amount in accounts.items():
|
|
amount = Decimal(round(sum(amount), 2))
|
|
line_ = {
|
|
'description': '',
|
|
'account': acc.id,
|
|
'debit': 0,
|
|
'credit': amount,
|
|
}
|
|
lines.append(line_)
|
|
material_costs.append(amount)
|
|
|
|
material_costs = sum(material_costs)
|
|
line_ = {
|
|
'description': '',
|
|
'account': account_expense.id,
|
|
'debit': material_costs,
|
|
'credit': 0,
|
|
}
|
|
Production.set_analytic_lines(line_, date_, analytic)
|
|
lines.append(line_)
|
|
self.create_move(lines, journal, period_id)
|
|
|
|
# Second move for load stock and discharge production
|
|
lines2 = []
|
|
dc_amount = []
|
|
for dc in bom.direct_costs:
|
|
account_id = dc.product.account_expense_used.id
|
|
amount = Decimal(dc.quantity) * dc.product.cost_price
|
|
amount = Decimal(round(float(amount) * factor, 2))
|
|
dc_amount.append(amount)
|
|
line_ = {
|
|
'description': '',
|
|
'account': account_id,
|
|
'debit': 0,
|
|
'credit': amount,
|
|
}
|
|
Production.set_analytic_lines(line_, date_, analytic)
|
|
lines2.append(line_)
|
|
line_ = {
|
|
'description': '',
|
|
'account': account_expense.id,
|
|
'debit': 0,
|
|
'credit': material_costs,
|
|
}
|
|
stock_amount = material_costs + sum(dc_amount)
|
|
lines2.append(line_)
|
|
line_ = {
|
|
'description': '',
|
|
'account': output.product.account_stock_used.id,
|
|
'debit': stock_amount,
|
|
'credit': 0,
|
|
}
|
|
lines2.append(line_)
|
|
self.create_move(lines2, journal, period_id)
|
|
|
|
def transition_accept(self):
|
|
pool = Pool()
|
|
SaleLine = pool.get('sale.line')
|
|
# Shop = pool.get('sale.shop')
|
|
Period = pool.get('account.period')
|
|
Journal = pool.get('account.journal')
|
|
Configuration = pool.get('production.configuration')
|
|
configuration = Configuration(1)
|
|
journals = Journal.search([
|
|
('code', '=', 'STO')
|
|
])
|
|
|
|
if journals:
|
|
journal = journals[0]
|
|
|
|
period_id = Period.find(self.start.company.id, date=self.start.date)
|
|
|
|
BOMOutput = pool.get('production.bom.output')
|
|
dom = [
|
|
('sale.state', 'in', ['processing', 'done']),
|
|
('sale.shop', '=', self.start.shop.id),
|
|
('sale.sale_date', '=', self.start.date),
|
|
('product.template.producible', '=', True),
|
|
('type', '=', 'line'),
|
|
('produced', '=', False),
|
|
]
|
|
|
|
lines = SaleLine.search(dom)
|
|
if not lines:
|
|
return 'end'
|
|
|
|
# shop = Shop(self.start.shop.id)
|
|
producibles = {}
|
|
|
|
for line in lines:
|
|
product_id = line.product.id
|
|
try:
|
|
producibles[product_id]['quantity'].append(line.quantity)
|
|
except:
|
|
outputs = BOMOutput.search([
|
|
('product', '=', product_id)
|
|
])
|
|
if not outputs:
|
|
continue
|
|
output = outputs[0]
|
|
producibles[product_id] = {
|
|
'quantity': [line.quantity],
|
|
'bom': output.bom,
|
|
}
|
|
if configuration.production_accounting:
|
|
self.create_production_moves(producibles, journal, period_id)
|
|
self.create_stock_moves(producibles)
|
|
SaleLine.write(lines, {'produced': True})
|
|
return 'end'
|
|
|
|
|
|
class ProductionDetailedStart(ModelView):
|
|
'Production Detailed Start'
|
|
__name__ = 'production.detailed.start'
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
grouped = fields.Boolean('Grouped', help='Grouped by products')
|
|
start_date = fields.Date('Start Date')
|
|
end_date = fields.Date('End Date', required=True)
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@staticmethod
|
|
def default_start_date():
|
|
Date = Pool().get('ir.date')
|
|
return Date.today()
|
|
|
|
@staticmethod
|
|
def default_end_date():
|
|
Date = Pool().get('ir.date')
|
|
return Date.today()
|
|
|
|
|
|
class ProductionDetailed(Wizard):
|
|
'Production Detailed'
|
|
__name__ = 'production.detailed'
|
|
start = StateView('production.detailed.start',
|
|
'production_accounting.production_detailed_start_view_form', [
|
|
Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('Print', 'print_', 'tryton-ok', default=True),
|
|
])
|
|
print_ = StateReport('production.detailed_report')
|
|
|
|
def do_print_(self, action):
|
|
data = {
|
|
'ids': [],
|
|
'company': self.start.company.id,
|
|
'start_date': self.start.start_date,
|
|
'end_date': self.start.end_date,
|
|
'grouped': self.start.grouped,
|
|
}
|
|
return action, data
|
|
|
|
def transition_print_(self):
|
|
return 'end'
|
|
|
|
|
|
class ProductionDetailedReport(Report):
|
|
'Production Detailed Report'
|
|
__name__ = 'production.detailed_report'
|
|
|
|
@classmethod
|
|
def get_context(cls, records, header, data):
|
|
report_context = super().get_context(records, header, data)
|
|
Production = Pool().get('production')
|
|
domain = [
|
|
('company', '=', data['company']),
|
|
('effective_date', '>=', data['start_date']),
|
|
('effective_date', '<=', data['end_date']),
|
|
]
|
|
fields_names = ['warehouse.name', 'location.name', 'effective_date',
|
|
'number', 'uom.name', 'quantity', 'cost', 'product.name', 'product.id']
|
|
productions = Production.search_read(domain, fields_names=fields_names)
|
|
if not data['grouped']:
|
|
records = productions
|
|
else:
|
|
records = {}
|
|
for p in productions:
|
|
key = str(p['product.']['id']) + p['location.']['name']
|
|
try:
|
|
records[key]['quantity'] += p['quantity']
|
|
records[key]['cost'] += p['cost']
|
|
except:
|
|
records[key] = p
|
|
records[key]['effective_date'] = None
|
|
records[key]['numero'] = None
|
|
records = records.values() if records else []
|
|
report_context['records'] = records
|
|
report_context['Decimal'] = Decimal
|
|
return report_context
|