From adb768505a5d621064edf21a1645f8c0ecd1183a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 11 Jun 2026 19:40:24 +0530 Subject: [PATCH 1/9] refactor: reports on duckdb --- .../report/general_ledger/general_ledger.py | 9 +++++++++ .../report/trial_balance/trial_balance.py | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index dec6e18da20..11111e0bd68 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -817,3 +817,12 @@ def get_columns(filters): columns.extend([{"label": _("Remarks"), "fieldname": "remarks", "width": 400}]) return columns + + +def execute_duckdb(filters, duckdb_conn): + print(filters) + conn = duckdb_conn + columns = get_columns(filters) + res = [] + + return columns, res diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 4aff8b3305c..f9cec7c2dac 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -581,3 +581,23 @@ def hide_group_accounts(data): d.update(indent=0) non_group_accounts_data.append(d) return non_group_accounts_data + + +def execute_duckdb(filters, duckdb_conn): + validate_filters(filters) + conn = duckdb_conn + data = [] + res = conn.sql( + f"select account, sum(debit), sum(credit), account_currency from \"tabGL Entry\" where company = '{filters.company}' and posting_date between '{filters.from_date}' and '{filters.to_date}' and is_opening = 'No' group by account, account_currency;" + ).fetchall() + for x in res: + data.append( + { + "account": x[0], + "debit": x[1], + "credit": x[2], + } + ) + + columns = get_columns() + return columns, data From b1c8e2cb5cf75dc4f3cf6a7e6a71534cb805069e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jun 2026 12:51:44 +0530 Subject: [PATCH 2/9] feat(trial-balance): implement execute_duckdb with full parity to normal report Replaces the placeholder stub with 8 focused functions that mirror the normal execute() flow using parameterized DuckDB SQL queries: account fetch, period GL entries, opening balances (with Period Closing Voucher path), and all filters (cost center, project, finance book, accounting dimensions). Reuses existing pure-Python processing functions unchanged. Co-Authored-By: Claude Sonnet 4.6 --- .../report/trial_balance/trial_balance.py | 284 +++++++++++++++++- 1 file changed, 270 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index f9cec7c2dac..502964443f9 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -585,19 +585,275 @@ def hide_group_accounts(data): def execute_duckdb(filters, duckdb_conn): validate_filters(filters) - conn = duckdb_conn - data = [] - res = conn.sql( - f"select account, sum(debit), sum(credit), account_currency from \"tabGL Entry\" where company = '{filters.company}' and posting_date between '{filters.from_date}' and '{filters.to_date}' and is_opening = 'No' group by account, account_currency;" - ).fetchall() - for x in res: - data.append( - { - "account": x[0], - "debit": x[1], - "credit": x[2], - } - ) - columns = get_columns() + data = get_data_duckdb(filters, duckdb_conn) return columns, data + + +def get_data_duckdb(filters, conn): + accounts = get_accounts_duckdb(conn, filters.company) + if not accounts: + return None + + company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company) + ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") + + accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) + + gl_entries_by_account = get_period_gl_entries_duckdb(conn, filters, ignore_is_opening) + opening_balances = get_opening_balances_duckdb(conn, filters, ignore_is_opening) + + calculate_values( + accounts, + gl_entries_by_account, + opening_balances, + filters.get("show_net_values"), + ignore_is_opening=ignore_is_opening, + ) + accumulate_values_into_parents(accounts, accounts_by_name) + + data = prepare_data(accounts, filters, parent_children_map, company_currency) + data = filter_out_zero_value_rows( + data, parent_children_map, show_zero_values=filters.get("show_zero_values") + ) + + return data + + +def get_accounts_duckdb(conn, company): + rows = conn.execute( + """SELECT name, account_number, parent_account, account_name, root_type, + report_type, is_group, lft, rgt + FROM "tabAccount" WHERE company = ? ORDER BY lft""", + [company], + ).fetchall() + cols = [ + "name", + "account_number", + "parent_account", + "account_name", + "root_type", + "report_type", + "is_group", + "lft", + "rgt", + ] + return [frappe._dict(zip(cols, row, strict=False)) for row in rows] + + +def _build_common_gl_filters(filters): + """Returns (sql_fragments, params) for filters shared across all GL/ACB queries.""" + sql = [] + params = [] + + if filters.get("cost_center"): + cost_centers = get_cost_centers_with_children(filters.get("cost_center")) + placeholders = ", ".join(["?" for _ in cost_centers]) + sql.append(f"AND cost_center IN ({placeholders})") + params.extend(cost_centers) + + if filters.get("project"): + proj_list = filters.project if isinstance(filters.project, list) else [filters.project] + placeholders = ", ".join(["?" for _ in proj_list]) + sql.append(f"AND project IN ({placeholders})") + params.extend(proj_list) + + if frappe.db.count("Finance Book"): + company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") + if filters.get("include_default_book_entries"): + if filters.get("finance_book") and company_fb and cstr(filters.finance_book) != cstr(company_fb): + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default FB Entries'") + ) + fb_list = [cstr(filters.get("finance_book")), cstr(company_fb), ""] + else: + fb_list = [cstr(filters.get("finance_book")), ""] + placeholders = ", ".join(["?" for _ in fb_list]) + sql.append(f"AND (finance_book IN ({placeholders}) OR finance_book IS NULL)") + params.extend(fb_list) + + accounting_dimensions = get_accounting_dimensions(as_list=False) + for dimension in accounting_dimensions: + if filters.get(dimension.fieldname): + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + dim_vals = filters[dimension.fieldname] + if not isinstance(dim_vals, list): + dim_vals = [dim_vals] + placeholders = ", ".join(["?" for _ in dim_vals]) + sql.append(f"AND {dimension.fieldname} IN ({placeholders})") + params.extend(dim_vals) + + return sql, params + + +def get_period_gl_entries_duckdb(conn, filters, ignore_is_opening): + ignore_closing_entries = not flt(filters.get("with_period_closing_entry_for_current_period")) + common_sql, common_params = _build_common_gl_filters(filters) + + sql_parts = [ + "SELECT account, SUM(debit) AS debit, SUM(credit) AS credit,", + " SUM(debit_in_account_currency) AS debit_in_account_currency,", + " SUM(credit_in_account_currency) AS credit_in_account_currency,", + " account_currency", + 'FROM "tabGL Entry"', + "WHERE company = ?", + " AND is_cancelled = 0", + " AND posting_date >= ?", + " AND posting_date <= ?", + ] + params = [filters.company, filters.from_date, filters.to_date] + + if not ignore_is_opening: + sql_parts.append(" AND is_opening = 'No'") + + if ignore_closing_entries: + sql_parts.append(" AND voucher_type != 'Period Closing Voucher'") + + sql_parts.extend(common_sql) + params.extend(common_params) + sql_parts.append("GROUP BY account, account_currency") + + rows = conn.execute("\n".join(sql_parts), params).fetchall() + cols = [ + "account", + "debit", + "credit", + "debit_in_account_currency", + "credit_in_account_currency", + "account_currency", + ] + entries = [frappe._dict(zip(cols, row, strict=False)) for row in rows] + + if filters.get("presentation_currency"): + convert_to_presentation_currency(entries, get_currency(filters)) + + gl_entries_by_account = {} + for entry in entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) + + return gl_entries_by_account + + +def get_opening_balances_duckdb(conn, filters, ignore_is_opening): + bs = _get_rootwise_opening_balances_duckdb(conn, filters, "Balance Sheet", ignore_is_opening) + pl = _get_rootwise_opening_balances_duckdb(conn, filters, "Profit and Loss", ignore_is_opening) + bs.update(pl) + return bs + + +def _get_rootwise_opening_balances_duckdb(conn, filters, report_type, ignore_is_opening): + ignore_closing_balances = frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance") + last_period_closing_voucher = None + + if not ignore_closing_balances: + pcv = frappe.db.get_all( + "Period Closing Voucher", + filters={"docstatus": 1, "company": filters.company, "period_end_date": ("<", filters.from_date)}, + fields=["period_end_date", "name"], + order_by="period_end_date desc", + limit=1, + ) + if pcv: + last_period_closing_voucher = pcv[0] + + gle = [] + if last_period_closing_voucher: + gle = _query_opening_balance_duckdb( + conn, + "Account Closing Balance", + filters, + report_type, + ignore_is_opening, + period_closing_voucher=last_period_closing_voucher.name, + ) + if getdate(last_period_closing_voucher.period_end_date) < getdate(add_days(filters.from_date, -1)): + start_date = add_days(last_period_closing_voucher.period_end_date, 1) + gle += _query_opening_balance_duckdb( + conn, + "GL Entry", + filters, + report_type, + ignore_is_opening, + start_date=start_date, + ) + else: + gle = _query_opening_balance_duckdb(conn, "GL Entry", filters, report_type, ignore_is_opening) + + opening = frappe._dict() + for d in gle: + opening.setdefault(d.account, {"account": d.account, "opening_debit": 0.0, "opening_credit": 0.0}) + opening[d.account]["opening_debit"] += flt(d.debit) + opening[d.account]["opening_credit"] += flt(d.credit) + + return opening + + +def _query_opening_balance_duckdb( + conn, doctype, filters, report_type, ignore_is_opening, period_closing_voucher=None, start_date=None +): + table = f'"tab{doctype}"' + common_sql, common_params = _build_common_gl_filters(filters) + + sql_parts = [ + "SELECT account, SUM(debit) AS debit, SUM(credit) AS credit,", + " SUM(debit_in_account_currency) AS debit_in_account_currency,", + " SUM(credit_in_account_currency) AS credit_in_account_currency,", + " account_currency", + f"FROM {table}", + "WHERE company = ?", + ' AND account IN (SELECT name FROM "tabAccount" WHERE report_type = ?)', + ] + params = [filters.company, report_type] + + if doctype == "GL Entry": + sql_parts.append(" AND is_cancelled = 0") + + if start_date: + sql_parts.append(" AND posting_date >= ?") + sql_parts.append(" AND posting_date < ?") + params.extend([start_date, filters.from_date]) + if not ignore_is_opening: + sql_parts.append(" AND is_opening = 'No'") + else: + if not ignore_is_opening: + sql_parts.append(" AND (posting_date < ? OR is_opening = 'Yes')") + params.append(filters.from_date) + else: + sql_parts.append(" AND posting_date < ?") + params.append(filters.from_date) + + if not filters.get("show_unclosed_fy_pl_balances") and report_type == "Profit and Loss": + sql_parts.append(" AND posting_date >= ?") + params.append(filters.year_start_date) + + if not flt(filters.get("with_period_closing_entry_for_opening")): + sql_parts.append(" AND voucher_type != 'Period Closing Voucher'") + else: + sql_parts.append(" AND period_closing_voucher = ?") + params.append(period_closing_voucher) + + if not flt(filters.get("with_period_closing_entry_for_opening")): + sql_parts.append(" AND is_period_closing_voucher_entry = 0") + + sql_parts.extend(common_sql) + params.extend(common_params) + sql_parts.append("GROUP BY account, account_currency") + + rows = conn.execute("\n".join(sql_parts), params).fetchall() + cols = [ + "account", + "debit", + "credit", + "debit_in_account_currency", + "credit_in_account_currency", + "account_currency", + ] + gle = [frappe._dict(zip(cols, row, strict=False)) for row in rows] + + if filters.get("presentation_currency"): + convert_to_presentation_currency(gle, get_currency(filters)) + + return gle From 55862f98f4327a0d7994981beccae17170c6acd8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jun 2026 13:47:46 +0530 Subject: [PATCH 3/9] refactor(trial-balance): execute_duckdb only reads GL Entry from duckdb Replaces the previous over-engineered stub with 7 short functions. Account data, Account Closing Balance, and all metadata come from frappe.db as normal; only tabGL Entry is read from the duckdb_conn. Reuses get_opening_balance() for Account Closing Balance unchanged, reuses all downstream compute helpers (calculate_values, prepare_data, etc.) unchanged. Co-Authored-By: Claude Sonnet 4.6 --- .../report/trial_balance/trial_balance.py | 257 +++++++----------- 1 file changed, 94 insertions(+), 163 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 502964443f9..ae1e8471689 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -591,13 +591,18 @@ def execute_duckdb(filters, duckdb_conn): def get_data_duckdb(filters, conn): - accounts = get_accounts_duckdb(conn, filters.company) + # accounts and all metadata via frappe.db — only GL Entry comes from DuckDB + 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, + ) if not accounts: return None company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company) ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") - accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) gl_entries_by_account = get_period_gl_entries_duckdb(conn, filters, ignore_is_opening) @@ -613,50 +618,24 @@ def get_data_duckdb(filters, conn): accumulate_values_into_parents(accounts, accounts_by_name) data = prepare_data(accounts, filters, parent_children_map, company_currency) - data = filter_out_zero_value_rows( + return filter_out_zero_value_rows( data, parent_children_map, show_zero_values=filters.get("show_zero_values") ) - return data - -def get_accounts_duckdb(conn, company): - rows = conn.execute( - """SELECT name, account_number, parent_account, account_name, root_type, - report_type, is_group, lft, rgt - FROM "tabAccount" WHERE company = ? ORDER BY lft""", - [company], - ).fetchall() - cols = [ - "name", - "account_number", - "parent_account", - "account_name", - "root_type", - "report_type", - "is_group", - "lft", - "rgt", - ] - return [frappe._dict(zip(cols, row, strict=False)) for row in rows] - - -def _build_common_gl_filters(filters): - """Returns (sql_fragments, params) for filters shared across all GL/ACB queries.""" - sql = [] - params = [] +def _extra_gl_conditions(filters): + """Returns (conditions, params) for optional shared GL Entry filters.""" + conditions, params = [], [] if filters.get("cost_center"): - cost_centers = get_cost_centers_with_children(filters.get("cost_center")) - placeholders = ", ".join(["?" for _ in cost_centers]) - sql.append(f"AND cost_center IN ({placeholders})") - params.extend(cost_centers) + cc = get_cost_centers_with_children(filters.get("cost_center")) + conditions.append(f"cost_center IN ({', '.join(['?'] * len(cc))})") + params.extend(cc) if filters.get("project"): - proj_list = filters.project if isinstance(filters.project, list) else [filters.project] - placeholders = ", ".join(["?" for _ in proj_list]) - sql.append(f"AND project IN ({placeholders})") - params.extend(proj_list) + proj = filters.project if isinstance(filters.project, list) else [filters.project] + conditions.append(f"project IN ({', '.join(['?'] * len(proj))})") + params.extend(proj) if frappe.db.count("Finance Book"): company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") @@ -668,55 +647,27 @@ def _build_common_gl_filters(filters): fb_list = [cstr(filters.get("finance_book")), cstr(company_fb), ""] else: fb_list = [cstr(filters.get("finance_book")), ""] - placeholders = ", ".join(["?" for _ in fb_list]) - sql.append(f"AND (finance_book IN ({placeholders}) OR finance_book IS NULL)") + conditions.append(f"(finance_book IN ({', '.join(['?'] * len(fb_list))}) OR finance_book IS NULL)") params.extend(fb_list) - accounting_dimensions = get_accounting_dimensions(as_list=False) - for dimension in accounting_dimensions: - if filters.get(dimension.fieldname): - if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): - filters[dimension.fieldname] = get_dimension_with_children( - dimension.document_type, filters.get(dimension.fieldname) + for dim in get_accounting_dimensions(as_list=False): + if filters.get(dim.fieldname): + if frappe.get_cached_value("DocType", dim.document_type, "is_tree"): + filters[dim.fieldname] = get_dimension_with_children( + dim.document_type, filters.get(dim.fieldname) ) - dim_vals = filters[dimension.fieldname] - if not isinstance(dim_vals, list): - dim_vals = [dim_vals] - placeholders = ", ".join(["?" for _ in dim_vals]) - sql.append(f"AND {dimension.fieldname} IN ({placeholders})") - params.extend(dim_vals) + vals = ( + filters[dim.fieldname] + if isinstance(filters[dim.fieldname], list) + else [filters[dim.fieldname]] + ) + conditions.append(f"{dim.fieldname} IN ({', '.join(['?'] * len(vals))})") + params.extend(vals) - return sql, params + return conditions, params -def get_period_gl_entries_duckdb(conn, filters, ignore_is_opening): - ignore_closing_entries = not flt(filters.get("with_period_closing_entry_for_current_period")) - common_sql, common_params = _build_common_gl_filters(filters) - - sql_parts = [ - "SELECT account, SUM(debit) AS debit, SUM(credit) AS credit,", - " SUM(debit_in_account_currency) AS debit_in_account_currency,", - " SUM(credit_in_account_currency) AS credit_in_account_currency,", - " account_currency", - 'FROM "tabGL Entry"', - "WHERE company = ?", - " AND is_cancelled = 0", - " AND posting_date >= ?", - " AND posting_date <= ?", - ] - params = [filters.company, filters.from_date, filters.to_date] - - if not ignore_is_opening: - sql_parts.append(" AND is_opening = 'No'") - - if ignore_closing_entries: - sql_parts.append(" AND voucher_type != 'Period Closing Voucher'") - - sql_parts.extend(common_sql) - params.extend(common_params) - sql_parts.append("GROUP BY account, account_currency") - - rows = conn.execute("\n".join(sql_parts), params).fetchall() +def _fetch_gl_rows_duckdb(conn, conditions, params): cols = [ "account", "debit", @@ -725,135 +676,115 @@ def get_period_gl_entries_duckdb(conn, filters, ignore_is_opening): "credit_in_account_currency", "account_currency", ] - entries = [frappe._dict(zip(cols, row, strict=False)) for row in rows] + sql = f"""SELECT account, SUM(debit), SUM(credit), + SUM(debit_in_account_currency), SUM(credit_in_account_currency), account_currency + FROM "tabGL Entry" WHERE {" AND ".join(conditions)} + GROUP BY account, account_currency""" + return [frappe._dict(zip(cols, row, strict=False)) for row in conn.execute(sql, params).fetchall()] + +def get_period_gl_entries_duckdb(conn, filters, ignore_is_opening): + conditions = ["company = ?", "is_cancelled = 0", "posting_date >= ?", "posting_date <= ?"] + params = [filters.company, filters.from_date, filters.to_date] + + if not ignore_is_opening: + conditions.append("is_opening = 'No'") + if not flt(filters.get("with_period_closing_entry_for_current_period")): + conditions.append("voucher_type != 'Period Closing Voucher'") + + extra_cond, extra_params = _extra_gl_conditions(filters) + conditions.extend(extra_cond) + params.extend(extra_params) + + entries = _fetch_gl_rows_duckdb(conn, conditions, params) if filters.get("presentation_currency"): convert_to_presentation_currency(entries, get_currency(filters)) gl_entries_by_account = {} for entry in entries: gl_entries_by_account.setdefault(entry.account, []).append(entry) - return gl_entries_by_account def get_opening_balances_duckdb(conn, filters, ignore_is_opening): - bs = _get_rootwise_opening_balances_duckdb(conn, filters, "Balance Sheet", ignore_is_opening) - pl = _get_rootwise_opening_balances_duckdb(conn, filters, "Profit and Loss", ignore_is_opening) + bs = _get_rootwise_opening_duckdb(conn, filters, "Balance Sheet", ignore_is_opening) + pl = _get_rootwise_opening_duckdb(conn, filters, "Profit and Loss", ignore_is_opening) bs.update(pl) return bs -def _get_rootwise_opening_balances_duckdb(conn, filters, report_type, ignore_is_opening): +def _get_rootwise_opening_duckdb(conn, filters, report_type, ignore_is_opening): + accounting_dimensions = get_accounting_dimensions(as_list=False) ignore_closing_balances = frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance") - last_period_closing_voucher = None + last_pcv = "" if not ignore_closing_balances: - pcv = frappe.db.get_all( + last_pcv = frappe.db.get_all( "Period Closing Voucher", filters={"docstatus": 1, "company": filters.company, "period_end_date": ("<", filters.from_date)}, fields=["period_end_date", "name"], order_by="period_end_date desc", limit=1, ) - if pcv: - last_period_closing_voucher = pcv[0] - gle = [] - if last_period_closing_voucher: - gle = _query_opening_balance_duckdb( - conn, + if last_pcv: + # Account Closing Balance fetched via frappe (not GL Entry) + gle = get_opening_balance( "Account Closing Balance", filters, report_type, - ignore_is_opening, - period_closing_voucher=last_period_closing_voucher.name, + accounting_dimensions, + period_closing_voucher=last_pcv[0].name, + ignore_is_opening=ignore_is_opening, ) - if getdate(last_period_closing_voucher.period_end_date) < getdate(add_days(filters.from_date, -1)): - start_date = add_days(last_period_closing_voucher.period_end_date, 1) - gle += _query_opening_balance_duckdb( - conn, - "GL Entry", - filters, - report_type, - ignore_is_opening, - start_date=start_date, + if getdate(last_pcv[0].period_end_date) < getdate(add_days(filters.from_date, -1)): + start_date = add_days(last_pcv[0].period_end_date, 1) + gle += _get_gl_entry_opening_duckdb( + conn, filters, report_type, ignore_is_opening, start_date=start_date ) else: - gle = _query_opening_balance_duckdb(conn, "GL Entry", filters, report_type, ignore_is_opening) + gle = _get_gl_entry_opening_duckdb(conn, filters, report_type, ignore_is_opening) opening = frappe._dict() for d in gle: opening.setdefault(d.account, {"account": d.account, "opening_debit": 0.0, "opening_credit": 0.0}) opening[d.account]["opening_debit"] += flt(d.debit) opening[d.account]["opening_credit"] += flt(d.credit) - return opening -def _query_opening_balance_duckdb( - conn, doctype, filters, report_type, ignore_is_opening, period_closing_voucher=None, start_date=None -): - table = f'"tab{doctype}"' - common_sql, common_params = _build_common_gl_filters(filters) +def _get_gl_entry_opening_duckdb(conn, filters, report_type, ignore_is_opening, start_date=None): + accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name") + if not accounts: + return [] - sql_parts = [ - "SELECT account, SUM(debit) AS debit, SUM(credit) AS credit,", - " SUM(debit_in_account_currency) AS debit_in_account_currency,", - " SUM(credit_in_account_currency) AS credit_in_account_currency,", - " account_currency", - f"FROM {table}", - "WHERE company = ?", - ' AND account IN (SELECT name FROM "tabAccount" WHERE report_type = ?)', - ] - params = [filters.company, report_type] + conditions = ["company = ?", f"account IN ({', '.join(['?'] * len(accounts))})", "is_cancelled = 0"] + params = [filters.company, *accounts] - if doctype == "GL Entry": - sql_parts.append(" AND is_cancelled = 0") - - if start_date: - sql_parts.append(" AND posting_date >= ?") - sql_parts.append(" AND posting_date < ?") - params.extend([start_date, filters.from_date]) - if not ignore_is_opening: - sql_parts.append(" AND is_opening = 'No'") - else: - if not ignore_is_opening: - sql_parts.append(" AND (posting_date < ? OR is_opening = 'Yes')") - params.append(filters.from_date) - else: - sql_parts.append(" AND posting_date < ?") - params.append(filters.from_date) - - if not filters.get("show_unclosed_fy_pl_balances") and report_type == "Profit and Loss": - sql_parts.append(" AND posting_date >= ?") - params.append(filters.year_start_date) - - if not flt(filters.get("with_period_closing_entry_for_opening")): - sql_parts.append(" AND voucher_type != 'Period Closing Voucher'") + if start_date: + conditions.append("posting_date >= ? AND posting_date < ?") + params.extend([start_date, filters.from_date]) + if not ignore_is_opening: + conditions.append("is_opening = 'No'") + elif not ignore_is_opening: + conditions.append("(posting_date < ? OR is_opening = 'Yes')") + params.append(filters.from_date) else: - sql_parts.append(" AND period_closing_voucher = ?") - params.append(period_closing_voucher) + conditions.append("posting_date < ?") + params.append(filters.from_date) - if not flt(filters.get("with_period_closing_entry_for_opening")): - sql_parts.append(" AND is_period_closing_voucher_entry = 0") + if not filters.get("show_unclosed_fy_pl_balances") and report_type == "Profit and Loss": + conditions.append("posting_date >= ?") + params.append(filters.year_start_date) - sql_parts.extend(common_sql) - params.extend(common_params) - sql_parts.append("GROUP BY account, account_currency") + if not flt(filters.get("with_period_closing_entry_for_opening")): + conditions.append("voucher_type != 'Period Closing Voucher'") - rows = conn.execute("\n".join(sql_parts), params).fetchall() - cols = [ - "account", - "debit", - "credit", - "debit_in_account_currency", - "credit_in_account_currency", - "account_currency", - ] - gle = [frappe._dict(zip(cols, row, strict=False)) for row in rows] + extra_cond, extra_params = _extra_gl_conditions(filters) + conditions.extend(extra_cond) + params.extend(extra_params) + gle = _fetch_gl_rows_duckdb(conn, conditions, params) if filters.get("presentation_currency"): convert_to_presentation_currency(gle, get_currency(filters)) - return gle From 5c536b8ad1e7a7c7274cd3f82ac9e9ab2f34891f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jun 2026 12:39:25 +0530 Subject: [PATCH 4/9] refactor: maintain sync dependency in report master --- .../report/accounts_payable/accounts_payable.json | 10 +++++++++- .../accounts_receivable/accounts_receivable.json | 10 +++++++++- .../report/general_ledger/general_ledger.json | 10 +++++++++- .../accounts/report/trial_balance/trial_balance.json | 12 ++++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.json b/erpnext/accounts/report/accounts_payable/accounts_payable.json index 40aa222cbb0..48380605ccf 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.json +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.json @@ -4,13 +4,20 @@ "columns": [], "creation": "2013-04-22 16:16:03", "default_print_format": "Accounts Payable Standard", + "disable_prepared_report_automation": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "doctype_to_sync": [ + { + "doc_type": "Payment Ledger Entry" + } + ], "filters": [], + "generate_csv": 0, "idx": 3, "is_standard": "Yes", - "modified": "2026-05-22 14:35:14.716933", + "modified": "2026-06-18 11:54:12.154865", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Payable", @@ -33,5 +40,6 @@ "role": "Auditor" } ], + "synced_report": 1, "timeout": 0 } diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json index b6e7820f91c..3b4d6594bf1 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json @@ -4,13 +4,20 @@ "columns": [], "creation": "2013-04-16 11:31:13", "default_print_format": "Accounts Receivable Standard", + "disable_prepared_report_automation": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "doctype_to_sync": [ + { + "doc_type": "Payment Ledger Entry" + } + ], "filters": [], + "generate_csv": 0, "idx": 5, "is_standard": "Yes", - "modified": "2026-05-22 14:34:57.666402", + "modified": "2026-06-18 11:53:59.190645", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Receivable", @@ -27,5 +34,6 @@ "role": "Accounts User" } ], + "synced_report": 1, "timeout": 0 } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 8dac581eae3..7f5d59a9f98 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -4,13 +4,20 @@ "columns": [], "creation": "2013-12-06 13:22:23", "default_print_format": "General Ledger Standard", + "disable_prepared_report_automation": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "doctype_to_sync": [ + { + "doc_type": "GL Entry" + } + ], "filters": [], + "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-05-22 14:34:35.246000", + "modified": "2026-06-18 11:53:29.057634", "modified_by": "Administrator", "module": "Accounts", "name": "General Ledger", @@ -30,5 +37,6 @@ "role": "Auditor" } ], + "synced_report": 1, "timeout": 0 } diff --git a/erpnext/accounts/report/trial_balance/trial_balance.json b/erpnext/accounts/report/trial_balance/trial_balance.json index b6c121bd5fd..321bf46d05b 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.json +++ b/erpnext/accounts/report/trial_balance/trial_balance.json @@ -4,13 +4,20 @@ "columns": [], "creation": "2014-07-22 11:41:23.743564", "default_print_format": "Trial Balance Standard", + "disable_prepared_report_automation": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "doctype_to_sync": [ + { + "doc_type": "GL Entry" + } + ], "filters": [], - "idx": 2, + "generate_csv": 0, + "idx": 4, "is_standard": "Yes", - "modified": "2026-05-22 14:35:44.889062", + "modified": "2026-06-18 11:41:42.774023", "modified_by": "Administrator", "module": "Accounts", "name": "Trial Balance", @@ -30,5 +37,6 @@ "role": "Auditor" } ], + "synced_report": 1, "timeout": 0 } From f40cd4180146b76e9b62854dc015b8c0ecfb96f4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jun 2026 16:39:55 +0530 Subject: [PATCH 5/9] refactor: DB agnostic method names --- .../report/trial_balance/trial_balance.json | 2 +- .../report/trial_balance/trial_balance.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.json b/erpnext/accounts/report/trial_balance/trial_balance.json index 321bf46d05b..7aca6d62acc 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.json +++ b/erpnext/accounts/report/trial_balance/trial_balance.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-18 11:41:42.774023", + "modified": "2026-06-18 16:37:42.112788", "modified_by": "Administrator", "module": "Accounts", "name": "Trial Balance", diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index ae1e8471689..85a5142b777 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -583,11 +583,16 @@ def hide_group_accounts(data): return non_group_accounts_data -def execute_duckdb(filters, duckdb_conn): - validate_filters(filters) - columns = get_columns() - data = get_data_duckdb(filters, duckdb_conn) - return columns, data +def execute_synced_report(filters): + from frappe.database.duckdb.database import get_latest_sync + + if conn := get_latest_sync("GL Entry"): + validate_filters(filters) + columns = get_columns() + data = get_data_duckdb(filters, conn) + return columns, data + else: + frappe.throw(_("Trial Balance requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry"))) def get_data_duckdb(filters, conn): From 6b4895bcc92be13d45d82bd31c3229c1914434c1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jun 2026 15:47:57 +0530 Subject: [PATCH 6/9] feat(general-ledger): implement execute_synced_report with full parity to normal report Co-Authored-By: Claude Sonnet 4.6 --- .../report/general_ledger/general_ledger.json | 2 +- .../report/general_ledger/general_ledger.py | 286 +++++++++++++++++- 2 files changed, 282 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 7f5d59a9f98..914fa496c07 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-18 11:53:29.057634", + "modified": "2026-06-22 11:50:08.020553", "modified_by": "Administrator", "module": "Accounts", "name": "General Ledger", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 11111e0bd68..cae1f27a556 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -819,10 +819,286 @@ def get_columns(filters): return columns -def execute_duckdb(filters, duckdb_conn): - print(filters) - conn = duckdb_conn - columns = get_columns(filters) - res = [] +def execute_synced_report(filters): + from frappe.database.duckdb.database import get_latest_sync + if conn := get_latest_sync("GL Entry"): + return _execute_with_duckdb_conn(filters, conn) + + frappe.throw(_("General Ledger requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry"))) + + +def _execute_with_duckdb_conn(filters, conn): + if not filters: + return [], [] + + account_details = {} + + if 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.get_all("Account", fields=["name", "is_group"]): + account_details.setdefault(acc.name, acc) + + if filters.get("party"): + filters.party = frappe.parse_json(filters.get("party")) + + validate_filters(filters, account_details) + validate_party(filters) + filters = set_account_currency(filters) + columns = get_columns(filters) + res = get_result_duckdb(filters, account_details, conn) return columns, res + + +def get_result_duckdb(filters, account_details, conn): + accounting_dimensions = [] + if filters.get("include_dimensions"): + accounting_dimensions = get_accounting_dimensions() + + gl_entries = get_gl_entries_duckdb(filters, accounting_dimensions, conn) + data = get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries) + return get_result_as_list(data, filters) + + +def get_gl_entries_duckdb(filters, accounting_dimensions, conn): + currency_map = get_currency(filters) + + col_names = [ + "gl_entry", + "posting_date", + "account", + "party_type", + "party", + "voucher_type", + "voucher_subtype", + "voucher_no", + "cost_center", + "project", + "against_voucher_type", + "against_voucher", + "account_currency", + "against", + "is_opening", + "creation", + "debit", + "credit", + "debit_in_account_currency", + "credit_in_account_currency", + ] + select_exprs = [ + "name", + "posting_date", + "account", + "party_type", + "party", + "voucher_type", + "voucher_subtype", + "voucher_no", + "cost_center", + "project", + "against_voucher_type", + "against_voucher", + "account_currency", + "against", + "is_opening", + "creation", + "debit", + "credit", + "debit_in_account_currency", + "credit_in_account_currency", + ] + + if filters.get("show_remarks"): + remarks_length = frappe.get_single_value("Accounts Settings", "general_ledger_remarks_length") + if remarks_length: + select_exprs.append(f"substr(remarks, 1, {int(remarks_length)})") + else: + select_exprs.append("remarks") + col_names.append("remarks") + + if filters.get("add_values_in_transaction_currency"): + select_exprs += [ + "debit_in_transaction_currency", + "credit_in_transaction_currency", + "transaction_currency", + ] + col_names += [ + "debit_in_transaction_currency", + "credit_in_transaction_currency", + "transaction_currency", + ] + + if accounting_dimensions: + select_exprs += accounting_dimensions + col_names += accounting_dimensions + + order_by = "posting_date, account, creation" + if filters.get("include_dimensions"): + order_by = "posting_date, creation" + if filters.get("categorize_by") == "Categorize by Voucher": + order_by = "posting_date, voucher_type, voucher_no" + if filters.get("categorize_by") == "Categorize by Account": + order_by = "account, posting_date, creation" + + if filters.get("include_default_book_entries"): + filters["company_fb"] = frappe.get_cached_value( + "Company", filters.get("company"), "default_finance_book" + ) + + conditions, params = _build_gl_conditions_duckdb(filters) + select_clause = ", ".join(select_exprs) + sql = f'SELECT {select_clause} FROM "tabGL Entry" WHERE {" AND ".join(conditions)} ORDER BY {order_by}' + + rows = conn.execute(sql, params).fetchall() + gl_entries = [frappe._dict(zip(col_names, row, strict=False)) for row in rows] + + party_name_map = get_party_name_map() + for gl_entry in gl_entries: + if gl_entry.party_type and gl_entry.party: + gl_entry.party_name = party_name_map.get(gl_entry.party_type, {}).get(gl_entry.party) + + if filters.get("presentation_currency"): + return convert_to_presentation_currency(gl_entries, currency_map, filters) + return gl_entries + + +def _build_gl_conditions_duckdb(filters): + ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") + + conditions = ["company = ?"] + params = [filters.company] + + if filters.get("account"): + filters.account = get_accounts_with_children(filters.account) + if filters.account: + conditions.append(f"account IN ({', '.join(['?'] * len(filters.account))})") + params.extend(filters.account) + + if filters.get("cost_center"): + filters.cost_center = get_cost_centers_with_children(filters.cost_center) + conditions.append(f"cost_center IN ({', '.join(['?'] * len(filters.cost_center))})") + params.extend(filters.cost_center) + + if filters.get("voucher_no"): + conditions.append("voucher_no = ?") + params.append(filters.voucher_no) + + if filters.get("against_voucher_no"): + conditions.append("against_voucher = ?") + params.append(filters.against_voucher_no) + + if filters.get("ignore_err"): + err_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": filters.get("company"), + "docstatus": 1, + "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), + }, + pluck="name", + ) + if err_journals: + filters.update({"voucher_no_not_in": err_journals}) + + if filters.get("ignore_cr_dr_notes"): + system_generated = frappe.db.get_all( + "Journal Entry", + filters={ + "company": filters.get("company"), + "docstatus": 1, + "voucher_type": ("in", ["Credit Note", "Debit Note"]), + "is_system_generated": 1, + }, + pluck="name", + ) + if system_generated: + vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + system_generated + filters.update({"voucher_no_not_in": vouchers_to_ignore}) + + if filters.get("voucher_no_not_in"): + vouchers = filters.voucher_no_not_in + conditions.append(f"voucher_no NOT IN ({', '.join(['?'] * len(vouchers))})") + params.extend(vouchers) + + if filters.get("categorize_by") == "Categorize by Party" and not filters.get("party_type"): + conditions.append("party_type IN ('Customer', 'Supplier')") + + if filters.get("party_type"): + conditions.append("party_type = ?") + params.append(filters.party_type) + + if filters.get("party"): + conditions.append(f"party IN ({', '.join(['?'] * len(filters.party))})") + params.extend(filters.party) + + # from_date: skip when filtering by account/party to allow opening balance calc in Python + if filters.get("disable_opening_balance_calculation"): + if not ignore_is_opening: + conditions.append("(posting_date >= ? OR is_opening = 'Yes')") + else: + conditions.append("posting_date >= ?") + params.append(filters.from_date) + elif not ( + filters.get("account") + or filters.get("party") + or filters.get("categorize_by") in ["Categorize by Account", "Categorize by Party"] + ): + if not ignore_is_opening: + conditions.append("(posting_date >= ? OR is_opening = 'Yes')") + else: + conditions.append("posting_date >= ?") + params.append(filters.from_date) + + if not ignore_is_opening: + conditions.append("(posting_date <= ? OR is_opening = 'Yes')") + else: + conditions.append("posting_date <= ?") + params.append(filters.to_date) + + if filters.get("project"): + conditions.append(f"project IN ({', '.join(['?'] * len(filters.project))})") + params.extend(filters.project) + + company_fb = filters.get("company_fb") or frappe.get_cached_value( + "Company", filters.company, "default_finance_book" + ) + if filters.get("include_default_book_entries"): + if filters.get("finance_book"): + if company_fb and cstr(filters.finance_book) != cstr(company_fb): + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default FB Entries'") + ) + fb_vals = [cstr(filters.finance_book), ""] + else: + fb_vals = [cstr(company_fb), ""] + conditions.append(f"(finance_book IN ({', '.join(['?'] * len(fb_vals))}) OR finance_book IS NULL)") + params.extend(fb_vals) + else: + if filters.get("finance_book"): + conditions.append("(finance_book IN (?, '') OR finance_book IS NULL)") + params.append(cstr(filters.finance_book)) + else: + conditions.append("(finance_book IN ('') OR finance_book IS NULL)") + + if not filters.get("show_cancelled_entries"): + conditions.append("is_cancelled = 0") + + accounting_dimensions_list = get_accounting_dimensions(as_list=False) + if accounting_dimensions_list: + for dimension in accounting_dimensions_list: + if not dimension.disabled and dimension.document_type != "Finance Book": + if filters.get(dimension.fieldname): + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + vals = ( + filters[dimension.fieldname] + if isinstance(filters[dimension.fieldname], list) + else [filters[dimension.fieldname]] + ) + conditions.append(f"{dimension.fieldname} IN ({', '.join(['?'] * len(vals))})") + params.extend(vals) + + return conditions, params From bb195408165aa6c69771e75dd36e3b80ca1f2f3a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jun 2026 13:29:02 +0530 Subject: [PATCH 7/9] feat(balance-sheet): implement execute_synced_report with full parity to normal report Co-Authored-By: Claude Sonnet 4.6 --- .../report/balance_sheet/balance_sheet.json | 10 +- .../report/balance_sheet/balance_sheet.py | 204 +++++++++++++++++- 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.json b/erpnext/accounts/report/balance_sheet/balance_sheet.json index 4c1d4b64030..a992e189d61 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.json +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.json @@ -4,13 +4,20 @@ "columns": [], "creation": "2014-07-14 05:24:20.385279", "default_print_format": "Balance Sheet Standard", + "disable_prepared_report_automation": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "doctype_to_sync": [ + { + "doc_type": "GL Entry" + } + ], "filters": [], + "generate_csv": 0, "idx": 3, "is_standard": "Yes", - "modified": "2026-05-22 14:35:28.187799", + "modified": "2026-06-22 13:06:12.602924", "modified_by": "Administrator", "module": "Accounts", "name": "Balance Sheet", @@ -30,5 +37,6 @@ "role": "Auditor" } ], + "synced_report": 1, "timeout": 0 } diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index a8531e58acb..756d0c2ebbb 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -4,18 +4,27 @@ import frappe from frappe import _ -from frappe.utils import cint, flt +from frappe.utils import add_days, cint, flt from erpnext.accounts.doctype.financial_report_template.financial_report_engine import ( FinancialReportEngine, get_xlsx_styles, #! DO NOT REMOVE - hook for styling ) from erpnext.accounts.report.financial_statements import ( + accumulate_values_into_parents, + add_total_row, + calculate_values, compute_growth_view_data, + filter_accounts, + filter_out_zero_value_rows, + get_accounting_entries, + get_accounts, + get_appropriate_currency, get_columns, get_data, get_filtered_list_for_consolidated_report, get_period_list, + prepare_data, ) @@ -266,3 +275,196 @@ def get_chart_data(filters, chart_columns, asset, liability, equity, currency): chart["currency"] = currency return chart + + +def execute_synced_report(filters): + from frappe.database.duckdb.database import get_latest_sync + + if not (conn := get_latest_sync("GL Entry")): + frappe.throw(_("Balance Sheet requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry"))) + + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) + filters.period_start_date = period_list[0]["year_start_date"] + + currency = filters.presentation_currency or frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) + + asset = _get_data_duckdb(conn, filters, "Asset", "Debit", period_list) + liability = _get_data_duckdb(conn, filters, "Liability", "Credit", period_list) + equity = _get_data_duckdb(conn, filters, "Equity", "Credit", period_list) + + provisional_profit_loss, total_credit = get_provisional_profit_loss( + asset, liability, equity, period_list, filters.company, currency + ) + message, opening_balance = check_opening_balance(asset, liability, equity) + + data = [] + data.extend(asset or []) + data.extend(liability or []) + data.extend(equity or []) + if opening_balance and round(opening_balance, 2) != 0: + unclosed = { + "account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", + "account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", + "warn_if_negative": True, + "currency": currency, + } + for period in period_list: + unclosed[period.key] = opening_balance + if provisional_profit_loss: + provisional_profit_loss[period.key] = provisional_profit_loss[period.key] - opening_balance + unclosed["total"] = opening_balance + data.append(unclosed) + + if provisional_profit_loss: + data.append(provisional_profit_loss) + if total_credit: + data.append(total_credit) + + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, company=filters.company + ) + chart = get_chart_data(filters, period_list, asset, liability, equity, currency) + report_summary, primitive_summary = get_report_summary( + period_list, asset, liability, equity, provisional_profit_loss, currency, filters + ) + + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + + return columns, data, message, chart, report_summary, primitive_summary + + +def _get_data_duckdb(conn, filters, root_type, balance_must_be, period_list): + accounts = get_accounts(filters.company, root_type) + if not accounts: + return None + + accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) + company_currency = get_appropriate_currency(filters.company, filters) + + gl_entries_by_account = {} + _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account, root_type) + + calculate_values( + accounts_by_name, + gl_entries_by_account, + period_list, + filters.accumulated_values, + False, + ) + accumulate_values_into_parents(accounts, accounts_by_name, period_list) + + out = prepare_data( + accounts, + balance_must_be, + period_list, + company_currency, + accumulated_values=filters.accumulated_values, + ) + out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values) + + if out: + add_total_row(out, root_type, balance_must_be, period_list, company_currency) + + return out + + +def _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account, root_type): + from erpnext.accounts.report.trial_balance.trial_balance import ( + _extra_gl_conditions, + _fetch_gl_rows_duckdb, + ) + from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency + + company = filters.company + year_start_date = period_list[0]["year_start_date"] + last_to_date = period_list[-1]["to_date"] + ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") + + leaf_accounts = [acc.name for acc in accounts if not acc.is_group] + if not leaf_accounts: + return + + opening_from_date = None + ignore_opening_entries = False + + ignore_closing_balances = frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance") + if not ignore_closing_balances: + last_pcv_list = frappe.db.get_all( + "Period Closing Voucher", + filters={ + "docstatus": 1, + "company": company, + "period_end_date": ("<", filters.get("period_start_date") or year_start_date), + }, + fields=["period_end_date", "name"], + order_by="period_end_date desc", + limit=1, + ) + if last_pcv_list: + last_pcv = last_pcv_list[0] + pcv_entries = get_accounting_entries( + "Account Closing Balance", + None, + last_to_date, + filters, + root_type=root_type, + ignore_closing_entries=False, + period_closing_voucher=last_pcv.name, + ) + if filters.get("presentation_currency"): + convert_to_presentation_currency(pcv_entries, get_currency(filters)) + for entry in pcv_entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) + opening_from_date = add_days(last_pcv.period_end_date, 1) + ignore_opening_entries = True + + extra_cond, extra_params = _extra_gl_conditions(filters) + account_placeholders = ", ".join(["?"] * len(leaf_accounts)) + base_conds = [ + "company = ?", + "is_cancelled = 0", + f"account IN ({account_placeholders})", + ] + base_params = [company, *leaf_accounts] + if ignore_opening_entries and not ignore_is_opening: + base_conds.append("is_opening = 'No'") + base_conds.extend(extra_cond) + base_params.extend(extra_params) + + # Opening GL entries from DuckDB (entries before year_start_date) + open_conds = [*base_conds, "posting_date < ?"] + open_params = [*base_params, year_start_date] + if opening_from_date: + open_conds = [*open_conds, "posting_date >= ?"] + open_params = [*open_params, opening_from_date] + + opening_entries = _fetch_gl_rows_duckdb(conn, open_conds, open_params) + if filters.get("presentation_currency"): + convert_to_presentation_currency(opening_entries, get_currency(filters)) + synthetic_open_date = add_days(year_start_date, -1) + for entry in opening_entries: + entry.posting_date = synthetic_open_date + gl_entries_by_account.setdefault(entry.account, []).append(entry) + + # Period GL entries from DuckDB (one aggregated query per period) + for period in period_list: + period_conds = [*base_conds, "posting_date >= ?", "posting_date <= ?"] + period_params = [*base_params, period.from_date, period.to_date] + + period_entries = _fetch_gl_rows_duckdb(conn, period_conds, period_params) + if filters.get("presentation_currency"): + convert_to_presentation_currency(period_entries, get_currency(filters)) + for entry in period_entries: + entry.posting_date = period.to_date + gl_entries_by_account.setdefault(entry.account, []).append(entry) From 6a93baacf05a82a0f643633b38178e13878d2283 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jun 2026 13:36:02 +0530 Subject: [PATCH 8/9] feat(profit-and-loss): implement execute_synced_report with full parity to normal report Co-Authored-By: Claude Sonnet 4.6 --- .../profit_and_loss_statement.json | 10 +- .../profit_and_loss_statement.py | 130 ++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json index 5abd51e2a30..7565c197119 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json @@ -4,13 +4,20 @@ "columns": [], "creation": "2014-07-18 11:43:33.173207", "default_print_format": "P&L Statement Standard", + "disable_prepared_report_automation": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "doctype_to_sync": [ + { + "doc_type": "GL Entry" + } + ], "filters": [], + "generate_csv": 0, "idx": 2, "is_standard": "Yes", - "modified": "2026-05-22 14:36:04.544347", + "modified": "2026-06-22 13:06:12.602924", "modified_by": "Administrator", "module": "Accounts", "name": "Profit and Loss Statement", @@ -30,5 +37,6 @@ "role": "Auditor" } ], + "synced_report": 1, "timeout": 0 } diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 9ce6cd77e5b..297aa961058 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -11,12 +11,20 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine get_xlsx_styles, #! DO NOT REMOVE - hook for styling ) from erpnext.accounts.report.financial_statements import ( + accumulate_values_into_parents, + add_total_row, + calculate_values, compute_growth_view_data, compute_margin_view_data, + filter_accounts, + filter_out_zero_value_rows, + get_accounts, + get_appropriate_currency, get_columns, get_data, get_filtered_list_for_consolidated_report, get_period_list, + prepare_data, ) @@ -197,3 +205,125 @@ def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, cur chart["currency"] = currency return chart + + +def execute_synced_report(filters): + from frappe.database.duckdb.database import get_latest_sync + + if not (conn := get_latest_sync("GL Entry")): + frappe.throw( + _("Profit and Loss Statement requires {0} to be synced to DuckDB").format(frappe.bold("GL Entry")) + ) + + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) + + income = _get_data_duckdb(conn, filters, "Income", "Credit", period_list) + expense = _get_data_duckdb(conn, filters, "Expense", "Debit", period_list) + + net_profit_loss = get_net_profit_loss( + income, expense, period_list, filters.company, filters.presentation_currency + ) + + data = [] + data.extend(income or []) + data.extend(expense or []) + if net_profit_loss: + data.append(net_profit_loss) + + columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + + currency = filters.presentation_currency or frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) + chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency) + + report_summary, primitive_summary = get_report_summary( + period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters + ) + + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + + if filters.get("selected_view") == "Margin": + compute_margin_view_data(data, period_list, filters.accumulated_values) + + return columns, data, None, chart, report_summary, primitive_summary + + +def _get_data_duckdb(conn, filters, root_type, balance_must_be, period_list): + accounts = get_accounts(filters.company, root_type) + if not accounts: + return None + + accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) + company_currency = get_appropriate_currency(filters.company, filters) + + gl_entries_by_account = {} + _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account) + + calculate_values( + accounts_by_name, + gl_entries_by_account, + period_list, + filters.accumulated_values, + False, + ) + accumulate_values_into_parents(accounts, accounts_by_name, period_list) + + out = prepare_data( + accounts, + balance_must_be, + period_list, + company_currency, + accumulated_values=filters.accumulated_values, + ) + out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values) + + if out: + add_total_row(out, root_type, balance_must_be, period_list, company_currency) + + return out + + +def _load_gl_entries_duckdb(conn, filters, period_list, accounts, gl_entries_by_account): + from erpnext.accounts.report.trial_balance.trial_balance import ( + _extra_gl_conditions, + _fetch_gl_rows_duckdb, + ) + from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency + + company = filters.company + leaf_accounts = [acc.name for acc in accounts if not acc.is_group] + if not leaf_accounts: + return + + extra_cond, extra_params = _extra_gl_conditions(filters) + account_placeholders = ", ".join(["?"] * len(leaf_accounts)) + base_conds = [ + "company = ?", + "is_cancelled = 0", + f"account IN ({account_placeholders})", + "voucher_type != 'Period Closing Voucher'", + ] + base_params = [company, *leaf_accounts] + base_conds.extend(extra_cond) + base_params.extend(extra_params) + + for period in period_list: + period_conds = [*base_conds, "posting_date >= ?", "posting_date <= ?"] + period_params = [*base_params, period.from_date, period.to_date] + + period_entries = _fetch_gl_rows_duckdb(conn, period_conds, period_params) + if filters.get("presentation_currency"): + convert_to_presentation_currency(period_entries, get_currency(filters)) + for entry in period_entries: + entry.posting_date = period.to_date + gl_entries_by_account.setdefault(entry.account, []).append(entry) From 963bbc8729e279c91582b942f43aaad645c872b1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jun 2026 13:39:01 +0530 Subject: [PATCH 9/9] refactor: synced reports should be enabled on sites based on requirements --- .../accounts/report/accounts_payable/accounts_payable.json | 4 ++-- .../report/accounts_receivable/accounts_receivable.json | 4 ++-- erpnext/accounts/report/balance_sheet/balance_sheet.json | 4 ++-- erpnext/accounts/report/general_ledger/general_ledger.json | 4 ++-- .../profit_and_loss_statement/profit_and_loss_statement.json | 4 ++-- erpnext/accounts/report/trial_balance/trial_balance.json | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.json b/erpnext/accounts/report/accounts_payable/accounts_payable.json index 48380605ccf..9c713fccf64 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.json +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 3, "is_standard": "Yes", - "modified": "2026-06-18 11:54:12.154865", + "modified": "2026-06-25 12:03:36.559152", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Payable", @@ -40,6 +40,6 @@ "role": "Auditor" } ], - "synced_report": 1, + "synced_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json index 3b4d6594bf1..dcc3c2c6a49 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.json +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 5, "is_standard": "Yes", - "modified": "2026-06-18 11:53:59.190645", + "modified": "2026-06-25 12:03:28.812092", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Receivable", @@ -34,6 +34,6 @@ "role": "Accounts User" } ], - "synced_report": 1, + "synced_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.json b/erpnext/accounts/report/balance_sheet/balance_sheet.json index a992e189d61..75277f72ac7 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.json +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 3, "is_standard": "Yes", - "modified": "2026-06-22 13:06:12.602924", + "modified": "2026-06-22 13:38:25.236839", "modified_by": "Administrator", "module": "Accounts", "name": "Balance Sheet", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 1, + "synced_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.json b/erpnext/accounts/report/general_ledger/general_ledger.json index 914fa496c07..083f7b62ae8 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.json +++ b/erpnext/accounts/report/general_ledger/general_ledger.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-22 11:50:08.020553", + "modified": "2026-06-22 13:38:35.057216", "modified_by": "Administrator", "module": "Accounts", "name": "General Ledger", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 1, + "synced_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json index 7565c197119..9aa088aefe0 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 2, "is_standard": "Yes", - "modified": "2026-06-22 13:06:12.602924", + "modified": "2026-06-22 13:38:15.898375", "modified_by": "Administrator", "module": "Accounts", "name": "Profit and Loss Statement", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 1, + "synced_report": 0, "timeout": 0 } diff --git a/erpnext/accounts/report/trial_balance/trial_balance.json b/erpnext/accounts/report/trial_balance/trial_balance.json index 7aca6d62acc..6793268a1e6 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.json +++ b/erpnext/accounts/report/trial_balance/trial_balance.json @@ -17,7 +17,7 @@ "generate_csv": 0, "idx": 4, "is_standard": "Yes", - "modified": "2026-06-18 16:37:42.112788", + "modified": "2026-06-22 13:38:42.740436", "modified_by": "Administrator", "module": "Accounts", "name": "Trial Balance", @@ -37,6 +37,6 @@ "role": "Auditor" } ], - "synced_report": 1, + "synced_report": 0, "timeout": 0 }