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):