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 b13186717..d840e7bc3 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(): @@ -536,7 +533,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 = [] @@ -666,7 +667,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 @@ -815,7 +820,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 += ( @@ -854,7 +860,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):