# 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.model import ModelSQL, ModelView, fields from trytond.pool import PoolMeta, Pool from trytond.pyson import Eval, Bool, If from trytond.exceptions import UserWarning, UserError from trytond.i18n import gettext from trytond.transaction import Transaction from trytond.wizard import Wizard, StateView, StateAction, Button from trytond.modules.product import round_price from trytond.model.exceptions import ValidationError class ProductionTemplate(ModelSQL, ModelView): "Produciton Template" __name__ = 'production.template' name = fields.Char('Name', required=True) uom = fields.Many2One('product.uom', 'Uom', states = { 'required': True }) unit_digits = fields.Function(fields.Integer('Unit Digits'), 'on_change_with_unit_digits') quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) inputs = fields.Many2Many('production.template.inputs-product.template', 'production_template', 'template', "Inputs") outputs = fields.Many2Many('production.template.outputs-product.template', 'production_template', 'template', "Outputs") enology_products = fields.One2Many('production.template.line', 'production_template', 'Complementary Products') pass_feature = fields.Boolean('Pass on Feature') pass_quality = fields.Boolean('Pass Quality') pass_certification = fields.Boolean('Pass Certification') pass_quality_sample = fields.Boolean('Pass Quality Sample') cost_distribution_template = fields.Many2One( 'production.cost_price.distribution.template', "Default cost distribution template", domain=[ ('production_template', '=', Eval('id', 0)), ], depends=['id']) cost_distribution_templates = fields.One2Many( 'production.cost_price.distribution.template', 'production_template', "Cost Distribution Templates") transfer_wine_aging = fields.Boolean("Transfer Wine Aging") inputs_products = fields.Function(fields.One2Many('product.product', None, 'Products'), 'get_products', searcher='search_input_products') def get_products(self, name=None): products = [] for template in self.inputs: products += template.products return [x.id for x in products] @classmethod def search_input_products(cls, name, clause): Inputs = Pool().get('production.template.inputs-product.template') product = clause[-1] inputs = Inputs.search([('template.products', '=', product)]) return [('id', 'in', [x.production_template.id for x in inputs])] @fields.depends('uom') def on_change_with_unit_digits(self, name=None): if self.uom: return self.uom.digits return 2 @classmethod def validate(cls, records): super().validate(records) for record in records: record.check_input_uoms() record.check_cost_distribution() def check_input_uoms(self): if not self.uom: return category_uom = self.uom.category uoms = [i.default_uom.category for i in self.inputs] uoms.append(category_uom) if len(list(set(uoms))) > 1: raise UserError(gettext('agronomics.msg_uom_not_fit', production=self.rec_name, uom=self.uom.rec_name, uoms=",".join([x.rec_name for x in set(uoms)]))) def check_cost_distribution(self): if not self.cost_distribution_template: return output_templates = set([o for o in self.outputs]) for cost in self.cost_distribution_template.cost_distribution_templates: if cost.template not in output_templates: raise ValidationError( gettext('agronomics.msg_check_cost_distribution_template', production=self.rec_name)) class ProductionTemplateInputsProductTemplate(ModelSQL): 'Production Template Inputs- Product Template' __name__ = 'production.template.inputs-product.template' production_template = fields.Many2One('production.template', 'Production Template', ondelete='CASCADE', required=True) template = fields.Many2One('product.template', 'Template', ondelete='CASCADE', required=True) class ProductionTemplateOutputsProductTemplate(ModelSQL): 'Production Template Inputs- Product Template' __name__ = 'production.template.outputs-product.template' production_template = fields.Many2One('production.template', 'Production Template', ondelete='CASCADE', required=True) template = fields.Many2One('product.template', 'Product', ondelete='CASCADE', required=True) class ProductionTemplateLine(ModelSQL, ModelView): "Production Template Line" __name__ = 'production.template.line' product = fields.Many2One('product.product', 'Producte', required=True) uom = fields.Many2One('product.uom', 'Uom') unit_digits = fields.Function(fields.Integer('Unit Digits'), 'on_change_with_unit_digits') quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) production_template = fields.Many2One('production.template', 'Production Template') @fields.depends('uom') def on_change_with_unit_digits(self, name=None): if self.uom: return self.uom.digits return 2 @fields.depends('product') def on_change_with_uom(self): if not self.product: return return self.product.default_uom and self.product.default_uom.id class Production(metaclass=PoolMeta): __name__ = 'production' production_template = fields.Many2One('production.template', "Production Template", states={ 'readonly': ~Eval('state').in_(['request', 'draft']), }, depends=['state']) production_template_cost_distribution_templates = fields.Function( fields.Many2Many('production.cost_price.distribution.template', None, None, "Cost Distribution Templates"), 'on_change_with_production_template_cost_distribution_templates') enology_products = fields.One2Many('production.enology.product', 'production', "Enology Products", domain=[('product', 'in', Eval('allowed_enology_products')), If((Eval('state').in_(['waiting', 'draft'])), ('product.quantity', '>', 0), ())], states={ 'invisible': ~Bool(Eval('production_template')), 'readonly': ~Eval('state').in_(['request', 'draft']), }, depends=['allowed_enology_products', 'state']) output_distribution = fields.One2Many('production.output.distribution', 'production', "Output Distribution", # domain=[('product', 'in', Eval('allowed_ouput_products'))], states={ 'invisible': ~Bool(Eval('production_template')), 'readonly': Eval('state').in_(['cancelled', 'done']), }, depends=['allowed_output_products', 'state']) allowed_enology_products = fields.Function(fields.One2Many( 'product.product', None, 'Allowed Enology Products', readonly=True, context={ 'company': Eval('company', -1), }, depends=['company']), 'on_change_with_allowed_enology_products', setter='set_allowed_products') allowed_output_products = fields.Function(fields.One2Many( 'product.template', None, 'Allowed Output Products', readonly=True, context={ 'company': Eval('company', -1), }, depends=['company']), 'on_change_with_allowed_output_products', setter='set_allowed_products') cost_distributions = fields.One2Many( 'production.cost_price.distribution', 'origin', "Cost Distributions", domain=[ ('template', 'in', Eval('cost_distribution_templates')), ], states={ 'readonly': Eval('state').in_(['cancelled', 'done']), }, context={ 'company': Eval('company', -1), }, depends=['state', 'cost_distribution_template', 'cost_distribution_templates', 'company']) cost_distribution_template = fields.Many2One( 'production.cost_price.distribution.template', "Cost Distribution Template", domain=[ ('id', 'in', Eval('production_template_cost_distribution_templates')) ], states={ 'readonly': Eval('state').in_(['cancelled', 'done']), }, context={ 'company': Eval('company', -1), }, depends=['state', 'production_template_cost_distribution_templates', 'company']) cost_distribution_templates = fields.Function( fields.Many2Many('product.template', None, None, "Cost Product Templates", context={ 'company': Eval('company', -1), }, depends=['company']), 'on_change_with_cost_distribution_templates') pass_quality = fields.Boolean('Pass Quality') pass_certification = fields.Boolean('Pass Certification') pass_quality_sample = fields.Boolean('Pass Quality Sample') @classmethod def set_allowed_products(cls, productions, name, value): pass @fields.depends('production_template') def on_change_with_pass_quality(self): if self.production_template: return self.production_template.pass_quality @fields.depends('production_template') def on_change_with_pass_certification(self): if self.production_template: return self.production_template.pass_certification @fields.depends('production_template') def on_change_with_pass_quality_sample(self): if self.production_template: return self.production_template.pass_quality_sample @fields.depends('production_template') def on_change_production_template(self): if (self.production_template and self.production_template.cost_distribution_template): self.cost_distribution_template = \ self.production_template.cost_distribution_template @fields.depends('production_template') def on_change_with_allowed_enology_products(self, name=None): products = [] if not self.production_template: return [] for input_ in self.production_template.inputs: products += input_.products return [x.id for x in products] @fields.depends('production_template') def on_change_with_allowed_output_products(self, name=None): if not self.production_template: return [] return [x.id for x in self.production_template.outputs] @fields.depends('production_template', '_parent_production_template.cost_distribution_templates') def on_change_with_production_template_cost_distribution_templates(self, name=None): if self.production_template: return [s.id for s in self.production_template.cost_distribution_templates] @fields.depends('production_template') def on_change_with_cost_distribution_templates(self, name=None): if self.production_template: return [s.id for s in self.production_template.outputs] @classmethod def validate(cls, productions): super(Production, cls).validate(productions) for production in productions: production.check_cost_distribution() production.check_percentatge() def check_cost_distribution(self): if (self.state in ('cancelled', 'done') or not self.cost_distribution_template or not self.cost_distributions): return distribution_templates = set([c.template for c in self.cost_distribution_template.cost_distribution_templates]) for c in self.cost_distributions: if c.template not in distribution_templates: raise ValidationError( gettext('agronomics.msg_check_cost_distribution', production=self.rec_name)) def check_percentatge(self): if not self.cost_distributions: return percentatge = sum(template.percentatge for template in self.cost_distributions) if percentatge != 1: raise ValidationError( gettext('agronomics.msg_check_production_percentatge', production=self.rec_name, percentatge=percentatge * 100, )) @classmethod def wait(cls, productions): pool = Pool() Move = pool.get('stock.move') Uom = pool.get('product.uom') OutputDistribution = pool.get('production.output.distribution') CostDistribution = pool.get('production.cost_price.distribution') moves = [] delete = [] outputs = [] costs = [] delete_outputs = [] for production in productions: if not production.production_template: continue delete += [x for x in production.inputs] input_quantity = 0 template_qty = production.production_template.quantity for enology in production.enology_products: move = production._move('input', enology.product, enology.uom, enology.quantity) move.production_input = production moves.append(move) input_quantity += Uom.compute_qty(enology.uom, enology.quantity, production.production_template.uom, round=True) enology_products = (production.production_template and production.production_template.enology_products or []) for enology in enology_products: quantity = enology.quantity qty = quantity * (input_quantity or 1) / template_qty qty = enology.uom.round(qty) move = production._move('input', enology.product, enology.uom, float(qty)) move.production_input = production moves.append(move) for output_product in production.production_template.outputs: delete_outputs += [x for x in production.output_distribution] output_distribution = OutputDistribution() output_distribution.product = output_product output_distribution.uom = ( output_distribution.on_change_with_uom()) output_distribution.production = production outputs.append(output_distribution) if not production.cost_distributions: if production.cost_distribution_template: cost_distribution_template = production.cost_distribution_template elif production.production_template: cost_distribution_template = production.production_template.cost_distribution_template else: cost_distribution_template = None if cost_distribution_template: for c in cost_distribution_template.cost_distribution_templates: cost = CostDistribution() cost.template = c.template cost.percentatge = c.percentatge cost.origin = str(production) costs.append(cost) CostDistribution.save(costs) OutputDistribution.delete(delete_outputs) OutputDistribution.save(outputs) Move.save(moves) Move.delete(delete) super().wait(productions) def create_variant(self, template, pass_feature): Product = Pool().get('product.product') product = Product() product.template = template return product def copy_certification(self, new_product): products = [x.product for x in self.inputs if x.product.certification] if not self.pass_certification or len(products) != 1: return new_product certification = products[0].certification new_product.certification = certification return new_product def copy_quality_samples(self, new_product): ProductSample = Pool().get('product.product-quality.sample') products = [x.product for x in self.inputs if x.product.quality_samples] if not self.pass_quality_sample or len(products) != 1: return new_product samples = products[0].quality_samples new_samples =[] for sample in samples: product_sample = ProductSample() product_sample.product = new_product product_sample.sample = sample new_samples.append(product_sample) ProductSample.save(new_samples) return new_product def copy_quality(self, new_product): Quality = Pool().get('quality.test') products = [x.product for x in self.inputs] if not self.pass_quality: return tests = [] for product in products: if tests and product.quality_tests: return tests += product.quality_tests or [] new_tests = Quality.copy(tests, {'document': str(new_product)}) Quality.confirmed(new_tests) Quality.manager_validate(new_tests) def pass_feature(self, product): Variety = Pool().get('product.variety') Uom = Pool().get('product.uom') total_output = sum([Uom.compute_qty(x.unit, x.quantity, x.product.default_uom) for x in self.inputs if x.product.template in self.production_template.inputs]) vintages = [] do = [] ecologicals = [] for input in self.inputs: vintages += input.product.vintages do += input.product.denominations_of_origin ecologicals = input.product.ecologicals product.denominations_of_origin = list(set(do)) product.ecologicals = list(set(ecologicals)) product.vintages = list(set(vintages)) varieties = {} for input in self.inputs: percent = round(input.quantity / total_output, 6) for variety in input.product.varieties: new_variety = varieties.get(variety.variety) if not new_variety: new_variety = Variety() new_variety.percent = 0 new_variety.variety = variety.variety new_variety.percent += variety.percent / 100.0 * percent varieties[new_variety.variety] = new_variety for key, variety in varieties.items(): variety.percent = "%.4f" % round(100.0 * variety.percent, 4) product.varieties = varieties.values() return product def create_wine_aged_history(self, input, outputs): pool = Pool() WineAgingHistory = pool.get('wine.wine_aging.history') effective_date = input.production_input.effective_date histories = WineAgingHistory.search([ ('product', '=', input.product), ('date_end', '=', None), ]) if histories: to_write = [] for history in histories: to_write.extend(([history], { 'date_end': effective_date, 'duration': (effective_date - history.date_start).days, })) WineAgingHistory.write(*to_write) new_histories = [] for output in outputs: new_histories += WineAgingHistory.create([{ 'production': output.production_output, 'location': output.to_location, 'material': output.to_location.material, 'product': output.product, 'date_start': effective_date, 'date_end': None }]) if histories: new_histories += WineAgingHistory.copy(histories, { 'production': output.production_output, 'product': output.product, }) return new_histories @classmethod def done(cls, productions): pool = Pool() Move = pool.get('stock.move') Warning = pool.get('res.user.warning') for production in productions: if production.production_template.transfer_wine_aging: if len(production.inputs) > 1: warning_name = 'transfer_wine_aging_input_%s' % production.id if Warning.check(warning_name): raise UserWarning(warning_name, gettext('agronomics.msg_transfer_wine_aging_inputs', production=production.rec_name)) moves = [] for production in productions: for distrib in production.output_distribution: if distrib.distribution_state == 'draft' and distrib.location: product = production.create_variant(distrib.product, production.production_template.pass_feature) product = production.pass_feature(product) product = production.copy_certification(product) move = production._move( 'output', product, distrib.uom, distrib.produced_quantity) move.production_output = production move.unit_price = Decimal(0) moves.append(move) Move.save(moves) super().done(productions) for production in productions: for output in production.outputs: production.copy_quality(output.product) production.copy_quality_samples(output.product) if production.production_template.transfer_wine_aging: inputs = production.inputs if len(inputs) == 1: input, = inputs outputs = production.outputs production.create_wine_aged_history(input, outputs) @classmethod def set_cost(cls, productions): pool = Pool() Move = pool.get('stock.move') Uom = pool.get('product.uom') not_cost_distribution = [] moves = [] for production in productions: if not production.cost_distributions: not_cost_distribution.append(production) continue production_cost = production.cost for output in production.outputs: has_product = False output_cost = Decimal(0) total_output = sum([Uom.compute_qty(x.unit, x.quantity, x.product.default_uom) for x in production.outputs if x.product.template == output.product.template]) for cdist in production.cost_distributions: products = cdist.template.products if output.product not in products: continue has_product = True cost = (production_cost * (1 + cdist.percentatge) - production_cost) output_cost += round_price(cost / Decimal(total_output)) output_cost = output_cost if has_product else Decimal(0) if output.unit_price != output_cost: output.unit_price = output_cost moves.append(output) if moves: Move.save(moves) if not_cost_distribution: super(Production, cls).set_cost(not_cost_distribution) class OutputDistribution(ModelSQL, ModelView): 'Output Distribution' __name__ = 'production.output.distribution' production = fields.Many2One('production', 'Production', required=True) product = fields.Many2One('product.template', 'Template', required=True) location = fields.Many2One('stock.location', 'Location', states={ 'required': Eval('production_state').in_(['done']) }, depends=['production_state']) uom = fields.Many2One('product.uom', 'Uom') unit_digits = fields.Function(fields.Integer('Unit Digits'), 'on_change_with_unit_digits') initial_quantity = fields.Float('Initial Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) initial_quantity_readonly = fields.Function(fields.Float('Initial Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']), 'on_change_with_initial_quantity_readonly') final_quantity = fields.Float('Final Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) produced_quantity = fields.Function(fields.Float('Produced Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']), 'on_change_with_produced_quantity') production_state = fields.Function(fields.Selection([ ('request', 'Request'), ('draft', 'Draft'), ('waiting', 'Waiting'), ('assigned', 'Assigned'), ('running', 'Running'), ('done', 'Done'), ('cancelled', 'Cancelled')], 'State'), 'on_change_with_production_state') distribution_state = fields.Selection([ ('draft', 'Draft'), ('done', 'Done')], 'Distribution State', readonly=True) @classmethod def __setup__(cls): super().__setup__() cls._buttons.update({ 'done': { 'readonly': Eval('distribution_state') != 'draft', 'depends': ['distribution_state'] } }) @classmethod def default_distribution_state(cls): return 'draft' @fields.depends('production', '_parent_production.state') def on_change_with_production_state(self, name=None): return self.production and self.production.state @fields.depends('product') def on_change_with_uom(self): if not self.product: return return self.product.default_uom and self.product.default_uom.id @fields.depends('uom') def on_change_with_unit_digits(self, name=None): if self.uom: return self.uom.digits return 2 @fields.depends('initial_quantity', 'final_quantity', 'produced_quantity', 'location', 'product') def on_change_product(self): Product = Pool().get('product.product') if not self.product: self.initial_quantity = 0 return if not self.location: return context = Transaction().context context['locations'] = [self.location.id] with Transaction().set_context(context): quantities = Product.get_quantity(self.product.products, 'quantity') self.initial_quantity = sum(quantities.values()) @fields.depends('location', methods=['on_change_product']) def on_change_location(self): if not self.location: return self.on_change_product() @fields.depends('product', 'location', 'initial_quantity') def on_change_with_initial_quantity_readonly(self, name=None): Product = Pool().get('product.product') if not self.product or not self.location: return if self.initial_quantity: return self.initial_quantity context = Transaction().context context['locations'] = [self.location.id] with Transaction().set_context(context): quantities = Product.get_quantity(self.product.products, 'quantity') return sum(quantities.values()) @fields.depends('final_quantity', 'initial_quantity') def on_change_with_produced_quantity(self, name=None): return ((self.final_quantity or 0) - (self.initial_quantity or 0)) @classmethod @ModelView.button def done(cls, distributions): pool = Pool() Move = pool.get('stock.move') moves = [] for distribution in distributions: product = distribution.production.create_variant( distribution.product, distribution.production.production_template.pass_feature) product = distribution.production.pass_feature(product) move = distribution.production._move( 'input', product, distribution.uom, distribution.produced_quantity) move.production_output = distribution.production move.unit_price = Decimal(0) moves.append(move) distribution.distribution_state = 'done' Move.save(moves) Move.do(moves) cls.save(distributions) class ProductionEnologyProduct(ModelSQL, ModelView): 'Production Enology Product' __name__ = 'production.enology.product' production = fields.Many2One('production', 'Production') product = fields.Many2One('product.product', 'Product', required=True) uom = fields.Many2One('product.uom', 'Uom') unit_digits = fields.Function(fields.Integer('Unit Digits'), 'on_change_with_unit_digits') quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) @fields.depends('uom') def on_change_with_unit_digits(self, name=None): if self.uom: return self.uom.digits return 2 @fields.depends('product') def on_change_with_uom(self): if not self.product: return return self.product.default_uom and self.product.default_uom.id @fields.depends('product') def on_change_product(self): if not self.product: return self.quantity = self.product.quantity class ProductionCostPriceDistribution(ModelSQL, ModelView): "Production Distribution Cost Price" __name__ = 'production.cost_price.distribution' template = fields.Many2One('product.template', "Template", required=True, ondelete='RESTRICT') origin = fields.Reference('Origin', selection='_get_models', required=True) percentatge = fields.Numeric("Percentatge", digits=(16, 4), required=True) @classmethod def __setup__(cls): BOMInput = Pool().get('production.bom.input') super(ProductionCostPriceDistribution, cls).__setup__() cls.template.domain = [('type', 'in', BOMInput.get_product_types())] @staticmethod def _get_models(): return [ ('production', 'Production'), ('production.cost_price.distribution.template', 'Templates'), ] class ProductionCostPriceDistributionTemplate(ModelSQL, ModelView): "Production Cost Price Distribution Template" __name__ = 'production.cost_price.distribution.template' name = fields.Char("Name", required=True) production_template = fields.Many2One('production.template', "Production Template", required=True) cost_distribution_templates = fields.One2Many( 'production.cost_price.distribution', 'origin', "Cost Distribution") @classmethod def validate(cls, templates): super(ProductionCostPriceDistributionTemplate, cls).validate(templates) for template in templates: template.check_percentatge() template.check_product_templates() def check_product_templates(self): for cost in self.cost_distribution_templates: if cost.template not in self.production_template.outputs: raise ValidationError(gettext( 'agronomics.msg_check_cost_templates', cost=cost.rec_name, template=cost.template.rec_name, )) def check_percentatge(self): percentatge = sum(t.percentatge for t in self.cost_distribution_templates) if percentatge != 1: raise ValidationError( gettext( 'agronomics.msg_check_cost_distribution_template_percentatge', distribution=self.rec_name, percentatge=percentatge * 100, )) class ProductionCostPriceDistributionTemplateProductionTemplateAsk(ModelView): 'Production Cost Price Distribution Template from Production Template Ask' __name__ = 'production.cost_price.distribution.template.ask' name = fields.Char("Name", required=True) cost_distribution_templates = fields.One2Many( 'production.cost_price.distribution', None, "Cost Distributions") class ProductionCostPriceDistributionTemplateProductionTemplate(Wizard): "Production Cost Price Distribution Template from Production Template" __name__ = 'production.cost_price.distribution.template.from.production.template' start_state = 'ask' ask = StateView('production.cost_price.distribution.template.ask', 'agronomics.create_cost_price_distribution_from_production_start_view_form', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Create', 'create_cost_distributions', 'tryton-ok', True), ]) create_cost_distributions = StateAction( 'agronomics.act_production_cost_distribution_template_tree') def do_create_cost_distributions(self, action): pool = Pool() Template = pool.get('production.cost_price.distribution.template') Distribution = pool.get('production.cost_price.distribution') to_create = [] for record in self.records: tpl = Template() tpl.name = self.ask.name tpl.production_template = record cost_distributions = [] for cost_distribution in self.ask.cost_distribution_templates: dt = Distribution() dt.template = cost_distribution.template dt.percentatge = cost_distribution.percentatge cost_distributions.append(dt) if cost_distributions: tpl.cost_distribution_templates = cost_distributions to_create.append(tpl._save_values) tpls = Template.create(to_create) data = {'res_id': [tpl.id for tpl in tpls]} if len(tpls) == 1: action['views'].reverse() return action, data def default_ask(self, fields): pool = Pool() ProductionTemplate = pool.get('production.template') default = {} context = Transaction().context active_id = context.get('active_id') if active_id: ptpl = ProductionTemplate(active_id) cost_distributions = [] for output in ptpl.outputs: cost_distributions.append({ 'template': output.id, 'template.': {'rec_name': output.rec_name}, }) default['cost_distribution_templates'] = cost_distributions return default