[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.
This commit is contained in:
Matt Taylor
2024-10-17 09:44:06 -06:00
parent 1f34d36b10
commit 33cf4af1ac
7 changed files with 83 additions and 20 deletions

View File

@@ -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(

View File

@@ -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"),

View File

@@ -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"

View File

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

View File

@@ -6,7 +6,10 @@
<field name="name">mrp.planned.order.tree</field>
<field name="model">mrp.planned.order</field>
<field name="arch" type="xml">
<tree decoration-info="fixed != True">
<tree
decoration-info="fixed != True and mrp_action != 'phantom'"
decoration-muted="mrp_action == 'phantom'"
>
<field name="name" />
<field name="origin" />
<field name="product_mrp_area_id" />
@@ -17,6 +20,7 @@
<field name="qty_released" />
<field name="mrp_qty" />
<field name="fixed" />
<field name="mrp_action" optional="hide" />
</tree>
</field>
</record>

View File

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

View File

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