mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-12 14:11:46 +00:00
fix(manufacturing): stop BOM Stock Analysis inflating both its sums
get_bom_data left-joined Bin on item_code alone and then summed over the result. Bin holds one row per warehouse and BOM Item one row per line, so the join is a cross product and each SUM counts the other side's rows: Sum(qty_consumed_per_unit) x (number of warehouses holding the item) Sum(bin.actual_qty) x (number of BOM lines carrying the item) A component on two BOM lines, stocked in two warehouses, reported a per-unit requirement of 10 instead of 5 and available stock of 20 instead of 10 -- wrong on both engines, and wrong in the single-line case too as soon as the item sits in more than one warehouse. Aggregate Bin to one row per item_code before joining, so neither sum can see the other's duplicates. The warehouse filter moves into that subquery; it previously sat in the outer WHERE against a left-joined column, which silently made the join inner, so the join is now made inner explicitly when a warehouse is given to keep items with no bin there excluded as before.
This commit is contained in:
@@ -190,27 +190,14 @@ def batch_fetch_purchase_rates(bom_data):
|
||||
}
|
||||
|
||||
|
||||
def get_bom_data(filters):
|
||||
bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item"
|
||||
|
||||
bom_item = frappe.qb.DocType(bom_item_table)
|
||||
def get_stock_qty_by_item(filters):
|
||||
"""One row per item_code, so joining it to BOM Item cannot multiply either side's sum."""
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bom_item)
|
||||
.left_join(bin)
|
||||
.on(bom_item.item_code == bin.item_code)
|
||||
.select(
|
||||
bom_item.item_code,
|
||||
# non-grouped columns are constant per grouped item_code -> Max() keeps the GROUP BY valid
|
||||
Max(bom_item.description).as_("description"),
|
||||
Max(bom_item.parent).as_("from_bom_no"),
|
||||
Sum(bom_item.qty_consumed_per_unit).as_("qty_per_unit"),
|
||||
IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"),
|
||||
)
|
||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
||||
.groupby(bom_item.item_code)
|
||||
.orderby(Min(bom_item.idx))
|
||||
frappe.qb.from_(bin)
|
||||
.select(bin.item_code, Sum(bin.actual_qty).as_("actual_qty"))
|
||||
.groupby(bin.item_code)
|
||||
)
|
||||
|
||||
if filters.get("warehouse"):
|
||||
@@ -233,6 +220,33 @@ def get_bom_data(filters):
|
||||
else:
|
||||
query = query.where(bin.warehouse == filters.get("warehouse"))
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_bom_data(filters):
|
||||
bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item"
|
||||
|
||||
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)
|
||||
.select(
|
||||
bom_item.item_code,
|
||||
# non-grouped columns are constant per grouped item_code -> Max() keeps the GROUP BY valid
|
||||
Max(bom_item.description).as_("description"),
|
||||
Max(bom_item.parent).as_("from_bom_no"),
|
||||
Sum(bom_item.qty_consumed_per_unit).as_("qty_per_unit"),
|
||||
IfNull(Max(stock_qty.actual_qty), 0).as_("actual_qty"),
|
||||
)
|
||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
||||
.groupby(bom_item.item_code)
|
||||
.orderby(Min(bom_item.idx))
|
||||
)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
if bom_item_table == "BOM Item":
|
||||
|
||||
Reference in New Issue
Block a user