# Copyright 2017-24 ForgeFlow S.L. (https://www.forgeflow.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging from datetime import timedelta as td from odoo import api, fields, models from ..models.ddmrp_adjustment import DAF_string, LTAF_string _logger = logging.getLogger(__name__) class StockBuffer(models.Model): _inherit = "stock.buffer" extra_demand_ids = fields.One2many( comodel_name="ddmrp.adjustment.demand", string="Extra Demand", inverse_name="buffer_id", help="Demand associated to Demand Adjustment Factors applied to " "parent buffers.", ) daf_text = fields.Char(compute="_compute_daf_text") parent_daf_text = fields.Char(compute="_compute_daf_text") pre_daf_adu = fields.Float(readonly=True) daf_applied = fields.Float(default=-1, readonly=True) parent_daf_applied = fields.Float(default=-1, readonly=True) count_ddmrp_adjustment_demand = fields.Integer( compute="_compute_count_ddmrp_adjustment_demand" ) def _compute_count_ddmrp_adjustment_demand(self): for rec in self: rec.count_ddmrp_adjustment_demand = len( self.env["ddmrp.adjustment.demand"].search( [("buffer_origin_id", "=", rec.id)] ) ) @api.depends("daf_applied", "parent_daf_applied") def _compute_daf_text(self): for rec in self: rec.daf_text = "DAF: *" + str(round(rec.daf_applied, 2)) rec.parent_daf_text = "P. DAF: +" + str(round(rec.parent_daf_applied, 2)) def _daf_to_apply_domain(self, current=True): self.ensure_one() today = fields.Date.today() domain = [ ("buffer_id", "=", self.id), ("adjustment_type", "=", DAF_string), ("date_range_id.date_end", ">=", today), ] if current: domain.append(("date_range_id.date_start", "<=", today)) return domain def _calc_adu(self): # Apply DAFs if existing for the buffer. res = super()._calc_adu() for rec in self: self.env["ddmrp.adjustment.demand"].search( [("buffer_origin_id", "=", rec.id)] ).unlink() dafs_to_apply = self.env["ddmrp.adjustment"].search( rec._daf_to_apply_domain() ) rec.daf_applied = -1 if dafs_to_apply: rec.daf_applied = 1 values = dafs_to_apply.mapped("value") for val in values: rec.daf_applied *= val rec.pre_daf_adu = rec.adu rec.adu *= rec.daf_applied _logger.debug( "DAF={} applied to {}. ADU: {} -> {}".format( rec.daf_applied, rec.name, rec.pre_daf_adu, rec.adu ) ) # Compute generated demand to be applied to components: dafs_to_explode = self.env["ddmrp.adjustment"].search( rec._daf_to_apply_domain(False) ) for daf in dafs_to_explode: prev = rec.adu increased_demand = prev * daf.value - prev rec.explode_demand_to_components(daf, increased_demand, rec.product_uom) return res def explode_demand_to_components(self, daf, demand, uom_id): demand_obj = self.env["ddmrp.adjustment.demand"] init_bom = self._get_manufactured_bom() if not init_bom: return def _get_extra_demand(bom, line, buffer_id, factor): qty = factor * line.product_qty / bom.product_qty extra = line.product_uom_id._compute_quantity(qty, buffer_id.product_uom) return extra def _create_demand(bom, factor=1, level=0, clt=0): level += 1 produce_delay = ( bom[0].product_id.produce_delay or bom[0].product_tmpl_id.produce_delay ) clt += produce_delay for line in bom.bom_line_ids: if line.is_buffered: buffer_id = line.buffer_id extra_demand = _get_extra_demand(bom, line, buffer_id, factor) date_start = daf.date_range_id.date_start - td(days=clt) date_end = daf.date_range_id.date_end - td(days=clt) demand_obj.sudo().create( { "buffer_id": buffer_id.id, "buffer_origin_id": self.id, "extra_demand": extra_demand, "date_start": date_start, "date_end": date_end, } ) location = line.location_id line_boms = line.product_id.bom_ids child_bom = line_boms.filtered( lambda bom: bom.location_id == location ) or line_boms.filtered(lambda b: not b.location_id) if child_bom: line_qty = line.product_uom_id._compute_quantity( line.product_qty, child_bom.product_uom_id ) new_factor = factor * line_qty / bom.product_qty _create_demand(child_bom[0], new_factor, level, clt) initial_factor = uom_id._compute_quantity(demand, init_bom.product_uom_id) _create_demand(init_bom, factor=initial_factor) return True @api.model def cron_ddmrp_adu(self, automatic=False): """Apply extra demand originated by Demand Adjustment Factors to components after the cron update of all the buffers.""" res = super().cron_ddmrp_adu(automatic) today = fields.Date.today() self.search( [("parent_daf_applied", "!=", -1), ("extra_demand_ids", "=", False)] ).write({"parent_daf_applied": -1}) for op in self.search([("extra_demand_ids", "!=", False)]): op.parent_daf_applied = -1 daf_parent = sum( op.extra_demand_ids.filtered( lambda r: r.date_start <= today <= r.date_end ).mapped("extra_demand") ) if daf_parent: op.parent_daf_applied = daf_parent op.adu += op.parent_daf_applied _logger.debug( "DAFs-originated demand applied. {}: ADU += {}".format( op.name, op.parent_daf_applied ) ) return res def _ltaf_to_apply_domain(self): self.ensure_one() today = fields.Date.today() return [ ("buffer_id", "=", self.id), ("adjustment_type", "=", LTAF_string), ("date_range_id.date_start", "<=", today), ("date_range_id.date_end", ">=", today), ] def _compute_dlt(self): """Apply Lead Time Adj Factor if existing""" res = super()._compute_dlt() for rec in self: ltaf_to_apply = self.env["ddmrp.adjustment"].search( rec._ltaf_to_apply_domain() ) if ltaf_to_apply: ltaf = 1 values = ltaf_to_apply.mapped("value") for val in values: ltaf *= val prev = rec.dlt rec.dlt *= ltaf _logger.debug( "LTAF=%s applied to %s. DLT: %s -> %s" % (ltaf, rec.name, prev, rec.dlt) ) return res def action_archive(self): self.env["ddmrp.adjustment.demand"].search( [("buffer_origin_id", "in", self.ids)] ).unlink() return super().action_archive() def action_view_demand_to_components(self): demand_ids = ( self.env["ddmrp.adjustment.demand"] .search([("buffer_origin_id", "=", self.id)]) .ids ) action = self.env["ir.actions.act_window"]._for_xml_id( "ddmrp_adjustment.ddmrp_adjustment_demand_action" ) action["domain"] = [("id", "in", demand_ids)] return action def action_view_affecting_adu(self): demand_ids = ( self.env["ddmrp.adjustment"] .search(self._daf_to_apply_domain(current=False)) .ids ) action = self.env["ir.actions.act_window"]._for_xml_id( "ddmrp_adjustment.ddmrp_adjustment_action" ) action["domain"] = [("id", "in", demand_ids)] action["context"] = {"search_default_current": 1} return action def action_view_parent_affecting_adu(self): demand_ids = self.extra_demand_ids.ids action = self.env["ir.actions.act_window"]._for_xml_id( "ddmrp_adjustment.ddmrp_adjustment_demand_action" ) action["domain"] = [("id", "in", demand_ids)] return action