From a1ed913eba853eaca8797cbfd985c593b0447a54 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 18:58:17 +0530 Subject: [PATCH] fix(selling): deterministic order-type row order in Sales Analytics on both engines get_teams fetched distinct order_types with get_all(distinct=True, order_by="order_type"). frappe drops ORDER BY for distinct queries on postgres (db_query), so the order_by is a no-op there and the report's order-type leaf rows are not guaranteed any order on PG (PostgreSQL only sorts them incidentally via its DISTINCT plan). Sort in python with key=str.casefold instead, matching MariaDB's case-insensitive collation and guaranteeing an identical, stable order on both engines (same pattern as the Sales/Purchase Register account-column fix). Add a test locking the sorted order-type row order. --- .../report/sales_analytics/sales_analytics.py | 18 +++++++++----- .../sales_analytics/test_sales_analytics.py | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 93f1abe8222..9eb879681ee 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -528,12 +528,18 @@ class Analytics: if not frappe.db.exists("DocType", self.filters.doc_type): frappe.throw(_("Invalid Document Type {0}").format(self.filters.doc_type)) - order_types = frappe.get_all( - self.filters.doc_type, - filters={"order_type": ["is", "set"]}, - pluck="order_type", - distinct=True, - order_by="order_type", + # frappe drops ORDER BY for distinct queries on postgres (db_query), so a SQL order_by="order_type" + # would be a no-op there and the leaf rows would come back unordered. Sort in python with casefold + # to keep the report's order-type row order deterministic, case-insensitive (matching MariaDB's + # collation), and identical on both engines. + order_types = sorted( + frappe.get_all( + self.filters.doc_type, + filters={"order_type": ["is", "set"]}, + pluck="order_type", + distinct=True, + ), + key=str.casefold, ) self.group_entries = [frappe._dict(name="Order Types", lft=0, rgt=2, parent="")] diff --git a/erpnext/selling/report/sales_analytics/test_sales_analytics.py b/erpnext/selling/report/sales_analytics/test_sales_analytics.py index b9827327b63..9d7ad3f8dad 100644 --- a/erpnext/selling/report/sales_analytics/test_sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_sales_analytics.py @@ -117,6 +117,30 @@ class TestSalesAnalytics(ERPNextTestSuite): self.assertAlmostEqual(rows["Sales"]["total"], expected, places=2) self.assertAlmostEqual(rows["Order Types"]["total"], expected, places=2) + def test_order_type_leaf_rows_in_sorted_order(self): + """get_teams fetches distinct order_types; frappe drops the SQL ORDER BY for distinct queries on + postgres, so the report sorts the order-type rows in python (key=str.casefold) to keep them in a + deterministic, case-insensitive order identical on both engines.""" + for order_type in ("Shopping Cart", "Maintenance", "Sales"): # created out of sorted order + so = make_sales_order( + company=COMPANY, + customer=CUSTOMER, + qty=1, + rate=100, + transaction_date="2019-04-12", + do_not_submit=True, + ) + so.order_type = order_type + so.submit() + + columns, data, *_ = execute(self._base_filters(tree_type="Order Type")) + + mine = {"Sales", "Maintenance", "Shopping Cart"} + leaves = [row["entity"] for row in data if row.get("entity") in mine] + # the order-type rows must appear in casefold-sorted order on both engines + self.assertEqual(leaves, sorted(leaves, key=str.casefold)) + self.assertEqual(set(leaves), mine) + def test_customer_group_by_quantity(self): """value_quantity='Quantity' switches the selected value column (total_qty).""" _columns, data, *_ = execute(