fix(manufacturing): correct nested BOM quantities

This commit is contained in:
Mihir Kandoi
2026-08-10 13:25:20 +05:30
parent a25decfa50
commit bf9744e145
2 changed files with 67 additions and 3 deletions

View File

@@ -25,10 +25,12 @@ def fetch_exploded_bom_items(root_bom):
recursive CTE -- replaces a query-per-node walk with a single query. UNION keeps it cycle-safe
and fetches each sub-BOM's items only once even when it is reused across the tree."""
bom_item = frappe.qb.DocType("BOM Item")
child_bom = frappe.qb.DocType("BOM").as_("child_bom")
tree = frappe.qb.Table("exploded_bom")
fields = [
bom_item.parent,
bom_item.qty,
bom_item.stock_qty,
bom_item.bom_no,
bom_item.item_code,
bom_item.item_name,
@@ -46,7 +48,11 @@ def fetch_exploded_bom_items(root_bom):
.where(tree.bom_no != "")
)
rows = (
frappe.qb.with_(seed + recursion, "exploded_bom", recursive=True).from_(tree).select(tree.star)
frappe.qb.with_(seed + recursion, "exploded_bom", recursive=True)
.from_(tree)
.left_join(child_bom)
.on(tree.bom_no == child_bom.name)
.select(tree.star, child_bom.quantity.as_("child_bom_qty"))
).run(as_dict=True)
children_map = defaultdict(list)
@@ -71,7 +77,13 @@ def build_exploded_rows(bom, children_map, data, indent=0, qty=1):
}
)
if item.bom_no:
build_exploded_rows(item.bom_no, children_map, data, indent + 1, item.qty)
build_exploded_rows(
item.bom_no,
children_map,
data,
indent + 1,
qty * item.stock_qty / item.child_bom_qty,
)
def get_columns():

View File

@@ -4,7 +4,7 @@
import frappe
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.manufacturing.report.bom_explorer.bom_explorer import execute
from erpnext.manufacturing.report.bom_explorer.bom_explorer import build_exploded_rows, execute
from erpnext.tests.utils import ERPNextTestSuite
@@ -78,3 +78,55 @@ class TestBOMExplorer(ERPNextTestSuite):
# The leaf belongs to the sub-assembly, so it is exploded one level deeper.
self.assertEqual(rows_by_item[leaf_item]["indent"], 1)
self.assertEqual(rows_by_item[leaf_item]["bom_level"], 1)
def test_nested_bom_uses_stock_qty_for_output_normalization(self):
parent_bom = create_nested_bom(
{"parent": {"sub": {"leaf": {}}}},
prefix="_Test explorer converted quantity ",
)
sub_bom = frappe.get_doc("BOM", parent_bom.items[0].bom_no)
# The parent needs two boxes (20 units). The child BOM produces five units per batch.
frappe.db.set_value("BOM", sub_bom.name, "quantity", 5)
frappe.db.set_value("BOM Item", sub_bom.items[0].name, {"qty": 3, "stock_qty": 3})
frappe.db.set_value(
"BOM Item",
parent_bom.items[0].name,
{"qty": 2, "uom": "Box", "conversion_factor": 10, "stock_qty": 20},
)
data = self.run_report(parent_bom.name)
rows_by_item = {row["item_code"]: row for row in data}
self.assertEqual(rows_by_item["_Test explorer converted quantity sub"]["qty"], 2)
self.assertEqual(rows_by_item["_Test explorer converted quantity leaf"]["qty"], 12)
def test_nested_bom_multiplies_qty_at_every_level(self):
children_map = {
"root": [
frappe._dict(
item_code="parent",
idx=1,
bom_no="parent-bom",
child_bom_qty=1,
qty=8,
stock_qty=8,
)
],
"parent-bom": [
frappe._dict(
item_code="child",
idx=1,
bom_no="child-bom",
child_bom_qty=1,
qty=4,
stock_qty=4,
)
],
"child-bom": [frappe._dict(item_code="raw-material", idx=1, bom_no="", qty=2, stock_qty=2)],
}
data = []
build_exploded_rows("root", children_map, data)
self.assertEqual([row["qty"] for row in data], [8, 32, 64])