feat: phantom bom (#50351)

* feat: add phantom bom settings in bom doctype

* feat: new explosion logic for production plan, subcontracting and work order/manufacturing

* feat: modify explosion logic in reports and bom creator

* fix: failing test

* feat: add convert to phantom item support in bom creator

* test: added test cases

* fix: always fetch rm rate if phantom bom

* refactor: PP phantom explosion logic

* fix: report test cases

* feat: add phantom item in description of item if phantom item in bom tree

* fix: hide create button if bom is phantom

* fix: bugs found by coderabbit
This commit is contained in:
Mihir Kandoi
2025-11-16 15:08:08 +05:30
committed by GitHub
parent 9b303a2272
commit e5e26cd92a
22 changed files with 383 additions and 59 deletions

View File

@@ -550,6 +550,8 @@ class SubcontractingController(StockController):
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
data = []
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
@@ -558,7 +560,7 @@ class SubcontractingController(StockController):
"name": "bom_detail_no",
"source_warehouse": "reserve_warehouse",
}
for field in [
fields_list = [
"item_code",
"name",
"rate",
@@ -567,7 +569,12 @@ class SubcontractingController(StockController):
"description",
"item_name",
"stock_uom",
]:
]
if doctype == "BOM Item":
fields_list.extend(["is_phantom_item", "bom_no"])
for field in fields_list:
fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}")
filters = [
@@ -577,7 +584,19 @@ class SubcontractingController(StockController):
[doctype, "sourced_by_supplier", "=", 0],
]
return frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
data = frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
to_remove = []
for item in data:
if item.is_phantom_item:
data += self.__get_materials_from_bom(
item.rm_item_code, item.bom_no, exploded_item=exploded_item
)
to_remove.append(item)
for item in to_remove:
data.remove(item)
return data
def __update_reserve_warehouse(self, row, item):
if (

View File

@@ -1141,6 +1141,28 @@ class TestSubcontractingController(IntegrationTestCase):
itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6],
)
def test_phantom_bom_explosion(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_tree_for_phantom_bom_tests
expected = create_tree_for_phantom_bom_tests()
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 11",
"qty": 5,
"rate": 100,
"fg_item": "Top Level Parent",
"fg_item_qty": 5,
},
]
sco = get_subcontracting_order(service_items=service_items, do_not_submit=True)
sco.items[0].include_exploded_items = 0
sco.save()
sco.submit()
sco.reload()
self.assertEqual([item.rm_item_code for item in sco.supplied_items], expected)
def add_second_row_in_scr(scr):
item_dict = {}
@@ -1313,6 +1335,7 @@ def make_subcontracted_items():
"create_new_batch": 1,
"batch_number_series": "SBAT.####",
},
"Top Level Parent": {},
}
for item, properties in sub_contracted_items.items():
@@ -1364,6 +1387,7 @@ def make_service_items():
"Subcontracted Service Item 8": {},
"Subcontracted Service Item 9": {},
"Subcontracted Service Item 10": {},
"Subcontracted Service Item 11": {},
}
for item, properties in service_items.items():
@@ -1389,6 +1413,7 @@ def make_bom_for_subcontracted_items():
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
"Subcontracted Item SA10": ["Subcontracted SRM Item 10"],
"Subcontracted Service Item 11": ["Top Level Parent"],
}
for item_code, raw_materials in boms.items():