From 2c3285286c4976ed2f60bfbfb654ba57d31c2e84 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:13:21 +0530 Subject: [PATCH 1/3] test: add coverage for Consolidated Financial Statement report --- .../test_consolidated_financial_statement.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py diff --git a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py new file mode 100644 index 00000000000..c928a82887e --- /dev/null +++ b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py @@ -0,0 +1,116 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.utils import flt, today + +from erpnext.accounts.report.consolidated_financial_statement.consolidated_financial_statement import ( + execute, +) +from erpnext.accounts.utils import get_fiscal_year +from erpnext.tests.utils import ERPNextTestSuite + +PARENT_COMPANY = "Parent Group Company India" +CHILD_COMPANY = "Child Company India" + + +class TestConsolidatedFinancialStatement(ERPNextTestSuite): + """Consolidation is exercised via the bootstrap group of companies + (`Parent Group Company India` with child `Child Company India`). Income and + expense posted in the child company must surface in the report that is run + for the parent (group) company.""" + + def setUp(self): + self.fiscal_year = get_fiscal_year(today(), company=PARENT_COMPANY)[0] + + def run_report(self, **extra): + filters = frappe._dict( + { + "company": PARENT_COMPANY, + "filter_based_on": "Fiscal Year", + "from_fiscal_year": self.fiscal_year, + "to_fiscal_year": self.fiscal_year, + "periodicity": "Yearly", + "include_default_book_entries": 1, + } + ) + filters.update(extra) + return execute(filters)[1] + + def post_journal_entry(self, debit_account, credit_account, amount): + je = frappe.new_doc("Journal Entry") + je.posting_date = today() + je.company = CHILD_COMPANY + je.set( + "accounts", + [ + {"account": debit_account, "debit_in_account_currency": amount}, + {"account": credit_account, "credit_in_account_currency": amount}, + ], + ) + je.save() + je.submit() + return je + + def get_row(self, data, account_name_fragment): + for row in data: + if account_name_fragment in str(row.get("account_name") or ""): + return row + return None + + def test_profit_and_loss_reflects_child_company_income(self): + amount = 7000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) + + self.assertTrue(data, "Report returned no rows") + + # child's Sales account is mapped onto the parent chart (Sales - PGCI) + sales_row = self.get_row(data, "Sales") + self.assertIsNotNone(sales_row, "Sales row missing from consolidated P&L") + self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + + total_income_row = self.get_row(data, "Total Income (Credit)") + self.assertIsNotNone(total_income_row, "Total Income row missing") + self.assertGreaterEqual(flt(total_income_row.get("total")), amount) + + def test_profit_and_loss_reflects_child_company_expense(self): + amount = 3000 + self.post_journal_entry("Marketing Expenses - CCI", "Cash - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) + + expense_row = self.get_row(data, "Marketing Expenses") + self.assertIsNotNone(expense_row, "Marketing Expenses row missing from consolidated P&L") + self.assertEqual(flt(expense_row.get(CHILD_COMPANY)), amount) + + total_expense_row = self.get_row(data, "Total Expense (Debit)") + self.assertIsNotNone(total_expense_row, "Total Expense row missing") + self.assertGreaterEqual(flt(total_expense_row.get("total")), amount) + + def test_accumulated_in_group_company_rolls_up_to_parent(self): + """With `accumulated_in_group_company`, the child's amount is also + accumulated into the parent company column.""" + amount = 5000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=1) + + sales_row = self.get_row(data, "Sales") + self.assertIsNotNone(sales_row) + self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + # parent column picks up the child value when accumulated + self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), amount) + + def test_balance_sheet_executes_and_returns_rows(self): + # posting income leaves a balancing entry in the child's Cash (Asset) account + amount = 4000 + self.post_journal_entry("Cash - CCI", "Sales - CCI", amount) + + data = self.run_report(report="Balance Sheet", accumulated_in_group_company=0) + + self.assertTrue(data, "Balance Sheet returned no rows") + cash_row = self.get_row(data, "Cash") + self.assertIsNotNone(cash_row, "Cash asset row missing from consolidated Balance Sheet") + self.assertGreaterEqual(flt(cash_row.get(CHILD_COMPANY)), amount) From 0e8b152c680fd9db0663f60057ecb5af59c30bef Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 21:26:04 +0530 Subject: [PATCH 2/3] fix: avoid double-counting the total in accumulated Consolidated Financial Statement --- .../consolidated_financial_statement.py | 7 ++++++- .../test_consolidated_financial_statement.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index fba7054e0a7..237f37e767f 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -582,7 +582,12 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com total += flt(row[company]) row["has_value"] = has_value - row["total"] = total + # when accumulating into the group company, that company's column already consolidates its + # descendants, so summing every company column would double-count; use the group total directly. + if filters.get("accumulated_in_group_company"): + row["total"] = flt(row.get(filters.company, 0.0), 3) + else: + row["total"] = total data.append(row) diff --git a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py index c928a82887e..202c495d378 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py @@ -102,6 +102,8 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) # parent column picks up the child value when accumulated self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), amount) + # the total must equal the consolidated (group) value, not the sum of parent + child columns + self.assertEqual(flt(sales_row.get("total")), amount) def test_balance_sheet_executes_and_returns_rows(self): # posting income leaves a balancing entry in the child's Cash (Asset) account From 5adbc7babae4f0ad1554248d4d4f7479d21e82e0 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 2 Jul 2026 14:10:24 +0530 Subject: [PATCH 3/3] test: target leaf accounts and robust amount assertions in Consolidated Financial Statement --- .../test_consolidated_financial_statement.py | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py index 202c495d378..1fb6a68e3b6 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/test_consolidated_financial_statement.py @@ -52,11 +52,19 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): je.submit() return je - def get_row(self, data, account_name_fragment): + def get_row(self, data, account_name_fragment, last_match=False): + """Return the first (or last) row whose account_name contains the fragment. + + Pass ``last_match=True`` to get the leaf/most-specific match when the fragment + is also a prefix of a parent group account (parents precede children in tree order). + """ + found = None for row in data: if account_name_fragment in str(row.get("account_name") or ""): - return row - return None + if not last_match: + return row + found = row + return found def test_profit_and_loss_reflects_child_company_income(self): amount = 7000 @@ -67,9 +75,10 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): self.assertTrue(data, "Report returned no rows") # child's Sales account is mapped onto the parent chart (Sales - PGCI) - sales_row = self.get_row(data, "Sales") + sales_row = self.get_row(data, "Sales", last_match=True) self.assertIsNotNone(sales_row, "Sales row missing from consolidated P&L") - self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + # >= so a pre-existing Sales balance in the fiscal year doesn't make this brittle + self.assertGreaterEqual(flt(sales_row.get(CHILD_COMPANY)), amount) total_income_row = self.get_row(data, "Total Income (Credit)") self.assertIsNotNone(total_income_row, "Total Income row missing") @@ -81,9 +90,9 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=0) - expense_row = self.get_row(data, "Marketing Expenses") + expense_row = self.get_row(data, "Marketing Expenses", last_match=True) self.assertIsNotNone(expense_row, "Marketing Expenses row missing from consolidated P&L") - self.assertEqual(flt(expense_row.get(CHILD_COMPANY)), amount) + self.assertGreaterEqual(flt(expense_row.get(CHILD_COMPANY)), amount) total_expense_row = self.get_row(data, "Total Expense (Debit)") self.assertIsNotNone(total_expense_row, "Total Expense row missing") @@ -97,13 +106,15 @@ class TestConsolidatedFinancialStatement(ERPNextTestSuite): data = self.run_report(report="Profit and Loss Statement", accumulated_in_group_company=1) - sales_row = self.get_row(data, "Sales") + sales_row = self.get_row(data, "Sales", last_match=True) self.assertIsNotNone(sales_row) - self.assertEqual(flt(sales_row.get(CHILD_COMPANY)), amount) + child_value = flt(sales_row.get(CHILD_COMPANY)) + self.assertGreaterEqual(child_value, amount) # parent column picks up the child value when accumulated - self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), amount) - # the total must equal the consolidated (group) value, not the sum of parent + child columns - self.assertEqual(flt(sales_row.get("total")), amount) + self.assertEqual(flt(sales_row.get(PARENT_COMPANY)), child_value) + # the total equals the consolidated (group) value, not the sum of parent + child + # columns -- this is the regression guard for the double-count fix + self.assertEqual(flt(sales_row.get("total")), child_value) def test_balance_sheet_executes_and_returns_rows(self): # posting income leaves a balancing entry in the child's Cash (Asset) account