mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-23 05:04:52 +01:00
Merge pull request #53396 from vorasmit/same-phantom-different-fg
This commit is contained in:
@@ -2104,16 +2104,16 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
|
||||
for item in query.run(as_dict=True):
|
||||
key = (item.item_code, item.bom_no)
|
||||
if item.is_phantom_item:
|
||||
sub_assembly_items[key] += item.get("qty")
|
||||
existing_key = (item.item_code, item.bom_no or item.main_bom)
|
||||
|
||||
if (item.bom_no and key not in sub_assembly_items) or (
|
||||
(item.item_code, item.bom_no or item.main_bom) in existing_sub_assembly_items
|
||||
):
|
||||
if item.bom_no and not item.is_phantom_item and key not in sub_assembly_items:
|
||||
continue
|
||||
|
||||
if not item.is_phantom_item and existing_key in existing_sub_assembly_items:
|
||||
continue
|
||||
|
||||
if item.bom_no:
|
||||
planned_qty = flt(sub_assembly_items[key])
|
||||
recursion_qty = flt(item.get("qty")) if item.is_phantom_item else flt(sub_assembly_items[key])
|
||||
get_raw_materials_of_sub_assembly_items(
|
||||
existing_sub_assembly_items,
|
||||
item_details,
|
||||
@@ -2121,9 +2121,10 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item.bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
planned_qty=recursion_qty,
|
||||
)
|
||||
existing_sub_assembly_items.add((item.item_code, item.bom_no or item.main_bom))
|
||||
if not item.is_phantom_item:
|
||||
existing_sub_assembly_items.add(existing_key)
|
||||
else:
|
||||
if not item.conversion_factor and item.purchase_uom:
|
||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
||||
|
||||
@@ -2710,6 +2710,92 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
[item.item_code for item in plan.mr_items], ["Item Level 1-3", "Item Level 2-3", "Item Level 3-1"]
|
||||
)
|
||||
|
||||
def test_phantom_bom_explosion_across_multiple_po_items(self):
|
||||
"""
|
||||
Regression: when the same phantom item (BOM) is referenced inside sub-assemblies
|
||||
of two different production plan items, its raw materials must be fully exploded
|
||||
for *both* plan items.
|
||||
"""
|
||||
# Setup items
|
||||
fg_a = make_item("FG for Cross-PO Phantom Test A")
|
||||
fg_b = make_item("FG for Cross-PO Phantom Test B")
|
||||
sa_a = make_item("SA for Cross-PO Phantom Test A")
|
||||
sa_b = make_item("SA for Cross-PO Phantom Test B")
|
||||
phantom = make_item("Phantom for Cross-PO Test")
|
||||
rm = make_item("RM for Cross-PO Phantom Test")
|
||||
|
||||
# Create the shared phantom BOM
|
||||
phantom_bom = make_bom(item=phantom.name, raw_materials=[rm.name], do_not_save=True)
|
||||
phantom_bom.is_phantom_bom = 1
|
||||
phantom_bom.save()
|
||||
phantom_bom.submit()
|
||||
|
||||
# Create SA-A BOM with phantom
|
||||
sa_a_bom = make_bom(item=sa_a.name, raw_materials=[phantom.name], do_not_save=True)
|
||||
sa_a_bom.items[0].bom_no = phantom_bom.name
|
||||
sa_a_bom.save()
|
||||
sa_a_bom.submit()
|
||||
|
||||
# Create SA-B BOM with the SAME phantom
|
||||
sa_b_bom = make_bom(item=sa_b.name, raw_materials=[phantom.name], do_not_save=True)
|
||||
sa_b_bom.items[0].bom_no = phantom_bom.name
|
||||
sa_b_bom.save()
|
||||
sa_b_bom.submit()
|
||||
|
||||
# Create FG-A BOM with SA-A
|
||||
fg_a_bom = make_bom(item=fg_a.name, raw_materials=[sa_a.name], do_not_save=True)
|
||||
fg_a_bom.items[0].bom_no = sa_a_bom.name
|
||||
fg_a_bom.save()
|
||||
fg_a_bom.submit()
|
||||
|
||||
# Create FG-B BOM with SA-B
|
||||
fg_b_bom = make_bom(item=fg_b.name, raw_materials=[sa_b.name], do_not_save=True)
|
||||
fg_b_bom.items[0].bom_no = sa_b_bom.name
|
||||
fg_b_bom.save()
|
||||
fg_b_bom.submit()
|
||||
|
||||
# Build Production Plan with both FGs
|
||||
plan = frappe.new_doc("Production Plan")
|
||||
plan.company = "_Test Company"
|
||||
plan.posting_date = nowdate()
|
||||
plan.ignore_existing_ordered_qty = 1
|
||||
plan.skip_available_sub_assembly_item = 1
|
||||
plan.sub_assembly_warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
for fg_item, bom in [(fg_a.name, fg_a_bom.name), (fg_b.name, fg_b_bom.name)]:
|
||||
plan.append(
|
||||
"po_items",
|
||||
{
|
||||
"use_multi_level_bom": 1,
|
||||
"item_code": fg_item,
|
||||
"bom_no": bom,
|
||||
"planned_qty": 1,
|
||||
"planned_start_date": now_datetime(),
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
)
|
||||
|
||||
plan.insert()
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
# Verify both sub-assemblies are present
|
||||
sa_items = {row.production_item for row in plan.sub_assembly_items}
|
||||
self.assertIn(sa_a.name, sa_items)
|
||||
self.assertIn(sa_b.name, sa_items)
|
||||
|
||||
plan.submit()
|
||||
|
||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||
|
||||
# Phantom raw material should be counted twice (once per FG → SA → shared phantom)
|
||||
rm_total_qty = sum(flt(d["quantity"]) for d in mr_items if d["item_code"] == rm.name)
|
||||
self.assertEqual(
|
||||
rm_total_qty,
|
||||
2.0,
|
||||
f"Expected RM qty=2 (1 per FG via shared phantom BOM), got {rm_total_qty}. "
|
||||
"The phantom BOM was not re-exploded for the second po_item.",
|
||||
)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user