From e26a499923dd96d2023da0110f072aaf53b035ba Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 07:27:13 +0530 Subject: [PATCH] fix(accounts): keep Sales Register account-column order MariaDB-faithful on both engines get_account_columns sorts the dynamic income / unrealized-P&L account columns with python sorted() (the original raw SQL used ORDER BY, which frappe drops for distinct queries on postgres). Plain sorted() is case-sensitive (ASCII), so it reordered the columns versus the pre-effort MariaDB output, whose ORDER BY ran under the case-insensitive utf8mb4 collation. Sort with key=str.casefold so the column order matches MariaDB's collation and is identical on MariaDB and Postgres. Add a regression test with two case-colliding account names ("aaa ..." / "ZZZ ...") that fails on case-sensitive sort and passes after, on both engines. --- .../report/sales_register/sales_register.py | 10 ++++-- .../sales_register/test_sales_register.py | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index e711ec299a3..f76bbc6c6ef 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -347,14 +347,17 @@ def get_account_columns(invoice_list, include_payments): if invoice_list: # frappe drops ORDER BY for distinct queries on postgres (db_query), so sort in python to keep - # the generated account-column order deterministic and identical on both backends. + # the generated account-column order deterministic and identical on both backends. casefold + # reproduces MariaDB's case-insensitive collation order (the original raw SQL ORDER BY); plain + # sorted() would be case-sensitive and reorder columns vs the pre-effort MariaDB output. income_accounts = sorted( frappe.get_all( "Sales Invoice Item", filters={"docstatus": 1, "parent": ["in", [inv.name for inv in invoice_list]]}, pluck="income_account", distinct=True, - ) + ), + key=str.casefold, ) sales_taxes_query = get_taxes_query(invoice_list, "Sales Taxes and Charges", "Sales Invoice") @@ -377,7 +380,8 @@ def get_account_columns(invoice_list, include_payments): }, pluck="unrealized_profit_loss_account", distinct=True, - ) + ), + key=str.casefold, ) for account in income_accounts: diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py index 132b7a0ee3b..f3ed2641633 100644 --- a/erpnext/accounts/report/sales_register/test_sales_register.py +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -55,6 +55,39 @@ class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin): si = si.submit() return si + def _ensure_income_account(self, account_name): + name = f"{account_name} - _TC" + if not frappe.db.exists("Account", name): + frappe.get_doc( + { + "doctype": "Account", + "account_name": account_name, + "parent_account": "Income - _TC", + "company": self.company, + "root_type": "Income", + "report_type": "Profit and Loss", + "account_type": "Income Account", + } + ).insert() + return name + + def test_income_account_columns_sorted_case_insensitively(self): + # The dynamic income-account columns must follow MariaDB's case-insensitive collation order and + # be identical on both engines. Plain python sorted() is case-sensitive (ASCII), so "ZZZ" would + # sort before "aaa"; casefold restores the pre-effort MariaDB order on both backends. + lower = self._ensure_income_account("aaa Test Income") + upper = self._ensure_income_account("ZZZ Test Income") + for account in (upper, lower): # submit in non-casefold order + si = self.create_sales_invoice(do_not_submit=True) + si.items[0].income_account = account + si.submit() + + filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company}) + columns = execute(filters)[0] + labels = [col["label"] for col in columns if col.get("label") in (lower, upper)] + + self.assertEqual(labels, sorted([lower, upper], key=str.casefold)) + def test_basic_report_output(self): si = self.create_sales_invoice(rate=98)