From 9f1915800fe8db603477b455ad32206e937764d5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 09:13:49 +0530 Subject: [PATCH 1/4] fix(edi): make Common Code docname lookup valid on Postgres get_docnames_for issued SELECT DISTINCT on Dynamic Link.link_name while ordering by Dynamic Link.idx, a column absent from the select list. This is a raw frappe.qb query (run via .run(), not get_all/get_list), so the ORDER BY is emitted verbatim and PostgreSQL rejects it: 'for SELECT DISTINCT, ORDER BY expressions must appear in select list'. Order by link_name (the selected, distinct column) instead; same docnames on both engines, now deterministically ordered. --- erpnext/edi/doctype/code_list/code_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py index e723157e7a0..b816f7dc3e7 100644 --- a/erpnext/edi/doctype/code_list/code_list.py +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -116,7 +116,7 @@ def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]: & (CommonCode.code_list == code_list) ) .distinct() - .orderby(DynamicLink.idx) + .orderby(DynamicLink.link_name) ).run() return tuple(d[0] for d in docnames) if docnames else () From bde630b88808e9d687a467831358657d6cb69431 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 09:13:50 +0530 Subject: [PATCH 2/4] fix(controllers): cast idx to varchar in child-row picker for Postgres get_filtered_child_rows searched child rows by row number with table.idx.like(...). idx is an integer column; frappe maps .like() to ILIKE on Postgres, which has no bigint ILIKE operator ('operator does not exist: bigint ~~* unknown'). Cast idx to string via frappe's Cast_ with 'varchar': a bare CAST(idx AS CHAR) is character(1) on Postgres and silently truncates a two-digit idx (11 -> '1'), dropping the row; CAST(idx AS VARCHAR) keeps the full value, and on MariaDB Cast_ rewrites to CONCAT(idx, '') matching the previous implicit coercion. MariaDB output unchanged. The test builds an order with >10 rows and searches row 11 (fails on Postgres with a char(1) cast). --- erpnext/controllers/queries.py | 4 +++- erpnext/controllers/tests/test_queries.py | 28 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 723c60964e4..8db277a4123 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -10,6 +10,7 @@ from frappe import qb, scrub from frappe.permissions import has_permission from frappe.query_builder import Case, Criterion, DocType from frappe.query_builder.functions import ( + Cast_, Concat, IfNull, Length, @@ -1135,7 +1136,8 @@ def get_filtered_child_rows( if txt: txt += "%" query = query.where( - ((table.idx.like(txt.replace("#", ""))) | (table.item_code.like(txt))) | (table.name.like(txt)) + ((Cast_(table.idx, "varchar").like(txt.replace("#", ""))) | (table.item_code.like(txt))) + | (table.name.like(txt)) ) return query.run(as_dict=False) diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 43603d2a6e7..ff48ad7ed1d 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -93,6 +93,34 @@ class TestQueries(ERPNextTestSuite): query = add_default_params(queries.get_purchase_invoices, "Purchase Invoice") self.assertIsInstance(query(txt="", filters={}), list | tuple) + def test_get_filtered_child_rows_query(self): + # idx is an integer column. Searching child rows by it must run on Postgres + # (a bare LIKE rejects "bigint ILIKE text") AND cast to a full-length string: + # CAST(idx AS CHAR) is character(1) on Postgres, so a two-digit idx like 11 + # would render as "1" and be missed. Build a Sales Order with >10 rows and + # search for row 11 to lock both behaviours. + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + frappe.db.set_single_value("Selling Settings", "allow_multiple_items", 1) + so = make_sales_order( + item_list=[ + {"item_code": "_Test Item", "qty": 1, "rate": 100, "warehouse": "_Test Warehouse - _TC"} + for _ in range(11) + ], + do_not_submit=True, + ) + + rows = queries.get_filtered_child_rows( + "Sales Order Item", + txt="#11", + searchfield="name", + start=0, + page_len=20, + filters={"parent": so.name}, + ) + # row label is "#, "; row 11 must be present + self.assertTrue(any(str(label).startswith("#11,") for _name, label in rows)) + def test_default_uoms(self): self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10) From 16b27ecdd1442aab4b4b8af434f01023c5b01680 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 09:13:51 +0530 Subject: [PATCH 3/4] fix(stock): group the Stock Ledger opening-balance dimension query get_opening_balance_for_inv_dimension selected item_code and warehouse alongside Sum() aggregates with no GROUP BY, which PostgreSQL rejects ('column ...item_code must appear in the GROUP BY clause'). Add GROUP BY item_code, warehouse. The query already returns early unless a single item and warehouse is selected, so this stays one row with identical values on MariaDB while becoming valid on Postgres. --- erpnext/stock/report/stock_ledger/stock_ledger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index f6abae53f5e..52ea26d4383 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -822,6 +822,8 @@ def get_opening_balance_for_inv_dimension(filters, inv_dimension_wise_value): else: query = query.where(sl_doctype[key] == value) + query = query.groupby(sl_doctype.item_code, sl_doctype.warehouse) + opening_data = query.run(as_dict=True) if opening_data: From f11f8cb0051dd913a446a61bb1428d44f8d0cfbf Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 23 Jun 2026 09:13:52 +0530 Subject: [PATCH 4/4] fix(regional): use correct lowercase fieldname in UAE VAT tax accounts get_tax_accounts fetched fields=['Account'] but the UAE VAT Account fieldname is lowercase account. PostgreSQL treats the double-quoted identifier case-sensitively ('column "Account" does not exist'); MariaDB identifiers are case-insensitive so it worked there. Use the real fieldname account; output unchanged on MariaDB. --- erpnext/regional/united_arab_emirates/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 671f726a740..2fb01117fcf 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -77,7 +77,7 @@ def get_account_currency(account): def get_tax_accounts(company): """Get the list of tax accounts for a specific company.""" tax_accounts_dict = frappe._dict() - tax_accounts_list = frappe.get_all("UAE VAT Account", filters={"parent": company}, fields=["Account"]) + tax_accounts_list = frappe.get_all("UAE VAT Account", filters={"parent": company}, fields=["account"]) if not tax_accounts_list and not frappe.in_test: frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company))