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

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
This commit is contained in:
Pandiyan P
2026-09-17 11:08:07 +05:30
committed by GitHub
parent 0e31182dca
commit 60913b722a
2 changed files with 38 additions and 4 deletions

View File

@@ -229,11 +229,10 @@ def get_bom_data(filters):
bom_item = frappe.qb.DocType(bom_item_table)
stock_qty = get_stock_qty_by_item(filters).as_("stock_qty")
base = frappe.qb.from_(bom_item)
base = base.join(stock_qty) if filters.get("warehouse") else base.left_join(stock_qty)
query = (
base.on(bom_item.item_code == stock_qty.item_code)
frappe.qb.from_(bom_item)
.left_join(stock_qty)
.on(bom_item.item_code == stock_qty.item_code)
.select(
bom_item.item_code,
# non-grouped columns are constant per grouped item_code -> Max() keeps the GROUP BY valid

View File

@@ -84,6 +84,29 @@ 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 _build_duplicate_component_bom(self, phantom_first):
"""Parent BOM that lists one `component` twice, once via a phantom sub-BOM and once via a
non-phantom sub-BOM. `phantom_first` controls which line is at idx 1. Returns the names of
@@ -187,6 +210,18 @@ class TestBOMStockAnalysis(ERPNextTestSuite):
self.assertAlmostEqual(flt(rows[0].actual_qty), 10.0, places=6)
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."""
data = [row for row in raw_data if row and not row.get("bold")]