from odoo import models, api from odoo.tools.float_utils import float_round class MrpProductionSchedule(models.Model): _inherit = 'mrp.production.schedule' @api.model_create_multi def create(self, vals_list): for vals in vals_list: if 'product_id' in vals and not vals.get('bom_id'): product = self.env['product.product'].browse(vals['product_id']) company_id = vals.get('company_id', self.env.company.id) bom_dict = self.env['mrp.bom']._bom_find(product, company_id=company_id) if bom_dict and bom_dict.get(product): vals['bom_id'] = bom_dict[product].id return super().create(vals_list) def set_replenish_qty(self, date_index, quantity, period_scale=False): """ Save the replenish quantity and mark the cells as manually updated. We override this to provide two-way (Trace up and Drill down) updating of linked components in the BOM hierarchy. """ self.ensure_one() # Calculate the difference between new quantity and old quantity date_start, date_stop = self.company_id._get_date_range(force_period=period_scale)[date_index] existing_forecast = self.forecast_ids.filtered(lambda f: f.date >= date_start and f.date <= date_stop) old_qty = sum(existing_forecast.mapped('replenish_qty')) if existing_forecast else 0.0 # Call super first to update this component res = super().set_replenish_qty(date_index, quantity, period_scale) quantity = float_round(float(quantity), precision_rounding=self.product_uom_id.rounding) diff_qty = quantity - old_qty # Determine propagation direction direction = self.env.context.get('propagate_direction', 'both') # If the quantity changed and we're not skipping propagation if diff_qty != 0 and direction != 'none': self._propagate_replenish_qty(diff_qty, date_index, period_scale, direction) return res def _propagate_replenish_qty(self, diff_qty, date_index, period_scale=False, direction='both'): """Propagate replenishment difference to parents (bottom-up) and children (top-down)""" if direction in ('both', 'up'): parent_schedules = self._get_impacted_parent_schedules() schedules_to_compute = parent_schedules | self indirect_demand_trees = schedules_to_compute._get_indirect_demand_tree() indirect_ratio_mps = schedules_to_compute._get_indirect_demand_ratio_mps(indirect_demand_trees) # TRACE UP: Update parents for parent_schedule in parent_schedules: # Odoo's indirect_ratio_mps gets non-zero values for direct MPS-connected components ratios = indirect_ratio_mps.get((parent_schedule.warehouse_id, parent_schedule.product_id), {}) ratio = ratios.get(self.product_id, 0.0) if ratio > 0: parent_diff = round(diff_qty / ratio, 8) p_date_start, p_date_stop = parent_schedule.company_id._get_date_range(force_period=period_scale)[date_index] p_exist = parent_schedule.forecast_ids.filtered(lambda f: f.date >= p_date_start and f.date <= p_date_stop) p_old_qty_base = sum(p_exist.mapped('replenish_qty')) if p_exist else 0.0 if hasattr(parent_schedule, 'packaging_id') and parent_schedule.packaging_id and parent_schedule.packaging_id.qty: p_old_qty_pack = round(p_old_qty_base / parent_schedule.packaging_id.qty, 8) parent_diff = round(parent_diff / parent_schedule.packaging_id.qty, 8) else: p_old_qty_pack = p_old_qty_base new_parent_qty = p_old_qty_pack + parent_diff if new_parent_qty < 0: new_parent_qty = 0.0 # Round to the UoM rounding of the product before saving new_parent_qty = float_round(new_parent_qty, precision_rounding=parent_schedule.product_uom_id.rounding) # Update parent with 'up' context. This propagates it all the way to the top recursively! parent_schedule.with_context(propagate_direction='up').set_replenish_qty(date_index, new_parent_qty, period_scale) if direction in ('both', 'down'): child_schedules = self._get_impacted_child_schedules() schedules_to_compute = child_schedules | self indirect_demand_trees = schedules_to_compute._get_indirect_demand_tree() indirect_ratio_mps = schedules_to_compute._get_indirect_demand_ratio_mps(indirect_demand_trees) # DRILL DOWN: Update children for child_schedule in child_schedules: ratios = indirect_ratio_mps.get((self.warehouse_id, self.product_id), {}) ratio = ratios.get(child_schedule.product_id, 0.0) if ratio > 0: child_diff = round(diff_qty * ratio, 8) if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id and child_schedule.packaging_id.qty: packaging_qty = child_schedule.packaging_id.qty child_diff = round(child_diff / packaging_qty, 8) c_date_start, c_date_stop = child_schedule.company_id._get_date_range(force_period=period_scale)[date_index] c_exist = child_schedule.forecast_ids.filtered(lambda f: f.date >= c_date_start and f.date <= c_date_stop) c_old_qty_base = sum(c_exist.mapped('replenish_qty')) if c_exist else 0.0 if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id and child_schedule.packaging_id.qty: c_old_qty_pack = round(c_old_qty_base / child_schedule.packaging_id.qty, 8) else: c_old_qty_pack = c_old_qty_base new_child_qty_pack = c_old_qty_pack + child_diff if new_child_qty_pack < 0: new_child_qty_pack = 0.0 # Round to the UoM rounding of the product before saving new_child_qty_pack = float_round(new_child_qty_pack, precision_rounding=child_schedule.product_uom_id.rounding) # Update child with 'down' context. This propagates it all the way to the bottom! child_schedule.with_context(propagate_direction='down').set_replenish_qty(date_index, new_child_qty_pack, period_scale)