diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index fd30ddf6884..0fbb4b31db1 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Max from frappe.utils import flt @@ -282,32 +283,34 @@ def get_po_entries(filters): frappe.qb.from_(parent) .from_(child) .select( - child.name, - child.parent, - child.cost_center, - child.project, - child.warehouse, - child.material_request, + Max(child.name).as_("name"), + Max(child.parent).as_("parent"), + Max(child.cost_center).as_("cost_center"), + Max(child.project).as_("project"), + Max(child.warehouse).as_("warehouse"), + Max(child.material_request).as_("material_request"), child.material_request_item, - child.item_code, - child.stock_uom, - child.qty, - child.amount, - child.base_amount, - child.schedule_date, - parent.transaction_date, - parent.supplier, - parent.status, - parent.owner, + Max(child.item_code).as_("item_code"), + Max(child.stock_uom).as_("stock_uom"), + Max(child.qty).as_("qty"), + Max(child.amount).as_("amount"), + Max(child.base_amount).as_("base_amount"), + Max(child.schedule_date).as_("schedule_date"), + Max(parent.transaction_date).as_("transaction_date"), + Max(parent.supplier).as_("supplier"), + Max(parent.status).as_("status"), + Max(parent.owner).as_("owner"), ) .where( (parent.docstatus == 1) & (parent.name == child.parent) & (parent.status.notin(("Closed", "Completed", "Cancelled"))) ) - # This is one row per PO item; the selected child.* columns are only functionally dependent - # on the child PK, which postgres requires in the GROUP BY (MariaDB allows omitting it). - .groupby(parent.name, child.material_request_item, child.name) + # Group only by the PO and material_request_item (the pre-effort key) and aggregate the rest + # with Max(): postgres requires every non-grouped column to be aggregated, and this keeps one + # row per (PO, material_request_item) — matching the prior MariaDB row count. Adding the PO + # Item PK to the GROUP BY would split a multi-line PO into one row per line. + .groupby(parent.name, child.material_request_item) ) query = apply_filters_on_query(filters, parent, child, query) diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index f1781c15e05..0d168c56f31 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -2,14 +2,16 @@ # For license information, please see license.txt +from frappe.utils import add_days, nowdate + from erpnext.tests.utils import ERPNextTestSuite class TestProcurementTracker(ERPNextTestSuite): def test_report_executes_and_lists_po(self): - # get_po_entries groups by (Purchase Order, material_request_item, Purchase Order Item) - # while selecting other child columns; this exercises that GROUP BY so the report stays - # valid on Postgres (which rejects selecting non-grouped columns). + # get_po_entries groups by (Purchase Order, material_request_item) and Max()-aggregates the + # other child columns; this exercises that GROUP BY so the report stays valid on Postgres + # (which rejects selecting non-grouped columns). from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.report.procurement_tracker.procurement_tracker import execute @@ -19,3 +21,32 @@ class TestProcurementTracker(ERPNextTestSuite): self.assertTrue(columns) self.assertIn(po.name, {row.get("purchase_order") for row in data}) + + def test_multi_line_po_stays_one_row(self): + # A PO can carry several lines that share the same (blank) material_request_item. get_po_entries + # groups by (Purchase Order, material_request_item) and Max()-aggregates the rest, so such a PO + # yields ONE row — matching the pre-effort MariaDB output. Adding the Purchase Order Item PK to + # the GROUP BY (the regression) splits it into one row per line, changing the MariaDB row count. + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.buying.report.procurement_tracker.procurement_tracker import execute + from erpnext.stock.doctype.item.test_item import make_item + + second_item = make_item("_Test Procurement Tracker Item", {"is_stock_item": 1}).name + po = create_purchase_order(company="_Test Company", do_not_submit=True) + po.append( + "items", + { + "item_code": second_item, + "warehouse": "_Test Warehouse - _TC", + "qty": 5, + "rate": 100, + "schedule_date": add_days(nowdate(), 1), + }, + ) + po.save() + po.submit() + + columns, data = execute({"company": "_Test Company"}) + + po_rows = [row for row in data if row.get("purchase_order") == po.name] + self.assertEqual(len(po_rows), 1)