# 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 = [] if not delete: outputs_to_update = [out.id for out in prod.outputs] origin_inputs = [] inputs_to_update = [] 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'