feat: Auto-assign BOM on MPS creation and enhance two-way propagation with directional control and improved packaging unit handling.

This commit is contained in:
Suherdy Yacob 2026-03-03 15:06:56 +07:00
parent e094a61cd7
commit 179b7b53b7

View File

@ -4,6 +4,17 @@ from odoo.tools.float_utils import float_round
class MrpProductionSchedule(models.Model): class MrpProductionSchedule(models.Model):
_inherit = 'mrp.production.schedule' _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): def set_replenish_qty(self, date_index, quantity, period_scale=False):
""" Save the replenish quantity and mark the cells as manually updated. """ Save the replenish quantity and mark the cells as manually updated.
We override this to provide two-way (Trace up and Drill down) updating We override this to provide two-way (Trace up and Drill down) updating
@ -24,79 +35,81 @@ class MrpProductionSchedule(models.Model):
quantity = float_round(float(quantity), precision_rounding=self.product_uom_id.rounding) quantity = float_round(float(quantity), precision_rounding=self.product_uom_id.rounding)
diff_qty = quantity - old_qty diff_qty = quantity - old_qty
# If the quantity changed and we're not already propagating an update to avoid infinite loops # Determine propagation direction
if diff_qty != 0 and not self.env.context.get('skip_two_way_mps'): direction = self.env.context.get('propagate_direction', 'both')
self._propagate_replenish_qty(diff_qty, date_index, period_scale)
# 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 return res
def _propagate_replenish_qty(self, diff_qty, date_index, period_scale=False): 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)""" """Propagate replenishment difference to parents (bottom-up) and children (top-down)"""
# We need to trace up (bottom-up) if direction in ('both', 'up'):
parent_schedules = self._get_impacted_parent_schedules() parent_schedules = self._get_impacted_parent_schedules()
# We need to drill down (top-down) schedules_to_compute = parent_schedules | self
child_schedules = self._get_impacted_child_schedules() indirect_demand_trees = schedules_to_compute._get_indirect_demand_tree()
indirect_ratio_mps = schedules_to_compute._get_indirect_demand_ratio_mps(indirect_demand_trees)
# Fetch the BOM hierarchy relationships
schedules_to_compute = parent_schedules | 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)
# TRACE UP: Update parents
for parent_schedule in parent_schedules:
# Find the ratio: how many of 'self.product_id' are needed to make 1 'parent_schedule.product_id'
ratios = indirect_ratio_mps.get((parent_schedule.warehouse_id, parent_schedule.product_id), {})
ratio = ratios.get(self.product_id, 0.0)
if ratio > 0: # TRACE UP: Update parents
parent_diff = diff_qty / ratio 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)
# Retrieve the existing parent quantity for this period if ratio > 0:
p_date_start, p_date_stop = parent_schedule.company_id._get_date_range(force_period=period_scale)[date_index] parent_diff = diff_qty / ratio
p_exist = parent_schedule.forecast_ids.filtered(lambda f: f.date >= p_date_start and f.date <= p_date_stop)
p_old_qty = sum(p_exist.mapped('replenish_qty')) if p_exist else 0.0 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)
new_parent_qty = p_old_qty + parent_diff p_old_qty_base = sum(p_exist.mapped('replenish_qty')) if p_exist else 0.0
if new_parent_qty < 0:
new_parent_qty = 0.0 if hasattr(parent_schedule, 'packaging_id') and parent_schedule.packaging_id and parent_schedule.packaging_id.qty:
p_old_qty_pack = p_old_qty_base / parent_schedule.packaging_id.qty
# Update parent with skip context to prevent loops parent_diff = parent_diff / parent_schedule.packaging_id.qty
parent_schedule.with_context(skip_two_way_mps=True).set_replenish_qty(date_index, new_parent_qty, period_scale) else:
p_old_qty_pack = p_old_qty_base
# DRILL DOWN: Update children
for child_schedule in child_schedules: new_parent_qty = p_old_qty_pack + parent_diff
# Find the ratio: how many of 'child.product_id' base units are needed to make 1 'self.product_id' base unit if new_parent_qty < 0:
ratios = indirect_ratio_mps.get((self.warehouse_id, self.product_id), {}) new_parent_qty = 0.0
ratio = ratios.get(child_schedule.product_id, 0.0)
# 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)
if ratio > 0: # DRILL DOWN: Update children
# child_diff is in base units for child_schedule in child_schedules:
child_diff = diff_qty * ratio ratios = indirect_ratio_mps.get((self.warehouse_id, self.product_id), {})
ratio = ratios.get(child_schedule.product_id, 0.0)
# If the packaging module is used, we must convert the base unit diff_qty if ratio > 0:
# into the packaging unit diff_qty. child_diff = diff_qty * ratio
# Because child_schedule.set_replenish_qty will do: quantity = float(quantity) * self.packaging_id.qty
# Therefore, we must pass the quantity *in packaging units*. if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id and child_schedule.packaging_id.qty:
if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id: packaging_qty = child_schedule.packaging_id.qty
packaging_qty = child_schedule.packaging_id.qty
if packaging_qty:
child_diff = child_diff / packaging_qty child_diff = child_diff / packaging_qty
c_date_start, c_date_stop = child_schedule.company_id._get_date_range(force_period=period_scale)[date_index] 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_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
# The existing replenish_qty in the database is actually stored in base units.
# BUT we need to pass the new value to set_replenish_qty in the *packaging* units if packaging is used. if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id and child_schedule.packaging_id.qty:
c_old_qty_base = sum(c_exist.mapped('replenish_qty')) if c_exist else 0.0 c_old_qty_pack = c_old_qty_base / child_schedule.packaging_id.qty
else:
if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id and child_schedule.packaging_id.qty: c_old_qty_pack = c_old_qty_base
c_old_qty_pack = c_old_qty_base / child_schedule.packaging_id.qty
else: new_child_qty_pack = c_old_qty_pack + child_diff
c_old_qty_pack = c_old_qty_base if new_child_qty_pack < 0:
new_child_qty_pack = 0.0
new_child_qty_pack = c_old_qty_pack + child_diff
if new_child_qty_pack < 0: # Update child with 'down' context. This propagates it all the way to the bottom!
new_child_qty_pack = 0.0 child_schedule.with_context(propagate_direction='down').set_replenish_qty(date_index, new_child_qty_pack, period_scale)
child_schedule.with_context(skip_two_way_mps=True).set_replenish_qty(date_index, new_child_qty_pack, period_scale)