fix: retain bom components without warehouse stock (#59116)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit 60913b722a)
This commit is contained in:
pandiyan
2026-09-17 11:08:07 +05:30
parent c42df7ef3e
commit 18ee3b9cc0
2 changed files with 59 additions and 21 deletions

View File

@@ -196,10 +196,29 @@ def get_bom_data(filters):
bom_item = frappe.qb.DocType(bom_item_table)
bin = frappe.qb.DocType("Bin")
stock_join_condition = bom_item.item_code == bin.item_code
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if warehouse_details:
wh = frappe.qb.DocType("Warehouse")
stock_join_condition &= ExistsCriterion(
frappe.qb.from_(wh)
.select(wh.name)
.where(
(wh.lft >= warehouse_details.lft)
& (wh.rgt <= warehouse_details.rgt)
& (bin.warehouse == wh.name)
)
)
else:
stock_join_condition &= bin.warehouse == filters.get("warehouse")
query = (
frappe.qb.from_(bom_item)
.left_join(bin)
.on(bom_item.item_code == bin.item_code)
.on(stock_join_condition)
.select(
bom_item.item_code,
bom_item.description,
@@ -212,26 +231,6 @@ def get_bom_data(filters):
.orderby(bom_item.idx)
)
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value(
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
)
if warehouse_details:
wh = frappe.qb.DocType("Warehouse")
query = query.where(
ExistsCriterion(
frappe.qb.from_(wh)
.select(wh.name)
.where(
(wh.lft >= warehouse_details.lft)
& (wh.rgt <= warehouse_details.rgt)
& (bin.warehouse == wh.name)
)
)
)
else:
query = query.where(bin.warehouse == filters.get("warehouse"))
if bom_item_table == "BOM Item":
query = query.select(bom_item.bom_no, bom_item.is_phantom_item)

View File

@@ -8,6 +8,10 @@ from erpnext.manufacturing.report.bom_stock_analysis.bom_stock_analysis import (
execute as bom_stock_analysis_report,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite
@@ -79,6 +83,41 @@ class TestBOMStockAnalysis(ERPNextTestSuite):
)
self.assertEqual(footer.get("description"), expected_min)
def test_components_without_stock_in_selected_warehouse_remain_visible(self):
group = create_warehouse("_Test BOM Stock Analysis Group", {"is_group": 1})
warehouse = create_warehouse("_Test BOM Stock Analysis Stores", {"parent_warehouse": group})
stocked_item, missing_item = self.rm_items
create_stock_reconciliation(item_code=stocked_item, warehouse=warehouse, qty=100, rate=100)
self.assertFalse(frappe.db.exists("Bin", {"item_code": missing_item, "warehouse": warehouse}))
for selected_warehouse in (warehouse, group):
for exploded in (False, True):
with self.subTest(warehouse=selected_warehouse, exploded=exploded):
items, footer = run_report(self.boms[0].name, selected_warehouse, exploded, qty_to_make=1)
self.assertEqual(set(items), {stocked_item, missing_item})
self.assertEqual(items[stocked_item]["available_qty"], fmt_qty(100))
self.assertEqual(items[missing_item]["available_qty"], fmt_qty(0))
self.assertEqual(items[missing_item]["required_qty"], fmt_qty(10))
self.assertEqual(items[missing_item]["difference_qty"], fmt_qty(-10))
self.assertEqual(footer["description"], 0)
items, footer = run_report(self.boms[0].name, warehouse, exploded=False, qty_to_make=0)
self.assertEqual(set(items), {stocked_item, missing_item})
self.assertEqual(items[missing_item]["available_qty"], fmt_qty(0))
self.assertEqual(footer["description"], 0)
def run_report(bom, warehouse, exploded, qty_to_make):
"""Component rows keyed by item code, plus the footer row."""
filters = {
"bom": bom,
"warehouse": warehouse,
"show_exploded_view": exploded,
"qty_to_make": qty_to_make,
}
data, footer = split_data_and_footer(bom_stock_analysis_report(filters)[1])
return {row["item"]: row for row in data}, footer
def split_data_and_footer(raw_data):
"""Separate component rows from the footer row. Skips blank spacer rows."""