Merge pull request #56275 from mihir-kandoi/pg-procurement-tracker-rowcount

fix(buying): keep Procurement Tracker one row per (PO, material_request_item) (MariaDB parity)
This commit is contained in:
Mihir Kandoi
2026-06-22 07:47:52 +05:30
committed by GitHub
2 changed files with 56 additions and 22 deletions

View File

@@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Max
from frappe.utils import flt from frappe.utils import flt
@@ -282,32 +283,34 @@ def get_po_entries(filters):
frappe.qb.from_(parent) frappe.qb.from_(parent)
.from_(child) .from_(child)
.select( .select(
child.name, Max(child.name).as_("name"),
child.parent, Max(child.parent).as_("parent"),
child.cost_center, Max(child.cost_center).as_("cost_center"),
child.project, Max(child.project).as_("project"),
child.warehouse, Max(child.warehouse).as_("warehouse"),
child.material_request, Max(child.material_request).as_("material_request"),
child.material_request_item, child.material_request_item,
child.item_code, Max(child.item_code).as_("item_code"),
child.stock_uom, Max(child.stock_uom).as_("stock_uom"),
child.qty, Max(child.qty).as_("qty"),
child.amount, Max(child.amount).as_("amount"),
child.base_amount, Max(child.base_amount).as_("base_amount"),
child.schedule_date, Max(child.schedule_date).as_("schedule_date"),
parent.transaction_date, Max(parent.transaction_date).as_("transaction_date"),
parent.supplier, Max(parent.supplier).as_("supplier"),
parent.status, Max(parent.status).as_("status"),
parent.owner, Max(parent.owner).as_("owner"),
) )
.where( .where(
(parent.docstatus == 1) (parent.docstatus == 1)
& (parent.name == child.parent) & (parent.name == child.parent)
& (parent.status.notin(("Closed", "Completed", "Cancelled"))) & (parent.status.notin(("Closed", "Completed", "Cancelled")))
) )
# This is one row per PO item; the selected child.* columns are only functionally dependent # Group only by the PO and material_request_item (the pre-effort key) and aggregate the rest
# on the child PK, which postgres requires in the GROUP BY (MariaDB allows omitting it). # with Max(): postgres requires every non-grouped column to be aggregated, and this keeps one
.groupby(parent.name, child.material_request_item, child.name) # 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) query = apply_filters_on_query(filters, parent, child, query)

View File

@@ -2,14 +2,16 @@
# For license information, please see license.txt # For license information, please see license.txt
from frappe.utils import add_days, nowdate
from erpnext.tests.utils import ERPNextTestSuite from erpnext.tests.utils import ERPNextTestSuite
class TestProcurementTracker(ERPNextTestSuite): class TestProcurementTracker(ERPNextTestSuite):
def test_report_executes_and_lists_po(self): def test_report_executes_and_lists_po(self):
# get_po_entries groups by (Purchase Order, material_request_item, Purchase Order Item) # get_po_entries groups by (Purchase Order, material_request_item) and Max()-aggregates the
# while selecting other child columns; this exercises that GROUP BY so the report stays # other child columns; this exercises that GROUP BY so the report stays valid on Postgres
# valid on Postgres (which rejects selecting non-grouped columns). # (which rejects selecting non-grouped columns).
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order 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.buying.report.procurement_tracker.procurement_tracker import execute
@@ -19,3 +21,32 @@ class TestProcurementTracker(ERPNextTestSuite):
self.assertTrue(columns) self.assertTrue(columns)
self.assertIn(po.name, {row.get("purchase_order") for row in data}) 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)