trytonpsk-production_accoun.../production.py

642 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')
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 = line.product
try:
producibles[product.id]['quantity'].append(line.quantity)
except:
outputs = BOMOutput.search([
('product', '=', product.id)
])
if not outputs:
continue
output = outputs[0]
producibles[product] = {
'quantity': [line.quantity],
'bom': output.bom,
}
self.create_production_moves(producibles, journal, period_id)
self.create_stock_moves(producibles)
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