From 33cf4af1accf415f056dfe497969e5933e40246f Mon Sep 17 00:00:00 2001 From: Matt Taylor Date: Thu, 17 Oct 2024 09:44:06 -0600 Subject: [PATCH] [FIX] mrp_multi_level: fix kit/phantom planning fixes #1362 Ignoring qty_available for phantom products prevents double counting the qty_available of components. Creating planned orders for phantom products is simpler than recursively exploding phantom BOMs. This also makes it easier to analyze the planning data generated by the MRP calculation. --- mrp_multi_level/models/mrp_inventory.py | 7 ++- mrp_multi_level/models/mrp_planned_order.py | 1 + mrp_multi_level/models/product_mrp_area.py | 4 -- mrp_multi_level/tests/test_mrp_multi_level.py | 57 +++++++++++++++++-- .../views/mrp_planned_order_views.xml | 6 +- .../wizards/mrp_inventory_procure.py | 2 + mrp_multi_level/wizards/mrp_multi_level.py | 26 ++++++--- 7 files changed, 83 insertions(+), 20 deletions(-) diff --git a/mrp_multi_level/models/mrp_inventory.py b/mrp_multi_level/models/mrp_inventory.py index dc271343b07..69fab6682b3 100644 --- a/mrp_multi_level/models/mrp_inventory.py +++ b/mrp_multi_level/models/mrp_inventory.py @@ -89,8 +89,11 @@ def _compute_uom_id(self): @api.depends("planned_order_ids", "planned_order_ids.qty_released") def _compute_to_procure(self): for rec in self: - rec.to_procure = sum(rec.planned_order_ids.mapped("mrp_qty")) - sum( - rec.planned_order_ids.mapped("qty_released") + rec.to_procure = ( + 0.0 + if rec.supply_method == "phantom" + else sum(rec.planned_order_ids.mapped("mrp_qty")) + - sum(rec.planned_order_ids.mapped("qty_released")) ) @api.depends( diff --git a/mrp_multi_level/models/mrp_planned_order.py b/mrp_multi_level/models/mrp_planned_order.py index 8d2576c8569..54aa0bf18d8 100644 --- a/mrp_multi_level/models/mrp_planned_order.py +++ b/mrp_multi_level/models/mrp_planned_order.py @@ -59,6 +59,7 @@ class MrpPlannedOrder(models.Model): mrp_action = fields.Selection( selection=[ ("manufacture", "Manufacturing Order"), + ("phantom", "Kit"), ("buy", "Purchase Order"), ("pull", "Pull From"), ("push", "Push To"), diff --git a/mrp_multi_level/models/product_mrp_area.py b/mrp_multi_level/models/product_mrp_area.py index fcebe6f429f..12055a84c9f 100644 --- a/mrp_multi_level/models/product_mrp_area.py +++ b/mrp_multi_level/models/product_mrp_area.py @@ -307,7 +307,3 @@ def _to_be_exploded(self): def _get_locations(self): self.ensure_one() return self.mrp_area_id._get_locations() - - def _should_create_planned_order(self): - self.ensure_one() - return not self.supply_method == "phantom" diff --git a/mrp_multi_level/tests/test_mrp_multi_level.py b/mrp_multi_level/tests/test_mrp_multi_level.py index 0df22de0652..52d979fd598 100644 --- a/mrp_multi_level/tests/test_mrp_multi_level.py +++ b/mrp_multi_level/tests/test_mrp_multi_level.py @@ -401,11 +401,8 @@ def test_16_phantom_comp_planning(self): sf_3_planned_order_1 = self.planned_order_obj.search( [("product_mrp_area_id.product_id", "=", self.sf_3.id)] ) - self.assertEqual(len(sf_3_planned_order_1), 0) - sf_3_mrp_parameter = self.product_mrp_area_obj.search( - [("product_id", "=", self.sf_3.id)] - ) - self.assertEqual(sf_3_mrp_parameter.supply_method, "phantom") + self.assertEqual(sf_3_planned_order_1.mrp_action, "phantom") + self.assertEqual(sf_3_planned_order_1.mrp_qty, 10.0) # PP-3 pp_3_line_1 = self.mrp_inventory_obj.search( [("product_mrp_area_id.product_id", "=", self.pp_3.id)] @@ -852,3 +849,53 @@ def test_24_prioritize_safety_stock_with_mrp_moves_today_grouped(self): f"unexpected value for {key}: {inv[key]} " f"(expected {test_vals[key]} on {inv.date})", ) + + def test_25_phantom_comp_on_hand(self): + """ + A phantom product with positive qty_available (which is computed from the + availability of its components) should not satisfy demand, because this leads + to double counting qty_available of its component products. + """ + quant = self.quant_obj.sudo().create( + { + "product_id": self.pp_3.id, + "inventory_quantity": 10.0, + "location_id": self.stock_location.id, + } + ) + quant.action_apply_inventory() + quant = self.quant_obj.sudo().create( + { + "product_id": self.pp_4.id, + "inventory_quantity": 30.0, + "location_id": self.stock_location.id, + } + ) + quant.action_apply_inventory() + self.assertEqual(self.sf_3.qty_available, 10.0) + self.mrp_multi_level_wiz.create({}).run_mrp_multi_level() + # PP-3 + pp_3_line_1 = self.mrp_inventory_obj.search( + [("product_mrp_area_id.product_id", "=", self.pp_3.id)] + ) + self.assertEqual(len(pp_3_line_1), 1) + self.assertEqual(pp_3_line_1.demand_qty, 20.0) + self.assertEqual(pp_3_line_1.to_procure, 10.0) + pp_3_planned_orders = self.planned_order_obj.search( + [("product_mrp_area_id.product_id", "=", self.pp_3.id)] + ) + self.assertEqual(len(pp_3_planned_orders), 1) + self.assertEqual(pp_3_planned_orders.mrp_qty, 10) + sf3_planned_orders = self.env["mrp.planned.order"].search( + [("product_id", "=", self.sf_3.id)] + ) + self.assertEqual(len(sf3_planned_orders), 1) + # Trying to procure a kit planned order will have no effect. + procure_wizard = ( + self.env["mrp.inventory.procure"] + .with_context( + active_model="mrp.planned.order", active_ids=sf3_planned_orders.ids + ) + .create({}) + ) + self.assertEqual(len(procure_wizard.item_ids), 0) diff --git a/mrp_multi_level/views/mrp_planned_order_views.xml b/mrp_multi_level/views/mrp_planned_order_views.xml index 3fd55d9c3ef..256bd7c97a7 100644 --- a/mrp_multi_level/views/mrp_planned_order_views.xml +++ b/mrp_multi_level/views/mrp_planned_order_views.xml @@ -6,7 +6,10 @@ mrp.planned.order.tree mrp.planned.order - + @@ -17,6 +20,7 @@ + diff --git a/mrp_multi_level/wizards/mrp_inventory_procure.py b/mrp_multi_level/wizards/mrp_inventory_procure.py index 5b9ac8414e7..8544b744189 100644 --- a/mrp_multi_level/wizards/mrp_inventory_procure.py +++ b/mrp_multi_level/wizards/mrp_inventory_procure.py @@ -62,6 +62,8 @@ def default_get(self, fields): elif active_model == "mrp.planned.order": mrp_planned_order_obj = self.env[active_model] for line in mrp_planned_order_obj.browse(active_ids): + if line.mrp_action == "phantom": + continue if line.qty_released < line.mrp_qty: items += item_obj.create(self._prepare_item(line)) if items: diff --git a/mrp_multi_level/wizards/mrp_multi_level.py b/mrp_multi_level/wizards/mrp_multi_level.py index 6d4e619e248..4116c3f7328 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -272,10 +272,7 @@ def create_planned_order( order_data = self._prepare_planned_order_data( product_mrp_area_id, qty, mrp_date_supply, mrp_action_date, name, values ) - # Do not create planned order for products that are Kits - planned_order = False - if product_mrp_area_id._should_create_planned_order(): - planned_order = self.env["mrp.planned.order"].create(order_data) + planned_order = self.env["mrp.planned.order"].create(order_data) qty_ordered = qty_ordered + qty if product_mrp_area_id._to_be_exploded(): @@ -535,7 +532,11 @@ def _get_qty_to_order(self, product_mrp_area, date, move_qty, onhand): def _init_mrp_move_grouped_demand(self, product_mrp_area): last_date = None last_qty = 0.00 - onhand = product_mrp_area.qty_available + onhand = ( + 0.0 + if product_mrp_area.supply_method == "phantom" + else product_mrp_area.qty_available + ) grouping_delta = product_mrp_area.mrp_nbr_days demand_origin = [] @@ -665,7 +666,11 @@ def _get_safety_stock_target_date(self, product_mrp_area): @api.model def _init_mrp_move_non_grouped_demand(self, product_mrp_area): - onhand = product_mrp_area.qty_available + onhand = ( + 0.0 + if product_mrp_area.supply_method == "phantom" + else product_mrp_area.qty_available + ) for move in product_mrp_area.mrp_move_ids: if self._exclude_move(move): continue @@ -814,7 +819,8 @@ def _prepare_mrp_inventory_data( supply_qty = supply_qty_by_date.get(mdt, 0.0) mrp_inventory_data["supply_qty"] = abs(supply_qty) mrp_inventory_data["initial_on_hand_qty"] = on_hand_qty - on_hand_qty += supply_qty + demand_qty + if product_mrp_area.supply_method != "phantom": + on_hand_qty += supply_qty + demand_qty mrp_inventory_data["final_on_hand_qty"] = on_hand_qty # Consider that MRP plan is followed exactly: running_availability += ( @@ -853,7 +859,11 @@ def _init_mrp_inventory(self, product_mrp_area): [("product_mrp_area_id", "=", product_mrp_area.id)], order="due_date" ).mapped("due_date") mrp_dates = set(moves_dates + action_dates) - on_hand_qty = product_mrp_area.qty_available + on_hand_qty = ( + 0.0 + if product_mrp_area.supply_method == "phantom" + else product_mrp_area.qty_available + ) running_availability = on_hand_qty mrp_inventory_vals = [] for mdt in sorted(mrp_dates):