From 19ec095ff8c198ee92e1afabe9158852a67dfb39 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jun 2026 15:47:57 +0530 Subject: [PATCH] feat(general-ledger): implement execute_synced_report with full parity to normal report Co-Authored-By: Claude Sonnet 4.6 (cherry picked from commit 6b4895bcc92be13d45d82bd31c3229c1914434c1) --- .../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 c9ee9784a8d..08c467d5c1c 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -820,10 +820,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