fix(accounts): keep Purchase Register account-column order identical across engines

get_account_columns fetched the dynamic expense / unrealized-P&L account lists with
frappe.get_all(distinct=True, order_by=...). frappe silently drops ORDER BY for
distinct queries on postgres (db_query), so the generated account columns came back
in arbitrary order on Postgres while MariaDB kept them ordered — a cross-engine
parity gap (the sibling Sales Register had already moved to a python sort).

Sort the lists in python with key=str.casefold (dropping the ignored order_by) so the
column order is deterministic, case-insensitive (matching MariaDB's collation), and
identical on both engines. Add a regression test with two case-colliding expense
account names asserting the casefold column order on both engines.
This commit is contained in:
Mihir Kandoi
2026-06-22 07:27:18 +05:30
parent c188ed59ec
commit cf075bd67e
2 changed files with 51 additions and 21 deletions

View File

@@ -309,17 +309,22 @@ def get_account_columns(invoice_list, include_payments):
unrealized_profit_loss_account_columns = []
if invoice_list:
expense_accounts = frappe.get_all(
"Purchase Invoice Item",
filters={
"docstatus": 1,
"expense_account": ["is", "set"],
"parenttype": "Purchase Invoice",
"parent": ["in", [inv.name for inv in invoice_list]],
},
pluck="expense_account",
distinct=True,
order_by="expense_account",
# frappe drops ORDER BY for distinct queries on postgres (db_query), so sort in python with
# casefold to keep the generated account-column order deterministic and identical on both
# backends, matching MariaDB's case-insensitive collation (the original ORDER BY).
expense_accounts = sorted(
frappe.get_all(
"Purchase Invoice Item",
filters={
"docstatus": 1,
"expense_account": ["is", "set"],
"parenttype": "Purchase Invoice",
"parent": ["in", [inv.name for inv in invoice_list]],
},
pluck="expense_account",
distinct=True,
),
key=str.casefold,
)
purchase_taxes_query = get_taxes_query(invoice_list, "Purchase Taxes and Charges", "Purchase Invoice")
@@ -331,16 +336,18 @@ def get_account_columns(invoice_list, include_payments):
advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
tax_accounts = set(tax_accounts + advance_tax_accounts)
unrealized_profit_loss_accounts = frappe.get_all(
"Purchase Invoice",
filters={
"docstatus": 1,
"name": ["in", [inv.name for inv in invoice_list]],
"unrealized_profit_loss_account": ["is", "set"],
},
pluck="unrealized_profit_loss_account",
distinct=True,
order_by="unrealized_profit_loss_account",
unrealized_profit_loss_accounts = sorted(
frappe.get_all(
"Purchase Invoice",
filters={
"docstatus": 1,
"name": ["in", [inv.name for inv in invoice_list]],
"unrealized_profit_loss_account": ["is", "set"],
},
pluck="unrealized_profit_loss_account",
distinct=True,
),
key=str.casefold,
)
for account in expense_accounts:

View File

@@ -24,6 +24,29 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_expense_account_columns_sorted_case_insensitively(self):
# The dynamic expense-account columns must follow MariaDB's case-insensitive collation order and
# be identical on both engines. frappe drops ORDER BY for distinct queries on postgres, so the
# report sorts in python with casefold; plain sorted() would be case-sensitive ("ZZZ" < "aaa").
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
company = "_Test Company"
lower = create_account(
account_name="aaa Test Expense", parent_account="Expenses - _TC", company=company
)
upper = create_account(
account_name="ZZZ Test Expense", parent_account="Expenses - _TC", company=company
)
for account in (upper, lower): # submit in non-casefold order
make_purchase_invoice(company=company, expense_account=account)
filters = frappe._dict(company=company, from_date=add_months(today(), -1), to_date=today())
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_purchase_register_ignores_tax_rows_from_other_doctype(self):
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())