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)