From 6bd6e62c8c5219b570556243a1d1965d39b432ff Mon Sep 17 00:00:00 2001 From: vorasmit Date: Fri, 17 Apr 2026 22:27:48 +0530 Subject: [PATCH 1/5] fix: filter opening entries in first year in custom financial statement (cherry picked from commit 3c8a0664849a0972dae7d92aea0bf9181566b4b8) --- .../financial_report_engine.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py index 46369b07cd9..14a12dde9dd 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py @@ -468,6 +468,7 @@ class FinancialQueryBuilder: self.periods = periods self.company = filters.get("company") self.account_meta = {} # {name: {account_name, account_number}} + self.ignore_opening_entries = False def fetch_account_balances(self, accounts: list[dict]) -> dict[str, AccountData]: """ @@ -505,6 +506,8 @@ class FinancialQueryBuilder: """ Return opening balances for *all accounts* defaulting to zero. """ + self.ignore_opening_entries = False + if frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance"): return self._get_opening_balances_from_gl(accounts) @@ -526,6 +529,8 @@ class FinancialQueryBuilder: closing_data = self._get_closing_balances(accounts, closing_voucher.name) if sum(closing_data.values()) != 0.0: + # opening in current period + self.ignore_opening_entries = True return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date) return self._get_opening_balances_from_gl(accounts) @@ -620,7 +625,10 @@ class FinancialQueryBuilder: .groupby(gl_table.account) ) - if not frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting"): + ignore_is_opening = frappe.get_single_value( + "Accounts Settings", "ignore_is_opening_check_for_reporting" + ) + if self.ignore_opening_entries and not ignore_is_opening: query = query.where(gl_table.is_opening == "No") # Add period-specific columns From d51ce66cb2989551d2dfba0e7d07df018b95dd7f Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Wed, 22 Apr 2026 13:27:36 +0530 Subject: [PATCH 2/5] fix: summing of values could be zero even if values exist (cherry picked from commit 7ae91cac01556ef2fffcf5f02a2788a2d88c564f) --- .../financial_report_template/financial_report_engine.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py index 14a12dde9dd..eb94ab7f4b8 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py @@ -527,11 +527,9 @@ class FinancialQueryBuilder: if last_closing_voucher: closing_voucher = last_closing_voucher[0] closing_data = self._get_closing_balances(accounts, closing_voucher.name) + self.ignore_opening_entries = True # Else it will double count - if sum(closing_data.values()) != 0.0: - # opening in current period - self.ignore_opening_entries = True - return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date) + return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date) return self._get_opening_balances_from_gl(accounts) From 46759210772387437ab5d1df6d02adadbbd0214c Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Fri, 24 Apr 2026 19:32:00 +0530 Subject: [PATCH 3/5] chore: comment (cherry picked from commit c94b8c41f37f17691bd2c4f53c0612abce8f5fb0) --- .../financial_report_template/financial_report_engine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py index eb94ab7f4b8..c24156d6c2c 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py @@ -627,6 +627,8 @@ class FinancialQueryBuilder: "Accounts Settings", "ignore_is_opening_check_for_reporting" ) if self.ignore_opening_entries and not ignore_is_opening: + # This filter here applies to all accounts (BS & PL) + # However, in legacy query, this filter only applies to BS accounts query = query.where(gl_table.is_opening == "No") # Add period-specific columns From 7f32c3aca7824e1c411c76f559985555e45d8d49 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 28 Apr 2026 16:02:32 +0530 Subject: [PATCH 4/5] test: opening entries after period closing (cherry picked from commit 5fc3ca1d4b0b32bed1330c1b57fd833c5b26ecb9) --- .../test_financial_report_engine.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py index ef6f2785184..00e8ceb60d9 100644 --- a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py @@ -1952,6 +1952,141 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase): jv_2023.cancel() + def test_opening_entries_roll_into_opening_after_period_closing(self): + """ + Sequence: + 1. is_opening JV of 3000 in current year (FY 2024) + 2. is_opening JV of 5000 in next year (FY 2025) + 3. Period Closing Voucher for previous year (FY 2023) + + Expected (BS report for FY 2024): + opening of FY 2024 = 3000 + 5000 = 8000 + (all is_opening entries roll into opening irrespective of fiscal year, + on top of the PCV carry-forward — here PCV closing for cash is 0). + """ + company = "_Test Company" + cash_account = "_Test Cash - _TC" + # Opening JVs cannot post against P&L accounts; use a Balance Sheet offset. + opening_offset_account = "Temporary Opening - _TC" + + pcv = None + jv_current_year = None + jv_next_year = None + original_pcv_setting = frappe.db.get_single_value( + "Accounts Settings", "use_legacy_controller_for_pcv" + ) + + try: + # Step 1: opening JV in current year (FY 2024) — must be posted before PCV + # exists, else `validate_against_pcv` rejects it. + jv_current_year = make_journal_entry( + account1=cash_account, + account2=opening_offset_account, + amount=3000, + posting_date="2024-06-15", + company=company, + save=False, + ) + jv_current_year.is_opening = "Yes" + jv_current_year.insert() + jv_current_year.submit() + + # Step 2: opening JV in next year (FY 2025) + jv_next_year = make_journal_entry( + account1=cash_account, + account2=opening_offset_account, + amount=5000, + posting_date="2025-06-15", + company=company, + save=False, + ) + jv_next_year.is_opening = "Yes" + jv_next_year.insert() + jv_next_year.submit() + + # Step 3: book Period Closing Voucher for previous year (FY 2023) + closing_account = frappe.db.get_value( + "Account", + { + "company": company, + "root_type": "Liability", + "is_group": 0, + "account_type": ["not in", ["Payable", "Receivable"]], + }, + "name", + ) + fy_2023 = get_fiscal_year("2023-06-15", company=company) + + frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1) + + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": "2023-12-31", + "period_start_date": fy_2023[1], + "period_end_date": fy_2023[2], + "company": company, + "fiscal_year": fy_2023[0], + "cost_center": "_Test Cost Center - _TC", + "closing_account_head": closing_account, + "remarks": "Test Period Closing", + } + ) + pcv.insert() + pcv.submit() + pcv.reload() + + # Run BS report for FY 2024 + filters = { + "company": company, + "from_fiscal_year": "2024", + "to_fiscal_year": "2024", + "period_start_date": "2024-01-01", + "period_end_date": "2024-12-31", + "filter_based_on": "Date Range", + "periodicity": "Yearly", + "ignore_closing_entries": True, + } + + periods = [{"key": "2024", "from_date": "2024-01-01", "to_date": "2024-12-31"}] + + query_builder = FinancialQueryBuilder(filters, periods) + accounts = [ + frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}), + ] + + balances_data = query_builder.fetch_account_balances(accounts) + cash_data = balances_data.get(cash_account) + self.assertIsNotNone(cash_data, "Cash account should exist in results") + + year_2024 = cash_data.get_period("2024") + self.assertIsNotNone(year_2024, "FY 2024 period should exist") + + # All is_opening JVs (current + next year) roll into FY 2024 opening = 8000 + self.assertEqual( + year_2024.opening, + 8000.0, + "FY 2024 opening must combine is_opening JVs from current and next year", + ) + self.assertEqual(year_2024.movement, 0.0, "Opening JVs must not be counted as period movement") + self.assertEqual(year_2024.closing, 8000.0, "Closing = opening when no non-opening movement") + + finally: + frappe.db.set_single_value( + "Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0 + ) + + if pcv: + pcv.reload() + if pcv.docstatus == 1: + pcv.cancel() + + if jv_next_year and jv_next_year.docstatus == 1: + jv_next_year.cancel() + + if jv_current_year and jv_current_year.docstatus == 1: + jv_current_year.cancel() + def test_account_with_gl_entries_but_no_prior_closing_balance(self): company = "_Test Company" cash_account = "_Test Cash - _TC" From 64a724baea14b90e33f1c3ef3ac89c4aa3490041 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 28 Apr 2026 16:45:26 +0530 Subject: [PATCH 5/5] test: include both accounts to test sum = 0 (cherry picked from commit 590f2ffe284ba46860de3106f4b7c5fb16241b72) --- .../test_financial_report_engine.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py index 00e8ceb60d9..91be22ba08b 100644 --- a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py @@ -2053,23 +2053,41 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase): query_builder = FinancialQueryBuilder(filters, periods) accounts = [ frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}), + frappe._dict( + { + "name": opening_offset_account, + "account_name": "Temporary Opening", + "account_number": "1900", + } + ), ] balances_data = query_builder.fetch_account_balances(accounts) cash_data = balances_data.get(cash_account) + offset_data = balances_data.get(opening_offset_account) self.assertIsNotNone(cash_data, "Cash account should exist in results") + self.assertIsNotNone(offset_data, "Offset account should exist in results") - year_2024 = cash_data.get_period("2024") - self.assertIsNotNone(year_2024, "FY 2024 period should exist") + year_2024_cash = cash_data.get_period("2024") + year_2024_offset = offset_data.get_period("2024") + self.assertIsNotNone(year_2024_cash, "FY 2024 period should exist for cash") + self.assertIsNotNone(year_2024_offset, "FY 2024 period should exist for offset") - # All is_opening JVs (current + next year) roll into FY 2024 opening = 8000 + # All is_opening JVs (current + next year) roll into FY 2024 opening self.assertEqual( - year_2024.opening, + year_2024_cash.opening, 8000.0, - "FY 2024 opening must combine is_opening JVs from current and next year", + "FY 2024 cash opening must combine is_opening JVs from current and next year", ) - self.assertEqual(year_2024.movement, 0.0, "Opening JVs must not be counted as period movement") - self.assertEqual(year_2024.closing, 8000.0, "Closing = opening when no non-opening movement") + self.assertEqual( + year_2024_offset.opening, + -8000.0, + "FY 2024 offset opening must combine is_opening JVs from current and next year", + ) + self.assertEqual( + year_2024_cash.movement, 0.0, "Opening JVs must not be counted as period movement" + ) + self.assertEqual(year_2024_cash.closing, 8000.0, "Closing = opening when no non-opening movement") finally: frappe.db.set_single_value(