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 dc271343b..69fab6682 100644 --- a/mrp_multi_level/models/mrp_inventory.py +++ b/mrp_multi_level/models/mrp_inventory.py @@ -89,8 +89,11 @@ class MrpInventory(models.Model): @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 8d2576c85..54aa0bf18 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 fcebe6f42..12055a84c 100644 --- a/mrp_multi_level/models/product_mrp_area.py +++ b/mrp_multi_level/models/product_mrp_area.py @@ -307,7 +307,3 @@ class ProductMRPArea(models.Model): 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 0df22de06..52d979fd5 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 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): 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 @@ class TestMrpMultiLevel(TestMrpMultiLevelCommon): 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 3fd55d9c3..256bd7c97 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 5b9ac8414..8544b7441 100644 --- a/mrp_multi_level/wizards/mrp_inventory_procure.py +++ b/mrp_multi_level/wizards/mrp_inventory_procure.py @@ -62,6 +62,8 @@ class MrpInventoryProcure(models.TransientModel): 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 6d4e619e2..4116c3f73 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -272,10 +272,7 @@ class MultiLevelMrp(models.TransientModel): 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 @@ class MultiLevelMrp(models.TransientModel): 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 @@ class MultiLevelMrp(models.TransientModel): @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 @@ class MultiLevelMrp(models.TransientModel): 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 @@ class MultiLevelMrp(models.TransientModel): [("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):