From 6849d292f8b87de6052106ae52337927cda7f4af Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:52 +0530 Subject: [PATCH 01/11] refactor(postgres): port financial statements helper queries to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../accounts/report/financial_statements.py | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 478c603dbf5..320637721d7 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -179,11 +179,10 @@ def get_data( company_currency = get_appropriate_currency(company, filters) gl_entries_by_account = {} - for root in frappe.db.sql( - """select lft, rgt from tabAccount - where root_type=%s and ifnull(parent_account, '') = ''""", - root_type, - as_dict=1, + for root in frappe.get_all( + "Account", + filters={"root_type": root_type, "parent_account": ["is", "not set"]}, + fields=["lft", "rgt"], ): set_gl_entries_by_account( company, @@ -373,13 +372,23 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency def get_accounts(company, root_type): - return frappe.db.sql( - """ - select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group, lft, rgt - from `tabAccount` - where company=%s and root_type=%s order by lft""", - (company, root_type), - as_dict=True, + return frappe.get_all( + "Account", + filters={"company": company, "root_type": root_type}, + fields=[ + "name", + "account_number", + "parent_account", + "lft", + "rgt", + "root_type", + "report_type", + "account_name", + "include_in_gross", + "account_type", + "is_group", + ], + order_by="lft", ) @@ -529,7 +538,11 @@ def get_accounting_entries( gl_entry.credit_in_account_currency if not group_by_account else Sum(gl_entry.credit_in_account_currency).as_("credit_in_account_currency"), - gl_entry.account_currency, + # when grouping by account the non-aggregated columns must be aggregated for postgres; + # account_currency is constant per account so Max() returns the same value. + gl_entry.account_currency + if not group_by_account + else Max(gl_entry.account_currency).as_("account_currency"), ) .where(gl_entry.company == filters.company) ) @@ -547,15 +560,29 @@ def get_accounting_entries( ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") if doctype == "GL Entry": - query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year) + # aggregate the non-grouped columns when grouping by account (postgres requirement) + if group_by_account: + query = query.select( + Max(gl_entry.posting_date).as_("posting_date"), + Max(gl_entry.is_opening).as_("is_opening"), + Max(gl_entry.fiscal_year).as_("fiscal_year"), + ) + else: + query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year) query = query.where(gl_entry.is_cancelled == 0) query = query.where(gl_entry.posting_date <= to_date) - query = query.force_index("posting_date_company_index") + # FORCE INDEX is MySQL-only; postgres has no index hints (its planner uses the index anyway) + if frappe.db.db_type != "postgres": + query = query.force_index("posting_date_company_index") if ignore_opening_entries and not ignore_is_opening: query = query.where(gl_entry.is_opening == "No") else: - query = query.select(gl_entry.closing_date.as_("posting_date")) + query = query.select( + Max(gl_entry.closing_date).as_("posting_date") + if group_by_account + else gl_entry.closing_date.as_("posting_date") + ) query = query.where(gl_entry.period_closing_voucher == period_closing_voucher) query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters) From e9eca1092777eb0c3d276d0219f2707aa2bd485a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:54 +0530 Subject: [PATCH 02/11] refactor(postgres): port Cash Flow report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../accounts/report/cash_flow/cash_flow.py | 58 ++++++++++--------- .../report/cash_flow/test_cash_flow.py | 27 +++++++++ 2 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 erpnext/accounts/report/cash_flow/test_cash_flow.py diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 4ed34ab8eb9..e6eae689ca9 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -7,6 +7,7 @@ from datetime import timedelta import frappe from frappe import _ from frappe.query_builder import DocType +from frappe.query_builder.functions import Sum from frappe.utils import cstr, flt from pypika import Order @@ -213,37 +214,43 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ def get_account_type_based_gl_data(company, filters=None): - cond = "" filters = frappe._dict(filters or {}) + gle = frappe.qb.DocType("GL Entry") + account = frappe.qb.DocType("Account") + + query = ( + frappe.qb.from_(gle) + .select(Sum(gle.credit) - Sum(gle.debit)) + .where( + (gle.company == company) + & (gle.posting_date >= filters.start_date) + & (gle.posting_date <= filters.end_date) + & (gle.voucher_type != "Period Closing Voucher") + & gle.account.isin( + frappe.qb.from_(account) + .select(account.name) + .where(account.account_type == filters.account_type) + ) + ) + ) + if filters.include_default_book_entries: company_fb = frappe.get_cached_value("Company", company, "default_finance_book") - cond = """ AND (finance_book in ({}, {}, '') OR finance_book IS NULL) - """.format( - frappe.db.escape(filters.finance_book), - frappe.db.escape(company_fb), + query = query.where( + gle.finance_book.isin([filters.finance_book, company_fb, ""]) | gle.finance_book.isnull() ) else: - cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" % ( - frappe.db.escape(cstr(filters.finance_book)) + query = query.where( + gle.finance_book.isin([cstr(filters.finance_book), ""]) | gle.finance_book.isnull() ) if filters.get("cost_center"): - filters.cost_center = get_cost_centers_with_children(filters.cost_center) - cond += " and cost_center in %(cost_center)s" + cost_centers = get_cost_centers_with_children(filters.cost_center) + query = query.where(gle.cost_center.isin(cost_centers)) - gl_sum = frappe.db.sql_list( - f""" - select sum(credit) - sum(debit) - from `tabGL Entry` - where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s - and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond} - """, - filters, - ) - - return gl_sum[0] if gl_sum and gl_sum[0] else 0 + gl_sum = query.run() + return gl_sum[0][0] if gl_sum and gl_sum[0][0] else 0 def get_start_date(period, accumulated_values, company): @@ -367,11 +374,10 @@ def get_net_income(company, period_list, filters): from_date, to_date = get_opening_range_using_fiscal_year(company, period_list) for root_type in ["Income", "Expense"]: - for root in frappe.db.sql( - """select lft, rgt from tabAccount - where root_type=%s and ifnull(parent_account, '') = ''""", - root_type, - as_dict=1, + for root in frappe.get_all( + "Account", + filters={"root_type": root_type, "parent_account": ["is", "not set"]}, + fields=["lft", "rgt"], ): set_gl_entries_by_account( company, diff --git a/erpnext/accounts/report/cash_flow/test_cash_flow.py b/erpnext/accounts/report/cash_flow/test_cash_flow.py new file mode 100644 index 00000000000..9cfae74f9e4 --- /dev/null +++ b/erpnext/accounts/report/cash_flow/test_cash_flow.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + +from erpnext.accounts.report.cash_flow.cash_flow import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestCashFlow(ERPNextTestSuite): + def test_report_executes(self): + # Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on + # both MariaDB and postgres. + company = frappe.db.get_value("Company", {}, "name") + fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc") + columns, *_rest = execute( + frappe._dict( + { + "company": company, + "from_fiscal_year": fy, + "to_fiscal_year": fy, + "filter_based_on": "Fiscal Year", + "periodicity": "Yearly", + } + ) + ) + self.assertTrue(columns) From 65c0d35f2e7822479c625037d472325763ad945e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:55 +0530 Subject: [PATCH 03/11] refactor(postgres): port Budget Variance report query to the query builder The raw get_fiscal_years query had no ORDER BY (de-facto oldest-first); the get_all port adds explicit order_by="name asc" so the Fiscal Year doctype default (name DESC) does not reverse the report column order / cumulative values. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../budget_variance_report.py | 44 ++++++++----------- .../test_budget_variance_report.py | 27 ++++++++++++ 2 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index a4d8480a848..c839d26490d 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -3,7 +3,7 @@ import frappe from frappe import _ -from frappe.query_builder import CustomFunction +from frappe.query_builder.custom import MonthName from frappe.utils import add_months, flt, formatdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions @@ -113,7 +113,6 @@ def build_budget_map(budget_records, filters): def get_actual_transactions(dimension_name, filters): budget_against = frappe.scrub(filters.get("budget_against")) - monthname = CustomFunction("MONTHNAME", ["date"]) gle = frappe.qb.DocType("GL Entry") budget = frappe.qb.DocType("Budget") @@ -126,7 +125,7 @@ def get_actual_transactions(dimension_name, filters): gle.debit, gle.credit, gle.fiscal_year, - monthname(gle.posting_date).as_("month_name"), + MonthName(gle.posting_date).as_("month_name"), budget[budget_against].as_("budget_against"), ) .where( @@ -137,7 +136,10 @@ def get_actual_transactions(dimension_name, filters): & (gle.is_cancelled == 0) & (budget[budget_against] == dimension_name) ) - .groupby(gle.name) + # budget[budget_against] is selected from the Budget table, which is not functionally + # dependent on the grouped GL Entry PK, so postgres requires it in the GROUP BY. The WHERE + # pins it to dimension_name (a constant), so grouping by it does not change the result. + .groupby(gle.name, budget[budget_against]) .orderby(gle.fiscal_year) ) @@ -157,15 +159,11 @@ def get_actual_transactions(dimension_name, filters): def get_budget_distributions(budget): - return frappe.db.sql( - """ - SELECT start_date, end_date, amount, percent - FROM `tabBudget Distribution` - WHERE parent = %s - ORDER BY start_date ASC - """, - (budget.name,), - as_dict=True, + return frappe.get_all( + "Budget Distribution", + filters={"parent": budget.name}, + fields=["start_date", "end_date", "amount", "percent"], + order_by="start_date asc", ) @@ -351,20 +349,16 @@ def get_columns(filters): def get_fiscal_years(filters): - fiscal_year = frappe.db.sql( - """ - select - name - from - `tabFiscal Year` - where - name between %(from_fiscal_year)s and %(to_fiscal_year)s - """, - {"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]}, + return frappe.get_all( + "Fiscal Year", + filters={"name": ["between", [filters["from_fiscal_year"], filters["to_fiscal_year"]]]}, + fields=["name"], + # the raw query had no ORDER BY (de-facto oldest-first); get_all would otherwise apply the + # Fiscal Year doctype default (name DESC) and reverse column order / cumulative-mode values. + order_by="name asc", + as_list=True, ) - return fiscal_year - def get_cost_center_with_children(cost_centers): """Expand each cost center to include itself and all its descendants.""" diff --git a/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py new file mode 100644 index 00000000000..a3a652cb658 --- /dev/null +++ b/erpnext/accounts/report/budget_variance_report/test_budget_variance_report.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + +from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestBudgetVarianceReport(ERPNextTestSuite): + def test_report_executes(self): + # Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on + # both MariaDB and postgres. + company = frappe.db.get_value("Company", {}, "name") + fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc") + columns, *_rest = execute( + frappe._dict( + { + "company": company, + "from_fiscal_year": fy, + "to_fiscal_year": fy, + "period": "Yearly", + "budget_against": "Cost Center", + } + ) + ) + self.assertTrue(columns) From 66e82c56b15237b4ff44411f562ef7c7825a8cfa Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:55 +0530 Subject: [PATCH 04/11] refactor(postgres): port Gross Profit report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/gross_profit/gross_profit.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 71af2b9d211..8785144a8da 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -713,20 +713,25 @@ class GrossProfitGenerator: ) def get_returned_invoice_items(self): - returned_invoices = frappe.db.sql( - """ - select - si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against - from - `tabSales Invoice` si, `tabSales Invoice Item` si_item - where - si.name = si_item.parent - and si.docstatus = 1 - and si.is_return = 1 - and si.posting_date between %(from_date)s and %(to_date)s - """, - {"from_date": self.filters.from_date, "to_date": self.filters.to_date}, - as_dict=1, + si = frappe.qb.DocType("Sales Invoice") + si_item = frappe.qb.DocType("Sales Invoice Item") + returned_invoices = ( + frappe.qb.from_(si) + .inner_join(si_item) + .on(si.name == si_item.parent) + .select( + si.name, + si_item.item_code, + si_item.stock_qty.as_("qty"), + si_item.base_net_amount.as_("base_amount"), + si.return_against, + ) + .where( + (si.docstatus == 1) + & (si.is_return == 1) + & si.posting_date.between(self.filters.from_date, self.filters.to_date) + ) + .run(as_dict=1) ) self.returned_invoices = frappe._dict() @@ -1241,7 +1246,4 @@ class GrossProfitGenerator: ).setdefault(d.parent_item, []).append(d) def load_non_stock_items(self): - self.non_stock_items = frappe.db.sql_list( - """select name from tabItem - where is_stock_item=0""" - ) + self.non_stock_items = frappe.get_all("Item", filters={"is_stock_item": 0}, pluck="name") From 44ca5878b87043f0c65059351aade378aea47042 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:56 +0530 Subject: [PATCH 05/11] refactor(postgres): port Consolidated Financial Statement report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../consolidated_financial_statement.py | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 78baf4484d5..fba7054e0a7 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -347,11 +347,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i filters.end_date = end_date gl_entries_by_account = {} - for root in frappe.db.sql( - """select lft, rgt from tabAccount - where root_type=%s and ifnull(parent_account, '') = ''""", - root_type, - as_dict=1, + for root in frappe.get_all( + "Account", + filters={"root_type": root_type, "parent_account": ["is", "not set"]}, + fields=["lft", "rgt"], ): set_gl_entries_by_account( start_date, @@ -512,9 +511,11 @@ def get_companies(filters): def get_subsidiary_companies(company): lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"]) - return frappe.db.sql_list( - f"""select name from `tabCompany` - where lft >= {lft} and rgt <= {rgt} order by lft, rgt""" + return frappe.get_all( + "Company", + filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, + pluck="name", + order_by="lft, rgt", ) @@ -604,14 +605,10 @@ def set_gl_entries_by_account( company_lft, company_rgt = frappe.get_cached_value("Company", filters.get("company"), ["lft", "rgt"]) - companies = frappe.db.sql( - """ select name, default_currency from `tabCompany` - where lft >= %(company_lft)s and rgt <= %(company_rgt)s""", - { - "company_lft": company_lft, - "company_rgt": company_rgt, - }, - as_dict=1, + companies = frappe.get_all( + "Company", + filters={"lft": [">=", company_lft], "rgt": ["<=", company_rgt]}, + fields=["name", "default_currency"], ) currency_info = frappe._dict( From 217c107549572a1b11bf5f3ea53b3b6bc202d30e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:57 +0530 Subject: [PATCH 06/11] refactor(postgres): port Consolidated Trial Balance report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../consolidated_trial_balance.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py index fc4affad3a7..17d0b9e1a6a 100644 --- a/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py +++ b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py @@ -126,12 +126,22 @@ def get_data(filters) -> list[list]: def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency): - accounts = frappe.db.sql( - """select name, account_number, parent_account, account_name, root_type, report_type, account_type, is_group, lft, rgt - - from `tabAccount` where company=%s order by lft""", - filters.company, - as_dict=True, + accounts = frappe.get_all( + "Account", + filters={"company": filters.company}, + fields=[ + "name", + "account_number", + "parent_account", + "account_name", + "root_type", + "report_type", + "account_type", + "is_group", + "lft", + "rgt", + ], + order_by="lft", ) ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") From 18c1f0f04d07934e031d8d8b8925feeea3cab7d7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:58 +0530 Subject: [PATCH 07/11] refactor(postgres): port Trial Balance report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/trial_balance/trial_balance.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 186a9eb71f0..4aff8b3305c 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Max, Sum from frappe.utils import add_days, cstr, flt, formatdate, getdate import erpnext @@ -82,12 +82,21 @@ def validate_filters(filters): def get_data(filters): - accounts = frappe.db.sql( - """select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt - - from `tabAccount` where company=%s order by lft""", - filters.company, - as_dict=True, + accounts = frappe.get_all( + "Account", + filters={"company": filters.company}, + fields=[ + "name", + "account_number", + "parent_account", + "account_name", + "root_type", + "report_type", + "is_group", + "lft", + "rgt", + ], + order_by="lft", ) company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company) @@ -240,7 +249,8 @@ def get_opening_balance( frappe.qb.from_(closing_balance) .select( closing_balance.account, - closing_balance.account_currency, + # account_currency is constant per grouped account -> Max() keeps the GROUP BY postgres-valid + Max(closing_balance.account_currency).as_("account_currency"), Sum(closing_balance.debit).as_("debit"), Sum(closing_balance.credit).as_("credit"), Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"), From 6a60f072a8500bb4e190a3ac5c245f87b57cb83a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:35:59 +0530 Subject: [PATCH 08/11] refactor(postgres): port Dimension-wise Account Balance report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dimension_wise_accounts_balance_report.py | 110 ++++++++---------- 1 file changed, 50 insertions(+), 60 deletions(-) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index 394a9636b78..e217d69039d 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cstr, flt +from frappe.utils import flt import erpnext from erpnext.accounts.report.financial_statements import ( @@ -31,18 +31,23 @@ def execute(filters=None): def get_data(filters, dimension_list): company_currency = erpnext.get_company_currency(filters.company) - acc = frappe.db.sql( - """ - select - name, account_number, parent_account, lft, rgt, root_type, - report_type, account_name, include_in_gross, account_type, is_group - from - `tabAccount` - where - company=%s - order by lft""", - (filters.company), - as_dict=True, + acc = frappe.get_all( + "Account", + filters={"company": filters.company}, + fields=[ + "name", + "account_number", + "parent_account", + "lft", + "rgt", + "root_type", + "report_type", + "account_name", + "include_in_gross", + "account_type", + "is_group", + ], + order_by="lft", ) if not acc: @@ -50,16 +55,17 @@ def get_data(filters, dimension_list): accounts, accounts_by_name, parent_children_map = filter_accounts(acc) - min_lft, max_rgt = frappe.db.sql( - """select min(lft), max(rgt) from `tabAccount` - where company=%s""", - (filters.company), + lft_rgt = frappe.get_all( + "Account", + filters={"company": filters.company}, + fields=[{"MIN": "lft", "as": "min_lft"}, {"MAX": "rgt", "as": "max_rgt"}], )[0] + min_lft, max_rgt = lft_rgt.min_lft, lft_rgt.max_rgt - account = frappe.db.sql_list( - """select name from `tabAccount` - where lft >= %s and rgt <= %s and company = %s""", - (min_lft, max_rgt, filters.company), + account = frappe.get_all( + "Account", + filters={"lft": [">=", min_lft], "rgt": ["<=", max_rgt], "company": filters.company}, + pluck="name", ) gl_entries_by_account = {} @@ -75,42 +81,34 @@ def get_data(filters, dimension_list): def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account): - condition = get_condition(filters.get("dimension")) - - if account: - condition += " and account in ({})".format(", ".join([frappe.db.escape(d) for d in account])) + dimension_field = frappe.scrub(filters.get("dimension")) gl_filters = { "company": filters.get("company"), - "from_date": filters.get("from_date"), - "to_date": filters.get("to_date"), - "finance_book": cstr(filters.get("finance_book")), + dimension_field: ["in", list(set(dimension_list))], + "posting_date": ["between", [filters.get("from_date"), filters.get("to_date")]], + "is_cancelled": 0, } + if account: + gl_filters["account"] = ["in", account] - gl_filters["dimensions"] = tuple(set(dimension_list)) - - if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book") - - gl_entries = frappe.db.sql( - """ - select - posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year, - debit_in_account_currency, credit_in_account_currency, account_currency - from - `tabGL Entry` - where - company=%(company)s - {condition} - and posting_date >= %(from_date)s - and posting_date <= %(to_date)s - and is_cancelled = 0 - order by account, posting_date""".format( - dimension=frappe.scrub(filters.get("dimension")), condition=condition - ), - gl_filters, - as_dict=True, - ) # nosec + gl_entries = frappe.get_all( + "GL Entry", + filters=gl_filters, + fields=[ + "posting_date", + "account", + dimension_field, + "debit", + "credit", + "is_opening", + "fiscal_year", + "debit_in_account_currency", + "credit_in_account_currency", + "account_currency", + ], + order_by="account, posting_date", + ) for entry in gl_entries: gl_entries_by_account.setdefault(entry.account, []).append(entry) @@ -178,14 +176,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): ].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0) -def get_condition(dimension): - conditions = [] - - conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s") - - return " and {}".format(" and ".join(conditions)) if conditions else "" - - def get_dimensions(filters): meta = frappe.get_meta(filters.get("dimension"), cached=False) query_filters = {} From e482c846c8150cdc7af83093aa8bbb70ed57f9a6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:36:00 +0530 Subject: [PATCH 09/11] refactor(postgres): port General Ledger report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- erpnext/accounts/report/general_ledger/general_ledger.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 8670a4fd175..042ae1e0e23 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -35,7 +35,7 @@ def execute(filters=None): if filters and filters.get("print_in_account_currency") and not filters.get("account"): frappe.throw(_("Select an account to print in account currency")) - for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): + for acc in frappe.get_all("Account", fields=["name", "is_group"]): account_details.setdefault(acc.name, acc) if filters.get("party"): @@ -650,10 +650,8 @@ def get_result_as_list(data, filters): def get_supplier_invoice_details(): inv_details = {} - for d in frappe.db.sql( - """ select name, bill_no from `tabPurchase Invoice` - where docstatus = 1 and bill_no is not null and bill_no != '' """, - as_dict=1, + for d in frappe.get_all( + "Purchase Invoice", filters={"docstatus": 1, "bill_no": ["is", "set"]}, fields=["name", "bill_no"] ): inv_details[d.name] = d.bill_no From 058be399c3f4df8d2bb3e214055585edfa1c51b2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:36:01 +0530 Subject: [PATCH 10/11] refactor(postgres): port Asset Depreciation Ledger report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../asset_depreciation_ledger.py | 11 +++-------- .../test_asset_depreciation_ledger.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 erpnext/accounts/report/asset_depreciation_ledger/test_asset_depreciation_ledger.py diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index ed132d942ba..79873bc6e12 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -15,10 +15,7 @@ def execute(filters=None): def get_data(filters): data = [] - depreciation_accounts = frappe.db.sql_list( - """ select name from tabAccount - where ifnull(account_type, '') = 'Depreciation' """ - ) + depreciation_accounts = frappe.get_all("Account", filters={"account_type": "Depreciation"}, pluck="name") filters_data = [ ["company", "=", filters.get("company")], @@ -33,10 +30,8 @@ def get_data(filters): filters_data.append(["against_voucher", "=", filters.get("asset")]) if filters.get("asset_category"): - assets = frappe.db.sql_list( - """select name from tabAsset - where asset_category = %s and docstatus=1""", - filters.get("asset_category"), + assets = frappe.get_all( + "Asset", filters={"asset_category": filters.get("asset_category"), "docstatus": 1}, pluck="name" ) filters_data.append(["against_voucher", "in", assets]) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/test_asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/test_asset_depreciation_ledger.py new file mode 100644 index 00000000000..a64c96a0174 --- /dev/null +++ b/erpnext/accounts/report/asset_depreciation_ledger/test_asset_depreciation_ledger.py @@ -0,0 +1,18 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + +from erpnext.accounts.report.asset_depreciation_ledger.asset_depreciation_ledger import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestAssetDepreciationLedger(ERPNextTestSuite): + def test_report_executes(self): + # Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on + # both MariaDB and postgres. + company = frappe.db.get_value("Company", {}, "name") + columns, *_rest = execute( + frappe._dict({"company": company, "from_date": "2020-01-01", "to_date": "2030-12-31"}) + ) + self.assertTrue(columns) From 1332ad7583cfbf02b6d85b1a53916a371b6f9ec1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 17:36:01 +0530 Subject: [PATCH 11/11] refactor(postgres): port Share Ledger report query to the query builder Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/share_ledger/share_ledger.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/share_ledger/share_ledger.py b/erpnext/accounts/report/share_ledger/share_ledger.py index ed65687a2ed..cfa6e2be67f 100644 --- a/erpnext/accounts/report/share_ledger/share_ledger.py +++ b/erpnext/accounts/report/share_ledger/share_ledger.py @@ -62,15 +62,10 @@ def get_columns(filters): def get_all_transfers(date, shareholder): - condition = " " - # if company: - # condition = 'AND company = %(company)s ' - return frappe.db.sql( - f"""SELECT * FROM `tabShare Transfer` - WHERE ((DATE(date) <= %(date)s AND from_shareholder = %(shareholder)s {condition}) - OR (DATE(date) <= %(date)s AND to_shareholder = %(shareholder)s {condition})) - AND docstatus = 1 - ORDER BY date""", - {"date": date, "shareholder": shareholder}, - as_dict=1, + return frappe.get_all( + "Share Transfer", + filters={"date": ["<=", date], "docstatus": 1}, + or_filters=[["from_shareholder", "=", shareholder], ["to_shareholder", "=", shareholder]], + fields=["*"], + order_by="date", )