From 181ad0bdcd0453008f58a2f8660885abd3e74f53 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 24 Sep 2025 18:28:13 +0530 Subject: [PATCH 1/3] feat: consolidated trial balance report --- .../consolidated_trial_balance/__init__.py | 0 .../consolidated_trial_balance.html | 1 + .../consolidated_trial_balance.js | 101 ++++ .../consolidated_trial_balance.json | 34 ++ .../consolidated_trial_balance.py | 469 ++++++++++++++++++ 5 files changed, 605 insertions(+) create mode 100644 erpnext/accounts/report/consolidated_trial_balance/__init__.py create mode 100644 erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.html create mode 100644 erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js create mode 100644 erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.json create mode 100644 erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py diff --git a/erpnext/accounts/report/consolidated_trial_balance/__init__.py b/erpnext/accounts/report/consolidated_trial_balance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.html b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.html new file mode 100644 index 00000000000..d4ae54d4f38 --- /dev/null +++ b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.html @@ -0,0 +1 @@ +{% include "accounts/report/financial_statements.html" %} diff --git a/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js new file mode 100644 index 00000000000..65f2e3b905a --- /dev/null +++ b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js @@ -0,0 +1,101 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Consolidated Trial Balance"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "MultiSelectList", + options: "Company", + get_data: function (txt) { + return frappe.db.get_link_options("Company", txt); + }, + reqd: 1, + }, + { + fieldname: "fiscal_year", + label: __("Fiscal Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + on_change: function (query_report) { + var fiscal_year = query_report.get_values().fiscal_year; + if (!fiscal_year) { + return; + } + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + from_date: fy.year_start_date, + to_date: fy.year_end_date, + }); + }); + }, + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + }, + { + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", + }, + { + fieldname: "presentation_currency", + label: __("Currency"), + fieldtype: "Select", + options: erpnext.get_presentation_currency_list(), + }, + { + fieldname: "with_period_closing_entry_for_opening", + label: __("With Period Closing Entry For Opening Balances"), + fieldtype: "Check", + default: 1, + }, + { + fieldname: "with_period_closing_entry_for_current_period", + label: __("Period Closing Entry For Current Period"), + fieldtype: "Check", + default: 1, + }, + { + fieldname: "show_zero_values", + label: __("Show zero values"), + fieldtype: "Check", + }, + { + fieldname: "show_unclosed_fy_pl_balances", + label: __("Show unclosed fiscal year's P&L balances"), + fieldtype: "Check", + }, + { + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, + }, + { + fieldname: "show_group_accounts", + label: __("Show Group Accounts"), + fieldtype: "Check", + default: 1, + }, + ], + formatter: erpnext.financial_statements.formatter, + tree: true, + name_field: "account", + parent_field: "parent_account", + initial_depth: 3, +}; diff --git a/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.json b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.json new file mode 100644 index 00000000000..e200fc0040f --- /dev/null +++ b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.json @@ -0,0 +1,34 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2025-09-03 00:53:22.230646", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-09-03 00:53:22.230646", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Consolidated Trial Balance", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "Consolidated Trial Balance", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ], + "timeout": 0 +} diff --git a/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py new file mode 100644 index 00000000000..81a9c426f44 --- /dev/null +++ b/erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.py @@ -0,0 +1,469 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import flt, getdate, now_datetime, nowdate + +import erpnext +from erpnext.accounts.doctype.account.account import get_root_company +from erpnext.accounts.report.financial_statements import ( + filter_accounts, + filter_out_zero_value_rows, + set_gl_entries_by_account, +) +from erpnext.accounts.report.trial_balance.trial_balance import ( + accumulate_values_into_parents, + calculate_values, + get_opening_balances, + hide_group_accounts, + prepare_opening_closing, + value_fields, +) +from erpnext.accounts.report.trial_balance.trial_balance import ( + validate_filters as tb_validate_filters, +) +from erpnext.accounts.report.utils import get_rate_as_at +from erpnext.accounts.utils import get_zero_cutoff +from erpnext.setup.utils import get_exchange_rate + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + validate_filters(filters=filters) + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def validate_filters(filters): + validate_companies(filters) + filters.show_net_values = True + tb_validate_filters(filters) + + +def validate_companies(filters): + if not filters.company: + return + + root_company = get_root_company(filters.company[0]) + root_company = root_company[0] if root_company else filters.company[0] + + lft, rgt = frappe.db.get_value("Company", root_company, fieldname=["lft", "rgt"]) + + company_subtree = frappe.db.get_all( + "Company", + {"lft": [">=", lft], "rgt": ["<=", rgt]}, + "name", + order_by="lft", + pluck="name", + ) + + for company in filters.company: + if company not in company_subtree: + frappe.throw( + _("Consolidated Trial Balance can be generated for Companies having same root Company.") + ) + + sort_companies(filters) + + +def sort_companies(filters): + companies = frappe.db.get_all( + "Company", {"name": ["in", filters.company]}, "name", order_by="lft", pluck="name" + ) + filters.company = companies + + +def get_data(filters) -> list[list]: + """Return data for the report. + + The report data is a list of rows, with each row being a list of cell values. + """ + data = [] + if filters.company: + reporting_currency, ignore_reporting_currency = get_reporting_currency(filters) + else: + return data + + for company in filters.company: + company_filter = frappe._dict(filters) + company_filter.company = company + + tb_data = get_company_wise_tb_data(company_filter, reporting_currency, ignore_reporting_currency) + consolidate_trial_balance_data(data, tb_data) + + for d in data: + prepare_opening_closing(d) + + total_row = calculate_total_row(data, reporting_currency) + + data.extend([{}, total_row]) + + if not filters.get("show_group_accounts"): + data = hide_group_accounts(data) + + if filters.get("presentation_currency"): + update_to_presentation_currency( + data, + reporting_currency, + filters.get("presentation_currency"), + filters.get("to_date"), + ignore_reporting_currency, + ) + + return data + + +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, + ) + + ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") + + default_currency = erpnext.get_company_currency(filters.company) + + opening_exchange_rate = get_exchange_rate( + default_currency, + reporting_currency, + filters.get("from_date"), + ) + current_date = ( + filters.get("to_date") if getdate(filters.get("to_date")) <= now_datetime().date() else nowdate() + ) + closing_exchange_rate = get_exchange_rate( + default_currency, + reporting_currency, + current_date, + ) + + if not (opening_exchange_rate and closing_exchange_rate): + frappe.throw( + _( + "Consolidated Trial balance could not be generated as Exchange Rate from {0} to {1} is not available for {2}.", + ).format(default_currency, reporting_currency, current_date) + ) + + if not accounts: + return [] + + accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) + + gl_entries_by_account = {} + + opening_balances = get_opening_balances( + filters, + ignore_is_opening, + exchange_rate=opening_exchange_rate, + ignore_reporting_currency=ignore_reporting_currency, + ) + + set_gl_entries_by_account( + filters.company, + filters.from_date, + filters.to_date, + filters, + gl_entries_by_account, + root_lft=None, + root_rgt=None, + ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period), + ignore_opening_entries=True, + group_by_account=True, + ignore_reporting_currency=ignore_reporting_currency, + ) + + calculate_values( + accounts, + gl_entries_by_account, + opening_balances, + filters.get("show_net_values"), + ignore_is_opening=ignore_is_opening, + exchange_rate=closing_exchange_rate, + ignore_reporting_currency=ignore_reporting_currency, + ) + + accumulate_values_into_parents(accounts, accounts_by_name) + + data = prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency) + data = filter_out_zero_value_rows( + data, parent_children_map, show_zero_values=filters.get("show_zero_values") + ) + + return data + + +def prepare_companywise_tb_data(accounts, filters, parent_children_map, reporting_currency): + data = [] + + for d in accounts: + # Prepare opening closing for group account + if parent_children_map.get(d.account) and filters.get("show_net_values"): + prepare_opening_closing(d) + + has_value = False + row = { + "account": d.name, + "parent_account": d.parent_account, + "indent": d.indent, + "from_date": filters.from_date, + "to_date": filters.to_date, + "currency": reporting_currency, + "is_group_account": d.is_group, + "acc_name": d.account_name, + "acc_number": d.account_number, + "account_name": ( + f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name + ), + "root_type": d.root_type, + "account_type": d.account_type, + } + + for key in value_fields: + row[key] = flt(d.get(key, 0.0), 3) + + if abs(row[key]) >= get_zero_cutoff(reporting_currency): + # ignore zero values + has_value = True + + row["has_value"] = has_value + data.append(row) + + return data + + +def calculate_total_row(data, reporting_currency): + total_row = { + "account": "'" + _("Total") + "'", + "account_name": "'" + _("Total") + "'", + "warn_if_negative": True, + "opening_debit": 0.0, + "opening_credit": 0.0, + "debit": 0.0, + "credit": 0.0, + "closing_debit": 0.0, + "closing_credit": 0.0, + "parent_account": None, + "indent": 0, + "has_value": True, + "currency": reporting_currency, + } + + for d in data: + if not d.get("parent_account"): + for field in value_fields: + total_row[field] += d[field] + + calculate_foreign_currency_translation_reserve(total_row, data) + + return total_row + + +def calculate_foreign_currency_translation_reserve(total_row, data): + opening_dr_cr_diff = total_row["opening_debit"] - total_row["opening_credit"] + dr_cr_diff = total_row["debit"] - total_row["credit"] + + idx = get_fctr_root_row_index(data) + + fctr_row = { + "account": _("Foreign Currency Translation Reserve"), + "account_name": _("Foreign Currency Translation Reserve"), + "warn_if_negative": True, + "opening_debit": abs(opening_dr_cr_diff) if opening_dr_cr_diff < 0 else 0.0, + "opening_credit": abs(opening_dr_cr_diff) if opening_dr_cr_diff > 0 else 0.0, + "debit": abs(dr_cr_diff) if dr_cr_diff < 0 else 0.0, + "credit": abs(dr_cr_diff) if dr_cr_diff > 0 else 0.0, + "closing_debit": 0.0, + "closing_credit": 0.0, + "root_type": data[idx].get("root_type"), + "account_type": "Equity", + "parent_account": data[idx].get("account"), + "indent": data[idx].get("indent") + 1, + "has_value": True, + "currency": total_row.get("currency"), + } + + fctr_row["closing_debit"] = fctr_row["opening_debit"] + fctr_row["debit"] + fctr_row["closing_credit"] = fctr_row["opening_credit"] + fctr_row["credit"] + + prepare_opening_closing(fctr_row) + + data.insert(idx + 1, fctr_row) + + for field in value_fields: + total_row[field] += fctr_row[field] + + +def get_fctr_root_row_index(data): + """ + Returns: index, root_type, parent_account + """ + liabilities_idx, equity_idx, tmp_idx = -1, -1, 0 + for d in data: + if liabilities_idx == -1 and d.get("root_type") == "Liability": + liabilities_idx = tmp_idx + + if equity_idx == -1 and d.get("root_type") == "Equity": + equity_idx = tmp_idx + + tmp_idx += 1 + + if equity_idx == -1: + return liabilities_idx + + return equity_idx + + +def consolidate_trial_balance_data(data, tb_data): + if not data: + data.extend(list(tb_data)) + return + + for entry in tb_data: + if entry: + consolidate_gle_data(data, entry, tb_data) + + +def get_reporting_currency(filters): + reporting_currency = frappe.get_cached_value("Company", filters.company[0], "reporting_currency") + default_currency = None + for company in filters.company: + company_default_currency = erpnext.get_company_currency(company) + if not default_currency: + default_currency = company_default_currency + + if company_default_currency != default_currency: + return (reporting_currency, False) + + return (default_currency, True) + + +def consolidate_gle_data(data, entry, tb_data): + entry_gle_exists = False + for gle in data: + if gle and gle["account_name"] == entry["account_name"]: + entry_gle_exists = True + gle["closing_credit"] += entry["closing_credit"] + gle["closing_debit"] += entry["closing_debit"] + gle["credit"] += entry["credit"] + gle["debit"] += entry["debit"] + gle["opening_credit"] += entry["opening_credit"] + gle["opening_debit"] += entry["opening_debit"] + gle["has_value"] = 1 + + if not entry_gle_exists: + entry_parent_account = next( + (d for d in tb_data if d.get("account") == entry.get("parent_account")), None + ) + parent_account_in_data = None + if entry_parent_account: + parent_account_in_data = next( + (d for d in data if d and d.get("account_name") == entry_parent_account.get("account_name")), + None, + ) + if parent_account_in_data: + entry["parent_account"] = parent_account_in_data.get("account") + entry["indent"] = (parent_account_in_data.get("indent") or 0) + 1 + data.insert(data.index(parent_account_in_data) + 1, entry) + else: + entry["parent_account"] = None + entry["indent"] = 0 + data.append(entry) + + +def update_to_presentation_currency(data, from_currency, to_currency, date, ignore_reporting_currency): + if from_currency == to_currency: + return + + exchange_rate = get_rate_as_at(date, from_currency, to_currency) + + for d in data: + if not ignore_reporting_currency: + for field in value_fields: + if d.get(field): + d[field] = d[field] * flt(exchange_rate) + d.update(currency=to_currency) + + +def get_columns(): + return [ + { + "fieldname": "account_name", + "label": _("Account"), + "fieldtype": "Data", + "width": 300, + }, + { + "fieldname": "acc_name", + "label": _("Account Name"), + "fieldtype": "Data", + "hidden": 1, + "width": 250, + }, + { + "fieldname": "acc_number", + "label": _("Account Number"), + "fieldtype": "Data", + "hidden": 1, + "width": 120, + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + }, + { + "fieldname": "opening_debit", + "label": _("Opening (Dr)"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "fieldname": "opening_credit", + "label": _("Opening (Cr)"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "fieldname": "debit", + "label": _("Debit"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "fieldname": "credit", + "label": _("Credit"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "fieldname": "closing_debit", + "label": _("Closing (Dr)"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "fieldname": "closing_credit", + "label": _("Closing (Cr)"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + ] From 71a8df2189f3b4c2bde0fb4b0bf918d5f4cfc0a0 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 24 Sep 2025 18:29:12 +0530 Subject: [PATCH 2/3] feat: gl entries with values in reporting_currency --- .../accounts/report/financial_statements.py | 16 +++- .../report/trial_balance/trial_balance.py | 87 +++++++++++++++---- 2 files changed, 83 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 75a52702c90..9c0e9880a55 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -437,6 +437,7 @@ def set_gl_entries_by_account( ignore_closing_entries=False, ignore_opening_entries=False, group_by_account=False, + ignore_reporting_currency=True, ): """Returns a dict like { "account": [gl entries], ... }""" gl_entries = [] @@ -467,6 +468,7 @@ def set_gl_entries_by_account( ignore_closing_entries, last_period_closing_voucher[0].name, group_by_account=group_by_account, + ignore_reporting_currency=ignore_reporting_currency, ) from_date = add_days(last_period_closing_voucher[0].period_end_date, 1) ignore_opening_entries = True @@ -482,9 +484,10 @@ def set_gl_entries_by_account( ignore_closing_entries, ignore_opening_entries=ignore_opening_entries, group_by_account=group_by_account, + ignore_reporting_currency=ignore_reporting_currency, ) - if filters and filters.get("presentation_currency"): + if filters and filters.get("presentation_currency") and ignore_reporting_currency: convert_to_presentation_currency(gl_entries, get_currency(filters)) for entry in gl_entries: @@ -505,6 +508,7 @@ def get_accounting_entries( period_closing_voucher=None, ignore_opening_entries=False, group_by_account=False, + ignore_reporting_currency=True, ): gl_entry = frappe.qb.DocType(doctype) query = ( @@ -524,6 +528,16 @@ def get_accounting_entries( .where(gl_entry.company == filters.company) ) + if not ignore_reporting_currency: + query = query.select( + gl_entry.debit_in_reporting_currency + if not group_by_account + else Sum(gl_entry.debit_in_reporting_currency).as_("debit_in_reporting_currency"), + gl_entry.credit_in_reporting_currency + if not group_by_account + else Sum(gl_entry.credit_in_reporting_currency).as_("credit_in_reporting_currency"), + ) + ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting") if doctype == "GL Entry": diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 0eecf8198d9..e5f8044aee2 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -135,15 +135,21 @@ def get_data(filters): return data -def get_opening_balances(filters, ignore_is_opening): - balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening) - pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening) +def get_opening_balances(filters, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True): + balance_sheet_opening = get_rootwise_opening_balances( + filters, "Balance Sheet", ignore_is_opening, exchange_rate, ignore_reporting_currency + ) + pl_opening = get_rootwise_opening_balances( + filters, "Profit and Loss", ignore_is_opening, exchange_rate, ignore_reporting_currency + ) balance_sheet_opening.update(pl_opening) return balance_sheet_opening -def get_rootwise_opening_balances(filters, report_type, ignore_is_opening): +def get_rootwise_opening_balances( + filters, report_type, ignore_is_opening, exchange_rate=None, ignore_reporting_currency=True +): gle = [] last_period_closing_voucher = "" @@ -168,6 +174,7 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening): accounting_dimensions, period_closing_voucher=last_period_closing_voucher[0].name, ignore_is_opening=ignore_is_opening, + ignore_reporting_currency=ignore_reporting_currency, ) # Report getting generate from the mid of a fiscal year @@ -180,24 +187,41 @@ def get_rootwise_opening_balances(filters, report_type, ignore_is_opening): accounting_dimensions, start_date=start_date, ignore_is_opening=ignore_is_opening, + ignore_reporting_currency=ignore_reporting_currency, ) else: gle = get_opening_balance( - "GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening + "GL Entry", + filters, + report_type, + accounting_dimensions, + ignore_is_opening=ignore_is_opening, + ignore_reporting_currency=ignore_reporting_currency, ) 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) + opening_dr_cr = { + "account": d.account, + "opening_debit": 0.0, + "opening_credit": 0.0, + } + + opening.setdefault(d.account, opening_dr_cr) + + if ignore_reporting_currency: + opening[d.account]["opening_debit"] += flt(d.debit) + opening[d.account]["opening_credit"] += flt(d.credit) + + else: + if d.get("report_type") == "Balance Sheet" and not ( + d.get("root_type") == "Equity" or d.get("account_type") == "Equity" + ): + opening[d.account]["opening_debit"] += flt(d.debit) * flt(exchange_rate) + opening[d.account]["opening_credit"] += flt(d.credit) * flt(exchange_rate) + else: + opening[d.account]["opening_debit"] += flt(d.debit_in_reporting_currency) + opening[d.account]["opening_credit"] += flt(d.credit_in_reporting_currency) return opening @@ -210,6 +234,7 @@ def get_opening_balance( period_closing_voucher=None, start_date=None, ignore_is_opening=0, + ignore_reporting_currency=True, ): closing_balance = frappe.qb.DocType(doctype) accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name") @@ -228,6 +253,12 @@ def get_opening_balance( .groupby(closing_balance.account) ) + if not ignore_reporting_currency: + opening_balance = opening_balance.select( + Sum(closing_balance.debit_in_reporting_currency).as_("debit_in_reporting_currency"), + Sum(closing_balance.credit_in_reporting_currency).as_("credit_in_reporting_currency"), + ) + if period_closing_voucher: opening_balance = opening_balance.where( closing_balance.period_closing_voucher == period_closing_voucher @@ -315,13 +346,21 @@ def get_opening_balance( gle = opening_balance.run(as_dict=1) - if filters and filters.get("presentation_currency"): + if filters and filters.get("presentation_currency") and ignore_reporting_currency: convert_to_presentation_currency(gle, get_currency(filters)) return gle -def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0): +def calculate_values( + accounts, + gl_entries_by_account, + opening_balances, + show_net_values, + ignore_is_opening=0, + exchange_rate=None, + ignore_reporting_currency=True, +): init = { "opening_debit": 0.0, "opening_credit": 0.0, @@ -340,8 +379,18 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net for entry in gl_entries_by_account.get(d.name, []): if cstr(entry.is_opening) != "Yes" or ignore_is_opening: - d["debit"] += flt(entry.debit) - d["credit"] += flt(entry.credit) + if ignore_reporting_currency: + d["debit"] += flt(entry.debit) + d["credit"] += flt(entry.credit) + else: + if d.report_type == "Balance Sheet" and not ( + d.root_type == "Equity" or d.account_type == "Equity" + ): + d["debit"] += flt(entry.debit) * flt(exchange_rate) + d["credit"] += flt(entry.credit) * flt(exchange_rate) + else: + d["debit"] += flt(entry.debit_in_reporting_currency) + d["credit"] += flt(entry.credit_in_reporting_currency) d["closing_debit"] = d["opening_debit"] + d["debit"] d["closing_credit"] = d["opening_credit"] + d["credit"] From a7a8ff20862fddd5a28217b5b702745444092718 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Tue, 30 Sep 2025 03:26:24 +0530 Subject: [PATCH 3/3] test: consolidated trial balance --- .../test_consolidated_trial_balance.py | 123 ++++++++++++++++++ .../trial_balance/test_trial_balance.py | 2 + 2 files changed, 125 insertions(+) create mode 100644 erpnext/accounts/report/consolidated_trial_balance/test_consolidated_trial_balance.py diff --git a/erpnext/accounts/report/consolidated_trial_balance/test_consolidated_trial_balance.py b/erpnext/accounts/report/consolidated_trial_balance/test_consolidated_trial_balance.py new file mode 100644 index 00000000000..d9d74f483b0 --- /dev/null +++ b/erpnext/accounts/report/consolidated_trial_balance/test_consolidated_trial_balance.py @@ -0,0 +1,123 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe import _ +from frappe.tests import IntegrationTestCase +from frappe.utils import flt, today + +from erpnext.accounts.report.consolidated_trial_balance.consolidated_trial_balance import execute +from erpnext.setup.utils import get_exchange_rate + + +class ForeignCurrencyTranslationReserveNotFoundError(frappe.ValidationError): + pass + + +class TestConsolidatedTrialBalance(IntegrationTestCase): + @classmethod + def setUpClass(cls): + from erpnext.accounts.report.trial_balance.test_trial_balance import create_company + from erpnext.accounts.utils import get_fiscal_year + + # Group Company + create_company(company_name="Parent Group Company India", is_group=1) + + create_company(company_name="Child Company India", parent_company="Parent Group Company India") + + # Child Company with different currency + create_company( + company_name="Child Company US", + country="United States", + currency="USD", + parent_company="Parent Group Company India", + ) + + create_journal_entry( + company="Parent Group Company India", + acc1="Marketing Expenses - PGCI", + acc2="Cash - PGCI", + amount=100000, + ) + + create_journal_entry( + company="Child Company India", acc1="Cash - CCI", acc2="Secured Loans - CCI", amount=50000 + ) + + create_journal_entry( + company="Child Company US", acc1="Marketing Expenses - CCU", acc2="Cash - CCU", amount=1000 + ) + + cls.fiscal_year = get_fiscal_year(today(), company="Parent Group Company India")[0] + + def test_single_company_report(self): + filters = frappe._dict({"company": ["Parent Group Company India"], "fiscal_year": self.fiscal_year}) + + report = execute(filters) + total_row = report[1][-1] + + self.assertEqual(total_row["closing_debit"], total_row["closing_credit"]) + self.assertEqual(total_row["closing_credit"], 100000) + + def test_child_company_report_with_same_default_currency_as_parent_company(self): + filters = frappe._dict( + { + "company": ["Parent Group Company India", "Child Company India"], + "fiscal_year": self.fiscal_year, + } + ) + + report = execute(filters) + total_row = report[1][-1] + + self.assertEqual(total_row["closing_debit"], total_row["closing_credit"]) + + def test_child_company_with_different_default_currency_from_parent_company(self): + filters = frappe._dict( + { + "company": ["Parent Group Company India", "Child Company US"], + "fiscal_year": self.fiscal_year, + } + ) + + report = execute(filters) + total_row = report[1][-1] + + exchange_rate = get_exchange_rate("USD", "INR") + + fctr = [d for d in report[1] if d.get("account") == _("Foreign Currency Translation Reserve")] + + if not fctr: + raise ForeignCurrencyTranslationReserveNotFoundError + + ccu_total_credit = 1000 * flt(exchange_rate) + + self.assertEqual(total_row["closing_debit"], total_row["closing_credit"]) + self.assertNotEqual(total_row["closing_credit"], ccu_total_credit) + + self.assertEqual(total_row["closing_credit"], flt(100000 + ccu_total_credit)) + + +def create_journal_entry(**args): + args = frappe._dict(args) + je = frappe.new_doc("Journal Entry") + je.posting_date = args.posting_date or today() + je.company = args.company + + je.set( + "accounts", + [ + { + "account": args.acc1, + "debit_in_account_currency": args.amount if args.amount > 0 else 0, + "credit_in_account_currency": abs(args.amount) if args.amount < 0 else 0, + }, + { + "account": args.acc2, + "credit_in_account_currency": args.amount if args.amount > 0 else 0, + "debit_in_account_currency": abs(args.amount) if args.amount < 0 else 0, + }, + ], + ) + je.save() + je.submit() diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index db44ffaa86c..a7922b716e7 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -75,6 +75,8 @@ def create_company(**args): "company_name": args.company_name or "Trial Balance Company", "country": args.country or "India", "default_currency": args.currency or "INR", + "parent_company": args.get("parent_company"), + "is_group": args.get("is_group"), } ) company.insert(ignore_if_duplicate=True)