mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[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:
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user