trytonpsk-farming/production.py

672 lines
25 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.
import copy
from datetime import timedelta, date, datetime
from sql import Table
from trytond.model import fields, ModelView, ModelSQL
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval
from trytond.report import Report
from trytond.wizard import (
Wizard, StateReport, StateView, Button, StateTransition)
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
class Production(metaclass=PoolMeta):
__name__ = 'production'
STATES = {
'readonly': Eval('state') == 'done',
}
pack = fields.Many2One('production', 'Pack')
subproductions = fields.One2Many('production', 'pack', 'Sub-Productions')
move_phytos = fields.One2Many('stock.lot.phyto.move', 'pack', 'Move Phyto')
tasks = fields.One2Many('production.task', 'production', 'Tasks')
customer = fields.Many2One('party.party', 'Customer', states=STATES)
delivery_date = fields.Date('Delivery Date', states=STATES)
factor_packing = fields.Float('Factor Packing')
units = fields.Function(fields.Float('Units', digits=(16, 2)), 'get_units')
notes = fields.Text('Notes')
primary = fields.Boolean('Primary')
@classmethod
def __setup__(cls):
super(Production, cls).__setup__()
cls.state_string = cls.state.translated('state')
@classmethod
def _get_origin(cls):
return super()._get_origin() | {'sale.line'}
def on_change_with_unit_qty(self, name=None):
if self.quantity and self.uom:
res = self.quantity * self.uom.rate
return res
def get_units(self, name=None):
res = []
for line in self.inputs:
if line.product.farming:
res.append(line.quantity * line.uom.factor)
for p in self.subproductions:
res.append(p.units)
return sum(res)
def get_boxes(self, name=None):
if self.packing_qty and self.packing_uom:
return self.packing_qty * self.packing_uom.factor
@classmethod
def assign_lot(cls, productions):
pool = Pool()
Lot = pool.get('stock.lot')
MovePhyto = pool.get('stock.lot.phyto.move')
for production in productions:
delivery_date = production.delivery_date
if production.state == 'assigned':
continue
for input in production.inputs:
has_origin = MovePhyto.search_read(
[('origin', '=', str(input))], limit=1
)
if input.product.farming and len(has_origin) <= 0:
lots = Lot.search([
('product', '=', input.product.id),
('balance', '>', 0)
], order=[('create_date', 'ASC')])
required_quantity = input.quantity
for lot in lots:
if required_quantity > 0:
move_in = 0
lot_balance = lot.balance
if required_quantity >= lot_balance:
lot.balance_export += lot_balance
lot.balance = 0
required_quantity -= lot_balance
move_in = lot_balance
elif required_quantity < lot_balance:
lot.balance -= required_quantity
lot.balance_export += required_quantity
move_in = required_quantity
required_quantity = 0
move_phyto = MovePhyto(
lot=lot.id,
origin=(input),
date_move=delivery_date,
move_in=move_in,
move_out=0
)
move_phyto.save()
lot.save()
else:
break
if required_quantity > 0:
raise UserError(
gettext(
'farming.msg_missing_quantity_lot',
product=input.product.name))
@classmethod
def draft(cls, productions):
for pd in productions:
if pd.subproductions:
cls.draft(pd.subproductions)
super(Production, cls).draft(productions)
@classmethod
def wait(cls, productions):
for pd in productions:
for move in pd.inputs:
move.origin = str(pd)
move.save()
if pd.subproductions:
cls.wait(pd.subproductions)
super(Production, cls).wait(productions)
@classmethod
def assign(cls, productions):
super(Production, cls).assign(productions)
for pd in productions:
if pd.subproductions:
cls.assign(pd.subproductions)
# cls.run(pd.subproductions)
@classmethod
def run(cls, productions):
super(Production, cls).run(productions)
for pd in productions:
if pd.subproductions:
cls.run(pd.subproductions)
@classmethod
def done(cls, productions):
super(Production, cls).done(productions)
for pd in productions:
cls.assign_lot([pd])
if pd.subproductions:
cls.done(pd.subproductions)
@classmethod
def copy(cls, records, default=None):
pass
class ProductionTask(ModelView, ModelSQL):
"Production Task"
__name__ = 'production.task'
STATES = {
'readonly': Eval('state') == 'finished',
}
operation = fields.Many2One('production.routing.operation', 'Operation',
required=True, states=STATES)
effective_date = fields.Date('Effective Date ', states=STATES,
required=True)
employee = fields.Many2One('company.employee', 'Employee',
states=STATES)
quantity = fields.Float('Quantity', states=STATES, required=True)
goal = fields.Integer('Goal', states={'readonly': True},
help="In minutes")
start_time = fields.DateTime('Start Time')
end_time = fields.DateTime('End Time', states=STATES)
planned_end_time = fields.Function(fields.DateTime('Planned End Time'),
'get_planned_end_time')
total_time = fields.Integer('Total Time', states={'readonly': True},
help="In minutes")
performance = fields.Function(fields.Float('Performance', digits=(4, 2)),
'get_performance')
production = fields.Many2One('production', 'Production', states=STATES)
customer = fields.Many2One('party.party', 'Customer')
reference = fields.Many2One('product.product', 'Product',
domain=[('type', '=', 'goods')])
units = fields.Integer('Units')
uom = fields.Many2One('product.uom', 'Uom')
factor = fields.Integer('Factor')
work_center = fields.Many2One('production.work.center', 'Work Center')
notes = fields.Char('Notes', states=STATES)
state = fields.Selection([
('draft', 'Draft'),
('processing', 'Processing'),
('finished', 'Finished'),
], 'State', select=True)
@classmethod
def __setup__(cls):
super(ProductionTask, cls).__setup__()
cls._buttons.update({
'duplicate_wizard': {},
'finishButton': {}
})
@classmethod
@ModelView.button
def finishButton(cls, records):
for rec in records:
rec.end_time = datetime.now()
rec.state = 'finished'
rec.total_time = int((rec.end_time - rec.start_time).seconds / 60)
rec.save()
@staticmethod
def default_state():
return 'draft'
@staticmethod
def default_effective_date():
return date.today()
@staticmethod
def default_start_time():
return datetime.now()
@staticmethod
def default_end_time():
return datetime.now()
@classmethod
@ModelView.button_action('farming.act_wizard_production_duplicate_task')
def duplicate_wizard(cls, records):
pass
def get_planned_end_time(self, name):
if self.start_time and self.goal:
return self.start_time + timedelta(minutes=self.goal)
@fields.depends('production')
def on_change_with_customer(self, name=None):
if self.production:
return self.production.customer.id
@fields.depends('production')
def on_change_with_reference(self, name=None):
if self.production:
return self.production.product.id
@fields.depends('operation', 'quantity')
def on_change_with_goal(self, name=None):
if self.operation and self.quantity and self.operation.productivity:
return int((self.operation.productivity / 60) * self.quantity)
@fields.depends('start_time', 'end_time')
def on_change_with_total_time(self, name=None):
if self.start_time and self.end_time:
return int((self.end_time - self.start_time).seconds / 60)
def get_performance(self, name=None):
if self.total_time and self.goal:
return round(self.goal / self.total_time, 2)
class ProductionSummaryStart(ModelView):
'Production Report Start'
__name__ = 'production.summary.start'
company = fields.Many2One('company.company', 'Company', required=True)
start_date = fields.Date("Start Date", required=True)
end_date = fields.Date("End Date", required=True)
@staticmethod
def default_company():
return Transaction().context.get('company')
@staticmethod
def default_start_date():
return date.today()
@staticmethod
def default_end_date():
return date.today()
class ProductionSummary(Wizard):
'Purchase Analytic Report'
__name__ = 'production.summary'
start = StateView(
'production.summary.start',
'farming.production_summary_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateReport('production.summary_report')
def do_print_(self, action):
data = {
'company': self.start.company.id,
'start_date': self.start.start_date,
'end_date': self.start.end_date,
}
return action, data
def transition_print_(self):
return 'end'
class ProductionSummaryReport(Report):
__name__ = 'production.summary_report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Company = pool.get('company.company')
Production = pool.get('production')
Product = pool.get('product.product')
records = Production.search([
('company', '=', data['company']),
('pack', '=', None),
('planned_date', '>=', data['start_date']),
('planned_date', '<=', data['end_date']),
], order=[('id', 'ASC')])
inputs = {}
types = {'solidos': {}, 'bouquets': {}}
styles = []
styles_append = styles.append
products = []
products_append = products.append
locations = set()
for rec in records:
state = rec.state
rec_id = rec.id
types['solidos'][rec_id] = rec
locations.add(rec.warehouse.storage_location.id)
for inp in rec.inputs:
_inputs = [inp]
if inp.product.template.producible and inp.production:
_inputs.extend(inp.production.inputs)
types['bouquets'][rec_id] = rec
for _inp in _inputs:
if _inp.style:
styles_append(_inp)
product = _inp.product
product_id = str(product.id)
products_append(product_id)
cat = None
if product.categories:
cat = product.categories[0]
cat_id = str(cat.id if cat else 'none')
cat_name = cat.name if cat else 'none'
key = cat_id + '_' + product_id
qty_pending = 0
if state != 'done':
qty_pending = _inp.quantity
if key not in inputs.keys():
inputs[key] = {
'id': product_id,
'category': cat_name,
'product': product.rec_name,
'quantity': _inp.quantity,
'uom': product.default_uom.name,
'solidos': 0,
'quantity_pending': qty_pending
}
else:
qty = inputs[key]['quantity']
pend = inputs[key]['quantity_pending']
inputs[key]['quantity'] = qty + _inp.quantity
inputs[key]['quantity_pending'] = pend + qty_pending
with Transaction().set_context({'locations': list(locations)}):
products = Product.search_read(
[('id', 'in', products)],
fields_names=['quantity'])
products = {str(p['id']): p for p in products}
totals = {
'box': {
'solidos': 0,
'bouquets': 0,
'process': 0,
'done': 0
},
'stem': {
'solidos': 0,
'bouquets': 0,
'process': 0,
'done': 0}
}
_solidos = []
for s in list(types['solidos'].keys()):
if s in types['bouquets'].keys():
del types['solidos'][s]
else:
quantity = types['solidos'][s].quantity
units = types['solidos'][s].units
state = types['solidos'][s].state
flag = 0
for _input in types['solidos'][s].inputs:
if _input.uom.symbol == 'STEM':
if state != 'done':
cat_id = str(
_input.product.categories[0].id) if _input.product.categories else 'none'
key = cat_id + '_' + str(_input.product.id)
inputs[key]['solidos'] += _input.quantity
flag += 1
if flag == 1:
_solidos.append({
'input': _input,
'pcc': _input.production_input
})
else:
_solidos.append({'input': _input, 'pcc': None})
state = 'done' if state == 'done' else 'process'
totals = cls.get_values(
totals, 'solidos', state, units, quantity)
_bouquets = []
for b in types['bouquets'].values():
flag = 0
for _input in b.inputs:
if _input.uom.symbol not in ['u', 'STEM']:
flag += 1
if flag == 1:
pcc = _input.production_input
_bouquets.append({'input': _input, 'pcc': pcc})
quantity = pcc.quantity
units = pcc.units
state = pcc.state
state = 'done' if state == 'done' else 'process'
totals = cls.get_values(
totals, 'bouquets', state, units, quantity)
else:
_bouquets.append({
'input': _input,
'pcc': None,
'factor_packing': _input.quantity/_input.production_input.quantity
})
report_context['records'] = records
report_context['inputs'] = dict(sorted(inputs.items())).values()
report_context['solidos'] = _solidos
report_context['bouquets'] = _bouquets
report_context['styles'] = styles
report_context['totals'] = totals
report_context['products'] = products
report_context['start_date'] = data['start_date']
report_context['end_date'] = data['end_date']
report_context['company'] = Company(data['company'])
return report_context
@classmethod
def get_values(cls, totals, type, state, units, quantity):
totals['stem'][type] += units
totals['stem'][state] += units
totals['box'][type] += quantity
totals['box'][state] += quantity
return totals
class MaterialsForecastStart(ModelView):
'Materials Forecast Start'
__name__ = 'production.materials_forecast.start'
company = fields.Many2One('company.company', 'Company', required=True)
date_ = fields.Date("Date", required=True)
storage = fields.Many2One('stock.location', 'Location', required=True,
domain=[('type', '=', 'storage')])
@staticmethod
def default_company():
return Transaction().context.get('company')
class MaterialsForecast(Wizard):
'Materials Forecast'
__name__ = 'production.materials_forecast'
start = StateView(
'production.materials_forecast.start',
'farming.production_materials_forecast_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Print', 'print_', 'tryton-ok', default=True),
])
print_ = StateReport('production.materials_forecast.report')
def do_print_(self, action):
data = {
'company': self.start.company.id,
'date': self.start.date_,
'storage': self.start.storage.id,
}
return action, data
def transition_print_(self):
return 'end'
class MaterialsForecastReport(Report):
'Materials Forecast Report'
__name__ = 'production.materials_forecast.report'
@classmethod
def get_context(cls, records, header, data):
report_context = super().get_context(records, header, data)
pool = Pool()
Company = pool.get('company.company')
Production = pool.get('production')
Purchase = pool.get('purchase.purchase')
Product = pool.get('product.product')
default_dates = {}
for i in range(6):
d = data['date'] + timedelta(days=i)
default_dates[str(d)] = {
'stock': 0,
'in': 0,
'pcc': 0,
'balance': 0,
}
report_context['date' + str(1+i)] = str(d)
last_date = data['date'] + timedelta(days=5)
productions = Production.search([
('company', '=', data['company']),
('planned_date', '>=', data['date']),
('planned_date', '<=', last_date),
], order=[('planned_date', 'ASC')])
products = {}
for pcc in productions:
for inp in pcc.inputs:
if not inp.product.template.purchasable:
continue
if inp.product not in products.keys():
products[inp.product] = copy.deepcopy(default_dates)
products[inp.product]['name'] = inp.product.rec_name
products[inp.product]['uom'] = inp.product.template.default_uom.symbol
products[inp.product][str(pcc.planned_date)]['pcc'] += inp.quantity
purchases = Purchase.search([
('company', '=', data['company']),
('delivery_date', '>=', data['date']),
('delivery_date', '<=', last_date),
('state', 'in', ['quotation', 'processing', 'done', 'confirmed']),
], order=[('delivery_date', 'ASC')])
for pch in purchases:
for line in pch.lines:
if line.product not in products.keys():
products[line.product] = copy.deepcopy(default_dates)
products[line.product]['name'] = line.product.rec_name
products[line.product]['uom'] = line.product.template.default_uom.symbol
products[line.product][str(pch.delivery_date)]['in'] += line.quantity
date_ = data['date'] - timedelta(days=1)
ctx = {
'locations': [data['storage']],
'stock_date_end': date_,
}
def get_balance(qty, product_date):
res = qty + product_date['in'] - product_date['pcc']
return res
# Search stock of products
product_ids = [pd.id for pd in products.keys()]
with Transaction().set_context(ctx):
_products = Product.search([
('id', 'in', product_ids),
('template.purchasable', '=', True),
], order=[('template.name', 'ASC')]
)
for pt in _products:
stock = pt.quantity
for dt in default_dates.keys():
product_date = products[pt][str(dt)]
product_date['stock'] = stock
value = get_balance(stock, product_date)
product_date['balance'] = value
stock = value
report_context['records'] = products.values()
report_context['date'] = data['date']
report_context['company'] = Company(data['company'])
return report_context
class ProductionForceDraft(Wizard):
'Production Force Draft'
__name__ = 'production.force_draft'
start_state = 'force_draft'
force_draft = StateTransition()
def _reset_data(self, prod, delete=None):
pool = Pool()
Move_Phyto = pool.get('stock.lot.phyto.move')
Lot = pool.get('stock.lot')
stock_move = Table('stock_move')
cursor = Transaction().connection.cursor()
lots_to_update = []
inputs_to_update = []
if not delete:
outputs_to_update = [out.id for out in prod.outputs]
origin_inputs = []
for rec in prod.inputs:
origin_inputs.append(str(rec))
inputs_to_update.append(rec.id)
moves_to_update = outputs_to_update + inputs_to_update
if moves_to_update:
cursor.execute(*stock_move.update(
columns=[stock_move.state],
values=['draft'],
where=stock_move.id.in_(moves_to_update))
)
move_phytos = Move_Phyto.search([('origin', 'in', origin_inputs)])
lots_to_update.extend(set(m.lot for m in move_phytos))
Move_Phyto.delete(move_phytos)
prod.state = 'draft'
prod.save()
else:
origin_inputs = []
inputs_to_delete = []
for rec in prod.inputs:
origin_inputs.append(str(rec))
inputs_to_update.append(rec.id)
move_phytos = Move_Phyto.search([('origin', 'in', origin_inputs)])
lots_to_update.extend(set(m.lot for m in move_phytos))
Move_Phyto.delete(move_phytos)
moves_to_update = [r.id for r in prod.outputs] + inputs_to_delete
if moves_to_update:
cursor.execute(*stock_move.update(
columns=[stock_move.state],
values=['draft'],
where=stock_move.id.in_(moves_to_update))
)
prod.state = 'draft'
prod.save()
if lots_to_update:
Lot.recompute_balance(list(set(lots_to_update)))
def transition_force_draft(self):
id_ = Transaction().context['active_id']
Production = Pool().get('production')
if id_:
prod = Production(id_)
self._reset_data(prod)
for pr in prod.subproductions:
self._reset_data(pr, True)
return 'end'
class Operation(metaclass=PoolMeta):
__name__ = 'production.routing.operation'
productivity = fields.Float('Productivity', digits=(6, 2),
help='Seconds neccesary for make one unit')
class DuplicateTask(Wizard):
'Duplicate Task'
__name__ = 'production.task.duplicate'
start_state = 'duplicate_lines'
duplicate_lines = StateTransition()
def transition_duplicate_lines(self):
active_id = Transaction().context.get('active_id')
Task = Pool().get('production.task')
task = Task(active_id)
default = {'production': task.production}
Task.copy([task], default=default)
return 'end'