From 2221f2c6f1ff6ae3291b3296ff1dd60836960727 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 21 Jun 2026 19:51:40 +0530 Subject: [PATCH] fix(manufacturing): keep Work Order Stock report one row per item (MariaDB parity) #56196's Postgres GROUP BY fix added bom.quantity, bom_item.stock_qty and bin.actual_qty to the GROUP BY. bom.quantity and bin.actual_qty are pinned to a single value by the WHERE/join, but a BOM may list the same item_code on multiple lines with different stock_qty (validate_materials does not dedupe), so grouping by stock_qty SPLITS the row and changes req_items/instock on MariaDB for such BOMs. Aggregate build_qty with Max() and group by item_code only: one row per item_code (identical to the pre-#56196 single-line result; deterministic for duplicate lines), and Postgres-valid. MariaDB output is unchanged for the common single-line case and its row count is restored for the duplicate-line case. --- .../test_work_order_stock_report.py | 6 +++--- .../work_order_stock_report.py | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/erpnext/manufacturing/report/work_order_stock_report/test_work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/test_work_order_stock_report.py index 583a89e9664..9c04452e44e 100644 --- a/erpnext/manufacturing/report/work_order_stock_report/test_work_order_stock_report.py +++ b/erpnext/manufacturing/report/work_order_stock_report/test_work_order_stock_report.py @@ -8,9 +8,9 @@ from erpnext.tests.utils import ERPNextTestSuite class TestWorkOrderStockReport(ERPNextTestSuite): def test_report_executes_and_lists_work_order(self): - # get_item_list computes build_qty by multiplying bin/bom/bom_item columns that are not - # functionally dependent on the grouped item_code; they must be in the GROUP BY for the - # report to run on Postgres. This exercises that query on both engines. + # get_item_list aggregates build_qty with Max() and groups by item_code, so it returns one + # row per item_code and runs on Postgres (which rejects non-grouped selected columns). + # This exercises that query on both engines. from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.report.work_order_stock_report.work_order_stock_report import execute diff --git a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py index 0637b14ccc7..b7f27c99bf0 100644 --- a/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py +++ b/erpnext/manufacturing/report/work_order_stock_report/work_order_stock_report.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.query_builder.functions import IfNull +from frappe.query_builder.functions import IfNull, Max, Sum from frappe.utils import cint @@ -40,17 +40,23 @@ def get_item_list(wo_list, filters): ) .select( bom_item.item_code.as_("item_code"), - IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"), + # Aggregate so the query stays one row per item_code and is Postgres + # GROUP-BY-valid. A BOM may list the same item on several lines; adding the + # qty columns to GROUP BY would split the row and change the row count on + # MariaDB. actual_qty (single warehouse) and bom.quantity (single BOM) are + # pinned to one value, so Max() returns that value; the per-unit requirement + # is the TOTAL of this item across its lines, so stock_qty is summed. For the + # common single-line item this equals the prior expression exactly. + IfNull(Max(bin.actual_qty) * Max(bom.quantity) / Sum(bom_item.stock_qty), 0).as_( + "build_qty" + ), ) .where( (bom.name == bom_item.parent) & (bom_item.item_code == wo_item_details.item_code) & (bom.name == wo_details.bom_no) ) - # build_qty multiplies columns from bin/bom/bom_item that aren't functionally - # dependent on the grouped item_code, so postgres requires them in the GROUP BY. - # The WHERE pins bom, item and warehouse to single rows, so this stays one row. - .groupby(bom_item.item_code, bom.quantity, bom_item.stock_qty, bin.actual_qty) + .groupby(bom_item.item_code) ).run(as_dict=1) stock_qty = 0