diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json
index b84f50f97c3..8b74dd823ec 100644
--- a/erpnext/accounts/doctype/account/account.json
+++ b/erpnext/accounts/doctype/account/account.json
@@ -21,6 +21,7 @@
"account_currency",
"column_break1",
"parent_account",
+ "account_category",
"account_type",
"tax_rate",
"freeze_account",
@@ -189,13 +190,20 @@
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disable"
+ },
+ {
+ "description": "Used with Financial Report Template",
+ "fieldname": "account_category",
+ "fieldtype": "Link",
+ "label": "Account Category",
+ "options": "Account Category"
}
],
"icon": "fa fa-money",
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2025-01-22 10:40:35.766017",
+ "modified": "2025-08-02 06:26:44.657146",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -250,6 +258,7 @@
"write": 1
}
],
+ "row_format": "Dynamic",
"search_fields": "account_number",
"show_name_in_global_search": 1,
"show_preview_popup": 1,
@@ -257,4 +266,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index 5ca373c2e96..1e6467e502f 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -31,6 +31,7 @@ class Account(NestedSet):
if TYPE_CHECKING:
from frappe.types import DF
+ account_category: DF.Link | None
account_currency: DF.Link | None
account_name: DF.Data
account_number: DF.Data | None
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
index 3513464fa77..c905bc56943 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
@@ -23,15 +23,7 @@ def create_charts(
if root_account:
root_type = child.get("root_type")
- if account_name not in [
- "account_name",
- "account_number",
- "account_type",
- "root_type",
- "is_group",
- "tax_rate",
- "account_currency",
- ]:
+ if account_name not in get_chart_metadata_fields():
account_number = cstr(child.get("account_number")).strip()
account_name, account_name_in_db = add_suffix_if_duplicate(
account_name, account_number, accounts
@@ -55,6 +47,7 @@ def create_charts(
"report_type": report_type,
"account_number": account_number,
"account_type": child.get("account_type"),
+ "account_category": child.get("account_category"),
"account_currency": child.get("account_currency")
if custom_chart
else frappe.get_cached_value("Company", company, "default_currency"),
@@ -97,20 +90,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
def identify_is_group(child):
if child.get("is_group"):
is_group = child.get("is_group")
- elif len(
- set(child.keys())
- - set(
- [
- "account_name",
- "account_type",
- "root_type",
- "is_group",
- "tax_rate",
- "account_number",
- "account_currency",
- ]
- )
- ):
+ elif len(set(child.keys()) - set(get_chart_metadata_fields())):
is_group = 1
else:
is_group = 0
@@ -253,13 +233,7 @@ def validate_bank_account(coa, bank_account):
def _get_account_names(account_master):
for account_name, child in account_master.items():
- if account_name not in [
- "account_number",
- "account_type",
- "root_type",
- "is_group",
- "tax_rate",
- ]:
+ if account_name not in get_chart_metadata_fields():
accounts.append(account_name)
_get_account_names(child)
@@ -284,15 +258,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"""recursively called to form a parent-child based list of dict from chart template"""
for account_name, child in children.items():
account = {}
- if account_name in [
- "account_name",
- "account_number",
- "account_type",
- "root_type",
- "is_group",
- "tax_rate",
- "account_currency",
- ]:
+ if account_name in get_chart_metadata_fields():
continue
if from_coa_importer:
@@ -310,3 +276,16 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
_import_accounts(chart, None)
return accounts
+
+
+def get_chart_metadata_fields():
+ return [
+ "account_name",
+ "account_number",
+ "account_type",
+ "account_category",
+ "root_type",
+ "is_group",
+ "tax_rate",
+ "account_currency",
+ ]
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
index 6f34a652822..0786da4f11b 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
@@ -9,103 +9,192 @@ def get():
return {
_("Application of Funds (Assets)"): {
_("Current Assets"): {
- _("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
- _("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
- _("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
+ _("Accounts Receivable"): {
+ _("Debtors"): {"account_type": "Receivable", "account_category": "Trade Receivables"}
+ },
+ _("Bank Accounts"): {
+ "account_type": "Bank",
+ "is_group": 1,
+ "account_category": "Cash and Cash Equivalents",
+ },
+ _("Cash In Hand"): {
+ _("Cash"): {"account_type": "Cash", "account_category": "Cash and Cash Equivalents"},
+ "account_type": "Cash",
+ "account_category": "Cash and Cash Equivalents",
+ },
_("Loans and Advances (Assets)"): {
- _("Employee Advances"): {"account_type": "Payable"},
+ _("Employee Advances"): {
+ "account_type": "Payable",
+ "account_category": "Other Receivables",
+ },
},
- _("Securities and Deposits"): {_("Earnest Money"): {}},
+ _("Securities and Deposits"): {
+ _("Earnest Money"): {"account_category": "Other Current Assets"}
+ },
+ _("Prepaid Expenses"): {"account_category": "Other Current Assets"},
+ _("Short-term Investments"): {"account_category": "Short-term Investments"},
_("Stock Assets"): {
- _("Stock In Hand"): {"account_type": "Stock"},
+ _("Stock In Hand"): {"account_type": "Stock", "account_category": "Stock Assets"},
"account_type": "Stock",
+ "account_category": "Stock Assets",
},
- _("Tax Assets"): {"is_group": 1},
+ _("Tax Assets"): {"is_group": 1, "account_category": "Other Current Assets"},
},
_("Fixed Assets"): {
- _("Capital Equipment"): {"account_type": "Fixed Asset"},
- _("Electronic Equipment"): {"account_type": "Fixed Asset"},
- _("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
- _("Office Equipment"): {"account_type": "Fixed Asset"},
- _("Plants and Machineries"): {"account_type": "Fixed Asset"},
- _("Buildings"): {"account_type": "Fixed Asset"},
- _("Software"): {"account_type": "Fixed Asset"},
- _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
+ _("Capital Equipment"): {
+ "account_type": "Fixed Asset",
+ "account_category": "Tangible Assets",
+ },
+ _("Electronic Equipment"): {
+ "account_type": "Fixed Asset",
+ "account_category": "Tangible Assets",
+ },
+ _("Furniture and Fixtures"): {
+ "account_type": "Fixed Asset",
+ "account_category": "Tangible Assets",
+ },
+ _("Office Equipment"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
+ _("Plants and Machineries"): {
+ "account_type": "Fixed Asset",
+ "account_category": "Tangible Assets",
+ },
+ _("Buildings"): {"account_type": "Fixed Asset", "account_category": "Tangible Assets"},
+ _("Software"): {"account_type": "Fixed Asset", "account_category": "Intangible Assets"},
+ _("Accumulated Depreciation"): {
+ "account_type": "Accumulated Depreciation",
+ "account_category": "Tangible Assets",
+ },
_("CWIP Account"): {
"account_type": "Capital Work in Progress",
+ "account_category": "Tangible Assets",
},
},
- _("Investments"): {"is_group": 1},
- _("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
+ _("Investments"): {"is_group": 1, "account_category": "Long-term Investments"},
+ _("Temporary Accounts"): {
+ _("Temporary Opening"): {
+ "account_type": "Temporary",
+ "account_category": "Other Non-current Assets",
+ }
+ },
"root_type": "Asset",
},
_("Expenses"): {
_("Direct Expenses"): {
_("Stock Expenses"): {
- _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
- _("Expenses Included In Asset Valuation"): {
- "account_type": "Expenses Included In Asset Valuation"
+ _("Cost of Goods Sold"): {
+ "account_type": "Cost of Goods Sold",
+ "account_category": "Cost of Goods Sold",
+ },
+ _("Expenses Included In Asset Valuation"): {
+ "account_type": "Expenses Included In Asset Valuation",
+ "account_category": "Other Direct Costs",
+ },
+ _("Expenses Included In Valuation"): {
+ "account_type": "Expenses Included In Valuation",
+ "account_category": "Other Direct Costs",
+ },
+ _("Stock Adjustment"): {
+ "account_type": "Stock Adjustment",
+ "account_category": "Other Direct Costs",
},
- _("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
- _("Stock Adjustment"): {"account_type": "Stock Adjustment"},
},
},
_("Indirect Expenses"): {
- _("Administrative Expenses"): {},
- _("Commission on Sales"): {},
- _("Depreciation"): {"account_type": "Depreciation"},
- _("Entertainment Expenses"): {},
- _("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
- _("Legal Expenses"): {},
- _("Marketing Expenses"): {"account_type": "Chargeable"},
- _("Miscellaneous Expenses"): {"account_type": "Chargeable"},
- _("Office Maintenance Expenses"): {},
- _("Office Rent"): {},
- _("Postal Expenses"): {},
- _("Print and Stationery"): {},
- _("Round Off"): {"account_type": "Round Off"},
- _("Salary"): {},
- _("Sales Expenses"): {},
- _("Telephone Expenses"): {},
- _("Travel Expenses"): {},
- _("Utility Expenses"): {},
- _("Write Off"): {},
- _("Exchange Gain/Loss"): {},
- _("Gain/Loss on Asset Disposal"): {},
- _("Impairment"): {},
+ _("Administrative Expenses"): {"account_category": "Operating Expenses"},
+ _("Commission on Sales"): {"account_category": "Operating Expenses"},
+ _("Depreciation"): {"account_type": "Depreciation", "account_category": "Operating Expenses"},
+ _("Entertainment Expenses"): {"account_category": "Operating Expenses"},
+ _("Freight and Forwarding Charges"): {
+ "account_type": "Chargeable",
+ "account_category": "Operating Expenses",
+ },
+ _("Legal Expenses"): {"account_category": "Operating Expenses"},
+ _("Marketing Expenses"): {
+ "account_type": "Chargeable",
+ "account_category": "Operating Expenses",
+ },
+ _("Miscellaneous Expenses"): {
+ "account_type": "Chargeable",
+ "account_category": "Operating Expenses",
+ },
+ _("Office Maintenance Expenses"): {"account_category": "Operating Expenses"},
+ _("Office Rent"): {"account_category": "Operating Expenses"},
+ _("Postal Expenses"): {"account_category": "Operating Expenses"},
+ _("Print and Stationery"): {"account_category": "Operating Expenses"},
+ _("Round Off"): {"account_type": "Round Off", "account_category": "Operating Expenses"},
+ _("Salary"): {"account_category": "Operating Expenses"},
+ _("Sales Expenses"): {"account_category": "Operating Expenses"},
+ _("Telephone Expenses"): {"account_category": "Operating Expenses"},
+ _("Travel Expenses"): {"account_category": "Operating Expenses"},
+ _("Utility Expenses"): {"account_category": "Operating Expenses"},
+ _("Write Off"): {"account_category": "Operating Expenses"},
+ _("Exchange Gain/Loss"): {"account_category": "Operating Expenses"},
+ _("Interest Expense"): {"account_category": "Finance Costs"},
+ _("Bank Charges"): {"account_category": "Finance Costs"},
+ _("Gain/Loss on Asset Disposal"): {"account_category": "Other Operating Income"},
+ _("Impairment"): {"account_category": "Operating Expenses"},
+ _("Tax Expense"): {"account_category": "Tax Expense"},
},
"root_type": "Expense",
},
_("Income"): {
- _("Direct Income"): {_("Sales"): {}, _("Service"): {}},
- _("Indirect Income"): {"is_group": 1},
+ _("Direct Income"): {
+ _("Sales"): {"account_category": "Revenue from Operations"},
+ _("Service"): {"account_category": "Revenue from Operations"},
+ },
+ _("Indirect Income"): {
+ _("Interest Income"): {"account_category": "Investment Income"},
+ _("Interest on Fixed Deposits"): {"account_category": "Investment Income"},
+ "is_group": 1,
+ },
"root_type": "Income",
},
_("Source of Funds (Liabilities)"): {
_("Current Liabilities"): {
_("Accounts Payable"): {
- _("Creditors"): {"account_type": "Payable"},
- _("Payroll Payable"): {},
+ _("Creditors"): {"account_type": "Payable", "account_category": "Trade Payables"},
+ _("Payroll Payable"): {"account_category": "Other Payables"},
},
+ _("Accrued Expenses"): {"account_category": "Other Current Liabilities"},
+ _("Customer Advances"): {"account_category": "Other Current Liabilities"},
_("Stock Liabilities"): {
- _("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
- _("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
+ _("Stock Received But Not Billed"): {
+ "account_type": "Stock Received But Not Billed",
+ "account_category": "Trade Payables",
+ },
+ _("Asset Received But Not Billed"): {
+ "account_type": "Asset Received But Not Billed",
+ "account_category": "Trade Payables",
+ },
},
- _("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
+ _("Duties and Taxes"): {
+ "account_type": "Tax",
+ "is_group": 1,
+ "account_category": "Current Tax Liabilities",
+ },
+ _("Short-term Provisions"): {"account_category": "Short-term Provisions"},
_("Loans (Liabilities)"): {
- _("Secured Loans"): {},
- _("Unsecured Loans"): {},
- _("Bank Overdraft Account"): {},
+ _("Secured Loans"): {"account_category": "Long-term Borrowings"},
+ _("Unsecured Loans"): {"account_category": "Long-term Borrowings"},
+ _("Bank Overdraft Account"): {"account_category": "Short-term Borrowings"},
},
},
+ _("Non-Current Liabilities"): {
+ _("Long-term Provisions"): {"account_category": "Long-term Provisions"},
+ _("Employee Benefits Obligation"): {"account_category": "Other Non-current Liabilities"},
+ "is_group": 1,
+ },
"root_type": "Liability",
},
_("Equity"): {
- _("Capital Stock"): {"account_type": "Equity"},
- _("Dividends Paid"): {"account_type": "Equity"},
- _("Opening Balance Equity"): {"account_type": "Equity"},
- _("Retained Earnings"): {"account_type": "Equity"},
- _("Revaluation Surplus"): {"account_type": "Equity"},
+ _("Capital Stock"): {"account_type": "Equity", "account_category": "Share Capital"},
+ _("Dividends Paid"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
+ _("Opening Balance Equity"): {
+ "account_type": "Equity",
+ "account_category": "Reserves and Surplus",
+ },
+ _("Retained Earnings"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
+ _("Revaluation Surplus"): {"account_type": "Equity", "account_category": "Reserves and Surplus"},
"root_type": "Equity",
},
}
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
index 07962df149e..d175967c628 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
@@ -10,49 +10,128 @@ def get():
_("Application of Funds (Assets)"): {
_("Current Assets"): {
_("Accounts Receivable"): {
- _("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
+ _("Debtors"): {
+ "account_type": "Receivable",
+ "account_number": "1310",
+ "account_category": "Trade Receivables",
+ },
"account_number": "1300",
},
- _("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
+ _("Bank Accounts"): {
+ "account_type": "Bank",
+ "is_group": 1,
+ "account_number": "1200",
+ "account_category": "Cash and Cash Equivalents",
+ },
_("Cash In Hand"): {
- _("Cash"): {"account_type": "Cash", "account_number": "1110"},
+ _("Cash"): {
+ "account_type": "Cash",
+ "account_number": "1110",
+ "account_category": "Cash and Cash Equivalents",
+ },
"account_type": "Cash",
"account_number": "1100",
+ "account_category": "Cash and Cash Equivalents",
},
_("Loans and Advances (Assets)"): {
- _("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
+ _("Employee Advances"): {
+ "account_number": "1610",
+ "account_type": "Payable",
+ "account_category": "Other Receivables",
+ },
"account_number": "1600",
},
_("Securities and Deposits"): {
- _("Earnest Money"): {"account_number": "1651"},
+ _("Earnest Money"): {
+ "account_number": "1651",
+ "account_category": "Other Current Assets",
+ },
"account_number": "1650",
},
+ _("Prepaid Expenses"): {
+ "account_number": "1660",
+ "account_category": "Other Current Assets",
+ },
+ _("Short-term Investments"): {
+ "account_number": "1670",
+ "account_category": "Short-term Investments",
+ },
_("Stock Assets"): {
- _("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
+ _("Stock In Hand"): {
+ "account_type": "Stock",
+ "account_number": "1410",
+ "account_category": "Stock Assets",
+ },
"account_type": "Stock",
"account_number": "1400",
+ "account_category": "Stock Assets",
+ },
+ _("Tax Assets"): {
+ "is_group": 1,
+ "account_number": "1500",
+ "account_category": "Other Current Assets",
},
- _("Tax Assets"): {"is_group": 1, "account_number": "1500"},
"account_number": "1100-1600",
},
_("Fixed Assets"): {
- _("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
- _("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
- _("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
- _("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"},
- _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
- _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
- _("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
+ _("Capital Equipment"): {
+ "account_type": "Fixed Asset",
+ "account_number": "1710",
+ "account_category": "Tangible Assets",
+ },
+ _("Electronic Equipment"): {
+ "account_type": "Fixed Asset",
+ "account_number": "1720",
+ "account_category": "Tangible Assets",
+ },
+ _("Furniture and Fixtures"): {
+ "account_type": "Fixed Asset",
+ "account_number": "1730",
+ "account_category": "Tangible Assets",
+ },
+ _("Office Equipment"): {
+ "account_type": "Fixed Asset",
+ "account_number": "1740",
+ "account_category": "Tangible Assets",
+ },
+ _("Plants and Machineries"): {
+ "account_type": "Fixed Asset",
+ "account_number": "1750",
+ "account_category": "Tangible Assets",
+ },
+ _("Buildings"): {
+ "account_type": "Fixed Asset",
+ "account_number": "1760",
+ "account_category": "Tangible Assets",
+ },
+ _("Software"): {
+ "account_type": "Fixed Asset",
+ "account_number": "1770",
+ "account_category": "Intangible Assets",
+ },
_("Accumulated Depreciation"): {
"account_type": "Accumulated Depreciation",
"account_number": "1780",
+ "account_category": "Tangible Assets",
+ },
+ _("CWIP Account"): {
+ "account_type": "Capital Work in Progress",
+ "account_number": "1790",
+ "account_category": "Tangible Assets",
},
- _("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
"account_number": "1700",
},
- _("Investments"): {"is_group": 1, "account_number": "1800"},
+ _("Investments"): {
+ "is_group": 1,
+ "account_number": "1800",
+ "account_category": "Long-term Investments",
+ },
_("Temporary Accounts"): {
- _("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
+ _("Temporary Opening"): {
+ "account_type": "Temporary",
+ "account_number": "1910",
+ "account_category": "Other Non-current Assets",
+ },
"account_number": "1900",
},
"root_type": "Asset",
@@ -61,97 +140,220 @@ def get():
_("Expenses"): {
_("Direct Expenses"): {
_("Stock Expenses"): {
- _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
+ _("Cost of Goods Sold"): {
+ "account_type": "Cost of Goods Sold",
+ "account_number": "5111",
+ "account_category": "Cost of Goods Sold",
+ },
_("Expenses Included In Asset Valuation"): {
"account_type": "Expenses Included In Asset Valuation",
"account_number": "5112",
+ "account_category": "Other Direct Costs",
},
_("Expenses Included In Valuation"): {
"account_type": "Expenses Included In Valuation",
"account_number": "5118",
+ "account_category": "Other Direct Costs",
+ },
+ _("Stock Adjustment"): {
+ "account_type": "Stock Adjustment",
+ "account_number": "5119",
+ "account_category": "Other Direct Costs",
},
- _("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
"account_number": "5110",
},
"account_number": "5100",
},
_("Indirect Expenses"): {
- _("Administrative Expenses"): {"account_number": "5201"},
- _("Commission on Sales"): {"account_number": "5202"},
- _("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
- _("Entertainment Expenses"): {"account_number": "5204"},
- _("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
- _("Legal Expenses"): {"account_number": "5206"},
- _("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
- _("Office Maintenance Expenses"): {"account_number": "5208"},
- _("Office Rent"): {"account_number": "5209"},
- _("Postal Expenses"): {"account_number": "5210"},
- _("Print and Stationery"): {"account_number": "5211"},
- _("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
- _("Salary"): {"account_number": "5213"},
- _("Sales Expenses"): {"account_number": "5214"},
- _("Telephone Expenses"): {"account_number": "5215"},
- _("Travel Expenses"): {"account_number": "5216"},
- _("Utility Expenses"): {"account_number": "5217"},
- _("Write Off"): {"account_number": "5218"},
- _("Exchange Gain/Loss"): {"account_number": "5219"},
- _("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
- _("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
- "account_number": "5200",
+ _("Administrative Expenses"): {
+ "account_number": "5201",
+ "account_category": "Operating Expenses",
+ },
+ _("Commission on Sales"): {
+ "account_number": "5202",
+ "account_category": "Operating Expenses",
+ },
+ _("Depreciation"): {
+ "account_type": "Depreciation",
+ "account_number": "5203",
+ "account_category": "Operating Expenses",
+ },
+ _("Entertainment Expenses"): {
+ "account_number": "5204",
+ "account_category": "Operating Expenses",
+ },
+ _("Freight and Forwarding Charges"): {
+ "account_type": "Chargeable",
+ "account_number": "5205",
+ "account_category": "Operating Expenses",
+ },
+ _("Legal Expenses"): {"account_number": "5206", "account_category": "Operating Expenses"},
+ _("Marketing Expenses"): {
+ "account_type": "Chargeable",
+ "account_number": "5207",
+ "account_category": "Operating Expenses",
+ },
+ _("Office Maintenance Expenses"): {
+ "account_number": "5208",
+ "account_category": "Operating Expenses",
+ },
+ _("Office Rent"): {"account_number": "5209", "account_category": "Operating Expenses"},
+ _("Postal Expenses"): {"account_number": "5210", "account_category": "Operating Expenses"},
+ _("Print and Stationery"): {
+ "account_number": "5211",
+ "account_category": "Operating Expenses",
+ },
+ _("Round Off"): {
+ "account_type": "Round Off",
+ "account_number": "5212",
+ "account_category": "Operating Expenses",
+ },
+ _("Salary"): {"account_number": "5213", "account_category": "Operating Expenses"},
+ _("Sales Expenses"): {"account_number": "5214", "account_category": "Operating Expenses"},
+ _("Telephone Expenses"): {"account_number": "5215", "account_category": "Operating Expenses"},
+ _("Travel Expenses"): {"account_number": "5216", "account_category": "Operating Expenses"},
+ _("Utility Expenses"): {"account_number": "5217", "account_category": "Operating Expenses"},
+ _("Write Off"): {"account_number": "5218", "account_category": "Operating Expenses"},
+ _("Exchange Gain/Loss"): {"account_number": "5219", "account_category": "Operating Expenses"},
+ _("Interest Expense"): {"account_number": "5220", "account_category": "Finance Costs"},
+ _("Bank Charges"): {"account_number": "5221", "account_category": "Finance Costs"},
+ _("Gain/Loss on Asset Disposal"): {
+ "account_number": "5222",
+ "account_category": "Other Operating Income",
+ },
+ _("Miscellaneous Expenses"): {
+ "account_type": "Chargeable",
+ "account_number": "5223",
+ "account_category": "Operating Expenses",
+ },
+ _("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
+ _("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
},
"root_type": "Expense",
"account_number": "5000",
},
_("Income"): {
_("Direct Income"): {
- _("Sales"): {"account_number": "4110"},
- _("Service"): {"account_number": "4120"},
+ _("Sales"): {"account_number": "4110", "account_category": "Revenue from Operations"},
+ _("Service"): {"account_number": "4120", "account_category": "Revenue from Operations"},
"account_number": "4100",
},
- _("Indirect Income"): {"is_group": 1, "account_number": "4200"},
+ _("Indirect Income"): {
+ _("Interest Income"): {"account_number": "4210", "account_category": "Investment Income"},
+ _("Interest on Fixed Deposits"): {
+ "account_number": "4220",
+ "account_category": "Investment Income",
+ },
+ "is_group": 1,
+ "account_number": "4200",
+ },
"root_type": "Income",
"account_number": "4000",
},
_("Source of Funds (Liabilities)"): {
_("Current Liabilities"): {
_("Accounts Payable"): {
- _("Creditors"): {"account_type": "Payable", "account_number": "2110"},
- _("Payroll Payable"): {"account_number": "2120"},
+ _("Creditors"): {
+ "account_type": "Payable",
+ "account_number": "2110",
+ "account_category": "Trade Payables",
+ },
+ _("Payroll Payable"): {"account_number": "2120", "account_category": "Other Payables"},
"account_number": "2100",
},
+ _("Accrued Expenses"): {
+ "account_number": "2150",
+ "account_category": "Other Current Liabilities",
+ },
+ _("Customer Advances"): {
+ "account_number": "2160",
+ "account_category": "Other Current Liabilities",
+ },
_("Stock Liabilities"): {
_("Stock Received But Not Billed"): {
"account_type": "Stock Received But Not Billed",
"account_number": "2210",
+ "account_category": "Trade Payables",
},
_("Asset Received But Not Billed"): {
"account_type": "Asset Received But Not Billed",
"account_number": "2211",
+ "account_category": "Trade Payables",
},
"account_number": "2200",
},
_("Duties and Taxes"): {
- _("TDS Payable"): {"account_number": "2310"},
+ _("TDS Payable"): {
+ "account_number": "2310",
+ "account_category": "Current Tax Liabilities",
+ },
"account_type": "Tax",
"is_group": 1,
"account_number": "2300",
+ "account_category": "Current Tax Liabilities",
+ },
+ _("Short-term Provisions"): {
+ "account_number": "2350",
+ "account_category": "Short-term Provisions",
},
_("Loans (Liabilities)"): {
- _("Secured Loans"): {"account_number": "2410"},
- _("Unsecured Loans"): {"account_number": "2420"},
- _("Bank Overdraft Account"): {"account_number": "2430"},
+ _("Secured Loans"): {
+ "account_number": "2410",
+ "account_category": "Long-term Borrowings",
+ },
+ _("Unsecured Loans"): {
+ "account_number": "2420",
+ "account_category": "Long-term Borrowings",
+ },
+ _("Bank Overdraft Account"): {
+ "account_number": "2430",
+ "account_category": "Short-term Borrowings",
+ },
"account_number": "2400",
},
"account_number": "2100-2400",
},
+ _("Non-Current Liabilities"): {
+ _("Long-term Provisions"): {
+ "account_number": "2510",
+ "account_category": "Long-term Provisions",
+ },
+ _("Employee Benefits Obligation"): {
+ "account_number": "2520",
+ "account_category": "Other Non-current Liabilities",
+ },
+ "is_group": 1,
+ "account_number": "2500",
+ },
"root_type": "Liability",
"account_number": "2000",
},
_("Equity"): {
- _("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
- _("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
- _("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
- _("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
+ _("Capital Stock"): {
+ "account_type": "Equity",
+ "account_number": "3100",
+ "account_category": "Share Capital",
+ },
+ _("Dividends Paid"): {
+ "account_type": "Equity",
+ "account_number": "3200",
+ "account_category": "Reserves and Surplus",
+ },
+ _("Opening Balance Equity"): {
+ "account_type": "Equity",
+ "account_number": "3300",
+ "account_category": "Reserves and Surplus",
+ },
+ _("Retained Earnings"): {
+ "account_type": "Equity",
+ "account_number": "3400",
+ "account_category": "Reserves and Surplus",
+ },
+ _("Revaluation Surplus"): {
+ "account_type": "Equity",
+ "account_number": "3500",
+ "account_category": "Reserves and Surplus",
+ },
"root_type": "Equity",
"account_number": "3000",
},
diff --git a/erpnext/accounts/doctype/account_category/__init__.py b/erpnext/accounts/doctype/account_category/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/account_category/account_category.js b/erpnext/accounts/doctype/account_category/account_category.js
new file mode 100644
index 00000000000..127eb380e00
--- /dev/null
+++ b/erpnext/accounts/doctype/account_category/account_category.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Account Category", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/erpnext/accounts/doctype/account_category/account_category.json b/erpnext/accounts/doctype/account_category/account_category.json
new file mode 100644
index 00000000000..d69d37bd78b
--- /dev/null
+++ b/erpnext/accounts/doctype/account_category/account_category.json
@@ -0,0 +1,71 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:account_category_name",
+ "creation": "2025-08-02 06:22:31.835063",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "account_category_name",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "account_category_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Account Category Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-10-15 03:19:47.171349",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Account Category",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "Auditor"
+ }
+ ],
+ "row_format": "Dynamic",
+ "search_fields": "account_category_name, description",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/erpnext/accounts/doctype/account_category/account_category.py b/erpnext/accounts/doctype/account_category/account_category.py
new file mode 100644
index 00000000000..8be84d0f8e2
--- /dev/null
+++ b/erpnext/accounts/doctype/account_category/account_category.py
@@ -0,0 +1,94 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+import json
+import os
+
+import frappe
+from frappe import _
+from frappe.model.document import Document, bulk_insert
+
+DOCTYPE = "Account Category"
+
+
+class AccountCategory(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ account_category_name: DF.Data
+ description: DF.SmallText | None
+ # end: auto-generated types
+
+ def after_rename(self, old_name, new_name, merge):
+ from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
+ FormulaFieldUpdater,
+ )
+
+ # get all template rows with this account category being used
+ row = frappe.qb.DocType("Financial Report Row")
+ rows = frappe._dict(
+ frappe.qb.from_(row)
+ .select(row.name, row.calculation_formula)
+ .where(row.calculation_formula.like(f"%{old_name}%"))
+ .run()
+ )
+
+ if not rows:
+ return
+
+ # Update formulas with new name
+ updater = FormulaFieldUpdater(
+ field_name="account_category",
+ value_mapping={old_name: new_name},
+ exclude_operators=["like", "not like"],
+ )
+
+ updated_formulas = updater.update_in_rows(rows)
+
+ if updated_formulas:
+ frappe.msgprint(
+ _("Updated {0} Financial Report Row(s) with new category name").format(len(updated_formulas))
+ )
+
+
+def import_account_categories(template_path: str):
+ categories_file = os.path.join(template_path, "account_categories.json")
+
+ if not os.path.exists(categories_file):
+ return
+
+ with open(categories_file) as f:
+ categories = json.load(f, object_hook=frappe._dict)
+
+ create_account_categories(categories)
+
+
+def create_account_categories(categories: list[dict]):
+ if not categories:
+ return
+
+ existing_categories = set(frappe.get_all(DOCTYPE, pluck="name"))
+ new_categories = []
+
+ for category_data in categories:
+ category_name = category_data.get("account_category_name")
+ if not category_name or category_name in existing_categories:
+ continue
+
+ doc = frappe.get_doc(
+ {
+ **category_data,
+ "doctype": DOCTYPE,
+ "name": category_name,
+ }
+ )
+
+ new_categories.append(doc)
+ existing_categories.add(category_name)
+
+ if new_categories:
+ bulk_insert(DOCTYPE, new_categories)
diff --git a/erpnext/accounts/doctype/account_category/test_account_category.py b/erpnext/accounts/doctype/account_category/test_account_category.py
new file mode 100644
index 00000000000..ea3c2c7783c
--- /dev/null
+++ b/erpnext/accounts/doctype/account_category/test_account_category.py
@@ -0,0 +1,20 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests import IntegrationTestCase
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record dependencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class IntegrationTestAccountCategory(IntegrationTestCase):
+ """
+ Integration tests for AccountCategory.
+ Use this class for testing interactions between multiple components.
+ """
+
+ pass
diff --git a/erpnext/accounts/doctype/financial_report_row/__init__.py b/erpnext/accounts/doctype/financial_report_row/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/financial_report_row/financial_report_row.json b/erpnext/accounts/doctype/financial_report_row/financial_report_row.json
new file mode 100644
index 00000000000..0acbfd69a47
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_row/financial_report_row.json
@@ -0,0 +1,187 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2025-09-06 09:39:46.503678",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_code",
+ "display_name",
+ "indentation_level",
+ "data_source",
+ "balance_type",
+ "column_break_hxqu",
+ "fieldtype",
+ "color",
+ "bold_text",
+ "italic_text",
+ "hidden_calculation",
+ "hide_when_empty",
+ "reverse_sign",
+ "include_in_charts",
+ "section_break_ornw",
+ "column_break_asfe",
+ "advanced_filtering",
+ "filters_editor",
+ "calculation_formula",
+ "section_break_pvro",
+ "formula_description"
+ ],
+ "fields": [
+ {
+ "columns": 1,
+ "description": "Code to reference this line in formulas (e.g., REV100, EXP200, ASSET100)",
+ "fieldname": "reference_code",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Line Reference"
+ },
+ {
+ "description": "Text displayed on the financial statement (e.g., 'Total Revenue', 'Cash and Cash Equivalents')",
+ "fieldname": "display_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Display Name"
+ },
+ {
+ "columns": 1,
+ "description": "Indentation level: 0 = Main heading, 1 = Sub-category, 2 = Individual accounts, etc.",
+ "fieldname": "indentation_level",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Indent Level"
+ },
+ {
+ "description": "How this line gets its data",
+ "fieldname": "data_source",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Data Source",
+ "options": "\nAccount Data\nCalculated Amount\nCustom API\nBlank Line\nColumn Break\nSection Break"
+ },
+ {
+ "depends_on": "eval:doc.data_source == 'Account Data'",
+ "description": "Opening Balance = Start of period, Closing Balance = End of period, Period Movement = Net change during period",
+ "fieldname": "balance_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Balance Type",
+ "mandatory_depends_on": "eval:doc.data_source == 'Account Data'",
+ "options": "\nOpening Balance\nClosing Balance\nPeriod Movement (Debits - Credits)"
+ },
+ {
+ "fieldname": "column_break_hxqu",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "Bold text for emphasis (totals, major headings)",
+ "fieldname": "bold_text",
+ "fieldtype": "Check",
+ "label": "Bold Text"
+ },
+ {
+ "default": "0",
+ "description": "Italic text for subtotals or notes",
+ "fieldname": "italic_text",
+ "fieldtype": "Check",
+ "label": "Italic Text"
+ },
+ {
+ "default": "0",
+ "description": "Calculate but don't show on final report",
+ "fieldname": "hidden_calculation",
+ "fieldtype": "Check",
+ "label": "Hidden Line (Internal Use Only)"
+ },
+ {
+ "default": "0",
+ "description": "Hide this line if amount is zero",
+ "fieldname": "hide_when_empty",
+ "fieldtype": "Check",
+ "label": "Hide If Zero"
+ },
+ {
+ "columns": 1,
+ "default": "0",
+ "description": "Show negative values as positive (for expenses in P&L)",
+ "fieldname": "reverse_sign",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Reverse Sign"
+ },
+ {
+ "fieldname": "section_break_ornw",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "eval: (doc.data_source === \"Account Data\" && doc.advanced_filtering) || [\"Calculated Amount\", \"Custom API\"].includes(doc.data_source);\n",
+ "fieldname": "calculation_formula",
+ "fieldtype": "Code",
+ "label": "Formula or Account Filter",
+ "mandatory_depends_on": "eval:doc.data_source != 'Blank Line' && doc.data_source != 'Column Break' && doc.data_source != 'Section Break'"
+ },
+ {
+ "fieldname": "formula_description",
+ "fieldtype": "HTML"
+ },
+ {
+ "default": "0",
+ "description": "If enabled, this row's values will be displayed on financial charts",
+ "fieldname": "include_in_charts",
+ "fieldtype": "Check",
+ "label": "Include in Charts"
+ },
+ {
+ "description": "Color to highlight values (e.g., red for exceptions)",
+ "fieldname": "color",
+ "fieldtype": "Color",
+ "label": "Color"
+ },
+ {
+ "description": "How to format and present values in the financial report (only if different from column fieldtype)",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "label": "Value Type",
+ "options": "\nCurrency\nFloat\nInt\nPercent"
+ },
+ {
+ "depends_on": "eval: doc.data_source === \"Account Data\" && !doc.advanced_filtering",
+ "fieldname": "filters_editor",
+ "fieldtype": "HTML"
+ },
+ {
+ "depends_on": "eval: ![\"Blank Line\", \"Column Break\", \"Section Break\"].includes(doc.data_source);",
+ "fieldname": "column_break_asfe",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.data_source === \"Account Data\"",
+ "description": "Use Python filters to get Accounts",
+ "fieldname": "advanced_filtering",
+ "fieldtype": "Check",
+ "label": "Advanced Filtering",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break_pvro",
+ "fieldtype": "Section Break"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2025-10-14 09:23:27.208072",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Financial Report Row",
+ "owner": "Administrator",
+ "permissions": [],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
diff --git a/erpnext/accounts/doctype/financial_report_row/financial_report_row.py b/erpnext/accounts/doctype/financial_report_row/financial_report_row.py
new file mode 100644
index 00000000000..0b2e9d2e4e5
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_row/financial_report_row.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class FinancialReportRow(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ advanced_filtering: DF.Check
+ balance_type: DF.Literal[
+ "", "Opening Balance", "Closing Balance", "Period Movement (Debits - Credits)"
+ ]
+ bold_text: DF.Check
+ calculation_formula: DF.Code | None
+ color: DF.Color | None
+ data_source: DF.Literal[
+ "",
+ "Account Data",
+ "Calculated Amount",
+ "Custom API",
+ "Blank Line",
+ "Column Break",
+ "Section Break",
+ ]
+ display_name: DF.Data | None
+ fieldtype: DF.Literal["", "Currency", "Float", "Int", "Percent"]
+ hidden_calculation: DF.Check
+ hide_when_empty: DF.Check
+ include_in_charts: DF.Check
+ indentation_level: DF.Int
+ italic_text: DF.Check
+ parent: DF.Data
+ parentfield: DF.Data
+ parenttype: DF.Data
+ reference_code: DF.Data | None
+ reverse_sign: DF.Check
+ # end: auto-generated types
+
+ pass
diff --git a/erpnext/accounts/doctype/financial_report_template/__init__.py b/erpnext/accounts/doctype/financial_report_template/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py
new file mode 100644
index 00000000000..67fcebb7ebf
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py
@@ -0,0 +1,1802 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import ast
+import json
+import math
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from functools import reduce
+from typing import Any, Union
+
+import frappe
+from frappe import _
+from frappe.database.operator_map import OPERATOR_MAP
+from frappe.query_builder import Case
+from frappe.query_builder.functions import Sum
+from frappe.utils import cstr, date_diff, flt, getdate
+from pypika.terms import LiteralValue
+
+from erpnext import get_company_currency
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ get_accounting_dimensions,
+ get_dimension_with_children,
+)
+from erpnext.accounts.doctype.financial_report_row.financial_report_row import FinancialReportRow
+from erpnext.accounts.doctype.financial_report_template.financial_report_template import (
+ FinancialReportTemplate,
+)
+from erpnext.accounts.doctype.financial_report_template.financial_report_validation import (
+ AccountFilterValidator,
+ CalculationFormulaValidator,
+ DependencyValidator,
+)
+from erpnext.accounts.report.financial_statements import (
+ get_columns,
+ get_cost_centers_with_children,
+ get_period_list,
+)
+from erpnext.accounts.utils import get_children, get_currency_precision
+
+# ============================================================================
+# DATA MODELS
+# ============================================================================
+
+
+@dataclass
+class PeriodValue:
+ """Represents financial data for a single period"""
+
+ period_key: str
+ opening: float = 0.0
+ closing: float = 0.0
+ movement: float = 0.0
+
+ def get_value(self, balance_type: str) -> float:
+ if balance_type == "Opening Balance":
+ return self.opening
+ elif balance_type == "Closing Balance":
+ return self.closing
+ elif balance_type == "Period Movement (Debits - Credits)":
+ return self.movement
+ return 0.0
+
+ def copy(self):
+ return PeriodValue(
+ period_key=self.period_key, opening=self.opening, closing=self.closing, movement=self.movement
+ )
+
+
+@dataclass
+class AccountData:
+ """Account data across all periods"""
+
+ account_name: str
+ period_values: dict[str, PeriodValue] = field(default_factory=dict)
+
+ def add_period(self, period_value: PeriodValue) -> None:
+ self.period_values[period_value.period_key] = period_value
+
+ def get_period(self, period_key: str) -> PeriodValue | None:
+ return self.period_values.get(period_key)
+
+ def get_values_by_type(self, balance_type: str) -> list[float]:
+ return [pv.get_value(balance_type) for pv in self.period_values.values()]
+
+ def get_ordered_values(self, period_keys: list[str], balance_type: str) -> list[float]:
+ return [
+ self.period_values[key].get_value(balance_type) if key in self.period_values else 0.0
+ for key in period_keys
+ ]
+
+ def has_periods(self) -> bool:
+ return len(self.period_values) > 0
+
+ def accumulate_values(self) -> None:
+ for period_value in self.period_values.values():
+ period_value.movement += period_value.opening
+ # closing is accumulated by default
+
+ def unaccumulate_values(self) -> None:
+ for period_value in self.period_values.values():
+ period_value.closing -= period_value.opening
+ # movement is unaccumulated by default
+
+ def copy(self):
+ copied = AccountData(account_name=self.account_name)
+ copied.period_values = {k: v.copy() for k, v in self.period_values.items()}
+ return copied
+
+ def reverse_values(self) -> None:
+ for period_value in self.period_values.values():
+ period_value.opening = -period_value.opening if period_value.opening else 0.0
+ period_value.closing = -period_value.closing if period_value.closing else 0.0
+ period_value.movement = -period_value.movement if period_value.movement else 0.0
+
+
+@dataclass
+class RowData:
+ """Represents a processed template row with calculated values"""
+
+ row: FinancialReportRow
+ values: list[float] = field(default_factory=list)
+ account_details: dict[str, AccountData] | None = None
+ is_detail_row: bool = False
+ parent_reference: str | None = None
+
+
+@dataclass
+class SegmentData:
+ """Represents a segment with its rows and metadata"""
+
+ rows: list[RowData] = field(default_factory=list)
+ label: str = ""
+ index: int = 0
+
+ @property
+ def id(self) -> str:
+ return f"seg_{self.index}"
+
+
+@dataclass
+class SectionData:
+ """Represents a horizontal section containing multiple column segments"""
+
+ segments: list[SegmentData]
+ label: str = ""
+ index: int = 0
+
+ @property
+ def id(self) -> str:
+ return f"section_{self.index}"
+
+
+@dataclass
+class ReportContext:
+ """Context object that flows through the pipeline"""
+
+ template: FinancialReportTemplate
+ filters: dict[str, Any]
+ period_list: list[dict] = field(default_factory=list)
+ processed_rows: list[RowData] = field(default_factory=list)
+ column_segments: list[list[RowData]] = field(default_factory=list)
+ account_data: dict[str, AccountData] = field(default_factory=dict)
+ raw_data: dict[str, Any] = field(default_factory=dict)
+ show_detailed: bool = False
+ currency: str | None = None
+
+ def get_result(self) -> tuple[list[dict], list[dict]]:
+ """Get final formatted columns and data"""
+ return (
+ self.raw_data.get("columns", []),
+ self.raw_data.get("formatted_data", []),
+ None,
+ self.raw_data.get("chart", {}),
+ )
+
+
+@dataclass
+class FormattingRule:
+ """Rule for applying formatting to rows"""
+
+ condition: callable
+ format_properties: Union[dict[str, Any], callable] # noqa: UP007
+
+ def applies_to(self, row_data: RowData) -> bool:
+ return self.condition(row_data)
+
+ def get_properties(self, row_data: RowData) -> dict[str, Any]:
+ """Get the format properties, handling both static and dynamic cases"""
+ if callable(self.format_properties):
+ return self.format_properties(row_data)
+ return self.format_properties
+
+
+# ============================================================================
+# REPORT ENGINE
+# ============================================================================
+
+
+class FinancialReportEngine:
+ def execute(self, filters: dict[str, Any]) -> tuple[list[dict], list[dict]]:
+ """Execute the complete report generation"""
+ self._validate_filters(filters)
+
+ # Initialize context
+ context = self._initialize_context(filters)
+
+ # Execute
+ self.collect_financial_data(context)
+ self.process_calculations(context)
+ self.format_report_data(context)
+ self.apply_view_transformation(context)
+
+ # Chart
+ self.generate_chart_data(context)
+ return context.get_result()
+
+ def _validate_filters(self, filters: dict[str, Any]) -> None:
+ required_filters = ["report_template", "period_start_date", "period_end_date"]
+
+ for filter_key in required_filters:
+ if not filters.get(filter_key):
+ frappe.throw(_("Missing required filter: {0}").format(filter_key))
+
+ if filters.get("presentation_currency"):
+ frappe.msgprint(_("Currency filters are currently unsupported in Custom Financial Report."))
+
+ # Margin view is dependent on first row being an income account. Hence not supported.
+ # Way to implement this would be using calculated rows with formulas.
+ supported_views = ("Report", "Growth")
+ if (view := filters.get("selected_view")) and view not in supported_views:
+ frappe.msgprint(_("{0} view is currently unsupported in Custom Financial Report.").format(view))
+
+ def _initialize_context(self, filters: dict[str, Any]) -> ReportContext:
+ template_name = filters.get("report_template")
+ template = frappe.get_doc("Financial Report Template", template_name)
+
+ if not template:
+ frappe.throw(_("Financial Report Template {0} not found").format(template_name))
+
+ if template.disabled:
+ frappe.throw(_("Financial Report Template {0} is disabled").format(template_name))
+
+ # Generate periods
+ 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,
+ )
+
+ # Support both old and new field names for backward compatibility
+ show_detailed = filters.get("show_account_details") == "Account Breakdown"
+
+ context = ReportContext(
+ template=template,
+ filters=filters,
+ period_list=period_list,
+ show_detailed=show_detailed,
+ # TODO: Enhance this to support report currencies
+ # after fixing which exchange rate to use for P&L
+ currency=get_company_currency(filters.company),
+ )
+ # Add period_keys to context
+ context.raw_data["period_keys"] = [p["key"] for p in period_list]
+ return context
+
+ def collect_financial_data(self, context: ReportContext) -> ReportContext:
+ collector = DataCollector(context.filters, context.period_list)
+
+ for row in context.template.rows:
+ if row.data_source == "Account Data":
+ collector.add_account_request(row)
+
+ all_data = collector.collect_all_data()
+ context.account_data = all_data["account_data"]
+ context.raw_data.update(all_data)
+
+ return context
+
+ def process_calculations(self, context: ReportContext) -> ReportContext:
+ processor = RowProcessor(context)
+ context.processed_rows = processor.process_all_rows()
+
+ return context
+
+ def format_report_data(self, context: ReportContext) -> ReportContext:
+ formatter = DataFormatter(context)
+ formatted_data, columns = formatter.format_for_display()
+
+ context.raw_data["formatted_data"] = formatted_data
+ context.raw_data["columns"] = columns
+
+ return context
+
+ def apply_view_transformation(self, context: ReportContext) -> ReportContext:
+ if context.filters.get("selected_view") == "Growth":
+ transformer = GrowthViewTransformer(context)
+ transformer.transform()
+
+ # Default is "Report" view - no transformation needed
+
+ return context
+
+ def generate_chart_data(self, context: ReportContext) -> dict[str, Any]:
+ generator = ChartDataGenerator(context)
+ generator.generate()
+
+ return context
+
+
+# ============================================================================
+# DATA COLLECTION
+# ============================================================================
+
+
+class DataCollector:
+ """Data collector that fetches all data in optimized queries"""
+
+ def __init__(self, filters: dict[str, Any], periods: list[dict]):
+ self.filters = filters
+ self.periods = periods
+ self.company = filters.get("company")
+ self.account_requests = []
+ self.query_builder = FinancialQueryBuilder(filters, periods)
+ self.account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
+
+ def add_account_request(self, row):
+ accounts = self._parse_account_filter(self.company, row)
+
+ self.account_requests.append(
+ {
+ "row": row,
+ "accounts": accounts,
+ "balance_type": row.balance_type,
+ "reference_code": row.reference_code,
+ "reverse_sign": row.reverse_sign,
+ }
+ )
+
+ def collect_all_data(self) -> dict[str, Any]:
+ if not self.account_requests:
+ return {"account_data": {}, "summary": {}, "account_details": {}}
+
+ # Get all unique accounts
+ all_accounts = set()
+ for request in self.account_requests:
+ all_accounts.update(request["accounts"])
+
+ all_accounts = list(all_accounts)
+ if not all_accounts:
+ return {"account_data": {}, "summary": {}, "account_details": {}}
+
+ # Fetch balance data for all accounts
+ account_data = self.query_builder.fetch_account_balances(all_accounts)
+
+ # Calculate summaries for each request
+ summary = {}
+ account_details = {}
+ period_keys = [p["key"] for p in self.periods]
+
+ for request in self.account_requests:
+ ref_code = request["reference_code"]
+ if not ref_code:
+ continue
+
+ balance_type = request["balance_type"]
+ accounts = request["accounts"]
+
+ total_values = [0.0] * len(self.periods)
+ request_account_details = {}
+
+ for account_name in accounts:
+ if account_name not in account_data:
+ continue
+
+ account_obj: AccountData = account_data[account_name].copy()
+ if request["reverse_sign"]:
+ account_obj.reverse_values()
+
+ account_values = account_obj.get_ordered_values(period_keys, balance_type)
+
+ # Add to totals
+ for i, value in enumerate(account_values):
+ total_values[i] += value
+
+ # Store for detailed view
+ request_account_details[account_name] = account_obj
+
+ summary[ref_code] = total_values
+ account_details[ref_code] = request_account_details
+
+ return {"account_data": account_data, "summary": summary, "account_details": account_details}
+
+ @staticmethod
+ def _parse_account_filter(company, report_row) -> list[str]:
+ """
+ Find accounts matching filter criteria.
+
+ Example:
+ Input: '["account_type", "=", "Cash"]'
+ Output: ["Cash - COMP", "Petty Cash - COMP", "Bank - COMP"]
+ """
+ filter_parser = FilterExpressionParser()
+
+ account = frappe.qb.DocType("Account")
+ query = (
+ frappe.qb.from_(account)
+ .select(account.name)
+ .where(account.disabled == 0)
+ .where(account.is_group == 0)
+ )
+
+ if company:
+ query = query.where(account.company == company)
+
+ where_condition = filter_parser.build_condition(report_row, account)
+ if where_condition is None:
+ return []
+
+ query = query.where(where_condition)
+ query = query.orderby(account.name)
+ result = query.run(as_dict=True)
+ return [row.name for row in result]
+
+ @staticmethod
+ def get_filtered_accounts(company: str, account_rows: list) -> list[str]:
+ filter_parser = FilterExpressionParser()
+
+ account = frappe.qb.DocType("Account")
+ query = (
+ frappe.qb.from_(account)
+ .select(account.name)
+ .distinct()
+ .where(account.disabled == 0)
+ .where(account.is_group == 0)
+ .orderby(account.name)
+ )
+
+ if company:
+ query = query.where(account.company == company)
+
+ if conditions := filter_parser.build_conditions(account_rows, account):
+ query = query.where(conditions)
+
+ return query.run(pluck=True)
+
+
+class FinancialQueryBuilder:
+ """Centralized query builder for financial data"""
+
+ def __init__(self, filters: dict[str, Any], periods: list[dict]):
+ self.filters = filters
+ self.periods = periods
+ self.company = filters.get("company")
+
+ def fetch_account_balances(self, accounts: list[str]) -> dict[str, AccountData]:
+ """
+ Fetch account balances for all periods with optimization.
+ Steps: get opening balances → fetch GL entries → calculate running totals
+
+ Returns:
+ dict: {account: AccountData}
+ """
+ balances_data = self._get_opening_balances(accounts)
+ gl_data = self._get_gl_movements(accounts)
+ self._calculate_running_balances(balances_data, gl_data)
+ self._handle_balance_accumulation(balances_data)
+
+ return balances_data
+
+ def _get_opening_balances(self, accounts: list[str]) -> dict[str, dict[str, dict[str, float]]]:
+ """
+ Return opening balances for *all accounts* defaulting to zero.
+ """
+ if frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance"):
+ return self._get_opening_balances_from_gl(accounts)
+
+ first_period_start = getdate(self.periods[0]["from_date"])
+ last_closing_voucher = frappe.db.get_all(
+ "Period Closing Voucher",
+ filters={
+ "docstatus": 1,
+ "company": self.company,
+ "period_end_date": ("<", first_period_start),
+ },
+ fields=["period_end_date", "name"],
+ order_by="period_end_date desc",
+ limit=1,
+ )
+
+ if last_closing_voucher:
+ closing_voucher = last_closing_voucher[0]
+ closing_data = self._get_closing_balances(accounts, closing_voucher.name)
+
+ if sum(closing_data.values()) != 0.0:
+ return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date)
+
+ return self._get_opening_balances_from_gl(accounts)
+
+ def _get_closing_balances(self, account_names: list[str], closing_voucher: str) -> dict[str, float]:
+ closing_balances = {account: 0.0 for account in account_names}
+ acb_table = frappe.qb.DocType("Account Closing Balance")
+
+ query = (
+ frappe.qb.from_(acb_table)
+ .select(
+ acb_table.account,
+ (acb_table.debit - acb_table.credit).as_("balance"),
+ )
+ .where(acb_table.company == self.company)
+ .where(acb_table.account.isin(account_names))
+ .where(acb_table.period_closing_voucher == closing_voucher)
+ )
+
+ query = self._apply_standard_filters(query, acb_table)
+ results = self._execute_with_permissions(query, "Account Closing Balance")
+
+ for row in results:
+ closing_balances[row["account"]] = row["balance"]
+
+ return closing_balances
+
+ def _rebase_closing_balances(
+ self, closing_data: dict[str, float], closing_date: str
+ ) -> dict[str, dict[str, dict[str, float]]]:
+ balances_data = {}
+
+ first_period_key = self.periods[0]["key"]
+ report_start = getdate(self.periods[0]["from_date"])
+ closing_end = getdate(closing_date)
+
+ has_gap = date_diff(report_start, closing_end) > 1
+
+ gap_movements = {}
+ if has_gap:
+ gap_movements = self._get_gap_movements(list(closing_data.keys()), closing_date, report_start)
+
+ for account, closing_balance in closing_data.items():
+ gap_movement = gap_movements.get(account, 0.0)
+ opening_balance = closing_balance + gap_movement
+
+ account_data = AccountData(account)
+ account_data.add_period(PeriodValue(first_period_key, opening_balance, 0, 0))
+ balances_data[account] = account_data
+
+ return balances_data
+
+ def _get_opening_balances_from_gl(self, accounts: list[str]) -> dict:
+ # Simulate zero closing balances
+ zero_closing_balances = {account: 0.0 for account in accounts}
+
+ # Use a very early date
+ earliest_date = "1900-01-01"
+
+ return self._rebase_closing_balances(zero_closing_balances, earliest_date)
+
+ def _get_gap_movements(self, account_names: list[str], from_date: str, to_date: str) -> dict[str, float]:
+ gl_table = frappe.qb.DocType("GL Entry")
+
+ query = (
+ frappe.qb.from_(gl_table)
+ .select(gl_table.account, Sum(gl_table.debit - gl_table.credit).as_("movement"))
+ .where(gl_table.company == self.company)
+ .where(gl_table.is_cancelled == 0)
+ .where(gl_table.account.isin(account_names))
+ .where(gl_table.posting_date > from_date)
+ .where(gl_table.posting_date < to_date)
+ .groupby(gl_table.account)
+ )
+
+ query = self._apply_standard_filters(query, gl_table)
+ results = self._execute_with_permissions(query, "GL Entry")
+
+ return {row["account"]: row["movement"] or 0.0 for row in results}
+
+ def _get_gl_movements(self, account_names: list[str]) -> list[dict]:
+ gl_table = frappe.qb.DocType("GL Entry")
+
+ query = (
+ frappe.qb.from_(gl_table)
+ .select(gl_table.account)
+ .where(gl_table.company == self.company)
+ .where(gl_table.is_cancelled == 0)
+ .where(gl_table.account.isin(account_names))
+ .where(gl_table.posting_date >= self.periods[0]["from_date"])
+ .groupby(gl_table.account)
+ )
+
+ if not frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting"):
+ query = query.where(gl_table.is_opening == "No")
+
+ # Add period-specific columns
+ for period in self.periods:
+ period_condition = (
+ Case()
+ .when(
+ (gl_table.posting_date >= period["from_date"])
+ & (gl_table.posting_date <= period["to_date"]),
+ gl_table.debit - gl_table.credit,
+ )
+ .else_(0)
+ )
+ query = query.select(Sum(period_condition).as_(period["key"]))
+
+ query = self._apply_standard_filters(query, gl_table)
+ return self._execute_with_permissions(query, "GL Entry")
+
+ def _calculate_running_balances(self, balances_data: dict, gl_data: list[dict]) -> dict:
+ for row in gl_data:
+ account = row["account"]
+ if account not in balances_data:
+ balances_data[account] = AccountData(account)
+
+ account_data: AccountData = balances_data[account]
+
+ if account_data.has_periods():
+ first_period = account_data.get_period(self.periods[0]["key"])
+ current_balance = first_period.get_value("Opening Balance") if first_period else 0.0
+ else:
+ current_balance = 0.0
+
+ for period in self.periods:
+ period_key = period["key"]
+ movement = row.get(period_key, 0.0)
+ closing_balance = current_balance + movement
+
+ account_data.add_period(PeriodValue(period_key, current_balance, closing_balance, movement))
+
+ current_balance = closing_balance
+
+ # Accounts with no movements
+ for account_data in balances_data.values():
+ for period in self.periods:
+ period_key = period["key"]
+ if period_key not in account_data.period_values:
+ account_data.add_period(PeriodValue(period_key, 0.0, 0.0, 0.0))
+
+ def _handle_balance_accumulation(self, balances_data):
+ for account_data in balances_data.values():
+ account_data: AccountData
+
+ accumulated_values = self.filters.get("accumulated_values")
+
+ if accumulated_values is None:
+ # respect user setting if not in filters
+ # closing = accumulated
+ # movement = unaccumulated
+ continue
+
+ # for legacy reports
+ elif accumulated_values:
+ account_data.accumulate_values()
+ else:
+ account_data.unaccumulate_values()
+
+ def _apply_standard_filters(self, query, table):
+ if self.filters.get("ignore_closing_entries"):
+ if hasattr(table, "is_period_closing_voucher_entry"):
+ query = query.where(table.is_period_closing_voucher_entry == 0)
+ else:
+ query = query.where(table.voucher_type != "Period Closing Voucher")
+
+ if self.filters.get("project"):
+ projects = self.filters.get("project")
+ if isinstance(projects, str):
+ projects = [projects]
+ query = query.where(table.project.isin(projects))
+
+ if self.filters.get("cost_center"):
+ self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
+ query = query.where(table.cost_center.isin(self.filters.cost_center))
+
+ finance_book = self.filters.get("finance_book")
+ if self.filters.get("include_default_book_entries"):
+ default_book = frappe.get_cached_value("Company", self.filters.company, "default_finance_book")
+
+ if finance_book and default_book and cstr(finance_book) != cstr(default_book):
+ frappe.throw(
+ _("To use a different finance book, please uncheck 'Include Default FB Entries'")
+ )
+
+ query = query.where(
+ (table.finance_book.isin([cstr(finance_book), cstr(default_book), ""]))
+ | (table.finance_book.isnull())
+ )
+ else:
+ query = query.where(
+ (table.finance_book.isin([cstr(finance_book), ""])) | (table.finance_book.isnull())
+ )
+
+ dimensions = get_accounting_dimensions(as_list=False)
+ for dimension in dimensions:
+ if self.filters.get(dimension.fieldname):
+ if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
+ self.filters[dimension.fieldname] = get_dimension_with_children(
+ dimension.document_type, self.filters.get(dimension.fieldname)
+ )
+
+ query = query.where(table[dimension.fieldname].isin(self.filters.get(dimension.fieldname)))
+
+ return query
+
+ def _execute_with_permissions(self, query, doctype):
+ from frappe.desk.reportview import build_match_conditions
+
+ user_conditions = build_match_conditions(doctype)
+
+ if user_conditions:
+ query = query.where(LiteralValue(user_conditions))
+
+ return query.run(as_dict=True)
+
+
+class FilterExpressionParser:
+ """Direct filter expression to SQL condition builder"""
+
+ def __init__(self):
+ self.validator = AccountFilterValidator()
+
+ def build_conditions(self, report_rows, table):
+ conditions = []
+ for row in report_rows or []:
+ condition = self.build_condition(row, table)
+ if condition is not None:
+ conditions.append(condition)
+
+ # ensure brackets in or condition
+ return reduce(lambda a, b: (a) | (b), conditions)
+
+ def build_condition(self, report_row, table):
+ """
+ Build SQL condition directly from filter formula.
+
+ Supports:
+ 1. Simple condition: ["field", "operator", "value"]
+ Example: ["account_type", "=", "Income"]
+
+ 2. Complex logical conditions:
+ {"and": [condition1, condition2, ...]} # All conditions must be true
+ {"or": [condition1, condition2, ...]} # Any condition can be true
+
+ Example:
+ {
+ "and": [
+ ["account_type", "=", "Income"],
+ {"or": [
+ ["category", "=", "Direct Income"],
+ ["category", "=", "Indirect Income"]
+ ]}
+ ]
+ }
+
+ Returns:
+ SQL condition object or None if invalid
+ """
+ filter_formula = report_row.calculation_formula
+ if not filter_formula:
+ return None
+
+ errors = self.validator.validate(report_row)
+ if not errors.is_valid:
+ error_messages = [str(issue) for issue in errors.issues]
+ frappe.log_error(f"Filter validation errors found:\n{'
'.join(error_messages)}")
+ return None
+
+ try:
+ parsed = ast.literal_eval(filter_formula)
+ return self._build_from_parsed(parsed, table)
+ except (ValueError, SyntaxError) as e:
+ frappe.log_error(f"Invalid filter formula syntax: {filter_formula} - {e}")
+ return None
+ except Exception as e:
+ frappe.log_error(f"Failed to build condition from formula: {filter_formula} - {e}")
+ return None
+
+ def _build_from_parsed(self, parsed, table):
+ if isinstance(parsed, dict):
+ return self._build_logical_condition(parsed, table)
+
+ if isinstance(parsed, list):
+ return self._build_simple_condition(parsed, table)
+
+ return None
+
+ def _build_simple_condition(self, condition_list: list[str, str, str | float], table):
+ field_name, operator, value = condition_list
+
+ if value is None:
+ return None
+
+ field = getattr(table, field_name, None)
+ operator_fn = OPERATOR_MAP.get(operator.casefold())
+
+ if "like" in operator.casefold() and "%" not in value:
+ value = f"%{value}%"
+
+ return operator_fn(field, value)
+
+ def _build_logical_condition(self, condition_dict: dict, table):
+ """Build SQL condition from logical {"and/or": [...]} format"""
+
+ logical_op = next(iter(condition_dict.keys())).lower()
+ sub_conditions = condition_dict.get(logical_op)
+
+ # recursive
+ built_conditions = []
+ for sub_condition in sub_conditions:
+ condition = self._build_from_parsed(sub_condition, table)
+ if condition is not None:
+ built_conditions.append(condition)
+
+ if not built_conditions:
+ return None
+
+ if len(built_conditions) == 1:
+ return built_conditions[0]
+
+ # combine
+ if logical_op == "and":
+ return reduce(lambda a, b: a & b, built_conditions)
+ else: # logical_op == "or"
+ return reduce(lambda a, b: a | b, built_conditions)
+
+
+class FormulaFieldExtractor:
+ """Extract field values from filter formulas without SQL execution"""
+
+ def __init__(self, field_name: str, exclude_operators: list[str] | None = None):
+ """
+ Initialize field extractor.
+
+ Args:
+ field_name: The field to extract values for (e.g., "account_category")
+ exclude_operators: List of operators to exclude (e.g., ["like"])
+ """
+ self.field_name = field_name
+ self.exclude_operators = [op.lower() for op in (exclude_operators or [])]
+
+ def extract_from_rows(self, rows: list) -> set:
+ values = set()
+
+ for row in rows:
+ if not hasattr(row, "calculation_formula") or not row.calculation_formula:
+ continue
+
+ try:
+ parsed = ast.literal_eval(row.calculation_formula)
+ self._extract_recursive(parsed, values)
+ except (ValueError, SyntaxError):
+ continue # Skip rows with invalid formulas
+
+ return values
+
+ def _extract_recursive(self, parsed, values: set):
+ if isinstance(parsed, list) and len(parsed) == 3:
+ # Simple condition: ["field", "operator", "value"]
+ field, operator, value = parsed
+
+ if field == self.field_name and operator.lower() not in self.exclude_operators:
+ if isinstance(value, str):
+ values.add(value)
+ elif isinstance(value, list):
+ # Handle "in" operator with list of values
+ values.update(v for v in value if isinstance(v, str))
+
+ elif isinstance(parsed, dict):
+ # Logical condition: {"and/or": [...]}
+ for sub_conditions in parsed.values():
+ if isinstance(sub_conditions, list):
+ for sub_condition in sub_conditions:
+ self._extract_recursive(sub_condition, values)
+
+
+class FormulaFieldUpdater:
+ """Update field values in filter formulas"""
+
+ def __init__(
+ self, field_name: str, value_mapping: dict[str, str], exclude_operators: list[str] | None = None
+ ):
+ """
+ Initialize field updater.
+
+ Args:
+ field_name: The field to update values for (e.g., "account_category")
+ value_mapping: Mapping of old values to new values (e.g., {"Old Name": "New Name"})
+ exclude_operators: List of operators to exclude from updates (e.g., ["like", "not like"])
+ """
+ self.field_name = field_name
+ self.value_mapping = value_mapping
+ self.exclude_operators = [op.lower() for op in (exclude_operators or [])]
+
+ def update_in_rows(self, rows: list) -> dict[str, dict[str, str]]:
+ updated_rows = {}
+
+ for row_name, formula in rows.items():
+ if not formula:
+ continue
+
+ try:
+ parsed = ast.literal_eval(formula)
+ updated = self._update_recursive(parsed)
+
+ if updated != parsed:
+ updated_formula = json.dumps(updated)
+ updated_rows[row_name] = {"calculation_formula": updated_formula}
+
+ except (ValueError, SyntaxError):
+ continue # Skip rows with invalid formulas
+
+ if updated_rows:
+ frappe.db.bulk_update("Financial Report Row", updated_rows, update_modified=False)
+
+ return updated_rows
+
+ def _update_recursive(self, parsed):
+ if isinstance(parsed, list) and len(parsed) == 3:
+ # Simple condition: ["field", "operator", "value"]
+ field, operator, value = parsed
+
+ if field == self.field_name and operator.lower() not in self.exclude_operators:
+ updated_value = self._update_value(value)
+ return [field, operator, updated_value]
+
+ return parsed
+
+ elif isinstance(parsed, dict):
+ # Logical condition: {"and/or": [...]}
+ updated_dict = {}
+ for key, sub_conditions in parsed.items():
+ updated_conditions = [
+ self._update_recursive(sub_condition) for sub_condition in sub_conditions
+ ]
+ updated_dict[key] = updated_conditions
+
+ return updated_dict
+
+ return parsed
+
+ def _update_value(self, value):
+ if isinstance(value, str):
+ return self.value_mapping.get(value, value)
+
+ elif isinstance(value, list):
+ # Handle "in" operator with list of values
+ return [self.value_mapping.get(v, v) if isinstance(v, str) else v for v in value]
+
+ return value
+
+
+@frappe.whitelist()
+def get_filtered_accounts(company: str, account_rows: str | list):
+ frappe.has_permission("Financial Report Template", ptype="read", throw=True)
+
+ if isinstance(account_rows, str):
+ account_rows = json.loads(account_rows, object_hook=frappe._dict)
+
+ return DataCollector.get_filtered_accounts(company, account_rows)
+
+
+@frappe.whitelist()
+def get_children_accounts(
+ doctype: str,
+ parent: str,
+ company: str,
+ filtered_accounts: list[str] | str | None = None,
+ missed: bool = False,
+ is_root: bool = False,
+ include_disabled: bool = False,
+):
+ """
+ Get children accounts based on the provided filters to view in tree.
+
+ Args:
+ parent: The parent account to get children for.
+ company: The company to filter accounts by.
+ account_rows: Template rows with `Data Source` == `Account Data`.
+ missed:
+ - If True, only missed by filters accounts will be included.
+ - If False, only filtered accounts will be included.
+ is_root: Whether the parent is a root account.
+ include_disabled: Whether to include disabled accounts.
+
+ Example:
+ ```python
+ [
+ {
+ value: "Current Liabilities - WP",
+ expandable: 1,
+ root_type: "Liability",
+ account_currency: "USD",
+ parent: "Source of Funds (Liabilities) - WP",
+ },
+ {
+ value: "Non-Current Liabilities - WP",
+ expandable: 1,
+ root_type: "Liability",
+ account_currency: "USD",
+ parent: "Source of Funds (Liabilities) - WP",
+ },
+ ]
+ ```
+ """
+ frappe.has_permission(doctype, ptype="read", throw=True)
+
+ children_accounts = get_children(
+ doctype, parent, company, is_root=is_root, include_disabled=include_disabled
+ )
+
+ if not children_accounts:
+ return []
+
+ if isinstance(filtered_accounts, str):
+ filtered_accounts = frappe.parse_json(filtered_accounts)
+
+ if not filtered_accounts:
+ return children_accounts if missed else []
+
+ valid_accounts = []
+
+ for account in children_accounts:
+ if account.expandable:
+ valid_accounts.append(account)
+ continue
+
+ is_in_filtered = account.value in filtered_accounts
+
+ if (missed and not is_in_filtered) or (not missed and is_in_filtered):
+ valid_accounts.append(account)
+
+ return valid_accounts
+
+
+# ============================================================================
+# PROCESS CALCULATIONS
+# ============================================================================
+
+
+class RowProcessor:
+ """
+ Processes individual rows of the financial report template.
+ Handles dependency resolution and calculation order.
+ """
+
+ def __init__(self, context: ReportContext):
+ self.context = context
+ self.period_list = context.period_list
+ self.row_values = {} # For formula calculations
+ self.dependency_resolver = DependencyResolver(context.template)
+
+ def process_all_rows(self) -> list[RowData]:
+ processing_order = self.dependency_resolver.get_processing_order()
+ processed_rows = []
+
+ # Get account data from context
+ account_summary = self.context.raw_data.get("summary", {})
+ account_details = self.context.raw_data.get("account_details", {})
+
+ for row in processing_order:
+ row_data = self._process_single_row(row, account_summary, account_details)
+ processed_rows.append(row_data)
+
+ processed_rows.sort(key=lambda x: getattr(x.row, "idx", 0) or 0)
+
+ return processed_rows
+
+ def _process_single_row(self, row, account_summary: dict, account_details: dict) -> RowData:
+ if row.data_source == "Account Data":
+ return self._process_account_row(row, account_summary, account_details)
+ elif row.data_source == "Custom API":
+ return self._process_api_row(row)
+ elif row.data_source == "Calculated Amount":
+ return self._process_formula_row(row)
+ elif row.data_source == "Blank Line":
+ return self._process_blank_row(row)
+ elif row.data_source == "Column Break":
+ return self._process_column_break_row(row)
+ elif row.data_source == "Section Break":
+ return self._process_section_break_row(row)
+ else:
+ return RowData(row=row, values=[0.0] * len(self.period_list))
+
+ def _process_account_row(self, row, account_summary: dict, account_details: dict) -> RowData:
+ ref_code = row.reference_code
+ values = account_summary.get(ref_code, [0.0] * len(self.period_list))
+ details = account_details.get(ref_code, {})
+
+ if ref_code:
+ self.row_values[ref_code] = values
+
+ return RowData(row=row, values=values, account_details=details)
+
+ def _process_api_row(self, row) -> RowData:
+ api_path = row.calculation_formula
+ # TODO
+
+ try:
+ values = frappe.call(api_path, filters=self.context.filters, periods=self.period_list, row=row)
+
+ if row.reverse_sign:
+ values = [-1 * v for v in values]
+
+ # TODO: add support for server script
+ # use form_dict to pass input in server script
+ except Exception as e:
+ frappe.log_error(f"Custom API Error: {api_path} - {e!s}")
+ values = [0.0] * len(self.period_list)
+
+ if row.reference_code:
+ self.row_values[row.reference_code] = values
+
+ return RowData(row=row, values=values)
+
+ def _process_formula_row(self, row) -> RowData:
+ calculator = FormulaCalculator(self.row_values, self.period_list)
+ values = calculator.evaluate_formula(row)
+
+ if row.reference_code:
+ self.row_values[row.reference_code] = values
+
+ return RowData(row=row, values=values)
+
+ def _process_blank_row(self, row) -> RowData:
+ return RowData(row=row, values=[""] * len(self.period_list))
+
+ def _process_column_break_row(self, row) -> RowData:
+ return RowData(row=row, values=[])
+
+ def _process_section_break_row(self, row) -> RowData:
+ return RowData(row=row, values=[])
+
+
+class DependencyResolver:
+ """Optimized dependency resolver with better circular reference detection"""
+
+ def __init__(self, template):
+ self.template: FinancialReportTemplate = template
+ self.rows = template.rows
+ self.row_map = {row.reference_code: row for row in self.rows if row.reference_code}
+ self.dependencies = {}
+ self._validate_dependencies()
+
+ def _validate_dependencies(self):
+ """Validate dependencies using the new validation framework"""
+
+ validator = DependencyValidator(self.template)
+ result = validator.validate()
+ result.notify_user()
+
+ self.dependencies = validator.dependencies
+
+ def get_processing_order(self) -> list:
+ # rows by type
+ api_rows = []
+ account_rows = []
+ formula_rows = []
+ other_rows = []
+
+ for row in self.rows:
+ if row.data_source == "Custom API":
+ api_rows.append(row)
+ elif row.data_source == "Account Data":
+ account_rows.append(row)
+ elif row.data_source == "Calculated Amount":
+ formula_rows.append(row)
+ else:
+ other_rows.append(row)
+
+ ordered_rows = api_rows + account_rows
+
+ # sort formula rows
+ if formula_rows:
+ ordered_formula_rows = self._topological_sort(formula_rows)
+ ordered_rows.extend(ordered_formula_rows)
+
+ ordered_rows.extend(other_rows)
+
+ return ordered_rows
+
+ def _topological_sort(self, formula_rows: list) -> list:
+ formula_row_map = {row.reference_code: row for row in formula_rows if row.reference_code}
+
+ adj_list = {code: [] for code in formula_row_map}
+ in_degree = {code: 0 for code in formula_row_map}
+
+ # Calculate in-degree
+ for code in formula_row_map:
+ deps = self.dependencies.get(code, [])
+ for dep in deps:
+ if dep in formula_row_map: # Only consider dependencies within formula rows
+ adj_list[dep].append(code)
+ in_degree[code] += 1
+
+ # Topological sort
+ queue = [code for code, degree in in_degree.items() if degree == 0]
+ result = []
+
+ while queue:
+ current = queue.pop(0)
+ result.append(formula_row_map[current])
+
+ # Reduce in-degree
+ for neighbor in adj_list[current]:
+ in_degree[neighbor] -= 1
+ if in_degree[neighbor] == 0:
+ queue.append(neighbor)
+
+ # Add any remaining formula rows
+ result_set = set(result)
+ for row in formula_rows:
+ if row not in result_set:
+ result.append(row)
+
+ return result
+
+
+class FormulaCalculator:
+ """Enhanced formula calculator with better error handling"""
+
+ def __init__(self, row_data: dict[str, list[float]], period_list: list[dict]):
+ self.row_data = row_data
+ self.period_list = period_list
+ self.precision = get_currency_precision()
+ self.validator = CalculationFormulaValidator(set(row_data.keys()))
+
+ self.math_functions = {
+ "abs": abs,
+ "round": round,
+ "min": min,
+ "max": max,
+ "sum": sum,
+ "sqrt": math.sqrt,
+ "pow": math.pow,
+ "ceil": math.ceil,
+ "floor": math.floor,
+ }
+
+ def evaluate_formula(self, report_row: dict[str, Any]) -> list[float]:
+ validation_result = self.validator.validate(report_row)
+ formula = report_row.calculation_formula
+ negation_factor = -1 if report_row.reverse_sign else 1
+
+ if validation_result.issues:
+ # TODO: Throw?
+ messages = "
".join(issue.message for issue in validation_result.issues)
+ frappe.log_error(f"Formula validation errors found:\n{messages}")
+ return [0.0] * len(self.period_list)
+
+ results = []
+ for i in range(len(self.period_list)):
+ result = self._evaluate_for_period(formula, i, negation_factor)
+ results.append(result)
+
+ return results
+
+ def _evaluate_for_period(self, formula: str, period_index: int, negation_factor: int) -> float:
+ # TODO: consistent error handling
+ try:
+ context = self._build_context(period_index)
+ result = frappe.safe_eval(formula, context)
+ return flt(result * negation_factor, self.precision)
+
+ except ZeroDivisionError:
+ frappe.log_error(f"Division by zero in formula: {formula}")
+ return 0.0
+ except Exception as e:
+ frappe.log_error(f"Formula evaluation error: {formula} - {e!s}")
+ return 0.0
+
+ def _build_context(self, period_index: int) -> dict[str, Any]:
+ context = {}
+
+ # row values
+ for code, values in self.row_data.items():
+ if period_index < len(values):
+ context[code] = values[period_index] or 0.0
+ else:
+ context[code] = 0.0
+
+ # math functions
+ context.update(self.math_functions)
+
+ return context
+
+
+# ============================================================================
+# DATA FORMATTING
+# ============================================================================
+
+
+class DataFormatter:
+ def __init__(self, context: ReportContext):
+ self.context = context
+ self.formatting_engine = FormattingEngine()
+
+ self.organizer = SegmentOrganizer(context.processed_rows)
+
+ if self.organizer.is_single_segment:
+ self.formatter = SingleSegmentFormatter(context, self.formatting_engine)
+ else:
+ self.formatter = MultiSegmentFormatter(context, self.formatting_engine)
+
+ if context.show_detailed:
+ self._expand_segments_with_details()
+
+ def format_for_display(self) -> tuple[list[dict], list[dict]]:
+ formatted_data = self._format_rows()
+ columns = self._generate_columns()
+ return formatted_data, columns
+
+ def _format_rows(self) -> list[dict]:
+ formatted_data = []
+
+ for section in self.organizer.sections:
+ for row_index in range(self.organizer.max_rows(section)):
+ formatted_row = self.formatter.format_row(section.segments, row_index)
+ if formatted_row: # Always include rows that were formatted
+ # Add metadata
+ formatted_row["_segment_info"] = {
+ "total_segments": len(section.segments),
+ "period_keys": [p["key"] for p in self.context.period_list], # Add period keys
+ }
+ formatted_data.append(formatted_row)
+
+ return formatted_data
+
+ def _generate_columns(self) -> list[dict]:
+ base_columns = get_columns(
+ self.context.filters.get("periodicity"),
+ self.context.period_list,
+ self.context.filters.get("accumulated_values") in (1, None),
+ self.context.filters.get("company"),
+ )
+
+ return self.formatter.get_columns(self.organizer.section_with_max_segments.segments, base_columns)
+
+ def _expand_segments_with_details(self):
+ for section in self.organizer.sections:
+ for segment in section.segments:
+ expanded_rows = []
+
+ for row_data in segment.rows:
+ expanded_rows.append(row_data)
+
+ if row_data.account_details:
+ detail_rows = DetailRowBuilder(self.context.filters, row_data).build()
+ expanded_rows.extend(detail_rows)
+
+ segment.rows = expanded_rows
+
+
+class FormattingEngine:
+ """Manages formatting rules and application"""
+
+ def __init__(self):
+ self.initialize_rules()
+
+ def initialize_rules(self):
+ self.rules = [
+ FormattingRule(
+ condition=lambda rd: getattr(rd.row, "bold_text", False), format_properties={"bold": True}
+ ),
+ FormattingRule(
+ condition=lambda rd: getattr(rd.row, "italic_text", False), format_properties={"italic": True}
+ ),
+ FormattingRule(
+ condition=lambda rd: rd.is_detail_row, format_properties={"is_detail": True, "prefix": "• "}
+ ),
+ FormattingRule(
+ condition=lambda rd: getattr(rd.row, "warn_if_negative", False),
+ format_properties={"warn_if_negative": True},
+ ),
+ FormattingRule(
+ condition=lambda rd: getattr(rd.row, "data_source", "") == "Blank Line",
+ format_properties={"is_blank_line": True},
+ ),
+ FormattingRule(
+ condition=lambda rd: getattr(rd.row, "fieldtype", ""),
+ format_properties=lambda rd: {"fieldtype": getattr(rd.row, "fieldtype", "").strip()},
+ ),
+ FormattingRule(
+ condition=lambda rd: getattr(rd.row, "color", ""),
+ format_properties=lambda rd: {"color": getattr(rd.row, "color", "").strip()},
+ ),
+ FormattingRule(
+ condition=lambda rd: getattr(rd.row, "data_source", "") == "Account Data",
+ format_properties=lambda rd: {
+ "account_filters": getattr(rd.row, "calculation_formula", "").strip()
+ },
+ ),
+ ]
+
+ def get_formatting(self, row_data: RowData) -> dict[str, Any]:
+ formatting = {}
+ for rule in self.rules:
+ if rule.applies_to(row_data):
+ properties = rule.get_properties(row_data)
+ formatting.update(properties)
+
+ return formatting
+
+
+class SegmentOrganizer:
+ """Handles segment organization by `Column Break`, `Section Break` and metadata extraction"""
+
+ def __init__(self, processed_rows: list[RowData]):
+ self.sections = self._organize_into_sections(processed_rows)
+
+ # ensure same segment length across sections
+ max_segments = self.max_segments
+ for section in self.sections:
+ if len(section.segments) >= max_segments:
+ continue
+
+ # Pad with empty segments
+ empty_segments = [SegmentData(index=i) for i in range(len(section.segments), max_segments)]
+ section.segments.extend(empty_segments)
+
+ def _organize_into_sections(self, rows: list[RowData]) -> list[SectionData]:
+ sections = []
+ current_section_rows = []
+ section_index = 0
+ section_label = ""
+
+ for row_data in rows:
+ if not self._should_show_row(row_data):
+ continue
+
+ if row_data.row.data_source == "Section Break":
+ # Process current section if we have rows
+ if current_section_rows:
+ section_segments = self._organize_into_segments(current_section_rows, section_label)
+ sections.append(
+ SectionData(segments=section_segments, label=section_label, index=section_index)
+ )
+ section_index += 1
+ current_section_rows = []
+
+ # Label for the next section
+ section_label = getattr(row_data.row, "display_name", "") or ""
+ else:
+ current_section_rows.append(row_data)
+
+ # Add final section
+ if current_section_rows or not sections:
+ section_segments = self._organize_into_segments(current_section_rows, section_label)
+ sections.append(SectionData(segments=section_segments, label=section_label, index=section_index))
+
+ return sections
+
+ def _organize_into_segments(self, rows: list[RowData], section_label: str) -> list[SegmentData]:
+ segments = []
+ current_rows = []
+ segment_index = 0
+ segment_label = ""
+
+ section_header = None
+ if section_label:
+ section_header = RowData(
+ row=frappe._dict(
+ {
+ "data_source": "Blank Line",
+ "display_name": section_label,
+ "bold_text": True,
+ }
+ )
+ )
+
+ for row_data in rows:
+ if row_data.row.data_source == "Column Break":
+ # Save current segment
+ if section_header and current_rows:
+ current_rows.insert(0, section_header)
+ section_header = RowData(row=frappe._dict({"data_source": "Blank Line"}))
+
+ if current_rows:
+ segments.append(SegmentData(rows=current_rows, label=segment_label, index=segment_index))
+ segment_index += 1
+ current_rows = []
+
+ # Label for the next segment
+ segment_label = getattr(row_data.row, "display_name", "") or ""
+ else:
+ current_rows.append(row_data)
+
+ # Add final segment
+ if section_header and current_rows:
+ current_rows.insert(0, section_header)
+
+ if current_rows or not segments:
+ segments.append(SegmentData(rows=current_rows, label=segment_label, index=segment_index))
+
+ return segments
+
+ @property
+ def is_single_segment(self) -> bool:
+ return self.max_segments == 1
+
+ def max_rows(self, section: SectionData) -> int:
+ return max(len(seg.rows) for seg in section.segments) if section.segments else 0
+
+ @property
+ def max_segments(self) -> bool:
+ return max(len(s.segments) for s in self.sections)
+
+ @property
+ def section_with_max_segments(self) -> SectionData:
+ return max(self.sections, key=lambda s: len(s.segments))
+
+ def _should_show_row(self, row_data: RowData) -> bool:
+ row = row_data.row
+
+ # Always show blank lines
+ if row.data_source == "Blank Line":
+ return True
+
+ if getattr(row, "hidden_calculation", False):
+ return False
+
+ if getattr(row, "hide_when_empty", False):
+ significant_values = [
+ val for val in row_data.values if isinstance(val, int | float) and abs(flt(val)) > 0.01
+ ]
+ return len(significant_values) > 0
+
+ return True
+
+
+class RowFormatterBase(ABC):
+ def __init__(self, context: ReportContext, formatting_engine: FormattingEngine):
+ self.context = context
+ self.period_list = context.period_list
+ self.formatting_engine = formatting_engine
+
+ @abstractmethod
+ def format_row(self, segments: list[SegmentData], row_index: int) -> dict[str, Any]:
+ pass
+
+ @abstractmethod
+ def get_columns(self, segments: list[SegmentData], base_columns: list[dict]) -> list[dict]:
+ pass
+
+ def _get_values(self, row_data: RowData) -> dict[str, Any]:
+ # TODO: can be commonify COA? @abdeali
+ child_accounts = []
+
+ if row_data.account_details:
+ child_accounts = list(row_data.account_details.keys())
+
+ values = {
+ "child_accounts": child_accounts,
+ "account": getattr(row_data.row, "display_name", "") or "",
+ "indent": getattr(row_data.row, "indentation_level", 0),
+ "account_name": getattr(row_data.row, "account", "") or "",
+ "currency": self.context.currency or "",
+ "period_start_date": getattr(self.context.filters, "period_start_date", "") or "",
+ "period_end_date": getattr(self.context.filters, "period_end_date", "") or "",
+ "total": 0,
+ }
+
+ for i, period in enumerate(self.period_list):
+ period_value = self._get_period_value(row_data, i)
+ values[period["key"]] = period_value
+
+ if self.context.filters.get("accumulated_values") == 0:
+ values["total"] += flt(period_value)
+
+ # avg for percent
+ if self.context.filters.get("accumulated_values") == 0 and row_data.row.fieldtype == "Percent":
+ values["total"] = values["total"] / len(self.period_list)
+
+ return values
+
+ def _get_period_value(self, row_data: RowData, period_index: int) -> Any:
+ if period_index < len(row_data.values):
+ return row_data.values[period_index]
+
+ return ""
+
+
+class SingleSegmentFormatter(RowFormatterBase):
+ def format_row(self, segments: list[SegmentData], row_index: int) -> dict[str, Any]:
+ if not segments or row_index >= len(segments[0].rows):
+ return {}
+
+ row_data = segments[0].rows[row_index]
+
+ formatted = self._get_values(row_data)
+
+ formatting = self.formatting_engine.get_formatting(row_data)
+ formatted.update(formatting)
+
+ return formatted
+
+ def get_columns(self, segments: list[SegmentData], base_columns: list[dict]) -> list[dict]:
+ for col in base_columns:
+ if col["fieldname"] == "account":
+ col["align"] = "left"
+
+ return base_columns
+
+
+class MultiSegmentFormatter(RowFormatterBase):
+ def format_row(self, segments: list[SegmentData], row_index: int) -> dict[str, Any]:
+ formatted = {"segment_values": {}}
+
+ for segment in segments:
+ if row_index < len(segment.rows):
+ row_data = segment.rows[row_index]
+ self._add_segment_data(formatted, row_data, segment)
+ else:
+ self._add_empty_segment(formatted, segment)
+
+ return formatted
+
+ def get_columns(self, segments: list[SegmentData], base_columns: list[dict]) -> list[dict]:
+ columns = []
+
+ # TODO: Refactor
+ for segment in segments:
+ for col in base_columns:
+ new_col = col.copy()
+
+ new_col["fieldname"] = f"{segment.id}_{col['fieldname']}"
+
+ if col["fieldname"] == "account":
+ new_col["label"] = segment.label or f"Account (Segment {segment.index + 1})"
+ new_col["align"] = "left"
+
+ if segment.label and col["fieldname"] in [p["key"] for p in self.period_list]:
+ new_col["label"] = f"{segment.label} - {col['label']}"
+
+ columns.append(new_col)
+
+ return columns
+
+ def _add_segment_data(self, formatted: dict, row_data: RowData, segment: SegmentData):
+ segment_values = self._get_values(row_data)
+
+ for key, value in segment_values.items():
+ formatted[f"{segment.id}_{key}"] = value
+
+ formatting = self.formatting_engine.get_formatting(row_data)
+ segment_values.update(formatting)
+
+ formatted["segment_values"][segment.id] = segment_values
+
+ def _add_empty_segment(self, formatted: dict, segment: SegmentData):
+ formatted[f"account_{segment.id}"] = ""
+ for period in self.period_list:
+ formatted[f"{segment.id}_{period['key']}"] = ""
+
+ formatted["segment_values"][segment.id] = {"is_blank_line": True}
+
+
+class DetailRowBuilder:
+ """Builds detail rows for account breakdown"""
+
+ def __init__(self, filters: dict, parent_row_data: RowData):
+ self.filters = filters
+ self.parent_row_data = parent_row_data
+
+ def build(self) -> list[RowData]:
+ if not self.parent_row_data.account_details:
+ return []
+
+ detail_rows = []
+ parent_row = self.parent_row_data.row
+
+ for account_name, account_data in self.parent_row_data.account_details.items():
+ detail_row = self._create_detail_row_object(account_name, parent_row)
+
+ balance_type = getattr(parent_row, "balance_type", "Closing Balance")
+ values = account_data.get_values_by_type(balance_type)
+
+ detail_row_data = RowData(
+ row=detail_row,
+ values=values,
+ is_detail_row=True,
+ parent_reference=parent_row.reference_code,
+ )
+
+ detail_rows.append(detail_row_data)
+
+ return detail_rows
+
+ def _create_detail_row_object(self, account_name: str, parent_row):
+ short_name = account_name.rsplit(" - ", 1)[0].strip()
+
+ return type(
+ "DetailRow",
+ (),
+ {
+ "display_name": short_name,
+ "account": account_name,
+ "account_name": short_name,
+ "data_source": "Account Detail",
+ "indentation_level": getattr(parent_row, "indentation_level", 0) + 1,
+ "fieldtype": getattr(parent_row, "fieldtype", None),
+ "bold_text": False,
+ "italic_text": True,
+ "reverse_sign": getattr(parent_row, "reverse_sign", False),
+ "warn_if_negative": getattr(parent_row, "warn_if_negative", False),
+ "hide_when_empty": getattr(parent_row, "hide_when_empty", False),
+ "hidden_calculation": False,
+ },
+ )()
+
+
+class ChartDataGenerator:
+ def __init__(self, context: ReportContext):
+ self.context = context
+ self.processed_rows = context.processed_rows
+ self.period_list = context.period_list
+ self.filters = context.filters
+ self.currency = context.currency
+
+ def generate(self) -> dict[str, Any]:
+ chart_rows = [
+ row
+ for row in self.processed_rows
+ if getattr(row.row, "include_in_charts", False)
+ and row.row.data_source not in ["Blank Line", "Column Break", "Section Break"]
+ ]
+
+ if not chart_rows:
+ return {}
+
+ labels = [p.get("label") for p in self.period_list]
+ datasets = []
+
+ for row_data in chart_rows:
+ display_name = getattr(row_data.row, "display_name", "")
+ values = []
+ for i, _period in enumerate(self.period_list):
+ if i < len(row_data.values):
+ value = row_data.values[i]
+ values.append(flt(value, 2))
+ else:
+ values.append(0.0)
+
+ # only non-zero values
+ if any(v != 0 for v in values):
+ datasets.append({"name": display_name, "values": values})
+
+ if not datasets:
+ return {}
+
+ # chart config
+ if not self.filters.get("accumulated_values") or len(labels) <= 1:
+ chart_type = "bar"
+ else:
+ chart_type = "line"
+
+ self.context.raw_data["chart"] = {
+ "data": {"labels": labels, "datasets": datasets},
+ "type": chart_type,
+ "fieldtype": "Currency",
+ "options": "currency",
+ "currency": self.currency,
+ }
+
+
+class GrowthViewTransformer:
+ def __init__(self, context: ReportContext):
+ self.context = context
+ self.formatted_rows = context.raw_data.get("formatted_data", [])
+ self.period_list = context.period_list
+
+ def transform(self) -> None:
+ for row_data in self.formatted_rows:
+ if row_data.get("is_blank_line"):
+ continue
+
+ transformed_values = {}
+ for i in range(len(self.period_list)):
+ current_period = self.period_list[i]["key"]
+
+ current_value = row_data[current_period]
+ previous_value = row_data[self.period_list[i - 1]["key"]] if i != 0 else 0
+
+ if i == 0:
+ transformed_values[current_period] = current_value
+ else:
+ growth_percent = self._calculate_growth(previous_value, current_value)
+ transformed_values[current_period] = growth_percent
+
+ row_data.update(transformed_values)
+
+ def _calculate_growth(self, previous_value: float, current_value: float) -> float | None:
+ if current_value is None:
+ return None
+
+ if previous_value == 0 and current_value > 0:
+ return 100.0
+ elif previous_value == 0 and current_value <= 0:
+ return 0.0
+ else:
+ return flt(((current_value - previous_value) / abs(previous_value)) * 100, 2)
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js
new file mode 100644
index 00000000000..739956631fd
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js
@@ -0,0 +1,433 @@
+// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Financial Report Template", {
+ refresh(frm) {
+ // add custom button to view missed accounts
+ frm.add_custom_button(__("View Account Coverage"), function () {
+ let selected_rows = frm.get_field("rows").grid.get_selected_children();
+ const has_selection = selected_rows.length > 0;
+ if (selected_rows.length === 0) selected_rows = frm.doc.rows;
+
+ show_accounts_tree(selected_rows, has_selection);
+ });
+
+ // add custom button to open the financial report
+ frm.add_custom_button(__("View Report"), function () {
+ frappe.set_route("query-report", frm.doc.report_type, {
+ report_template: frm.doc.name,
+ });
+ });
+ },
+
+ validate(frm) {
+ if (!frm.doc.rows || frm.doc.rows.length === 0) {
+ frappe.msgprint(__("At least one row is required for a financial report template"));
+ }
+ },
+});
+
+frappe.ui.form.on("Financial Report Row", {
+ data_source(frm, cdt, cdn) {
+ const row = locals[cdt][cdn];
+
+ update_formula_label(frm, row.data_source);
+ update_formula_description(frm, row.data_source);
+
+ if (row.data_source !== "Account Data") {
+ frappe.model.set_value(cdt, cdn, "balance_type", "");
+ }
+
+ if (["Blank Line", "Column Break", "Section Break"].includes(row.data_source)) {
+ frappe.model.set_value(cdt, cdn, "calculation_formula", "");
+ }
+
+ set_up_filters_editor(frm, cdt, cdn);
+ },
+
+ form_render(frm, cdt, cdn) {
+ const row = locals[cdt][cdn];
+
+ update_formula_label(frm, row.data_source);
+ update_advanced_formula_property(frm, cdt, cdn);
+ set_up_filters_editor(frm, cdt, cdn);
+ update_formula_description(frm, row.data_source);
+ },
+
+ calculation_formula(frm, cdt, cdn) {
+ update_advanced_formula_property(frm, cdt, cdn);
+ },
+
+ advanced_filtering(frm, cdt, cdn) {
+ set_up_filters_editor(frm, cdt, cdn);
+ },
+});
+
+// FILTERS EDITOR
+
+function set_up_filters_editor(frm, cdt, cdn) {
+ const row = locals[cdt][cdn];
+
+ if (row.data_source !== "Account Data" || row.advanced_filtering) return;
+
+ const grid_row = frm.fields_dict["rows"].grid.get_row(cdn);
+ const wrapper = grid_row.get_field("filters_editor").$wrapper;
+ wrapper.empty();
+
+ const ACCOUNT = "Account";
+ const FIELD_IDX = 1;
+ const OPERATOR_IDX = 2;
+ const VALUE_IDX = 3;
+
+ // Parse saved filters
+ let saved_filters = [];
+
+ if (row.calculation_formula) {
+ try {
+ const parsed = JSON.parse(row.calculation_formula);
+
+ if (Array.isArray(parsed)) saved_filters = [parsed];
+ else if (parsed.and) saved_filters = parsed.and;
+ } catch (e) {
+ frappe.show_alert({
+ message: __("Invalid filter formula. Please check the syntax."),
+ indicator: "red",
+ });
+ }
+ }
+
+ if (saved_filters.length)
+ // Ensure every filter starts with "Account"
+ saved_filters = saved_filters.map((f) => [ACCOUNT, ...f]);
+
+ frappe.model.with_doctype(ACCOUNT, () => {
+ const filter_group = new frappe.ui.FilterGroup({
+ parent: wrapper,
+ doctype: ACCOUNT,
+ on_change: () => {
+ // only need [[field, operator, value]]
+ const filters = filter_group
+ .get_filters()
+ .map((f) => [f[FIELD_IDX], f[OPERATOR_IDX], f[VALUE_IDX]]);
+
+ const current = filters.length > 1 ? { and: filters } : filters[0];
+ frappe.model.set_value(cdt, cdn, "calculation_formula", JSON.stringify(current));
+ },
+ });
+
+ filter_group.add_filters_to_filter_group(saved_filters);
+ });
+}
+
+function update_advanced_formula_property(frm, cdt, cdn) {
+ const row = locals[cdt][cdn];
+ const is_advanced = is_advanced_formula(row);
+
+ frm.set_df_property("rows", "read_only", is_advanced, frm.doc.name, "advanced_filtering", cdn);
+
+ if (is_advanced && !row.advanced_filtering) {
+ row.advanced_filtering = 1;
+ frm.refresh_field("rows");
+ }
+}
+
+function is_advanced_formula(row) {
+ if (!row || row.data_source !== "Account Data") return false;
+
+ let parsed = null;
+ if (row.calculation_formula) {
+ try {
+ parsed = JSON.parse(row.calculation_formula);
+ } catch (e) {
+ console.warn("Invalid JSON in calculation_formula:", e);
+ return false;
+ }
+ }
+
+ if (Array.isArray(parsed)) return false;
+ if (parsed?.or) return true;
+ if (parsed?.and) return parsed.and.some((cond) => !Array.isArray(cond));
+
+ return false;
+}
+
+// ACCOUNTS TREE VIEW
+
+function show_accounts_tree(template_rows, has_selection) {
+ // filtered rows
+ const account_rows = template_rows.filter((row) => row.data_source === "Account Data");
+
+ if (account_rows.length === 0) {
+ frappe.show_alert(__("No Account Data row found"));
+ return;
+ }
+
+ const dialog = new frappe.ui.Dialog({
+ title: __("Accounts Missing from Report"),
+ fields: [
+ {
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ label: "Company",
+ reqd: 1,
+ default: frappe.defaults.get_user_default("Company"),
+ onchange: () => {
+ const company_field = dialog.get_field("company");
+ if (!company_field.value || company_field.value === company_field.last_value) return;
+ refresh_tree_view(dialog, account_rows);
+ },
+ },
+ {
+ fieldname: "view_type",
+ fieldtype: "Select",
+ options: ["Missing Accounts", "Filtered Accounts"],
+ label: "View",
+ default: has_selection ? "Filtered Accounts" : "Missing Accounts",
+ reqd: 1,
+ onchange: () => {
+ dialog.set_title(
+ dialog.get_value("view_type") === "Missing Accounts"
+ ? __("Accounts Missing from Report")
+ : __("Accounts Included in Report")
+ );
+
+ refresh_tree_view(dialog, account_rows);
+ },
+ },
+ {
+ fieldname: "tip",
+ fieldtype: "HTML",
+ label: "Tip",
+ options: `
+
+ Tip: Select report lines to view their accounts
+
+ `,
+ depends_on: has_selection ? "eval: false" : "eval: true",
+ },
+ {
+ fieldname: "tree_area",
+ fieldtype: "HTML",
+ label: "Chart of Accounts",
+ read_only: 1,
+ depends_on: "eval: doc.company",
+ },
+ ],
+ primary_action_label: __("Done"),
+ primary_action() {
+ dialog.hide();
+ },
+ });
+
+ dialog.show();
+ refresh_tree_view(dialog, account_rows);
+}
+
+async function refresh_tree_view(dialog, account_rows) {
+ const missed = dialog.get_value("view_type") === "Missing Accounts";
+ const company = dialog.get_value("company");
+
+ const wrapper = dialog.get_field("tree_area").$wrapper;
+ wrapper.empty();
+
+ // get filtered accounts
+ const { message: filtered_accounts } = await frappe.call({
+ method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_filtered_accounts",
+ args: { company: company, account_rows: account_rows },
+ });
+
+ // render tree
+ const tree = new FilteredTree({
+ parent: wrapper,
+ label: company,
+ root_value: company,
+ method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
+ args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
+ toolbar: [],
+ });
+
+ tree.load_children(tree.root_node, true);
+}
+
+class FilteredTree extends frappe.ui.Tree {
+ render_children_of_all_nodes(data_list) {
+ data_list = this.get_filtered_data_list(data_list);
+ super.render_children_of_all_nodes(data_list);
+ }
+
+ get_filtered_data_list(data_list) {
+ let removed_nodes = new Set();
+
+ // Filter nodes with no data
+ data_list = data_list.filter((d) => {
+ if (d.data.length === 0) {
+ removed_nodes.add(d.parent);
+ return false;
+ }
+ return true;
+ });
+
+ // Remove references to removed nodes and iteratively remove empty parents
+ while (removed_nodes.size > 0) {
+ const current_removed = [...removed_nodes];
+ removed_nodes.clear();
+
+ data_list = data_list.filter((d) => {
+ d.data = d.data.filter((a) => !current_removed.includes(a.value));
+
+ if (d.data.length === 0) {
+ removed_nodes.add(d.parent);
+ return false;
+ }
+ return true;
+ });
+ }
+
+ return data_list;
+ }
+}
+
+function update_formula_label(frm, data_source) {
+ const grid = frm.fields_dict.rows.grid;
+ const field = grid.fields_map.calculation_formula;
+ if (!field) return;
+
+ const labels = {
+ "Account Data": "Account Filter",
+ "Custom API": "API Method Path",
+ };
+
+ grid.update_docfield_property(
+ "calculation_formula",
+ "label",
+ labels[data_source] || "Calculation Formula"
+ );
+}
+
+// FORMULA DESCRIPTION
+
+function update_formula_description(frm, data_source) {
+ if (!data_source) return;
+
+ let grid = frm.fields_dict.rows.grid;
+ let field = grid.fields_map.formula_description;
+ if (!field) return;
+
+ // Common CSS styles and elements
+ const container_style = `style="padding: var(--padding-md); border: 1px solid var(--border-color); border-radius: var(--border-radius); margin-top: var(--margin-sm);"`;
+ const title_style = `style="margin-top: 0; color: var(--text-color);"`;
+ const subtitle_style = `style="color: var(--text-color); margin-bottom: var(--margin-xs);"`;
+ const text_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted);"`;
+ const list_style = `style="margin-bottom: var(--margin-sm); color: var(--text-muted); font-size: 0.9em;"`;
+ const note_style = `style="margin-bottom: 0; color: var(--text-muted); font-size: 0.9em;"`;
+ const tip_style = `style="margin-bottom: 0; color: var(--text-color); font-size: 0.85em;"`;
+
+ let description_html = "";
+
+ if (data_source === "Account Data") {
+ description_html = `
+
+
Account Filter Guide
+
Specify which accounts to include in this line.
+
+
Basic Examples:
+
+ ["account_type", "=", "Cash"] - All Cash accounts
+ ["root_type", "in", ["Asset", "Liability"]] - All Asset and Liability accounts
+ ["account_category", "like", "Revenue"] - Revenue accounts
+
+
+
Multiple Conditions (AND/OR):
+
+ {"and": [["root_type", "=", "Asset"], ["account_type", "=", "Cash"]]}
+ {"or": [["account_category", "like", "Revenue"], ["account_category", "like", "Income"]]}
+
+
+
Available operators: =, !=, in, not in, like, not like, is
+
Multi-Company Tip: Use fields like account_type, root_type, and account_category for templates that work across multiple companies.
+
`;
+ } else if (data_source === "Calculated Amount") {
+ description_html = `
+
+
Formula Guide
+
Create calculations using reference codes from other lines.
+
+
Basic Examples:
+
+ REV100 + REV200 - Add two revenue lines
+ ASSETS - LIABILITIES - Calculate equity
+ REVENUE * 0.1 - 10% of revenue
+
+
+
Common Functions:
+
+ abs(value) - Remove negative sign
+ round(value) - Round to whole number
+ max(val1, val2) - Larger of two values
+ min(val1, val2) - Smaller of two values
+
+
+
Required: Use "Reference Code" from other rows in your formulas.
+
`;
+ } else if (data_source === "Custom API") {
+ description_html = `
+
+
Custom API Setup
+
Path to your custom method that returns financial data.
+
+
Format:
+
+ erpnext.custom.financial_apis.get_custom_revenue
+ my_app.financial_reports.get_kpi_data
+
+
+
Return Format:
+
Numbers for each period: [1000.0, 1200.0, 1150.0]
+
`;
+ } else if (data_source === "Blank Line") {
+ description_html = `
+
+
Blank Line
+
Adds empty space for better visual separation.
+
+
Use For:
+
+ - Separating major sections
+ - Adding space before totals
+
+
+
Note: No formula needed - creates visual spacing only.
+
`;
+ } else if (data_source === "Column Break") {
+ description_html = `
+
+
Column Break
+
Creates a visual break for side-by-side layout.
+
+
Use For:
+
+ - Horizontal P&L statements
+ - Side-by-side Balance Sheet sections
+
+
+
Note: No formula needed - this is for formatting only.
+
`;
+ } else if (data_source === "Section Break") {
+ description_html = `
+
+
Section Break
+
Creates a visual break for separating different sections.
+
+
Use For:
+
+ - Separating major sections in a report - say trading & profit and loss
+ - Improving readability by adding space
+
+
+
Note: No formula needed - this is for formatting only.
+
`;
+ }
+
+ grid.update_docfield_property("formula_description", "options", description_html);
+}
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.json b/erpnext/accounts/doctype/financial_report_template/financial_report_template.json
new file mode 100644
index 00000000000..7383306f332
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.json
@@ -0,0 +1,102 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:template_name",
+ "creation": "2025-08-02 04:44:15.184541",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "template_name",
+ "report_type",
+ "module",
+ "column_break_lvnq",
+ "disabled",
+ "section_break_fvlw",
+ "rows"
+ ],
+ "fields": [
+ {
+ "description": "Descriptive name for your template (e.g., 'Standard P&L', 'Detailed Balance Sheet')",
+ "fieldname": "template_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Template Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "description": "Type of financial statement this template generates",
+ "fieldname": "report_type",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Report Type",
+ "options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement"
+ },
+ {
+ "depends_on": "eval:frappe.boot.developer_mode",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module (for Export)",
+ "options": "Module Def"
+ },
+ {
+ "fieldname": "column_break_lvnq",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_fvlw",
+ "fieldtype": "Section Break"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "rows",
+ "fieldtype": "Table",
+ "label": "Report Line Items",
+ "options": "Financial Report Row"
+ },
+ {
+ "default": "0",
+ "description": "Disable template to prevent use in reports",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2025-11-14 00:11:03.508139",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Financial Report Template",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "Accounts User"
+ },
+ {
+ "read": 1,
+ "role": "Auditor"
+ }
+ ],
+ "row_format": "Dynamic",
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": [],
+ "title_field": "template_name"
+}
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.py b/erpnext/accounts/doctype/financial_report_template/financial_report_template.py
new file mode 100644
index 00000000000..69ee7e4f7dd
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.py
@@ -0,0 +1,179 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import os
+import shutil
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+from erpnext.accounts.doctype.account_category.account_category import import_account_categories
+from erpnext.accounts.doctype.financial_report_template.financial_report_validation import TemplateValidator
+
+
+class FinancialReportTemplate(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ from erpnext.accounts.doctype.financial_report_row.financial_report_row import FinancialReportRow
+
+ disabled: DF.Check
+ module: DF.Link | None
+ report_type: DF.Literal[
+ "", "Profit and Loss Statement", "Balance Sheet", "Cash Flow", "Custom Financial Statement"
+ ]
+ rows: DF.Table[FinancialReportRow]
+ template_name: DF.Data
+ # end: auto-generated types
+
+ def validate(self):
+ validator = TemplateValidator(self)
+ result = validator.validate()
+ result.notify_user()
+
+ def on_update(self):
+ self._export_template()
+
+ def on_trash(self):
+ self._delete_template()
+
+ def _export_template(self):
+ from frappe.modules.utils import export_module_json
+
+ if not self.module:
+ return
+
+ export_module_json(self, True, self.module)
+ self._export_account_categories()
+
+ def _delete_template(self):
+ if not self.module or not frappe.conf.developer_mode:
+ return
+
+ module_path = frappe.get_module_path(self.module)
+ dir_path = os.path.join(module_path, "financial_report_template", frappe.scrub(self.name))
+
+ shutil.rmtree(dir_path, ignore_errors=True)
+
+ def _export_account_categories(self):
+ import json
+
+ from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
+ FormulaFieldExtractor,
+ )
+
+ if not self.module or not frappe.conf.developer_mode or frappe.flags.in_import:
+ return
+
+ # Extract category from rows
+ extractor = FormulaFieldExtractor(
+ field_name="account_category", exclude_operators=["like", "not like"]
+ )
+ account_data_rows = [row for row in self.rows if row.data_source == "Account Data"]
+ category_names = extractor.extract_from_rows(account_data_rows)
+
+ if not category_names:
+ return
+
+ # Get path
+ module_path = frappe.get_module_path(self.module)
+ categories_file = os.path.join(module_path, "financial_report_template", "account_categories.json")
+
+ # Load existing categories
+ existing_categories = {}
+ if os.path.exists(categories_file):
+ try:
+ with open(categories_file) as f:
+ existing_data = json.load(f)
+ existing_categories = {cat["account_category_name"]: cat for cat in existing_data}
+ except (json.JSONDecodeError, KeyError):
+ pass # Create new file
+
+ # Fetch categories from database
+ if category_names:
+ db_categories = frappe.get_all(
+ "Account Category",
+ filters={"account_category_name": ["in", list(category_names)]},
+ fields=["account_category_name", "description"],
+ )
+
+ for cat in db_categories:
+ existing_categories[cat["account_category_name"]] = cat
+
+ # Sort by category name
+ sorted_categories = sorted(existing_categories.values(), key=lambda x: x["account_category_name"])
+
+ # Write to file
+ os.makedirs(os.path.dirname(categories_file), exist_ok=True)
+ with open(categories_file, "w") as f:
+ json.dump(sorted_categories, f, indent=2)
+
+
+def sync_financial_report_templates(chart_of_accounts=None, existing_company=None):
+ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import get_chart
+
+ # If COA is being created for an existing company,
+ # skip syncing templates as they are likely already present
+ if existing_company:
+ return
+
+ # Allow regional templates to completely override ERPNext
+ # templates based on the chart of accounts selected
+ disable_default_financial_report_template = False
+ if chart_of_accounts:
+ coa = get_chart(chart_of_accounts)
+ if coa.get("disable_default_financial_report_template", False):
+ disable_default_financial_report_template = True
+
+ installed_apps = frappe.get_installed_apps()
+
+ for app in installed_apps:
+ if disable_default_financial_report_template and app == "erpnext":
+ continue
+
+ _sync_templates_for(app)
+
+
+def _sync_templates_for(app_name):
+ templates = []
+
+ for module_name in frappe.local.app_modules.get(app_name) or []:
+ module_path = frappe.get_module_path(module_name)
+ template_path = os.path.join(module_path, "financial_report_template")
+
+ if not os.path.isdir(template_path):
+ continue
+
+ import_account_categories(template_path)
+
+ for template_dir in os.listdir(template_path):
+ json_file = os.path.join(template_path, template_dir, f"{template_dir}.json")
+ if os.path.isfile(json_file):
+ templates.append(json_file)
+
+ if not templates:
+ return
+
+ # ensure files are not exported
+ frappe.flags.in_import = True
+
+ for template_path in templates:
+ with open(template_path) as f:
+ template_data = frappe._dict(frappe.parse_json(f.read()))
+
+ template_name = template_data.get("name")
+
+ if not frappe.db.exists("Financial Report Template", template_name):
+ doc = frappe.get_doc(template_data)
+ doc.flags.ignore_mandatory = True
+ doc.flags.ignore_permissions = True
+ doc.flags.ignore_validate = True
+ doc.insert()
+
+ frappe.flags.in_import = False
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py b/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py
new file mode 100644
index 00000000000..306fb562585
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py
@@ -0,0 +1,545 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import ast
+import json
+import re
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, ClassVar
+
+import frappe
+from frappe import _
+from frappe.database.operator_map import OPERATOR_MAP
+from frappe.database.query import SQLFunctionParser
+
+
+@dataclass
+class ValidationIssue:
+ """Represents a single validation issue"""
+
+ message: str
+ row_idx: int | None = None
+ field: str | None = None
+ details: dict[str, Any] = None
+
+ def __post_init__(self):
+ if self.details is None:
+ self.details = {}
+
+ def __str__(self) -> str:
+ prefix = f"Row {self.row_idx}: " if self.row_idx else ""
+ field_info = f"[{self.field}] " if self.field else ""
+ message = f"{prefix}{field_info}{self.message}"
+ return _(message)
+
+
+@dataclass
+class ValidationResult:
+ issues: list[ValidationIssue] = field(default_factory=list)
+ warnings: list[ValidationIssue] = field(default_factory=list)
+
+ @property
+ def is_valid(self) -> bool:
+ return len(self.issues) == 0
+
+ @property
+ def has_warnings(self) -> bool:
+ return len(self.warnings) > 0
+
+ @property
+ def error_count(self) -> int:
+ return len(self.issues)
+
+ @property
+ def warning_count(self) -> int:
+ return len(self.warnings)
+
+ def merge(self, other: "ValidationResult") -> "ValidationResult":
+ self.issues.extend(other.issues)
+ self.warnings.extend(other.warnings)
+ return self
+
+ def add_error(self, issue: ValidationIssue) -> None:
+ """Add a critical error that prevents functionality"""
+ self.issues.append(issue)
+
+ def add_warning(self, issue: ValidationIssue) -> None:
+ """Add a warning for recommendatory validation"""
+ self.warnings.append(issue)
+
+ def notify_user(self) -> None:
+ warnings = "
".join(str(w) for w in self.warnings)
+ errors = "
".join(str(e) for e in self.issues)
+
+ if warnings:
+ frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
+
+ if errors:
+ frappe.throw(errors, title=_("Errors"))
+
+
+class TemplateValidator:
+ """Main validator that orchestrates all validations"""
+
+ def __init__(self, template):
+ self.template = template
+ self.validators = [
+ TemplateStructureValidator(),
+ DependencyValidator(template),
+ ]
+ self.formula_validator = FormulaValidator(template)
+
+ def validate(self) -> ValidationResult:
+ result = ValidationResult([])
+
+ # Run template-level validators
+ for validator in self.validators:
+ result.merge(validator.validate(self.template))
+
+ # Run row-level validations
+ account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
+ for row in self.template.rows:
+ result.merge(self.formula_validator.validate(row, account_fields))
+
+ return result
+
+
+class Validator(ABC):
+ @abstractmethod
+ def validate(self, context: Any) -> ValidationResult:
+ pass
+
+
+class TemplateStructureValidator(Validator):
+ def validate(self, template) -> ValidationResult:
+ result = ValidationResult()
+
+ result.merge(self._validate_reference_codes(template))
+ result.merge(self._validate_required_fields(template))
+
+ return result
+
+ def _validate_reference_codes(self, template) -> ValidationResult:
+ result = ValidationResult()
+ used_codes = set()
+
+ for row in template.rows:
+ if not row.reference_code:
+ continue
+
+ ref_code = row.reference_code.strip()
+
+ # Check format
+ if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
+ result.add_error(
+ ValidationIssue(
+ message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
+ row_idx=row.idx,
+ )
+ )
+
+ # Check uniqueness
+ if ref_code in used_codes:
+ result.add_error(
+ ValidationIssue(
+ message=f"Duplicate line reference: '{ref_code}'",
+ row_idx=row.idx,
+ )
+ )
+ used_codes.add(ref_code)
+
+ return result
+
+ def _validate_required_fields(self, template) -> ValidationResult:
+ result = ValidationResult()
+
+ for row in template.rows:
+ # Balance type required
+ if row.data_source == "Account Data" and not row.balance_type:
+ result.add_error(
+ ValidationIssue(
+ message="Balance Type is required for Account Data",
+ row_idx=row.idx,
+ )
+ )
+
+ # Calculation formula required
+ if row.data_source in ["Account Data", "Calculated Amount", "Custom API"]:
+ if not row.calculation_formula:
+ result.add_error(
+ ValidationIssue(
+ message=f"Formula is required for {row.data_source}",
+ row_idx=row.idx,
+ )
+ )
+
+ return result
+
+
+class DependencyValidator(Validator):
+ def __init__(self, template):
+ self.template = template
+ self.dependencies = self._build_dependency_graph()
+
+ def validate(self, context=None) -> ValidationResult:
+ result = ValidationResult()
+
+ result.merge(self._validate_circular_dependencies())
+ result.merge(self._validate_missing_dependencies())
+
+ return result
+
+ def _build_dependency_graph(self) -> dict[str, list[str]]:
+ graph = {}
+ available_codes = {row.reference_code for row in self.template.rows if row.reference_code}
+
+ for row in self.template.rows:
+ if row.reference_code and row.data_source == "Calculated Amount" and row.calculation_formula:
+ deps = extract_reference_codes_from_formula(row.calculation_formula, list(available_codes))
+ if deps:
+ graph[row.reference_code] = deps
+
+ return graph
+
+ def _validate_circular_dependencies(self) -> ValidationResult:
+ """
+ Efficient cycle detection using DFS (Depth-First Search) with three-color algorithm:
+ - WHITE (0): unvisited node
+ - GRAY (1): currently being processed (on recursion stack)
+ - BLACK (2): fully processed
+
+ Example cycle detection:
+ A → B → C → A (cycle detected when A is GRAY and visited again)
+ """
+ result = ValidationResult()
+ WHITE, GRAY, BLACK = 0, 1, 2
+ colors = {node: WHITE for node in self.dependencies}
+
+ def dfs(node, path):
+ if node not in colors:
+ return # External dependency
+
+ if colors[node] == GRAY:
+ # Found cycle
+ cycle_start = path.index(node)
+ cycle = [*path[cycle_start:], node]
+ result.add_error(
+ ValidationIssue(
+ message=f"Circular dependency detected: {' → '.join(cycle)}",
+ )
+ )
+ return
+
+ if colors[node] == BLACK:
+ return # Already processed
+
+ colors[node] = GRAY
+ path.append(node)
+
+ for neighbor in self.dependencies.get(node, []):
+ dfs(neighbor, path.copy())
+
+ colors[node] = BLACK
+
+ for node in self.dependencies:
+ if colors[node] == WHITE:
+ dfs(node, [])
+
+ return result
+
+ def _validate_missing_dependencies(self) -> ValidationResult:
+ available = {row.reference_code for row in self.template.rows if row.reference_code}
+ result = ValidationResult()
+
+ for ref_code, deps in self.dependencies.items():
+ undefined = [d for d in deps if d not in available]
+ if undefined:
+ row_idx = self._get_row_idx(ref_code)
+ result.add_error(
+ ValidationIssue(
+ message=f"Line References undefined in Formula: {', '.join(undefined)}",
+ row_idx=row_idx,
+ )
+ )
+
+ return result
+
+ def _get_row_idx(self, reference_code: str) -> int | None:
+ for row in self.template.rows:
+ if row.reference_code == reference_code:
+ return row.idx
+ return None
+
+
+class CalculationFormulaValidator(Validator):
+ """Validates calculation formulas used in Calculated Amount rows"""
+
+ def __init__(self, reference_codes: set[str]):
+ self.reference_codes = reference_codes
+
+ def validate(self, row) -> ValidationResult:
+ """Validate calculation formula for a single row"""
+ result = ValidationResult()
+
+ if row.data_source != "Calculated Amount":
+ return result
+
+ if not row.calculation_formula:
+ result.add_error(
+ ValidationIssue(
+ message="Formula is required for Calculated Amount",
+ row_idx=row.idx,
+ field="Formula",
+ )
+ )
+ return result
+
+ formula = self._preprocess_formula(row.calculation_formula)
+ row.calculation_formula = formula
+
+ # Check parentheses
+ if not self._are_parentheses_balanced(formula):
+ result.add_error(
+ ValidationIssue(
+ message="Formula has unbalanced parentheses",
+ row_idx=row.idx,
+ )
+ )
+ return result
+
+ # Check self-reference
+ available_codes = list(self.reference_codes)
+ refs = extract_reference_codes_from_formula(formula, available_codes)
+ if row.reference_code and row.reference_code in refs:
+ result.add_error(
+ ValidationIssue(
+ message=f"Formula references itself ('{row.reference_code}')",
+ row_idx=row.idx,
+ )
+ )
+
+ # Check undefined references
+ undefined = set(refs) - set(available_codes)
+ if undefined:
+ result.add_error(
+ ValidationIssue(
+ message=f"Formula references undefined codes: {', '.join(undefined)}",
+ row_idx=row.idx,
+ )
+ )
+
+ # Try to evaluate with dummy values
+ eval_error = self._test_formula_evaluation(formula, available_codes)
+ if eval_error:
+ result.add_error(
+ ValidationIssue(
+ message=f"Formula evaluation error: {eval_error}",
+ row_idx=row.idx,
+ )
+ )
+
+ return result
+
+ def _preprocess_formula(self, formula: str) -> str:
+ if not formula or not isinstance(formula, str):
+ return ""
+
+ return formula.strip()
+
+ @staticmethod
+ def _are_parentheses_balanced(formula: str) -> bool:
+ return formula.count("(") == formula.count(")")
+
+ def _test_formula_evaluation(self, formula: str, available_codes: list[str]) -> str | None:
+ try:
+ context = {code: 1.0 for code in available_codes}
+ context.update(
+ {
+ "abs": abs,
+ "round": round,
+ "min": min,
+ "max": max,
+ "sum": sum,
+ "sqrt": lambda x: x**0.5,
+ "pow": pow,
+ "ceil": lambda x: int(x) + (1 if x % 1 else 0),
+ "floor": lambda x: int(x),
+ }
+ )
+
+ result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
+
+ if not isinstance(result, (int, float)): # noqa: UP038
+ return f"Formula must return a numeric value, got {type(result).__name__}"
+
+ return None
+ except Exception as e:
+ return str(e)
+
+
+class AccountFilterValidator(Validator):
+ """Validates account filter expressions used in Account Data rows"""
+
+ def __init__(self, account_fields: set | None = None):
+ self.account_fields = account_fields or set(frappe.get_meta("Account")._valid_columns)
+
+ def validate(self, row) -> ValidationResult:
+ result = ValidationResult()
+
+ if row.data_source != "Account Data":
+ return result
+
+ if not row.calculation_formula:
+ result.add_error(
+ ValidationIssue(
+ message="Account filter is required for Account Data",
+ row_idx=row.idx,
+ field="Formula",
+ )
+ )
+ return result
+
+ try:
+ filter_config = json.loads(row.calculation_formula)
+ error = self._validate_filter_structure(filter_config, self.account_fields)
+
+ if error:
+ result.add_error(
+ ValidationIssue(
+ message=error,
+ row_idx=row.idx,
+ field="Account Filter",
+ )
+ )
+
+ except json.JSONDecodeError as e:
+ result.add_error(
+ ValidationIssue(
+ message=f"Invalid JSON format: {e!s}",
+ row_idx=row.idx,
+ field="Account Filter",
+ )
+ )
+
+ return result
+
+ def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None:
+ # simple condition: [field, operator, value]
+ if isinstance(filter_config, list):
+ if len(filter_config) != 3:
+ return "Filter must be [field, operator, value]"
+
+ field, operator, value = filter_config
+
+ if not isinstance(field, str) or not isinstance(operator, str):
+ return "Field and operator must be strings"
+
+ if field not in account_fields:
+ return f"Field '{field}' is not a valid account field"
+
+ if operator.casefold() not in OPERATOR_MAP:
+ return f"Invalid operator '{operator}'"
+
+ if operator in ["in", "not in"] and not isinstance(value, list):
+ return f"Operator '{operator}' requires a list value"
+
+ # logical condition: {"and": [condition1, condition2]}
+ elif isinstance(filter_config, dict):
+ if len(filter_config) != 1:
+ return "Logical condition must have exactly one operator"
+
+ op = next(iter(filter_config.keys())).lower()
+ if op not in ["and", "or"]:
+ return "Logical operators must be 'and' or 'or'"
+
+ conditions = filter_config[next(iter(filter_config.keys()))]
+ if not isinstance(conditions, list) or len(conditions) < 1:
+ return "Logical conditions need at least 1 sub-condition"
+
+ # recursive
+ for condition in conditions:
+ error = self._validate_filter_structure(condition, account_fields)
+ if error:
+ return error
+ else:
+ return "Filter must be a list or dict"
+
+ return None
+
+
+class FormulaValidator(Validator):
+ def __init__(self, template):
+ self.template = template
+ reference_codes = {row.reference_code for row in template.rows if row.reference_code}
+ self.calculation_validator = CalculationFormulaValidator(reference_codes)
+ self.account_filter_validator = AccountFilterValidator()
+
+ def validate(self, row, account_fields: set) -> ValidationResult:
+ result = ValidationResult()
+
+ if not row.calculation_formula:
+ return result
+
+ if row.data_source == "Calculated Amount":
+ return self.calculation_validator.validate(row)
+
+ elif row.data_source == "Account Data":
+ # Update account fields if provided
+ if account_fields:
+ self.account_filter_validator.account_fields = account_fields
+ return self.account_filter_validator.validate(row)
+
+ elif row.data_source == "Custom API":
+ result.merge(self._validate_custom_api(row))
+
+ return result
+
+ def _validate_custom_api(self, row) -> ValidationResult:
+ result = ValidationResult()
+ api_path = row.calculation_formula
+
+ if "." not in api_path:
+ result.add_error(
+ ValidationIssue(
+ message="Custom API path should be in format: app.module.method",
+ row_idx=row.idx,
+ field="Formula",
+ )
+ )
+ return result
+
+ # Method exists?
+ try:
+ module_path, method_name = api_path.rsplit(".", 1)
+ module = frappe.get_module(module_path)
+
+ if not hasattr(module, method_name):
+ result.add_error(
+ ValidationIssue(
+ message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
+ row_idx=row.idx,
+ field="Formula",
+ )
+ )
+ except Exception as e:
+ result.add_error(
+ ValidationIssue(
+ message=f"Could not validate API path: {e!s}",
+ row_idx=row.idx,
+ field="Formula",
+ )
+ )
+
+ return result
+
+
+def extract_reference_codes_from_formula(formula: str, available_codes: list[str]) -> list[str]:
+ found_codes = []
+ for code in available_codes:
+ # Match complete words only to avoid partial matches
+ pattern = r"\b" + re.escape(code) + r"\b"
+ if re.search(pattern, formula):
+ found_codes.append(code)
+ return found_codes
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
new file mode 100644
index 00000000000..1a3ffc7ab61
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py
@@ -0,0 +1,1674 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.utils import flt
+
+from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
+ AccountData,
+ DataCollector,
+ DependencyResolver,
+ FilterExpressionParser,
+ FinancialQueryBuilder,
+ FormulaCalculator,
+ PeriodValue,
+)
+from erpnext.accounts.doctype.financial_report_template.test_financial_report_template import (
+ FinancialReportTemplateTestCase,
+)
+from erpnext.accounts.utils import get_currency_precision
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record dependencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class TestDependencyResolver(FinancialReportTemplateTestCase):
+ """Test cases for DependencyResolver class"""
+
+ # 1. BASIC FUNCTIONALITY
+ def test_resolve_basic_processing_order(self):
+ resolver = DependencyResolver(self.test_template)
+ order = resolver.get_processing_order()
+
+ # Should process account rows before formula rows
+ account_indices = [i for i, row in enumerate(order) if row.data_source == "Account Data"]
+ formula_indices = [i for i, row in enumerate(order) if row.data_source == "Calculated Amount"]
+
+ self.assertTrue(all(ai < fi for ai in account_indices for fi in formula_indices))
+
+ def test_resolve_simple_dependency(self):
+ # Create test rows with dependencies
+ test_rows = [
+ {
+ "reference_code": "A001",
+ "display_name": "Base Account",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "B001",
+ "display_name": "Calculated Row",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "A001 * 2",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # Check dependencies were correctly identified
+ self.assertIn("B001", resolver.dependencies)
+ self.assertEqual(resolver.dependencies["B001"], ["A001"])
+
+ # Check processing order
+ order = resolver.get_processing_order()
+ a001_index = next(i for i, row in enumerate(order) if row.reference_code == "A001")
+ b001_index = next(i for i, row in enumerate(order) if row.reference_code == "B001")
+
+ self.assertLess(a001_index, b001_index, "A001 should be processed before B001")
+
+ # 2. DEPENDENCY PATTERNS
+ def test_resolve_multiple_dependencies(self):
+ test_rows = [
+ {
+ "reference_code": "INC001",
+ "display_name": "Income",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["root_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "EXP001",
+ "display_name": "Expenses",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["root_type", "=", "Expense"]',
+ },
+ {
+ "reference_code": "GROSS001",
+ "display_name": "Gross Profit",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "INC001 - EXP001",
+ },
+ {
+ "reference_code": "MARGIN001",
+ "display_name": "Profit Margin",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "GROSS001 / INC001 * 100",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # Check dependencies
+ self.assertEqual(set(resolver.dependencies["GROSS001"]), {"INC001", "EXP001"})
+ self.assertEqual(set(resolver.dependencies["MARGIN001"]), {"GROSS001", "INC001"})
+
+ # Check processing order
+ order = resolver.get_processing_order()
+ positions = {row.reference_code: i for i, row in enumerate(order) if row.reference_code}
+
+ # Account rows should come before formula rows
+ self.assertLess(positions["INC001"], positions["GROSS001"])
+ self.assertLess(positions["EXP001"], positions["GROSS001"])
+
+ # GROSS001 should come before MARGIN001 (which depends on it)
+ self.assertLess(positions["GROSS001"], positions["MARGIN001"])
+
+ def test_resolve_chain_dependencies(self):
+ """Test dependency resolution with chain of dependencies (A -> B -> C -> D)"""
+ test_rows = [
+ {
+ "reference_code": "A001",
+ "display_name": "Base",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "B001",
+ "display_name": "Level 1",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "A001 + 100",
+ },
+ {
+ "reference_code": "C001",
+ "display_name": "Level 2",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "B001 * 1.2",
+ },
+ {
+ "reference_code": "D001",
+ "display_name": "Level 3",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "C001 - 50",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ order = resolver.get_processing_order()
+ positions = {row.reference_code: i for i, row in enumerate(order) if row.reference_code}
+
+ # Verify chain order
+ self.assertLess(positions["A001"], positions["B001"])
+ self.assertLess(positions["B001"], positions["C001"])
+ self.assertLess(positions["C001"], positions["D001"])
+
+ def test_resolve_diamond_dependency_pattern(self):
+ """Test Diamond Dependency Pattern - A → B, A → C, and both B,C → D"""
+ test_rows = [
+ {
+ "reference_code": "A001",
+ "display_name": "Base Data",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "B001",
+ "display_name": "Branch B",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "A001 * 0.6", # B depends on A
+ },
+ {
+ "reference_code": "C001",
+ "display_name": "Branch C",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "A001 * 0.4", # C depends on A
+ },
+ {
+ "reference_code": "D001",
+ "display_name": "Final Result",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "B001 + C001", # D depends on both B and C
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ order = resolver.get_processing_order()
+ positions = {row.reference_code: i for i, row in enumerate(order)}
+
+ # A should be processed first
+ self.assertLess(positions["A001"], positions["B001"])
+ self.assertLess(positions["A001"], positions["C001"])
+ self.assertLess(positions["A001"], positions["D001"])
+
+ # Both B and C should be processed before D
+ self.assertLess(positions["B001"], positions["D001"])
+ self.assertLess(positions["C001"], positions["D001"])
+
+ # Verify D has correct dependencies
+ self.assertEqual(set(resolver.dependencies["D001"]), {"B001", "C001"})
+
+ def test_resolve_independent_formula_row_groups(self):
+ test_rows = [
+ # Chain 1: A → B → C
+ {
+ "reference_code": "A001",
+ "display_name": "Chain 1 Base",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Asset"]',
+ },
+ {
+ "reference_code": "B001",
+ "display_name": "Chain 1 Level 2",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "A001 * 1.1",
+ },
+ {
+ "reference_code": "C001",
+ "display_name": "Chain 1 Final",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "B001 + 100",
+ },
+ # Chain 2: X → Y → Z (independent)
+ {
+ "reference_code": "X001",
+ "display_name": "Chain 2 Base",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Liability"]',
+ },
+ {
+ "reference_code": "Y001",
+ "display_name": "Chain 2 Level 2",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "X001 * 0.9",
+ },
+ {
+ "reference_code": "Z001",
+ "display_name": "Chain 2 Final",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "Y001 - 50",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ order = resolver.get_processing_order()
+ positions = {row.reference_code: i for i, row in enumerate(order)}
+
+ # Verify Chain 1 order
+ self.assertLess(positions["A001"], positions["B001"])
+ self.assertLess(positions["B001"], positions["C001"])
+
+ # Verify Chain 2 order
+ self.assertLess(positions["X001"], positions["Y001"])
+ self.assertLess(positions["Y001"], positions["Z001"])
+
+ # Verify chains are independent (no cross-dependencies)
+ chain1_codes = {"A001", "B001", "C001"}
+ chain2_codes = {"X001", "Y001", "Z001"}
+
+ for code in chain1_codes:
+ if code in resolver.dependencies:
+ deps = set(resolver.dependencies[code])
+ self.assertFalse(deps.intersection(chain2_codes), f"{code} should not depend on chain 2")
+
+ for code in chain2_codes:
+ if code in resolver.dependencies:
+ deps = set(resolver.dependencies[code])
+ self.assertFalse(deps.intersection(chain1_codes), f"{code} should not depend on chain 1")
+
+ # 3. DATA SOURCE PROCESSING
+ def test_resolve_mixed_data_sources(self):
+ test_rows = [
+ {
+ "reference_code": "CALC001",
+ "display_name": "Calculated",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "ACC001 + 100",
+ },
+ {
+ "reference_code": None,
+ "display_name": "Spacing",
+ "data_source": "Blank Line",
+ },
+ {
+ "reference_code": "ACC001",
+ "display_name": "Account",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": None,
+ "display_name": "Custom",
+ "data_source": "Custom API",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ order = resolver.get_processing_order()
+
+ # Find positions
+ positions = {}
+ for i, row in enumerate(order):
+ if row.reference_code:
+ positions[row.reference_code] = i
+ else:
+ positions[f"{row.data_source}_{i}"] = i
+
+ # Account data should come before calculated
+ self.assertLess(positions["ACC001"], positions["CALC001"])
+
+ # All rows should be present
+ self.assertEqual(len(order), 4)
+
+ def test_resolve_api_to_formula_dependencies(self):
+ test_rows = [
+ {
+ "reference_code": "API001",
+ "display_name": "Custom API Result",
+ "data_source": "Custom API",
+ },
+ {
+ "reference_code": "ACC001",
+ "display_name": "Account Data",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "CALC001",
+ "display_name": "Calculated Result",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "API001 + ACC001",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ order = resolver.get_processing_order()
+ positions = {row.reference_code: i for i, row in enumerate(order)}
+
+ # API001 should be processed before CALC001
+ self.assertLess(positions["API001"], positions["CALC001"])
+ # ACC001 should be processed before CALC001
+ self.assertLess(positions["ACC001"], positions["CALC001"])
+ # API001 should be processed before ACC001 (API rows come first)
+ self.assertLess(positions["API001"], positions["ACC001"])
+
+ def test_resolve_cross_datasource_dependencies(self):
+ test_rows = [
+ {
+ "reference_code": "API001",
+ "display_name": "API Data",
+ "data_source": "Custom API",
+ },
+ {
+ "reference_code": "ACC001",
+ "display_name": "Account Total",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "MIXED001",
+ "display_name": "Mixed Calculation",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "(API001 + ACC001) * 0.5",
+ },
+ {
+ "reference_code": "FINAL001",
+ "display_name": "Final Result",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "MIXED001 + API001",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ order = resolver.get_processing_order()
+ positions = {row.reference_code: i for i, row in enumerate(order)}
+
+ # API rows should be processed first
+ self.assertLess(positions["API001"], positions["ACC001"])
+ self.assertLess(positions["API001"], positions["MIXED001"])
+
+ # Account data should be processed before formula rows
+ self.assertLess(positions["ACC001"], positions["MIXED001"])
+
+ # Mixed calculation should be processed before final result
+ self.assertLess(positions["MIXED001"], positions["FINAL001"])
+
+ # Verify dependencies
+ self.assertEqual(set(resolver.dependencies["MIXED001"]), {"API001", "ACC001"})
+ self.assertEqual(set(resolver.dependencies["FINAL001"]), {"MIXED001", "API001"})
+
+ # 4. FORMULA PARSING
+ def test_extract_from_complex_formulas(self):
+ test_rows = [
+ {
+ "reference_code": "INCOME",
+ "display_name": "Total Income",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["root_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "EXPENSE",
+ "display_name": "Total Expense",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["root_type", "=", "Expense"]',
+ },
+ {
+ "reference_code": "TAX_RATE",
+ "display_name": "Tax Rate",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_name", "like", "Tax"]',
+ },
+ {
+ "reference_code": "NET_RESULT",
+ "display_name": "Net Result",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "(INCOME - EXPENSE) * (1 - TAX_RATE / 100)",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # Should correctly identify all three dependencies in complex formula
+ net_deps = resolver.dependencies.get("NET_RESULT", [])
+ self.assertEqual(set(net_deps), {"INCOME", "EXPENSE", "TAX_RATE"})
+
+ def test_extract_references_with_math_functions(self):
+ test_rows = [
+ {
+ "reference_code": "INCOME",
+ "display_name": "Total Income",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["root_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "EXPENSE",
+ "display_name": "Total Expense",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["root_type", "=", "Expense"]',
+ },
+ {
+ "reference_code": "TAX",
+ "display_name": "Tax Amount",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_name", "like", "Tax"]',
+ },
+ {
+ "reference_code": "MATH_TEST1",
+ "display_name": "Mathematical Test 1",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "max(INCOME, EXPENSE) + min(TAX, 0)",
+ },
+ {
+ "reference_code": "MATH_TEST2",
+ "display_name": "Mathematical Test 2",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "abs(INCOME - EXPENSE) + round(TAX, 2)",
+ },
+ {
+ "reference_code": "MATH_TEST3",
+ "display_name": "Mathematical Test 3",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "sqrt(pow(INCOME, 2) + pow(EXPENSE, 2))",
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # MATH_TEST1 should correctly identify dependencies despite max/min functions
+ self.assertEqual(set(resolver.dependencies["MATH_TEST1"]), {"INCOME", "EXPENSE", "TAX"})
+
+ # MATH_TEST2 should correctly identify dependencies despite abs/round functions
+ self.assertEqual(set(resolver.dependencies["MATH_TEST2"]), {"INCOME", "EXPENSE", "TAX"})
+
+ # MATH_TEST3 should correctly identify dependencies despite sqrt/pow functions
+ self.assertEqual(set(resolver.dependencies["MATH_TEST3"]), {"INCOME", "EXPENSE"})
+
+ def test_extract_accurate_reference_matching(self):
+ test_rows = [
+ {
+ "reference_code": "INC001",
+ "display_name": "Income Base",
+ "data_source": "Account Data",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ "balance_type": "Closing Balance",
+ },
+ {
+ "reference_code": "INC002",
+ "display_name": "Income Secondary",
+ "data_source": "Account Data",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ "balance_type": "Closing Balance",
+ },
+ {
+ "reference_code": "INC001_2023", # Should not match INC001
+ "display_name": "Income 2023",
+ "data_source": "Account Data",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ "balance_type": "Closing Balance",
+ },
+ {
+ "reference_code": "TEST1",
+ "display_name": "Test Formula 1",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "2 * INC001", # Should correctly extract INC001
+ },
+ {
+ "reference_code": "TEST2",
+ "display_name": "Test Formula 2",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "INC001 + INC002", # Word boundaries require separation
+ },
+ {
+ "reference_code": "TEST3",
+ "display_name": "Test Formula 3",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "INC001_2023 + INC001", # Should match both correctly
+ },
+ {
+ "reference_code": "TEST4",
+ "display_name": "Test Formula 4",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "INC001_2023*INC001", # No space separation but different tokens
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # TEST1 should only depend on INC001
+ self.assertEqual(resolver.dependencies["TEST1"], ["INC001"])
+
+ # TEST2 should match both INC001 and INC002 (separated by space and +)
+ self.assertEqual(set(resolver.dependencies["TEST2"]), {"INC001", "INC002"})
+
+ # TEST3 should depend on both INC001_2023 and INC001
+ self.assertEqual(set(resolver.dependencies["TEST3"]), {"INC001_2023", "INC001"})
+
+ # TEST4 should depend on both INC001_2023 and INC001 (separated by *)
+ self.assertEqual(set(resolver.dependencies["TEST4"]), {"INC001_2023", "INC001"})
+
+ def test_prevent_partial_reference_matches(self):
+ test_rows = [
+ {
+ "reference_code": "INC001",
+ "display_name": "Income",
+ "data_source": "Account Data",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ "balance_type": "Closing Balance",
+ },
+ {
+ "reference_code": "INC001_ADJ", # Contains INC001 but shouldn't match
+ "display_name": "Income Adjustment",
+ "data_source": "Account Data",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ "balance_type": "Closing Balance",
+ },
+ {
+ "reference_code": "RESULT",
+ "display_name": "Result",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "INC001 + 500", # Should only match INC001, not INC001_ADJ
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # RESULT should only depend on INC001, not INC001_ADJ
+ self.assertEqual(resolver.dependencies["RESULT"], ["INC001"])
+
+ # Processing order should work correctly
+ order = resolver.get_processing_order()
+ positions = {row.reference_code: i for i, row in enumerate(order)}
+
+ self.assertLess(positions["INC001"], positions["RESULT"])
+ # INC001_ADJ can be processed in any order relative to RESULT since there's no dependency
+ self.assertIn("INC001_ADJ", positions)
+
+ # 5. EDGE CASES
+ def test_resolve_rows_without_dependencies(self):
+ test_rows = [
+ {
+ "reference_code": "A001",
+ "display_name": "Account Row",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "B001",
+ "display_name": "Static Value",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "1000 + 500", # No reference codes
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # B001 should have no dependencies
+ self.assertEqual(resolver.dependencies.get("B001", []), [])
+
+ # Should still process correctly
+ order = resolver.get_processing_order()
+ self.assertEqual(len(order), 2)
+
+ def test_handle_empty_reference_codes(self):
+ test_rows = [
+ {
+ "reference_code": "VALID001",
+ "display_name": "Valid Row",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "", # Empty string
+ "display_name": "Empty Reference",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Asset"]',
+ },
+ {
+ "reference_code": " ", # Whitespace only
+ "display_name": "Whitespace Reference",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Liability"]',
+ },
+ {
+ "reference_code": None, # None value
+ "display_name": "None Reference",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Expense"]',
+ },
+ {
+ "reference_code": "CALC001",
+ "display_name": "Calculated Row",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "VALID001 * 2", # Should only depend on VALID001
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+
+ # Should not break dependency resolution
+ order = resolver.get_processing_order()
+ self.assertEqual(len(order), 5) # All rows should be present
+
+ # CALC001 should only depend on VALID001
+ self.assertEqual(resolver.dependencies.get("CALC001", []), ["VALID001"])
+
+ # Verify processing order
+ positions = {
+ row.reference_code: i
+ for i, row in enumerate(order)
+ if row.reference_code and row.reference_code.strip()
+ }
+ self.assertLess(positions["VALID001"], positions["CALC001"])
+
+ def test_resolve_include_orphaned_nodes(self):
+ test_rows = [
+ {
+ "reference_code": "USED001",
+ "display_name": "Used Row",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "ORPHAN001",
+ "display_name": "Orphaned Row 1",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Asset"]',
+ },
+ {
+ "reference_code": "ORPHAN002",
+ "display_name": "Orphaned Row 2",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Liability"]',
+ },
+ {
+ "reference_code": "DEPENDENT",
+ "display_name": "Dependent Row",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "USED001 * 2", # Only uses USED001
+ },
+ ]
+
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ order = resolver.get_processing_order()
+
+ # All rows should be included in processing order
+ self.assertEqual(len(order), 4)
+
+ positions = {row.reference_code: i for i, row in enumerate(order) if row.reference_code}
+
+ # USED001 should be processed before DEPENDENT
+ self.assertLess(positions["USED001"], positions["DEPENDENT"])
+
+ # Orphaned rows should be included but have no dependencies
+ self.assertIn("ORPHAN001", positions)
+ self.assertIn("ORPHAN002", positions)
+
+ # Orphaned rows should have no dependencies recorded
+ self.assertEqual(resolver.dependencies.get("ORPHAN001", []), [])
+ self.assertEqual(resolver.dependencies.get("ORPHAN002", []), [])
+
+ def test_handle_valid_missing_references(self):
+ test_rows = [
+ {
+ "reference_code": "A001",
+ "display_name": "Row A",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Asset"]',
+ },
+ {
+ "reference_code": "B001",
+ "display_name": "Row B",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "A001 * 2", # Valid reference
+ },
+ ]
+
+ # This should work without errors
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ resolver = DependencyResolver(test_template)
+ # Basic test - ensure it doesn't crash
+ processing_order = resolver.get_processing_order()
+ self.assertEqual(len(processing_order), 2)
+
+ # 6. ERROR DETECTION
+ def test_detect_circular_dependency(self):
+ """Test detection of circular dependency (A -> B -> C -> A)"""
+ test_rows = [
+ {
+ "reference_code": "A001",
+ "display_name": "Row A",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "C001 + 100", # A depends on C
+ },
+ {
+ "reference_code": "B001",
+ "display_name": "Row B",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "A001 + 200", # B depends on A
+ },
+ {
+ "reference_code": "C001",
+ "display_name": "Row C",
+ "data_source": "Calculated Amount",
+ "calculation_formula": "B001 * 1.5", # C depends on B -> creates cycle
+ },
+ ]
+
+ # Should raise ValidationError for circular dependency
+ test_template = FinancialReportTemplateTestCase.create_test_template_with_rows(test_rows)
+ with self.assertRaises(frappe.ValidationError):
+ DependencyResolver(test_template)
+
+
+class TestFormulaCalculator(FinancialReportTemplateTestCase):
+ """Test cases for FormulaCalculator class"""
+
+ def _create_mock_report_row(self, formula: str, reference_code: str = "TEST_ROW"):
+ class MockReportRow:
+ def __init__(self, formula, ref_code):
+ self.calculation_formula = formula
+ self.reference_code = ref_code
+ self.data_source = "Calculated Amount"
+ self.idx = 1
+ self.reverse_sign = 0
+
+ return MockReportRow(formula, reference_code)
+
+ # 1. FOUNDATION TESTS
+ def test_evaluate_basic_operations(self):
+ # Mock row data with different scenarios
+ row_data = {
+ "INC001": [1000.0, 1200.0, 1500.0],
+ "EXP001": [800.0, 900.0, 1100.0],
+ "TAX001": [50.0, 60.0, 75.0],
+ "ZERO_VAL": [0.0, 0.0, 0.0],
+ "NEG_VAL": [-100.0, -200.0, -150.0],
+ }
+
+ period_list = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("INC001 - EXP001"))
+ expected = [200.0, 300.0, 400.0] # [1000-800, 1200-900, 1500-1100]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("INC001 * 2"))
+ expected = [2000.0, 2400.0, 3000.0]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("INC001 / 10"))
+ expected = [100.0, 120.0, 150.0]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("(INC001 - EXP001) * 0.8"))
+ expected = [160.0, 240.0, 320.0] # [(1000-800)*0.8, (1200-900)*0.8, (1500-1100)*0.8]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("abs(NEG_VAL)"))
+ expected = [100.0, 200.0, 150.0]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("max(INC001, EXP001)"))
+ expected = [1000.0, 1200.0, 1500.0] # INC001 is always larger
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("min(INC001, EXP001)"))
+ expected = [800.0, 900.0, 1100.0] # EXP001 is always smaller
+ self.assertEqual(result, expected)
+
+ def test_handle_division_by_zero(self):
+ row_data = {
+ "NUMERATOR": [100.0, 200.0, 300.0],
+ "ZERO_VAL": [0.0, 0.0, 0.0],
+ }
+
+ period_list = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("NUMERATOR / ZERO_VAL"))
+ expected = [0.0, 0.0, 0.0]
+ self.assertEqual(result, expected)
+
+ # 2. DATA HANDLING TESTS
+ def test_handle_missing_values(self):
+ row_data = {
+ "SHORT_DATA": [100.0, 200.0], # Only 2 periods instead of 3
+ "NORMAL_DATA": [50.0, 60.0, 70.0],
+ }
+
+ period_list = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("SHORT_DATA + NORMAL_DATA"))
+
+ expected = [150.0, 260.0, 70.0] # [100+50, 200+60, 0+70]
+ self.assertEqual(result, expected)
+
+ # Empty row_data
+ empty_calculator = FormulaCalculator({}, period_list)
+ result = empty_calculator.evaluate_formula(self._create_mock_report_row("MISSING_CODE * 2"))
+ expected = [0.0, 0.0, 0.0]
+ self.assertEqual(result, expected)
+
+ # None values
+ row_data_with_none = {
+ "WITH_NONE": [100.0, None, 300.0],
+ "NORMAL": [10.0, 20.0, 30.0],
+ }
+ none_calculator = FormulaCalculator(row_data_with_none, period_list)
+ result = none_calculator.evaluate_formula(self._create_mock_report_row("WITH_NONE + NORMAL"))
+ expected = [110.0, 20.0, 330.0] # [100+10, 0+20, 300+30]
+ self.assertEqual(result, expected)
+
+ # Zero periods
+ zero_period_calculator = FormulaCalculator({"TEST": [100.0]}, [])
+ result = zero_period_calculator.evaluate_formula(self._create_mock_report_row("TEST * 2"))
+ expected = [] # No periods means no results
+ self.assertEqual(result, expected)
+
+ def test_handle_invalid_reference_codes(self):
+ """Test formula calculator handles invalid reference codes"""
+ row_data = {
+ "VALID_CODE": [100.0, 200.0, 300.0],
+ "123_INVALID": [50.0, 60.0, 70.0], # Starts with number - invalid identifier
+ "VALID-DASH": [25.0, 30.0, 35.0], # Contains dash - invalid identifier
+ }
+
+ period_list = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ # Test with valid reference code
+ result = calculator.evaluate_formula(self._create_mock_report_row("VALID_CODE * 2"))
+ expected = [200.0, 400.0, 600.0]
+ self.assertEqual(result, expected)
+
+ # Test with invalid reference code - should return 0.0 (code won't be in context)
+ result = calculator.evaluate_formula(self._create_mock_report_row("INVALID_CODE * 2"))
+ expected = [0.0, 0.0, 0.0]
+ self.assertEqual(result, expected)
+
+ # Test reference code case sensitivity
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("valid_code * 2")
+ ) # lowercase version
+ expected = [0.0, 0.0, 0.0] # Should fail since codes are case-sensitive
+ self.assertEqual(result, expected)
+
+ def test_handle_mismatched_period_data_lengths(self):
+ """Test scenarios with mismatched period data"""
+ # Test when row_data has more values than periods
+ row_data_extra = {
+ "EXTRA_DATA": [100.0, 200.0, 300.0, 400.0, 500.0], # 5 values
+ }
+ period_list_short = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ ] # Only 2 periods
+
+ calculator_extra = FormulaCalculator(row_data_extra, period_list_short)
+ result = calculator_extra.evaluate_formula(self._create_mock_report_row("EXTRA_DATA * 2"))
+ expected = [200.0, 400.0] # Only processes first 2 values
+ self.assertEqual(result, expected)
+
+ # Test when all row data arrays have different lengths
+ row_data_mixed = {
+ "SHORT": [100.0], # 1 value
+ "MEDIUM": [200.0, 300.0], # 2 values
+ "LONG": [400.0, 500.0, 600.0], # 3 values
+ }
+ period_list_three = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator_mixed = FormulaCalculator(row_data_mixed, period_list_three)
+ result = calculator_mixed.evaluate_formula(self._create_mock_report_row("SHORT + MEDIUM + LONG"))
+ # Period 0: 100 + 200 + 400 = 700
+ # Period 1: 0 + 300 + 500 = 800
+ # Period 2: 0 + 0 + 600 = 600
+ expected = [700.0, 800.0, 600.0]
+ self.assertEqual(result, expected)
+
+ # 3. COMPLEX EXPRESSIONS
+ def test_evaluate_complex_expressions(self):
+ row_data = {
+ "REVENUE": [10000.0, 12000.0, 15000.0],
+ "COST": [6000.0, 7200.0, 9000.0],
+ "TAX_RATE": [0.25, 0.25, 0.30], # 25%, 25%, 30%
+ }
+
+ period_list = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("(REVENUE - COST) * (1 - TAX_RATE)")
+ )
+ expected = [
+ (10000 - 6000) * (1 - 0.25),
+ (12000 - 7200) * (1 - 0.25),
+ (15000 - 9000) * (1 - 0.30),
+ ]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("round(REVENUE / COST, 2)"))
+ expected = [
+ round(10000 / 6000, 2),
+ round(12000 / 7200, 2),
+ round(15000 / 9000, 2),
+ ]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("REVENUE + COST * TAX_RATE - 100")
+ ) # Tests PEMDAS order
+ expected = [
+ 10000 + 6000 * 0.25 - 100,
+ 12000 + 7200 * 0.25 - 100,
+ 15000 + 9000 * 0.30 - 100,
+ ]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("((REVENUE + COST) * (TAX_RATE + 0.1)) / 2")
+ )
+ expected = [
+ ((10000 + 6000) * (0.25 + 0.1)) / 2,
+ ((12000 + 7200) * (0.25 + 0.1)) / 2,
+ ((15000 + 9000) * (0.30 + 0.1)) / 2,
+ ]
+ self.assertEqual(result, expected)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("REVENUE * 2.5 + 100"))
+ expected = [
+ 10000 * 2.5 + 100,
+ 12000 * 2.5 + 100,
+ 15000 * 2.5 + 100,
+ ]
+ self.assertEqual(result, expected)
+
+ def test_evaluate_nested_function_combinations(self):
+ row_data = {
+ "BASE": [4.0],
+ "POSITIVE": [16.0], # Use positive number for sqrt
+ "DECIMAL": [2.7],
+ }
+ period_list = [{"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"}]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ result = calculator.evaluate_formula(self._create_mock_report_row("round(sqrt(POSITIVE), 2)"))
+ expected = round((16.0**0.5), 2) # round(sqrt(16), 2) = round(4.0, 2) = 4.0
+ self.assertEqual(result[0], expected)
+
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("max(POSITIVE, min(BASE, DECIMAL))")
+ )
+ expected = max(16.0, min(4.0, 2.7)) # max(16.0, 2.7) = 16.0
+ self.assertEqual(result[0], expected)
+
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("pow(max(BASE, 2), min(DECIMAL, 3))")
+ )
+ expected = pow(max(4.0, 2), min(2.7, 3)) # pow(4.0, 2.7)
+ self.assertAlmostEqual(result[0], expected, places=2)
+
+ # 4. FINANCIAL DOMAIN
+ def test_calculate_financial_use_cases(self):
+ row_data = {
+ "REVENUE_Q1": [1000000.0],
+ "REVENUE_Q2": [1200000.0],
+ "EXPENSES": [800000.0],
+ "BUDGET_VARIANCE": [-50000.0],
+ "ACTUAL_COSTS": [123456.78],
+ "GROWTH_RATE": [1.15], # 15% growth
+ "YEARS": [5.0],
+ }
+ period_list = [{"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"}]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ # Best quarterly performance
+ result = calculator.evaluate_formula(self._create_mock_report_row("max(REVENUE_Q1, REVENUE_Q2)"))
+ self.assertEqual(result[0], 1200000.0)
+
+ # Absolute variance (remove negative sign for reporting)
+ result = calculator.evaluate_formula(self._create_mock_report_row("abs(BUDGET_VARIANCE)"))
+ self.assertEqual(result[0], 50000.0)
+
+ # Rounded reporting figures
+ result = calculator.evaluate_formula(self._create_mock_report_row("round(ACTUAL_COSTS)"))
+ self.assertEqual(result[0], 123457.0) # Rounded to nearest whole number
+
+ # Conservative estimates
+ result = calculator.evaluate_formula(self._create_mock_report_row("floor(ACTUAL_COSTS / 1000)"))
+ self.assertEqual(result[0], 123.0) # Conservative thousands
+
+ # Compound growth calculations
+ result = calculator.evaluate_formula(self._create_mock_report_row("pow(GROWTH_RATE, YEARS)"))
+ expected = flt(1.15**5, get_currency_precision())
+ self.assertEqual(result[0], expected)
+
+ # Profit calculation with rounding
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("round((REVENUE_Q1 - EXPENSES) / REVENUE_Q1 * 100)")
+ )
+ self.assertEqual(result[0], 20.0) # 20% profit margin
+
+ def test_calculate_common_financial_patterns(self):
+ """Test patterns commonly used in financial calculations"""
+ row_data = {
+ "ACTUAL": [100000.0],
+ "BUDGET": [80000.0],
+ "PREVIOUS_YEAR": [90000.0],
+ "LOWER_BOUND": [50000.0],
+ "UPPER_BOUND": [150000.0],
+ }
+ period_list = [{"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"}]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("(ACTUAL - BUDGET) / (BUDGET + 0.0001) * 100")
+ )
+ expected = (100000.0 - 80000.0) / (80000.0 + 0.0001) * 100
+ self.assertAlmostEqual(result[0], expected, places=2)
+
+ # conditional logic simulation: max(0, ACTUAL - BUDGET) (similar to IF positive)
+ result = calculator.evaluate_formula(self._create_mock_report_row("max(0, ACTUAL - BUDGET)"))
+ expected = max(0, 100000.0 - 80000.0) # 20000.0
+ self.assertEqual(result[0], expected)
+
+ # clamping patterns: min(max(ACTUAL, LOWER_BOUND), UPPER_BOUND)
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("min(max(ACTUAL, LOWER_BOUND), UPPER_BOUND)")
+ )
+ expected = min(max(100000.0, 50000.0), 150000.0) # min(100000.0, 150000.0) = 100000.0
+ self.assertEqual(result[0], expected)
+
+ # year-over-year growth calculation
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("(ACTUAL - PREVIOUS_YEAR) / PREVIOUS_YEAR * 100")
+ )
+ expected = (100000.0 - 90000.0) / 90000.0 * 100
+ self.assertAlmostEqual(result[0], expected, places=2)
+
+ # 5. EDGE CASES
+ def test_handle_error_cases(self):
+ """Test formula calculator error handling for various edge cases"""
+ row_data = {
+ "NORMAL": [100.0, 200.0, 300.0],
+ }
+
+ period_list = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ # Test invalid syntax - should return 0.0 for all periods
+ result = calculator.evaluate_formula(self._create_mock_report_row("NORMAL + +")) # Invalid syntax
+ expected = [0.0, 0.0, 0.0]
+ self.assertEqual(result, expected)
+
+ # Test undefined variable - should return 0.0 for all periods
+ result = calculator.evaluate_formula(self._create_mock_report_row("UNDEFINED_VAR * 2"))
+ expected = [0.0, 0.0, 0.0]
+ self.assertEqual(result, expected)
+
+ # Test empty formula - should return 0.0 for all periods
+ result = calculator.evaluate_formula(self._create_mock_report_row(""))
+ expected = [0.0, 0.0, 0.0]
+ self.assertEqual(result, expected)
+
+ # Test whitespace and formatting tolerance
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row(" NORMAL + 100 ")
+ ) # Extra spaces
+ expected = [200.0, 300.0, 400.0]
+ self.assertEqual(result, expected)
+
+ # Test extremely long formulas
+ long_formula = "NORMAL + " + " + ".join(["10"] * 100) # Very long formula
+ result = calculator.evaluate_formula(self._create_mock_report_row(long_formula))
+ expected = [1100.0, 1200.0, 1300.0] # 100 + (100 * 10) = 1100 added to each value
+ self.assertEqual(result, expected)
+
+ # Test Unicode characters in formula (should fail gracefully)
+ result = calculator.evaluate_formula(
+ self._create_mock_report_row("NORMAL + ∞")
+ ) # Unicode infinity symbol
+ expected = [0.0, 0.0, 0.0]
+ self.assertEqual(result, expected)
+
+ def test_evaluate_math_function_edge_cases(self):
+ """Test edge cases for mathematical functions"""
+ row_data = {
+ "ZERO": [0.0],
+ "SMALL_DECIMAL": [0.0001],
+ }
+ period_list = [{"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"}]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ # Test sqrt with zero values
+ result = calculator.evaluate_formula(self._create_mock_report_row("sqrt(ZERO)"))
+ self.assertEqual(result[0], 0.0)
+
+ # Test very small numbers precision
+ result = calculator.evaluate_formula(self._create_mock_report_row("SMALL_DECIMAL * SMALL_DECIMAL"))
+ expected = 0.0001 * 0.0001
+ # Depends on currency precision
+ self.assertTrue(result[0] == 0.0 or abs(result[0] - expected) < 1e-6)
+
+ # 6. OTHER
+ def test_prevent_security_vulnerabilities(self):
+ row_data = {"TEST_VAL": [100.0]}
+ period_list = [{"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"}]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ # Test that potentially harmful expressions are safely handled
+ # These should all return 0.0 due to safe evaluation failures
+ harmful_expressions = [
+ "__import__('os').system('ls')", # Import attempts
+ "eval('1+1')", # Nested eval attempts
+ "exec('print(1)')", # Exec attempts
+ "open('/etc/passwd')", # File operations
+ "globals()", # Global namespace access
+ "locals()", # Local namespace access
+ ]
+
+ for expr in harmful_expressions:
+ with self.subTest(expression=expr):
+ result = calculator.evaluate_formula(self._create_mock_report_row(expr))
+ self.assertEqual(result, [0.0], f"Harmful expression '{expr}' should return [0.0]")
+
+ # Only safe mathematical operations work
+ safe_expressions = [
+ "TEST_VAL + 50",
+ "abs(TEST_VAL - 200)",
+ "min(TEST_VAL, 50)",
+ "max(TEST_VAL, 150)",
+ "round(TEST_VAL / 3, 2)",
+ ]
+
+ for expr in safe_expressions:
+ with self.subTest(expression=expr):
+ result = calculator.evaluate_formula(self._create_mock_report_row(expr))
+ self.assertNotEqual(result, [0.0], f"Safe expression '{expr}' should not return [0.0]")
+ self.assertIsInstance(result[0], float, f"Safe expression '{expr}' should return a float")
+
+ def test_build_context_validation(self):
+ row_data = {
+ "TEST1": [100.0, 200.0, 300.0],
+ "TEST2": [10.0, 20.0, 30.0],
+ }
+ period_list = [
+ {"key": "2023_q1", "from_date": "2023-01-01", "to_date": "2023-03-31"},
+ {"key": "2023_q2", "from_date": "2023-04-01", "to_date": "2023-06-30"},
+ {"key": "2023_q3", "from_date": "2023-07-01", "to_date": "2023-09-30"},
+ ]
+
+ calculator = FormulaCalculator(row_data, period_list)
+
+ # Test that context for each period contains the correct values
+ context_0 = calculator._build_context(0)
+ self.assertEqual(context_0["TEST1"], 100.0)
+ self.assertEqual(context_0["TEST2"], 10.0)
+
+ context_1 = calculator._build_context(1)
+ self.assertEqual(context_1["TEST1"], 200.0)
+ self.assertEqual(context_1["TEST2"], 20.0)
+
+ context_2 = calculator._build_context(2)
+ self.assertEqual(context_2["TEST1"], 300.0)
+ self.assertEqual(context_2["TEST2"], 30.0)
+
+ # Verify all expected math functions are available in context
+ math_functions = ["abs", "round", "min", "max", "sum", "sqrt", "pow", "ceil", "floor"]
+ for func_name in math_functions:
+ self.assertIn(func_name, context_0)
+ self.assertTrue(callable(context_0[func_name]))
+
+
+class TestFilterExpressionParser(FinancialReportTemplateTestCase):
+ """Test cases for FilterExpressionParser class"""
+
+ def _create_mock_report_row(self, formula: str, reference_code: str = "TEST_ROW"):
+ class MockReportRow:
+ def __init__(self, formula, ref_code):
+ self.calculation_formula = formula
+ self.reference_code = ref_code
+ self.data_source = "Account Data"
+ self.idx = 1
+ self.reverse_sign = 0
+
+ return MockReportRow(formula, reference_code)
+
+ # 1. BASIC PARSING
+ def test_parse_simple_equality_condition(self):
+ parser = FilterExpressionParser()
+
+ # Test simple equality condition
+ simple_formula = '["account_type", "=", "Income"]'
+
+ # Test with mock table
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+ mock_row = self._create_mock_report_row(simple_formula)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNotNone(condition)
+
+ # Verify the condition contains the expected field and value
+ condition_str = str(condition)
+ self.assertIn("account_type", condition_str)
+ self.assertIn("Income", condition_str)
+
+ def test_parse_logical_and_or_conditions(self):
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Test AND condition
+ and_formula = """{"and": [["account_type", "=", "Income"], ["is_group", "=", 0]]}"""
+ mock_row_and = self._create_mock_report_row(and_formula)
+ condition = parser.build_condition(mock_row_and, account_table)
+ self.assertIsNotNone(condition)
+
+ condition_str = str(condition)
+ self.assertIn("account_type", condition_str)
+ self.assertIn("is_group", condition_str)
+ self.assertIn("AND", condition_str)
+
+ # Test OR condition
+ or_formula = """{"or": [["root_type", "=", "Asset"], ["root_type", "=", "Liability"]]}"""
+ mock_row_or = self._create_mock_report_row(or_formula)
+ condition = parser.build_condition(mock_row_or, account_table)
+ self.assertIsNotNone(condition)
+
+ condition_str = str(condition)
+ self.assertIn("root_type", condition_str)
+ self.assertIn("Asset", condition_str)
+ self.assertIn("Liability", condition_str)
+ self.assertIn("OR", condition_str)
+
+ # 2. OPERATOR SUPPORT
+ def test_parse_valid_operators(self):
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ test_cases = [
+ ('["account_name", "!=", "Cash"]', "!="),
+ ('["account_number", "like", "1000"]', "like"),
+ ('["account_type", "in", ["Income", "Expense"]]', "in"),
+ ('["account_type", "not in", ["Asset", "Liability"]]', "not in"),
+ ('["account_name", "not like", "Expense"]', "not like"),
+ ('["account_number", ">=", 1000]', ">="),
+ ('["account_number", ">", 0]', ">"),
+ ('["account_number", "<=", 5000]', "<="),
+ ('["account_number", "<", 100]', "<"),
+ ('["is_group", "=", 0]', "="),
+ ]
+
+ for formula, expected_op in test_cases:
+ mock_row = self._create_mock_report_row(formula)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNotNone(condition, f"Failed to build condition for operator {expected_op}")
+
+ def test_build_logical_condition_with_reduce(self):
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Test AND logic with multiple conditions
+ and_formula = '{"and": [["account_type", "=", "Income"], ["is_group", "=", 0], ["disabled", "=", 0]]}'
+ mock_row_and = self._create_mock_report_row(and_formula)
+ condition = parser.build_condition(mock_row_and, account_table)
+ self.assertIsNotNone(condition)
+ condition_str = str(condition)
+ self.assertEqual(condition_str.count("AND"), 2)
+
+ # Test OR logic with multiple conditions
+ or_formula = '{"or": [["root_type", "=", "Asset"], ["root_type", "=", "Liability"], ["root_type", "=", "Income"]]}'
+ mock_row_or = self._create_mock_report_row(or_formula)
+ condition = parser.build_condition(mock_row_or, account_table)
+ self.assertIsNotNone(condition)
+ condition_str = str(condition)
+ self.assertEqual(condition_str.count("OR"), 2)
+
+ def test_operator_value_compatibility(self):
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Test "in" operator with list value - should work
+ in_formula = '["account_type", "in", ["Income", "Expense"]]'
+ mock_row_in = self._create_mock_report_row(in_formula)
+ condition = parser.build_condition(mock_row_in, account_table)
+ self.assertIsNotNone(condition) # Should work with list
+
+ # Test numeric operators with proper values
+ numeric_formulas = [
+ '["tax_rate", ">", 10.0]',
+ '["tax_rate", ">=", 0]',
+ '["tax_rate", "<", 50.0]',
+ '["tax_rate", "<=", 100.0]',
+ ]
+
+ for formula in numeric_formulas:
+ mock_row = self._create_mock_report_row(formula)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNotNone(condition)
+
+ # 3. COMPLEX STRUCTURES
+ def test_parse_complex_nested_filters(self):
+ """Test complex nested filter expressions"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Complex nested condition: ((Income OR Expense) AND NOT Other) AND is_group=0
+ complex_formula = """{
+ "and": [
+ {
+ "and": [
+ {
+ "or": [
+ ["root_type", "=", "Income"],
+ ["root_type", "=", "Expense"]
+ ]
+ },
+ ["account_category", "!=", "Other Income"]
+ ]
+ },
+ ["is_group", "=", 0]
+ ]
+ }"""
+
+ mock_row_complex = self._create_mock_report_row(complex_formula)
+ condition = parser.build_condition(mock_row_complex, account_table)
+ self.assertIsNotNone(condition)
+
+ condition_str = str(condition)
+ self.assertIn("root_type", condition_str)
+ self.assertIn("account_category", condition_str)
+ self.assertIn("is_group", condition_str)
+ self.assertIn("AND", condition_str)
+ self.assertIn("OR", condition_str)
+
+ def test_parse_deeply_nested_conditions(self):
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Triple nesting: AND containing OR containing AND
+ deep_nested = """{
+ "and": [
+ {
+ "or": [
+ {
+ "and": [
+ ["account_type", "=", "Income Account"],
+ ["is_group", "=", 0]
+ ]
+ },
+ ["root_type", "=", "Asset"]
+ ]
+ },
+ ["disabled", "=", 0]
+ ]
+ }"""
+
+ mock_row_deep = self._create_mock_report_row(deep_nested)
+ condition = parser.build_condition(mock_row_deep, account_table)
+ self.assertIsNotNone(condition)
+
+ condition_str = str(condition)
+ self.assertIn("account_type", condition_str)
+ self.assertIn("root_type", condition_str)
+ self.assertIn("disabled", condition_str)
+ self.assertIn("AND", condition_str)
+ self.assertIn("OR", condition_str)
+
+ # 4. VALUE TYPES
+ def test_parse_different_value_types(self):
+ """Test different value types in conditions"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ test_cases = [
+ '["tax_rate", ">=", 10.50]', # Float
+ '["is_group", "=", 1]', # Integer
+ '["account_name", "=", ""]', # Empty string
+ '["account_type", "in", ["Income Account", "Expense Account"]]', # List value
+ ]
+
+ for formula in test_cases:
+ mock_row = self._create_mock_report_row(formula)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNotNone(condition, f"Failed to build condition for {formula}")
+
+ # 5. EDGE CASES
+ def test_parse_special_characters_in_values(self):
+ """Test special characters in filter values"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ test_cases = [
+ ('["account_name", "=", "John\'s Account"]', "apostrophe"),
+ ('["account_number", "like", "%100%"]', "wildcards"),
+ ('["account_name", "=", "Test & Development"]', "ampersand"),
+ ]
+
+ for formula, _case_type in test_cases:
+ mock_row = self._create_mock_report_row(formula)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNotNone(condition, f"Failed to build condition for {_case_type} case")
+
+ def test_parse_logical_operator_edge_cases(self):
+ """Test edge cases for logical operators"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Test empty conditions list - should return None
+ empty_and = '{"and": []}'
+ mock_row_empty = self._create_mock_report_row(empty_and)
+ condition = parser.build_condition(mock_row_empty, account_table)
+ self.assertIsNone(condition)
+
+ # Test single condition in logical operator
+ single_condition = '{"and": [["account_type", "=", "Bank"]]}'
+ mock_row_single = self._create_mock_report_row(single_condition)
+ condition = parser.build_condition(mock_row_single, account_table)
+ self.assertIsNotNone(condition)
+
+ # Test case sensitivity - should be invalid
+ wrong_case = '{"AND": [["account_type", "=", "Bank"]]}'
+ mock_row_wrong = self._create_mock_report_row(wrong_case)
+ condition = parser.build_condition(mock_row_wrong, account_table)
+ self.assertIsNone(condition) # Should return None due to invalid logical operator
+
+ def test_build_condition_accepts_document_instance(self):
+ parser = FilterExpressionParser()
+ account_table = frappe.qb.DocType("Account")
+ row_obj = frappe._dict(
+ {
+ "doctype": "Financial Report Row",
+ "reference_code": "DOCROW1",
+ "display_name": "Doc Row",
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "calculation_formula": '["account_type", "=", "Income"]',
+ }
+ )
+
+ # Unsaved child doc is sufficient for validation
+ row_doc = frappe.get_doc(row_obj)
+ cond = parser.build_condition(row_doc, account_table)
+ self.assertIsNotNone(cond)
+
+ # Also accepts plain frappe._dict object
+ cond = parser.build_condition(row_obj, account_table)
+ self.assertIsNotNone(cond)
+
+ # 6. ERROR HANDLING
+ def test_parse_invalid_filter_expressions(self):
+ """Test handling of invalid filter expressions"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Test malformed expressions - all should return None
+ invalid_expressions = [
+ '["incomplete"]', # Missing operator and value
+ '{"invalid": "structure"}', # Wrong structure
+ "not_a_list_or_dict", # Invalid format
+ '["field", "=", "value", "extra"]', # Too many elements - actually might work due to slicing
+ '["field"]', # Single element
+ '["field", "="]', # Missing value - actually gets handled as empty value
+ '{"AND": [["field", "=", "value"]]}', # Wrong case
+ '{"and": [["field", "=", "value"]], "or": [["field2", "=", "value2"]]}', # Multiple keys
+ '{"xor": [["field", "=", "value"]]}', # Invalid logical operator
+ '{"and": "not_a_list"}', # Non-list value for logical operator
+ "not even close to valid syntax", # Unparseable string
+ ]
+
+ for expr in invalid_expressions:
+ mock_row = self._create_mock_report_row(expr)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNone(condition, f"Expression {expr} should be invalid and return None")
+
+ def test_parse_malformed_logical_conditions(self):
+ """Test malformed logical conditions"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ malformed_expressions = [
+ '{"and": [["field", "=", "value"]], "or": [["field2", "=", "value2"]]}', # Multiple keys
+ '{"xor": [["field", "=", "value"]]}', # Invalid logical operator
+ '{"and": "not_a_list"}', # Non-list value for logical operator
+ ]
+
+ for expr in malformed_expressions:
+ mock_row = self._create_mock_report_row(expr)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNone(condition, f"Malformed expression {expr} should return None")
+
+ # Test mixed types in conditions - should return None due to validation failure
+ mixed_types = '{"and": [["account_type", "=", "Bank"], "string", 123]}'
+ mock_row_mixed = self._create_mock_report_row(mixed_types)
+ condition = parser.build_condition(mock_row_mixed, account_table)
+ # Should return None because invalid sub-conditions cause validation to fail
+ self.assertIsNone(condition)
+
+ def test_handle_exception_robustness(self):
+ """Test exception handling for various inputs"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ problematic_inputs = [
+ "not even close to valid syntax", # Unparseable string
+ '{"field": "value"}', # JSON-like but not proper format
+ ]
+
+ for test_input in problematic_inputs:
+ mock_row = self._create_mock_report_row(test_input)
+ condition = parser.build_condition(mock_row, account_table)
+ self.assertIsNone(condition, f"Input {test_input} should result in None")
+
+ # 7. BUILD CONDITIONS
+ def test_build_condition_field_validation(self):
+ """Test field validation behavior"""
+ parser = FilterExpressionParser()
+ from frappe.query_builder import DocType
+
+ account_table = DocType("Account")
+
+ # Test with existing field - should work
+ valid_formula = '["account_name", "=", "test"]'
+ mock_row_valid = self._create_mock_report_row(valid_formula)
+ condition = parser.build_condition(mock_row_valid, account_table)
+ self.assertIsNotNone(condition)
+
+ # Test with invalid formula - should return None
+ invalid_formula = "invalid formula"
+ mock_row_invalid = self._create_mock_report_row(invalid_formula)
+ condition = parser.build_condition(mock_row_invalid, account_table)
+ self.assertIsNone(condition)
diff --git a/erpnext/accounts/doctype/financial_report_template/test_financial_report_template.py b/erpnext/accounts/doctype/financial_report_template/test_financial_report_template.py
new file mode 100644
index 00000000000..ef5404bd478
--- /dev/null
+++ b/erpnext/accounts/doctype/financial_report_template/test_financial_report_template.py
@@ -0,0 +1,79 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.tests import IntegrationTestCase
+from frappe.tests.utils import make_test_records
+
+# On IntegrationTestCase, the doctype test records and all
+# link-field test record dependencies are recursively loaded
+# Use these module variables to add/remove to/from that list
+EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
+
+
+class TestFinancialReportTemplate(IntegrationTestCase):
+ pass
+
+
+class FinancialReportTemplateTestCase(IntegrationTestCase):
+ """Utility class with common setup and helper methods for all test classes"""
+
+ @classmethod
+ def setUpClass(cls):
+ """Set up test data"""
+ make_test_records("Company")
+ make_test_records("Fiscal Year")
+ cls.create_test_template()
+
+ @classmethod
+ def create_test_template(cls):
+ """Create a test financial report template"""
+ if not frappe.db.exists("Financial Report Template", "Test P&L Template"):
+ template = frappe.get_doc(
+ {
+ "doctype": "Financial Report Template",
+ "template_name": "Test P&L Template",
+ "report_type": "Profit and Loss Statement",
+ "rows": [
+ {
+ "reference_code": "INC001",
+ "display_name": "Income",
+ "indentation_level": 0,
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "bold_text": 1,
+ "calculation_formula": '["root_type", "=", "Income"]',
+ },
+ {
+ "reference_code": "EXP001",
+ "display_name": "Expenses",
+ "indentation_level": 0,
+ "data_source": "Account Data",
+ "balance_type": "Closing Balance",
+ "bold_text": 1,
+ "calculation_formula": '["root_type", "=", "Expense"]',
+ },
+ {
+ "reference_code": "NET001",
+ "display_name": "Net Profit/Loss",
+ "indentation_level": 0,
+ "data_source": "Calculated Amount",
+ "bold_text": 1,
+ "calculation_formula": "INC001 - EXP001",
+ },
+ ],
+ }
+ )
+ template.insert()
+
+ cls.test_template = frappe.get_doc("Financial Report Template", "Test P&L Template")
+
+ @staticmethod
+ def create_test_template_with_rows(rows_data):
+ """Helper method to create test template with specific rows"""
+ template_name = f"Test Template {frappe.generate_hash()[:8]}"
+ template = frappe.get_doc(
+ {"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
+ )
+ return template
diff --git a/erpnext/accounts/financial_report_template/__init__.py b/erpnext/accounts/financial_report_template/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/financial_report_template/account_categories.json b/erpnext/accounts/financial_report_template/account_categories.json
new file mode 100644
index 00000000000..f9af2698f10
--- /dev/null
+++ b/erpnext/accounts/financial_report_template/account_categories.json
@@ -0,0 +1,118 @@
+[
+ {
+ "account_category_name": "Cash and Cash Equivalents",
+ "description": "Cash on hand, demand deposits, and short-term highly liquid investments readily convertible to cash with original maturities of three months or less. Examples: Cash in hand, bank current accounts, money market funds, treasury bills \u22643 months."
+ },
+ {
+ "account_category_name": "Cost of Goods Sold",
+ "description": "Direct costs attributable to cost of goods sold. Examples: Raw materials, stock in trade."
+ },
+ {
+ "account_category_name": "Current Tax Liabilities",
+ "description": "Income tax obligations for current and prior periods. Examples: Provision for income tax, advance tax paid, tax deducted at source."
+ },
+ {
+ "account_category_name": "Finance Costs",
+ "description": "Interest and financing-related expenses. Examples: Interest on borrowings, bank charges, lease interest, foreign exchange losses."
+ },
+ {
+ "account_category_name": "Intangible Assets",
+ "description": "Identifiable non-monetary assets without physical substance. Examples: Software, patents, trademarks, licenses, development costs."
+ },
+ {
+ "account_category_name": "Investment Income",
+ "description": "Returns generated from financial investments and cash management. Examples: Interest income, dividend income, rental income, fair value gains."
+ },
+ {
+ "account_category_name": "Long-term Borrowings",
+ "description": "Interest-bearing debt obligations with maturity beyond one year. Examples: Term loans, bonds, debentures, mortgages."
+ },
+ {
+ "account_category_name": "Long-term Investments",
+ "description": "Investments held for strategic purposes or extended periods. Examples: Equity investments, bonds, associates, joint ventures, deposits."
+ },
+ {
+ "account_category_name": "Long-term Provisions",
+ "description": "Present obligations beyond one year with uncertain timing/amount. Examples: Asset retirement obligations, environmental remediation, legal settlements."
+ },
+ {
+ "account_category_name": "Operating Expenses",
+ "description": "Costs incurred in ordinary business operations excluding direct costs. Examples: Selling expenses, administrative costs, marketing, utilities, rent."
+ },
+ {
+ "account_category_name": "Other Current Assets",
+ "description": "Current assets not classified elsewhere including prepaid expenses and advances. Examples: Prepaid insurance, prepaid rent, advance to suppliers, security deposits recoverable within one year."
+ },
+ {
+ "account_category_name": "Other Current Liabilities",
+ "description": "Short-term obligations not classified elsewhere. Examples: Accrued expenses, statutory liabilities, employee payables."
+ },
+ {
+ "account_category_name": "Other Direct Costs",
+ "description": "Direct costs excluding cost of goods sold. Examples: Direct labor, manufacturing overhead, freight inward."
+ },
+ {
+ "account_category_name": "Other Non-current Assets",
+ "description": "Long-term assets not classified elsewhere. Examples: Security deposits, long-term prepayments, advances for capital goods."
+ },
+ {
+ "account_category_name": "Other Non-current Liabilities",
+ "description": "Long-term obligations not classified elsewhere. Examples: Long-term deposits, deferred income, government grants."
+ },
+ {
+ "account_category_name": "Other Operating Income",
+ "description": "Incidental income related to business operations but not core revenue. Examples: Scrap sales, government grants, insurance claims, foreign exchange gains."
+ },
+ {
+ "account_category_name": "Other Payables",
+ "description": "Non-trade payables and obligations to parties other than suppliers. Examples: Employee payables, accrued expenses, customer advances, security deposits received."
+ },
+ {
+ "account_category_name": "Other Receivables",
+ "description": "Non-trade amounts due to the entity excluding financing arrangements. Examples: Employee advances, insurance claims, tax refunds, deposits recoverable."
+ },
+ {
+ "account_category_name": "Reserves and Surplus",
+ "description": "Accumulated profits and other reserves created from profits or share premium. Examples: General reserves, retained earnings, statutory reserves, share premium."
+ },
+ {
+ "account_category_name": "Revenue from Operations",
+ "description": "Income from primary business activities in ordinary course. Examples: Sales of goods, service revenue, commission income, royalty income."
+ },
+ {
+ "account_category_name": "Share Capital",
+ "description": "Nominal value of issued and paid-up equity shares. Examples: Common stock, ordinary shares, preference shares."
+ },
+ {
+ "account_category_name": "Short-term Borrowings",
+ "description": "Interest-bearing debt obligations due within one year. Examples: Bank overdrafts, short-term loans, current portion of long-term debt."
+ },
+ {
+ "account_category_name": "Short-term Investments",
+ "description": "Financial instruments held for short-term investment purposes, readily convertible to cash. Examples: Marketable securities, fixed deposits >3 months, mutual funds."
+ },
+ {
+ "account_category_name": "Short-term Provisions",
+ "description": "Present obligations due within one year with uncertain timing or amount. Examples: Warranty provisions, legal claims, restructuring costs."
+ },
+ {
+ "account_category_name": "Stock Assets",
+ "description": "Inventory and stock-related assets including raw materials, work in progress, finished goods, and stock in trade. Examples: Raw materials, finished goods, trading merchandise, consumables."
+ },
+ {
+ "account_category_name": "Tangible Assets",
+ "description": "Physical assets used in business operations including property, plant, and equipment. Examples: Land, buildings, machinery, equipment, vehicles, furniture, capital work in progress."
+ },
+ {
+ "account_category_name": "Tax Expense",
+ "description": "Current and deferred income tax obligations. Examples: Current tax provision, deferred tax expense, withholding taxes."
+ },
+ {
+ "account_category_name": "Trade Payables",
+ "description": "Amounts owed to suppliers. Examples: Supplier invoices, accrued purchases, bills payable."
+ },
+ {
+ "account_category_name": "Trade Receivables",
+ "description": "Amounts due from customers for goods sold or services provided in ordinary course of business. Examples: Accounts receivable, notes receivable from customers, unbilled revenue."
+ }
+]
\ No newline at end of file
diff --git a/erpnext/accounts/financial_report_template/financial_ratios_analysis/__init__.py b/erpnext/accounts/financial_report_template/financial_ratios_analysis/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/financial_report_template/financial_ratios_analysis/financial_ratios_analysis.json b/erpnext/accounts/financial_report_template/financial_ratios_analysis/financial_ratios_analysis.json
new file mode 100644
index 00000000000..f924a57e4f2
--- /dev/null
+++ b/erpnext/accounts/financial_report_template/financial_ratios_analysis/financial_ratios_analysis.json
@@ -0,0 +1,1087 @@
+{
+ "creation": "2025-10-15 04:37:23.781661",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Financial Report Template",
+ "idx": 0,
+ "modified": "2025-10-15 04:37:23.781661",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Financial Ratios Analysis",
+ "owner": "Administrator",
+ "report_type": "Custom Financial Statement",
+ "rows": [
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "FINANCIAL RATIOS ANALYSIS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "RATIO_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Cash and Cash Equivalents\", \"Trade Receivables\", \"Other Receivables\", \"Stock Assets\", \"Short-term Investments\", \"Other Current Assets\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_CURRENT_ASSETS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Cash and Cash Equivalents\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Cash and Cash Equivalents",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_CASH",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Receivables\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Trade Receivables",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_RECEIVABLES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Inventories",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_INVENTORY",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Tangible Assets\", \"Intangible Assets\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Fixed Assets",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_FIXED_ASSETS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"=\", \"Asset\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Total Assets",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_TOTAL_ASSETS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Trade Payables\", \"Other Payables\", \"Short-term Borrowings\", \"Current Tax Liabilities\", \"Short-term Provisions\", \"Other Current Liabilities\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_CURRENT_LIAB",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Payables\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Trade Payables",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_PAYABLES",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Short-term Borrowings\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Short-term Borrowings",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_SHORT_DEBT",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Long-term Borrowings\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Long-term Borrowings",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_LONG_DEBT",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"=\", \"Liability\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Total Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_TOTAL_LIAB",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"=\", \"Equity\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Total Equity",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BS_EQUITY",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Revenue from Operations\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Revenue",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_REVENUE",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Cost of Goods Sold\", \"Other Direct Costs\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Cost of Goods Sold",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_COGS",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_REVENUE + IS_COGS",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Profit",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_GROSS_PROFIT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Operating Expenses\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Operating Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_OPERATING_EXP",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_GROSS_PROFIT + IS_OPERATING_EXP",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "EBIT",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_EBIT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Finance Costs\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Interest Expense",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_INTEREST",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Tax Expense\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Tax Expense",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_TAX",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_EBIT + IS_INTEREST",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "EBT",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_EBT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_EBT + IS_TAX",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Income",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "IS_NET_INCOME",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "1. LIQUIDITY RATIOS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "LIQUIDITY_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "BS_CURRENT_ASSETS / BS_CURRENT_LIAB if BS_CURRENT_LIAB != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Current Ratio (Target: 1.5 - 2.0)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_CURRENT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(BS_CURRENT_ASSETS - BS_INVENTORY) / BS_CURRENT_LIAB if BS_CURRENT_LIAB != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Quick Ratio / Acid Test (Target: > 1.0)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_QUICK",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "BS_CASH / BS_CURRENT_LIAB if BS_CURRENT_LIAB != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Cash Ratio (Target: > 0.2)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_CASH",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "BS_CURRENT_ASSETS - BS_CURRENT_LIAB",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Working Capital (Target: Positive)",
+ "fieldtype": "Currency",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_NWC",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(BS_CURRENT_ASSETS - BS_CURRENT_LIAB) / BS_TOTAL_ASSETS if BS_TOTAL_ASSETS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Working Capital to Total Assets (Target: 0.1 - 0.3)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_WC_TO_ASSETS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "2. EFFICIENCY/ACTIVITY RATIOS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EFFICIENCY_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_REVENUE / BS_RECEIVABLES if BS_RECEIVABLES != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Receivables Turnover (times)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_REC_TURNOVER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(BS_RECEIVABLES / IS_REVENUE * 365) if IS_REVENUE != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Days Sales Outstanding (Target: < 45 days)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_DSO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_COGS / BS_INVENTORY if BS_INVENTORY != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Inventory Turnover (times)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_INV_TURNOVER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(BS_INVENTORY / IS_COGS * 365) if IS_COGS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Days Inventory Outstanding",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_DIO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_COGS / BS_PAYABLES if BS_PAYABLES != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Payables Turnover (times)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_PAY_TURNOVER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(BS_PAYABLES / IS_COGS * 365) if IS_COGS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Days Payables Outstanding",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_DPO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "RATIO_DSO + RATIO_DIO - RATIO_DPO",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Cash Conversion Cycle (Target: Lower)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_CCC",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_REVENUE / BS_TOTAL_ASSETS if BS_TOTAL_ASSETS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Asset Turnover (Target: > 1.0)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_ASSET_TURNOVER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_REVENUE / BS_FIXED_ASSETS if BS_FIXED_ASSETS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Fixed Asset Turnover",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_FIXED_ASSET_TURNOVER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_REVENUE / RATIO_NWC if RATIO_NWC != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Working Capital Turnover (Target: 3-6x)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_WC_TURNOVER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "3. LEVERAGE/SOLVENCY RATIOS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "LEVERAGE_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(BS_SHORT_DEBT + BS_LONG_DEBT) / BS_EQUITY if BS_EQUITY != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Debt-to-Equity (Target: < 1.0)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_DEBT_TO_EQUITY",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(BS_SHORT_DEBT + BS_LONG_DEBT) / BS_TOTAL_ASSETS if BS_TOTAL_ASSETS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Debt-to-Assets (Target: < 0.5)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_DEBT_TO_ASSETS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "BS_EQUITY / BS_TOTAL_ASSETS if BS_TOTAL_ASSETS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Equity Ratio (Target: > 0.3)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_EQUITY",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "IS_EBIT / IS_INTEREST if IS_INTEREST != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Interest Coverage / Times Interest Earned (Target: > 3.0)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_INTEREST_COVERAGE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "BS_TOTAL_ASSETS / BS_EQUITY if BS_EQUITY != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Equity Multiplier",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_EQUITY_MULT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "4. PROFITABILITY RATIOS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "PROFITABILITY_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(IS_GROSS_PROFIT / IS_REVENUE * 100) if IS_REVENUE != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Profit Margin % (Target: 20-50%)",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_GP_MARGIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(IS_EBIT / IS_REVENUE * 100) if IS_REVENUE != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Operating Profit Margin % (Target: > 10%)",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_OP_MARGIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(IS_NET_INCOME / IS_REVENUE * 100) if IS_REVENUE != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Profit Margin % (Target: > 5%)",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_NP_MARGIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(IS_NET_INCOME / BS_TOTAL_ASSETS * 100) if BS_TOTAL_ASSETS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Return on Assets (ROA) % (Target: > 5%)",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_ROA",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(IS_NET_INCOME / BS_EQUITY * 100) if BS_EQUITY != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Return on Equity (ROE) % (Target: 15-20%)",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_ROE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(IS_EBIT / (BS_TOTAL_ASSETS - BS_CURRENT_LIAB) * 100) if (BS_TOTAL_ASSETS - BS_CURRENT_LIAB) != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Return on Capital Employed (ROCE) % (Target: > 15%)",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_ROCE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(IS_EBIT / BS_TOTAL_ASSETS * 100) if BS_TOTAL_ASSETS != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Basic Earning Power % (Target: > 10%)",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "RATIO_BEP",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ }
+ ],
+ "template_name": "Financial Ratios Analysis"
+}
diff --git a/erpnext/accounts/financial_report_template/horizontal_balance_sheet_(columnar)/__init__.py b/erpnext/accounts/financial_report_template/horizontal_balance_sheet_(columnar)/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/financial_report_template/horizontal_balance_sheet_(columnar)/horizontal_balance_sheet_(columnar).json b/erpnext/accounts/financial_report_template/horizontal_balance_sheet_(columnar)/horizontal_balance_sheet_(columnar).json
new file mode 100644
index 00000000000..e27fe5bb0a7
--- /dev/null
+++ b/erpnext/accounts/financial_report_template/horizontal_balance_sheet_(columnar)/horizontal_balance_sheet_(columnar).json
@@ -0,0 +1,993 @@
+{
+ "creation": "2025-09-07 07:24:40.762641",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Financial Report Template",
+ "idx": 0,
+ "modified": "2025-10-15 03:12:19.165699",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Horizontal Balance Sheet (Columnar)",
+ "owner": "Administrator",
+ "report_type": "Balance Sheet",
+ "rows": [
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Section Break",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Column Break",
+ "display_name": "Equity & Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "Capital & Reserves",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CAPITAL_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Share Capital\"]",
+ "data_source": "Account Data",
+ "display_name": "Share Capital",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_SHARE_CAPITAL",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\":[[\"account_category\",\"=\",\"Reserves and Surplus\"],[\"account_name\",\"not like\",\"%Retained Earnings%\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Reserves & Surplus",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_RESERVES",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\":[[\"account_name\",\"like\",\"%Retained Earnings%\"],[\"account_category\",\"=\",\"Reserves and Surplus\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Retained Earnings",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_RETAINED_EARNINGS",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "L_SHARE_CAPITAL + L_RESERVES + L_RETAINED_EARNINGS",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Capital & Reserves",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "L_TOTAL_EQUITY",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "Non-Current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "NCL_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Long-term Borrowings\"], [\"account_name\", \"not like\", \"Unsecured\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Secured Loans",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_SECURED_LOANS",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Long-term Borrowings\"], [\"account_name\", \"like\", \"Unsecured\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Unsecured Loans",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_UNSECURED_LOANS",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Long-term Provisions\"]",
+ "data_source": "Account Data",
+ "display_name": "Long-term Provisions",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_LT_PROVISIONS",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Liabilities\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Non-current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_OTHER_NCL",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "L_SECURED_LOANS + L_UNSECURED_LOANS + L_LT_PROVISIONS + L_OTHER_NCL",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Non-current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "L_TOTAL_NCL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "Current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CL_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Short-term Borrowings\"]",
+ "data_source": "Account Data",
+ "display_name": "Short-term Borrowings",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_ST_BORROWINGS",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Payables\"]",
+ "data_source": "Account Data",
+ "display_name": "Trade Payables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_TRADE_PAYABLES",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Payables\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Payables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_OTHER_PAYABLES",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Current Tax Liabilities\"]",
+ "data_source": "Account Data",
+ "display_name": "Current Tax Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_TAX_LIABILITIES",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Short-term Provisions\"]",
+ "data_source": "Account Data",
+ "display_name": "Short-term Provisions",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_PROVISIONS",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Current Liabilities\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_OTHER_CL",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "L_ST_BORROWINGS + L_TRADE_PAYABLES + L_OTHER_PAYABLES + L_TAX_LIABILITIES + L_PROVISIONS + L_OTHER_CL",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "L_TOTAL_CL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "Current Year Earnings",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "PL_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 1,
+ "calculation_formula": "[\"root_type\", \"in\", [\"Income\", \"Expense\"]]",
+ "color": "#28a745",
+ "data_source": "Account Data",
+ "display_name": "Net Profit/(Loss) for the Year",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "L_CURRENT_PL",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "display_name": "Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "Non-Current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "NCA_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Tangible Assets\"], [\"account_type\", \"!=\", \"Accumulated Depreciation\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Property, Plant & Equipment",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_TANGIBLE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_type\", \"like\", \"Accumulated Depreciation\"]",
+ "data_source": "Account Data",
+ "display_name": "Less: Accumulated Depreciation",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 2,
+ "italic_text": 1,
+ "reference_code": "A_ACC_DEPRECIATION",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "A_TANGIBLE - A_ACC_DEPRECIATION",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Tangible Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_NET_TANGIBLE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Intangible Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Intangible Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_INTANGIBLE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Long-term Investments\"]",
+ "data_source": "Account Data",
+ "display_name": "Long-term Investments",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_LT_INVESTMENTS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Non-current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_OTHER_NCA",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "(A_NET_TANGIBLE if A_ACC_DEPRECIATION else A_TANGIBLE) + A_INTANGIBLE + A_LT_INVESTMENTS + A_OTHER_NCA",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Non-current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "A_TOTAL_NCA",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "Current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CA_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Inventories",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_STOCK",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Receivables\"]",
+ "data_source": "Account Data",
+ "display_name": "Trade Receivables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_TRADE_RECEIVABLES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Receivables\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Receivables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_OTHER_RECEIVABLES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Short-term Investments\"]",
+ "data_source": "Account Data",
+ "display_name": "Short-term Investments",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_ST_INVESTMENTS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Cash and Cash Equivalents\"]",
+ "data_source": "Account Data",
+ "display_name": "Cash & Bank Balances",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_CASH_BANK",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Current Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "A_OTHER_CA",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "A_STOCK + A_TRADE_RECEIVABLES + A_OTHER_RECEIVABLES + A_ST_INVESTMENTS + A_CASH_BANK + A_OTHER_CA",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "A_TOTAL_CA",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Section Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "L_TOTAL_EQUITY + L_TOTAL_NCL + L_TOTAL_CL + L_CURRENT_PL",
+ "data_source": "Calculated Amount",
+ "display_name": "TOTAL",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "L_GRAND_TOTAL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "A_TOTAL_NCA + A_TOTAL_CA",
+ "data_source": "Calculated Amount",
+ "display_name": "TOTAL",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "A_GRAND_TOTAL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Section Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "L_GRAND_TOTAL - A_GRAND_TOTAL",
+ "color": "#EC864B",
+ "data_source": "Calculated Amount",
+ "display_name": "Balance Check (should be zero)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "BALANCE_CHECK",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "A_TOTAL_NCA + A_TOTAL_CA",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Assets",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "TOTAL_ASSETS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "L_TOTAL_NCL + L_TOTAL_CL",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "TOTAL_LIABILITIES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "L_TOTAL_EQUITY + L_CURRENT_PL",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Equity",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "TOTAL_EQUITY",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "KEY FINANCIAL RATIOS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "METRICS_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(A_TOTAL_CA / L_TOTAL_CL) if L_TOTAL_CL != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Current Ratio",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "CURRENT_RATIO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "((A_TRADE_RECEIVABLES + A_ST_INVESTMENTS + A_CASH_BANK) / L_TOTAL_CL) if L_TOTAL_CL != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Quick Ratio (Acid Test)",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "QUICK_RATIO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "((L_SECURED_LOANS + L_UNSECURED_LOANS + L_ST_BORROWINGS) / TOTAL_EQUITY) if TOTAL_EQUITY != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Debt to Equity Ratio",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "DEBT_EQUITY_RATIO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "A_TOTAL_CA - L_TOTAL_CL",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Working Capital",
+ "fieldtype": "Currency",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "WORKING_CAPITAL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(TOTAL_EQUITY / TOTAL_ASSETS * 100) if TOTAL_ASSETS != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Equity Ratio %",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "EQUITY_RATIO",
+ "reverse_sign": 0
+ }
+ ],
+ "template_name": "Horizontal Balance Sheet (Columnar)"
+}
diff --git a/erpnext/accounts/financial_report_template/horizontal_profit_and_loss_(columnar)/__init__.py b/erpnext/accounts/financial_report_template/horizontal_profit_and_loss_(columnar)/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/financial_report_template/horizontal_profit_and_loss_(columnar)/horizontal_profit_and_loss_(columnar).json b/erpnext/accounts/financial_report_template/horizontal_profit_and_loss_(columnar)/horizontal_profit_and_loss_(columnar).json
new file mode 100644
index 00000000000..b1891491ced
--- /dev/null
+++ b/erpnext/accounts/financial_report_template/horizontal_profit_and_loss_(columnar)/horizontal_profit_and_loss_(columnar).json
@@ -0,0 +1,1008 @@
+{
+ "creation": "2025-09-07 07:08:46.031035",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Financial Report Template",
+ "idx": 0,
+ "modified": "2025-10-14 03:35:49.918519",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Horizontal Profit and Loss (Columnar)",
+ "owner": "Administrator",
+ "report_type": "Profit and Loss Statement",
+ "rows": [
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Section Break",
+ "display_name": "TRADING ACCOUNT",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "display_name": "Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Opening Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Opening Stock",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_OPENING_STOCK",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Cost of Goods Sold\"]",
+ "data_source": "Account Data",
+ "display_name": "Cost of Goods Sold",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_COGS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "INC_CLOSING_STOCK + EXP_COGS - EXP_OPENING_STOCK",
+ "data_source": "Calculated Amount",
+ "display_name": "Purchases",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_PURCHASES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Direct Costs\"]",
+ "data_source": "Account Data",
+ "display_name": "Direct Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_DIRECT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "EXP_OPENING_STOCK + EXP_PURCHASES + EXP_DIRECT",
+ "data_source": "Calculated Amount",
+ "display_name": "Subtotal",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "LEFT_SUBTOTAL_1",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "max(0, RIGHT_SUBTOTAL_1 - LEFT_SUBTOTAL_1)",
+ "color": "#28a745",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Profit c/d",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "GP_CD_LEFT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "display_name": "Income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\":[[\"account_category\",\"=\",\"Revenue from Operations\"],[\"account_name\",\"not like\",\"%Service%\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Sales",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "INC_SALES",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Closing Stock",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "INC_CLOSING_STOCK",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Revenue from Operations\"], [\"account_name\", \"like\", \"Service\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Service Income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "INC_SERVICE",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "INC_SALES + INC_CLOSING_STOCK + INC_SERVICE",
+ "data_source": "Calculated Amount",
+ "display_name": "Subtotal",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "RIGHT_SUBTOTAL_1",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "max(0, LEFT_SUBTOTAL_1 - RIGHT_SUBTOTAL_1)",
+ "color": "#CB2929",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Loss c/d",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "GL_CD_RIGHT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Section Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "LEFT_SUBTOTAL_1 + GP_CD_LEFT",
+ "data_source": "Calculated Amount",
+ "display_name": "Total",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "LEFT_TOTAL_1",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "RIGHT_SUBTOTAL_1 + GL_CD_RIGHT",
+ "data_source": "Calculated Amount",
+ "display_name": "Total",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "RIGHT_TOTAL_1",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "LEFT_TOTAL_1 - RIGHT_TOTAL_1",
+ "color": "#EC864B",
+ "data_source": "Calculated Amount",
+ "display_name": "Trading Balance Check (should be zero)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "TRADING_VALIDATION",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Section Break",
+ "display_name": "PROFIT & LOSS ACCOUNT",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "display_name": "P&L Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "GL_CD_RIGHT",
+ "color": "#CB2929",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Loss b/d",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "GL_BD_LEFT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Operating Expenses\"], [\"account_name\", \"like\", \"Administrative\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Administrative Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_ADMIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Operating Expenses\"], [\"account_name\", \"like\", \"Sales\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Selling & Distribution Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_SELLING",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Finance Costs\"]",
+ "data_source": "Account Data",
+ "display_name": "Finance Costs",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_FINANCE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_type\", \"like\", \"Depreciation\"]",
+ "data_source": "Account Data",
+ "display_name": "Depreciation",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_DEPRECIATION",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Operating Expenses\"], [\"account_name\", \"not like\", \"Administrative\"], [\"account_name\", \"not like\", \"Sales\"], [\"account_type\", \"not like\", \"Depreciation\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Other Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_OTHER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Tax Expense\"]",
+ "data_source": "Account Data",
+ "display_name": "Provision for Tax",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EXP_TAX",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "GL_BD_LEFT + EXP_ADMIN + EXP_SELLING + EXP_FINANCE + EXP_DEPRECIATION + EXP_OTHER + EXP_TAX",
+ "data_source": "Calculated Amount",
+ "display_name": "Subtotal",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "LEFT_SUBTOTAL_2",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "max(0, RIGHT_SUBTOTAL_2 - LEFT_SUBTOTAL_2)",
+ "color": "#28a745",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Profit",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "NET_PROFIT_LEFT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "display_name": "P&L Income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "GP_CD_LEFT",
+ "color": "#28a745",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Profit b/d",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "GP_BD_RIGHT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Operating Income\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "INC_OTHER",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Investment Income\"]",
+ "data_source": "Account Data",
+ "display_name": "Investment Income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "INC_INVESTMENT",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "GP_BD_RIGHT + INC_OTHER + INC_INVESTMENT",
+ "data_source": "Calculated Amount",
+ "display_name": "Subtotal",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "RIGHT_SUBTOTAL_2",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "max(0, LEFT_SUBTOTAL_2 - RIGHT_SUBTOTAL_2)",
+ "color": "#CB2929",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Loss",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "NET_LOSS_RIGHT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Section Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "LEFT_SUBTOTAL_2 + NET_PROFIT_LEFT",
+ "data_source": "Calculated Amount",
+ "display_name": "Total",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "LEFT_TOTAL_2",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Column Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "RIGHT_SUBTOTAL_2 + NET_LOSS_RIGHT",
+ "data_source": "Calculated Amount",
+ "display_name": "Total",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "RIGHT_TOTAL_2",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Section Break",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "LEFT_TOTAL_2 - RIGHT_TOTAL_2",
+ "color": "#EC864B",
+ "data_source": "Calculated Amount",
+ "display_name": "P&L Balance Check (should be zero)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "PL_VALIDATION",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"=\", \"Income\"]",
+ "data_source": "Account Data",
+ "display_name": "Total Income",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "ACT_TOTAL_INCOME",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"=\", \"Expense\"]",
+ "data_source": "Account Data",
+ "display_name": "Total Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "ACT_TOTAL_EXPENSES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "ACT_TOTAL_INCOME - ACT_TOTAL_EXPENSES",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Profit",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "ACT_NET_PROFIT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "NET_PROFIT_LEFT - NET_LOSS_RIGHT",
+ "data_source": "Calculated Amount",
+ "display_name": "Calculated Net Profit",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CALC_NET_PROFIT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CALC_NET_PROFIT - ACT_NET_PROFIT",
+ "color": "#CB2929",
+ "data_source": "Calculated Amount",
+ "display_name": "VARIANCE (Calculated vs Actual - should be zero)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "VARIANCE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "KEY PERFORMANCE METRICS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "METRICS_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "((GP_CD_LEFT - GL_CD_RIGHT) / INC_SALES * 100) if INC_SALES != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Profit %",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "GP_PERCENT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(CALC_NET_PROFIT / INC_SALES * 100) if INC_SALES != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Profit %",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "NP_PERCENT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "((EXP_ADMIN + EXP_SELLING + EXP_OTHER) / INC_SALES * 100) if INC_SALES != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Operating Expense Ratio %",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "EXPENSE_RATIO",
+ "reverse_sign": 0
+ }
+ ],
+ "template_name": "Horizontal Profit and Loss (Columnar)"
+}
diff --git a/erpnext/accounts/financial_report_template/standard_balance_sheet_(ifrs)/__init__.py b/erpnext/accounts/financial_report_template/standard_balance_sheet_(ifrs)/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/financial_report_template/standard_balance_sheet_(ifrs)/standard_balance_sheet_(ifrs).json b/erpnext/accounts/financial_report_template/standard_balance_sheet_(ifrs)/standard_balance_sheet_(ifrs).json
new file mode 100644
index 00000000000..17ea6570e14
--- /dev/null
+++ b/erpnext/accounts/financial_report_template/standard_balance_sheet_(ifrs)/standard_balance_sheet_(ifrs).json
@@ -0,0 +1,824 @@
+{
+ "creation": "2025-09-06 21:47:56.970556",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Financial Report Template",
+ "idx": 0,
+ "modified": "2025-10-15 03:13:38.485684",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Standard Balance Sheet (IFRS)",
+ "owner": "Administrator",
+ "report_type": "Balance Sheet",
+ "rows": [
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "ASSETS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "ASSETS_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "CURRENT ASSETS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CA_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Cash and Cash Equivalents\"]",
+ "data_source": "Account Data",
+ "display_name": "Cash and Cash Equivalents",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CA100",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Receivables\"]",
+ "data_source": "Account Data",
+ "display_name": "Trade Receivables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CA200",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Receivables\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Receivables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CA300",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Inventories",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CA400",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Short-term Investments\"]",
+ "data_source": "Account Data",
+ "display_name": "Short-term Investments",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CA500",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Current Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CA600",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CA100 + CA200 + CA300 + CA400 + CA500 + CA600",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "CA_TOTAL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "NON-CURRENT ASSETS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "NCA_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Tangible Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Property, Plant and Equipment",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "NCA100",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Intangible Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Intangible Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "NCA200",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Long-term Investments\"]",
+ "data_source": "Account Data",
+ "display_name": "Long-term Investments",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "NCA300",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Assets\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Non-current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "NCA400",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "NCA100 + NCA200 + NCA300 + NCA400",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Non-current Assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "NCA_TOTAL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CA_TOTAL + NCA_TOTAL",
+ "data_source": "Calculated Amount",
+ "display_name": "TOTAL ASSETS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "TOTAL_ASSETS",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "LIABILITIES AND EQUITY",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "LIAB_EQUITY_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "CURRENT LIABILITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CL_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Payables\"]",
+ "data_source": "Account Data",
+ "display_name": "Trade Payables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CL100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Payables\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Payables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CL200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Short-term Borrowings\"]",
+ "data_source": "Account Data",
+ "display_name": "Short-term Borrowings",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CL300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Current Tax Liabilities\"]",
+ "data_source": "Account Data",
+ "display_name": "Current Tax Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CL400",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Short-term Provisions\"]",
+ "data_source": "Account Data",
+ "display_name": "Short-term Provisions",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CL500",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Current Liabilities\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CL600",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CL100 + CL200 + CL300 + CL400 + CL500 + CL600",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "CL_TOTAL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "NON-CURRENT LIABILITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "NCL_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Long-term Borrowings\"]",
+ "data_source": "Account Data",
+ "display_name": "Long-term Borrowings",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "NCL100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Long-term Provisions\"]",
+ "data_source": "Account Data",
+ "display_name": "Long-term Provisions",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "NCL200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Non-current Liabilities\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Non-current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "NCL300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "NCL100 + NCL200 + NCL300",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Non-current Liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "NCL_TOTAL",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CL_TOTAL + NCL_TOTAL",
+ "data_source": "Calculated Amount",
+ "display_name": "TOTAL LIABILITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "TOTAL_LIABILITIES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "data_source": "Blank Line",
+ "display_name": "EQUITY",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EQ_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Share Capital\"]",
+ "data_source": "Account Data",
+ "display_name": "Share Capital",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "EQ100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Reserves and Surplus\"]",
+ "data_source": "Account Data",
+ "display_name": "Reserves and Surplus",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "EQ200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"in\", [\"Income\", \"Expense\"]]",
+ "data_source": "Account Data",
+ "display_name": "Provisional Profit and Loss",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "EQ300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "EQ100 + EQ200 + EQ300",
+ "data_source": "Calculated Amount",
+ "display_name": "Total Equity",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "TOTAL_EQUITY",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "TOTAL_EQUITY",
+ "data_source": "Calculated Amount",
+ "display_name": "TOTAL EQUITY",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "TOTAL_LIABILITIES + TOTAL_EQUITY",
+ "data_source": "Calculated Amount",
+ "display_name": "TOTAL LIABILITIES AND EQUITY",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "TOTAL_LIAB_EQUITY",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "TOTAL_ASSETS - TOTAL_LIAB_EQUITY",
+ "color": "#CB2929",
+ "data_source": "Calculated Amount",
+ "display_name": "Balance Check (should be zero)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "BALANCE_CHECK",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "CA_TOTAL / CL_TOTAL if CL_TOTAL != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Current Ratio",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "CURRENT_RATIO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(CL300 + NCL100) / TOTAL_EQUITY if TOTAL_EQUITY != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Debt to Equity Ratio",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "DEBT_EQUITY_RATIO",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "CA_TOTAL - CL_TOTAL",
+ "data_source": "Calculated Amount",
+ "display_name": "Working Capital",
+ "fieldtype": "Currency",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "WORKING_CAPITAL",
+ "reverse_sign": 0
+ }
+ ],
+ "template_name": "Standard Balance Sheet (IFRS)"
+}
diff --git a/erpnext/accounts/financial_report_template/standard_cash_flow_statement_(ifrs)/__init__.py b/erpnext/accounts/financial_report_template/standard_cash_flow_statement_(ifrs)/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/financial_report_template/standard_cash_flow_statement_(ifrs)/standard_cash_flow_statement_(ifrs).json b/erpnext/accounts/financial_report_template/standard_cash_flow_statement_(ifrs)/standard_cash_flow_statement_(ifrs).json
new file mode 100644
index 00000000000..38c25841443
--- /dev/null
+++ b/erpnext/accounts/financial_report_template/standard_cash_flow_statement_(ifrs)/standard_cash_flow_statement_(ifrs).json
@@ -0,0 +1,832 @@
+{
+ "creation": "2025-09-07 22:45:05.754628",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Financial Report Template",
+ "idx": 0,
+ "modified": "2025-10-27 08:25:12.870928",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Standard Cash Flow Statement (IFRS)",
+ "owner": "Administrator",
+ "report_type": "Cash Flow",
+ "rows": [
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "CASH FLOWS FROM OPERATING ACTIVITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_OP_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"root_type\", \"in\", [\"Income\", \"Expense\"]], [\"account_category\", \"!=\", \"Tax Expense\"]]}",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Profit before tax",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_OP100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "Adjustments for:",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "CF_ADJ_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_type\", \"=\", \"Depreciation\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Depreciation and amortization",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_ADJ100",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Finance Costs\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Finance costs",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_ADJ200",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Investment Income\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Investment income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_ADJ300",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CF_OP100 + CF_ADJ100 + CF_ADJ200 + CF_ADJ300",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Operating profit before working capital changes",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "CF_OP_BEFORE_WC",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "Working capital changes:",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "CF_WC_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Receivables\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "(Increase)/decrease in trade receivables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_WC100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Stock Assets\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "(Increase)/decrease in inventories",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_WC200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Other Receivables\", \"Other Current Assets\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "(Increase)/decrease in other current assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_WC300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Trade Payables\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Increase/(decrease) in trade payables",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_WC400",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Other Payables\", \"Current Tax Liabilities\", \"Short-term Borrowings\", \"Short-term Provisions\", \"Other Current Liabilities\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Increase/(decrease) in other current liabilities",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 0,
+ "reference_code": "CF_WC500",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CF_OP_BEFORE_WC + CF_WC100 + CF_WC200 + CF_WC300 + CF_WC400 + CF_WC500",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Cash generated from operations",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_CASH_GEN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Finance Costs\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Interest paid",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_OP200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Tax Expense\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Income taxes paid",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_OP300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CF_CASH_GEN + CF_OP200 + CF_OP300",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "NET CASH FROM OPERATING ACTIVITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_OP_NET",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "CASH FLOWS FROM INVESTING ACTIVITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Tangible Assets\"], [\"account_type\", \"!=\", \"Accumulated Depreciation\"]]}",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Purchase / Sale of property, plant and equipment",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_type\", \"=\", \"Accumulated Depreciation\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Accumulated Depreciation",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV101",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "max(0, CF_ADJ100 - CF_INV101)",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Depreciation and amortization",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 1,
+ "italic_text": 1,
+ "reference_code": "CF_INV102",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Intangible Assets\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Purchase of intangible assets",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Long-term Investments\", \"Short-term Investments\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Purchase of investments",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Investment Income\"], [\"account_name\", \"not like\", \"dividend\"]]}",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Interest received",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV400",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Investment Income\"], [\"account_name\", \"like\", \"dividend\"]]}",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Dividends received",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV500",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CF_INV100 + CF_INV102 + CF_INV200 + CF_INV300 + CF_INV400 + CF_INV500",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "NET CASH FROM INVESTING ACTIVITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_INV_NET",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "CASH FLOWS FROM FINANCING ACTIVITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_FIN_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"in\", [\"Share Capital\", \"Reserves and Surplus\"]], [\"account_name\", \"not like\", \"Dividends Paid\"]]}",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Proceeds from issue of share capital",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_FIN100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"in\", [\"Long-term Borrowings\", \"Short-term Borrowings\"]]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Proceeds from / Repayment of borrowings",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_FIN200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"in\", [\"Share Capital\", \"Reserves and Surplus\"]], [\"account_name\", \"like\", \"Dividends Paid\"]]}",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Dividends paid",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_FIN300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CF_FIN100 + CF_FIN200 + CF_FIN300",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "NET CASH FROM FINANCING ACTIVITIES",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_FIN_NET",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CF_OP_NET + CF_INV_NET + CF_FIN_NET",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "NET INCREASE/(DECREASE) IN CASH AND CASH EQUIVALENTS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_NET_INCREASE",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Opening Balance",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Cash and Cash Equivalents\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Cash and cash equivalents at beginning of period",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_CASH_BEGIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_type\", \"like\", \"Exchange\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Effect of exchange rate changes",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_FX_EFFECT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Closing Balance",
+ "bold_text": 1,
+ "calculation_formula": "[\"account_category\", \"=\", \"Cash and Cash Equivalents\"]",
+ "color": "",
+ "data_source": "Account Data",
+ "display_name": "Cash and cash equivalents at end of period",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_CASH_END",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "CF_CASH_END - (CF_CASH_BEGIN + CF_NET_INCREASE + CF_FX_EFFECT)",
+ "color": "#CB2929",
+ "data_source": "Calculated Amount",
+ "display_name": "Validation Check (should be zero)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "CF_VALIDATION",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "",
+ "color": "",
+ "data_source": "Blank Line",
+ "display_name": "CASH FLOW METRICS",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "CF_METRICS_HEADER",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "CF_OP_NET + CF_INV100",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Free Cash Flow",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "CF_FREE_CASH",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(CF_OP_NET / (CF_FIN300 + CF_OP200)) if (CF_FIN300 + CF_OP200) != 0 else 0",
+ "color": "",
+ "data_source": "Calculated Amount",
+ "display_name": "Cash Flow Coverage Ratio",
+ "fieldtype": "Float",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "CF_COVERAGE",
+ "reverse_sign": 0
+ }
+ ],
+ "template_name": "Standard Cash Flow Statement (IFRS)"
+}
diff --git a/erpnext/accounts/financial_report_template/standard_profit_and_loss_(ifrs)/__init__.py b/erpnext/accounts/financial_report_template/standard_profit_and_loss_(ifrs)/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/financial_report_template/standard_profit_and_loss_(ifrs)/standard_profit_and_loss_(ifrs).json b/erpnext/accounts/financial_report_template/standard_profit_and_loss_(ifrs)/standard_profit_and_loss_(ifrs).json
new file mode 100644
index 00000000000..8bb62a8be2b
--- /dev/null
+++ b/erpnext/accounts/financial_report_template/standard_profit_and_loss_(ifrs)/standard_profit_and_loss_(ifrs).json
@@ -0,0 +1,418 @@
+{
+ "creation": "2025-09-06 10:23:05.259864",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Financial Report Template",
+ "idx": 0,
+ "modified": "2025-09-15 15:02:15.911105",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Standard Profit and Loss (IFRS)",
+ "owner": "Administrator",
+ "report_type": "Profit and Loss Statement",
+ "rows": [
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\":[[\"account_category\",\"=\",\"Revenue from Operations\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Sales Revenue",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "REV100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Cost of Goods Sold\"]",
+ "data_source": "Account Data",
+ "display_name": "Cost of Goods Sold",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "COGS100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Direct Costs\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Direct Costs",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "COGS200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "REV100 + COGS100 + COGS200",
+ "data_source": "Calculated Amount",
+ "display_name": "GROSS PROFIT (GP)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "GROSS_PROFIT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Other Operating Income\"]",
+ "data_source": "Account Data",
+ "display_name": "Other Operating Income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "OPIN100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Operating Expenses\"], [\"account_name\", \"like\", \"Sales\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Selling & Distribution Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "OPEX100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Operating Expenses\"], [\"account_name\", \"like\", \"Administrative\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Administrative Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "OPEX200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "{\"and\": [[\"account_category\", \"=\", \"Operating Expenses\"], [\"account_name\", \"not like\", \"Sales\"], [\"account_name\", \"not like\", \"Administrative\"]]}",
+ "data_source": "Account Data",
+ "display_name": "Other Operating Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "OPEX300",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "GROSS_PROFIT + OPIN100 + OPEX100 + OPEX200 + OPEX300",
+ "data_source": "Calculated Amount",
+ "display_name": "OPERATING PROFIT (EBIT)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "EBIT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Investment Income\"]",
+ "data_source": "Account Data",
+ "display_name": "Investment Income",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "FIN100",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Finance Costs\"]",
+ "data_source": "Account Data",
+ "display_name": "Finance Costs",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "FIN200",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "EBIT + FIN100 + FIN200",
+ "data_source": "Calculated Amount",
+ "display_name": "PROFIT BEFORE TAX (EBT)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "PBT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"account_category\", \"=\", \"Tax Expense\"]",
+ "data_source": "Account Data",
+ "display_name": "Tax Expense",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "TAX",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "PBT + TAX",
+ "data_source": "Calculated Amount",
+ "display_name": "NET PROFIT (NP)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "NET_PROFIT",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "NET_PROFIT - ACT_NET_PROFIT",
+ "color": "#CB2929",
+ "data_source": "Calculated Amount",
+ "display_name": "VARIANCE (Calculated vs Actual)",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 1,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "VAL_DIFF",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "data_source": "Blank Line",
+ "fieldtype": "",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(GROSS_PROFIT / REV100) * 100 if REV100 != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Gross Profit Margin %",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "GP_MARGIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(EBIT / REV100) * 100 if REV100 != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Operating Profit Margin %",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "OP_MARGIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 0,
+ "calculation_formula": "(NET_PROFIT / REV100) * 100 if REV100 != 0 else 0",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Profit Margin %",
+ "fieldtype": "Percent",
+ "hidden_calculation": 0,
+ "hide_when_empty": 0,
+ "include_in_charts": 0,
+ "indentation_level": 0,
+ "italic_text": 1,
+ "reference_code": "NP_MARGIN",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"=\", \"Income\"]",
+ "data_source": "Account Data",
+ "display_name": "Total Income",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "ACT_TOTAL_INCOME",
+ "reverse_sign": 1
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "Period Movement (Debits - Credits)",
+ "bold_text": 0,
+ "calculation_formula": "[\"root_type\", \"=\", \"Expense\"]",
+ "data_source": "Account Data",
+ "display_name": "Total Expenses",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "ACT_TOTAL_EXPENSES",
+ "reverse_sign": 0
+ },
+ {
+ "advanced_filtering": 0,
+ "balance_type": "",
+ "bold_text": 1,
+ "calculation_formula": "ACT_TOTAL_INCOME - ACT_TOTAL_EXPENSES",
+ "data_source": "Calculated Amount",
+ "display_name": "Net Profit",
+ "fieldtype": "",
+ "hidden_calculation": 1,
+ "hide_when_empty": 0,
+ "include_in_charts": 1,
+ "indentation_level": 0,
+ "italic_text": 0,
+ "reference_code": "ACT_NET_PROFIT",
+ "reverse_sign": 0
+ }
+ ],
+ "template_name": "Standard Profit and Loss (IFRS)"
+}
diff --git a/erpnext/accounts/report/account_balance/test_account_balance.py b/erpnext/accounts/report/account_balance/test_account_balance.py
index dd265ce94bc..37d6f292fb6 100644
--- a/erpnext/accounts/report/account_balance/test_account_balance.py
+++ b/erpnext/accounts/report/account_balance/test_account_balance.py
@@ -39,6 +39,16 @@ class TestAccountBalance(IntegrationTestCase):
"currency": "EUR",
"balance": 0.0,
},
+ {
+ "account": "Interest Income - _TC2",
+ "currency": "EUR",
+ "balance": 0.0,
+ },
+ {
+ "account": "Interest on Fixed Deposits - _TC2",
+ "currency": "EUR",
+ "balance": 0.0,
+ },
{
"account": "Sales - _TC2",
"currency": "EUR",
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js
index 0273ee7277e..17d31cd1683 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.js
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js
@@ -1,11 +1,28 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements);
+const BS_REPORT_NAME = "Balance Sheet";
-erpnext.utils.add_dimensions("Balance Sheet", 10);
+frappe.query_reports[BS_REPORT_NAME] = $.extend({}, erpnext.financial_statements);
-frappe.query_reports["Balance Sheet"]["filters"].push(
+erpnext.utils.add_dimensions(BS_REPORT_NAME, 10);
+
+frappe.query_reports[BS_REPORT_NAME]["filters"].push(
+ {
+ fieldname: "report_template",
+ label: __("Report Template"),
+ fieldtype: "Link",
+ options: "Financial Report Template",
+ get_query: { filters: { report_type: BS_REPORT_NAME, disabled: 0 } },
+ },
+ {
+ fieldname: "show_account_details",
+ label: __("Account Detail Level"),
+ fieldtype: "Select",
+ options: ["Summary", "Account Breakdown"],
+ default: "Summary",
+ depends_on: "eval:doc.report_template",
+ },
{
fieldname: "selected_view",
label: __("Select View"),
@@ -33,7 +50,8 @@ frappe.query_reports["Balance Sheet"]["filters"].push(
fieldname: "show_zero_values",
label: __("Show zero values"),
fieldtype: "Check",
+ depends_on: "eval:!doc.report_template",
}
);
-frappe.query_reports["Balance Sheet"]["export_hidden_cols"] = true;
+frappe.query_reports[BS_REPORT_NAME]["export_hidden_cols"] = true;
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py
index fc19c40f8f9..3606de08ca3 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.py
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py
@@ -6,6 +6,9 @@ import frappe
from frappe import _
from frappe.utils import cint, flt
+from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
+ FinancialReportEngine,
+)
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,
get_columns,
@@ -16,6 +19,9 @@ from erpnext.accounts.report.financial_statements import (
def execute(filters=None):
+ if filters and filters.report_template:
+ return FinancialReportEngine().execute(filters)
+
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js
index 6c44c07a508..bf3f636bf7c 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.js
+++ b/erpnext/accounts/report/cash_flow/cash_flow.js
@@ -1,20 +1,37 @@
// Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.query_reports["Cash Flow"] = $.extend(erpnext.financial_statements, {
+const CF_REPORT_NAME = "Cash Flow";
+
+frappe.query_reports[CF_REPORT_NAME] = $.extend(erpnext.financial_statements, {
name_field: "section",
parent_field: "parent_section",
});
-erpnext.utils.add_dimensions("Cash Flow", 10);
+erpnext.utils.add_dimensions(CF_REPORT_NAME, 10);
// The last item in the array is the definition for Presentation Currency
// filter. It won't be used in cash flow for now so we pop it. Please take
// of this if you are working here.
-frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
+frappe.query_reports[CF_REPORT_NAME]["filters"].splice(8, 1);
-frappe.query_reports["Cash Flow"]["filters"].push(
+frappe.query_reports[CF_REPORT_NAME]["filters"].push(
+ {
+ fieldname: "report_template",
+ label: __("Report Template"),
+ fieldtype: "Link",
+ options: "Financial Report Template",
+ get_query: { filters: { report_type: CF_REPORT_NAME, disabled: 0 } },
+ },
+ {
+ fieldname: "show_account_details",
+ label: __("Account Detail Level"),
+ fieldtype: "Select",
+ options: ["Summary", "Account Breakdown"],
+ default: "Summary",
+ depends_on: "eval:doc.report_template",
+ },
{
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py
index 1d53ac6b680..02a1f87a8d9 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/cash_flow.py
@@ -10,6 +10,9 @@ from frappe.query_builder import DocType
from frappe.utils import cstr, flt
from pypika import Order
+from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
+ FinancialReportEngine,
+)
from erpnext.accounts.report.financial_statements import (
get_columns,
get_cost_centers_with_children,
@@ -25,6 +28,9 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None):
+ if filters and filters.report_template:
+ return FinancialReportEngine().execute(filters)
+
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
diff --git a/erpnext/accounts/report/custom_financial_statement/__init__.py b/erpnext/accounts/report/custom_financial_statement/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.js b/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.js
new file mode 100644
index 00000000000..e5329bdd301
--- /dev/null
+++ b/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.js
@@ -0,0 +1,35 @@
+// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+const CFS_REPORT_NAME = "Custom Financial Statement";
+
+frappe.query_reports[CFS_REPORT_NAME] = $.extend({}, erpnext.financial_statements);
+
+erpnext.utils.add_dimensions(CFS_REPORT_NAME, 10);
+
+frappe.query_reports[CFS_REPORT_NAME]["filters"].push(
+ {
+ fieldname: "report_template",
+ label: __("Report Template"),
+ fieldtype: "Link",
+ options: "Financial Report Template",
+ get_query: { filters: { disabled: 0 } },
+ reqd: 1,
+ },
+ {
+ fieldname: "show_account_details",
+ label: __("Account Detail Level"),
+ fieldtype: "Select",
+ options: ["Summary", "Account Breakdown"],
+ default: "Summary",
+ depends_on: "eval:doc.report_template",
+ },
+ {
+ fieldname: "include_default_book_entries",
+ label: __("Include Default FB Entries"),
+ fieldtype: "Check",
+ default: 1,
+ }
+);
+
+frappe.query_reports[CFS_REPORT_NAME]["export_hidden_cols"] = true;
diff --git a/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.json b/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.json
new file mode 100644
index 00000000000..c51407b6a28
--- /dev/null
+++ b/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.json
@@ -0,0 +1,34 @@
+{
+ "add_total_row": 0,
+ "add_translate_data": 0,
+ "columns": [],
+ "creation": "2025-10-03 01:21:24.043064",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "letterhead": null,
+ "modified": "2025-10-03 01:21:24.043064",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Custom Financial Statement",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "Custom Financial Statement",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Accounts User"
+ },
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Auditor"
+ }
+ ],
+ "timeout": 0
+}
diff --git a/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.py b/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.py
new file mode 100644
index 00000000000..dc506071f01
--- /dev/null
+++ b/erpnext/accounts/report/custom_financial_statement/custom_financial_statement.py
@@ -0,0 +1,11 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
+ FinancialReportEngine,
+)
+
+
+def execute(filters: dict | None = None):
+ if filters and filters.report_template:
+ return FinancialReportEngine().execute(filters)
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
index 666f41394b6..e679f1f5728 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
@@ -1,11 +1,28 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financial_statements);
+const PL_REPORT_NAME = "Profit and Loss Statement";
-erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
+frappe.query_reports[PL_REPORT_NAME] = $.extend({}, erpnext.financial_statements);
-frappe.query_reports["Profit and Loss Statement"]["filters"].push(
+erpnext.utils.add_dimensions(PL_REPORT_NAME, 10);
+
+frappe.query_reports[PL_REPORT_NAME]["filters"].push(
+ {
+ fieldname: "report_template",
+ label: __("Report Template"),
+ fieldtype: "Link",
+ options: "Financial Report Template",
+ get_query: { filters: { report_type: PL_REPORT_NAME, disabled: 0 } },
+ },
+ {
+ fieldname: "show_account_details",
+ label: __("Account Detail Level"),
+ fieldtype: "Select",
+ options: ["Summary", "Account Breakdown"],
+ default: "Summary",
+ depends_on: "eval:doc.report_template",
+ },
{
fieldname: "selected_view",
label: __("Select View"),
@@ -34,7 +51,8 @@ frappe.query_reports["Profit and Loss Statement"]["filters"].push(
fieldname: "show_zero_values",
label: __("Show zero values"),
fieldtype: "Check",
+ depends_on: "eval:!doc.report_template",
}
);
-frappe.query_reports["Profit and Loss Statement"]["export_hidden_cols"] = true;
+frappe.query_reports[PL_REPORT_NAME]["export_hidden_cols"] = true;
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 ccb4d26f77b..b4280ca3067 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
@@ -6,6 +6,9 @@ import frappe
from frappe import _
from frappe.utils import flt
+from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
+ FinancialReportEngine,
+)
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,
compute_margin_view_data,
@@ -17,6 +20,9 @@ from erpnext.accounts.report.financial_statements import (
def execute(filters=None):
+ if filters and filters.report_template:
+ return FinancialReportEngine().execute(filters)
+
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 5fa0f56e8e2..a1edea61b40 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -440,6 +440,7 @@ erpnext.patches.v16_0.make_workstation_operating_components #1
erpnext.patches.v16_0.set_reporting_currency
erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes
erpnext.patches.v16_0.update_serial_no_reference_name
+erpnext.patches.v16_0.update_account_categories_for_existing_accounts
erpnext.patches.v16_0.rename_subcontracted_quantity
erpnext.patches.v16_0.add_new_stock_entry_types
erpnext.patches.v15_0.set_asset_status_if_not_already_set
diff --git a/erpnext/patches/v16_0/update_account_categories_for_existing_accounts.py b/erpnext/patches/v16_0/update_account_categories_for_existing_accounts.py
new file mode 100644
index 00000000000..0290ef44c7d
--- /dev/null
+++ b/erpnext/patches/v16_0/update_account_categories_for_existing_accounts.py
@@ -0,0 +1,69 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+
+from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import get_chart_metadata_fields
+from erpnext.accounts.doctype.account.chart_of_accounts.verified import standard_chart_of_accounts
+from erpnext.accounts.doctype.financial_report_template.financial_report_template import (
+ sync_financial_report_templates,
+)
+
+
+def execute():
+ """
+ Patch to create default account categories and update existing accounts
+ with appropriate account categories based on standard chart of accounts mapping
+ """
+ sync_financial_report_templates()
+ update_account_categories()
+
+
+def update_account_categories():
+ account_mapping = get_standard_account_category_mapping()
+ companies = frappe.get_all("Company", pluck="name")
+
+ mapped_account_categories = {}
+
+ for company in companies:
+ map_account_categories_for_company(company, account_mapping, mapped_account_categories)
+
+ if not mapped_account_categories:
+ return
+
+ frappe.db.bulk_update("Account", mapped_account_categories)
+
+
+def get_standard_account_category_mapping():
+ account_mapping = {}
+
+ def _extract_account_mapping(chart_data, prefix=""):
+ for account_name, account_details in chart_data.items():
+ if account_name in get_chart_metadata_fields():
+ continue
+
+ if isinstance(account_details, dict) and account_details.get("account_category"):
+ account_mapping[account_name] = account_details["account_category"]
+
+ if isinstance(account_details, dict):
+ _extract_account_mapping(account_details, prefix)
+
+ standard_chart = standard_chart_of_accounts.get()
+ _extract_account_mapping(standard_chart)
+
+ return account_mapping
+
+
+def map_account_categories_for_company(company, account_mapping, mapped_account_categories):
+ accounts = frappe.get_all(
+ "Account",
+ filters={"company": company, "account_category": ["is", "not set"]},
+ fields=["name", "account_name"],
+ )
+
+ for account in accounts:
+ account_category = account_mapping.get(account.account_name)
+
+ if account_category:
+ mapped_account_categories[account.name] = {"account_category": account_category}
diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js
index 2783055613a..8a3ac40d212 100644
--- a/erpnext/public/js/financial_statements.js
+++ b/erpnext/public/js/financial_statements.js
@@ -4,45 +4,160 @@ erpnext.financial_statements = {
filters: get_filters(),
baseData: null,
formatter: function (value, row, column, data, default_formatter, filter) {
- if (
- frappe.query_report.get_filter_value("selected_view") == "Growth" &&
- data &&
- column.colIndex >= 3
- ) {
- const growthPercent = data[column.fieldname];
+ // Growth/Margin
+ if (this._is_special_view(column, data))
+ return this._format_special_view(value, row, column, data, default_formatter);
- if (growthPercent == undefined) return "NA"; //making this not applicable for undefined/null values
+ if (frappe.query_report.get_filter_value("report_template"))
+ return this._format_custom_report(value, row, column, data, default_formatter, filter);
+ else return this._format_standard_report(value, row, column, data, default_formatter, filter);
+ },
+
+ _is_special_view: function (column, data) {
+ if (!data) return false;
+ const view = frappe.query_report.get_filter_value("selected_view");
+ return (view === "Growth" && column.colIndex >= 3) || (view === "Margin" && column.colIndex >= 2);
+ },
+
+ _format_custom_report: function (value, row, column, data, default_formatter, filter) {
+ const columnInfo = this._parse_column_info(column.fieldname, data);
+ const formatting = this._get_formatting_for_column(data, columnInfo);
+
+ if (columnInfo.isAccount) {
+ return this._format_custom_account_column(
+ value,
+ data,
+ formatting,
+ column,
+ default_formatter,
+ row
+ );
+ } else {
+ return this._format_custom_value_column(value, data, formatting, column, default_formatter, row);
+ }
+ },
+
+ _parse_column_info: function (fieldname, data) {
+ const valueMatch = fieldname.match(/^(?:seg_(\d+)_)?(.+)$/);
+
+ const periodKeys = data._segment_info?.period_keys || [];
+ const baseName = valueMatch ? valueMatch[2] : fieldname;
+ const isPeriodColumn = periodKeys.includes(baseName);
+
+ return {
+ isAccount: baseName === "account",
+ isPeriod: isPeriodColumn,
+ segmentIndex: valueMatch && valueMatch[1] ? parseInt(valueMatch[1]) : null,
+ fieldname: baseName,
+ };
+ },
+
+ _get_formatting_for_column: function (data, columnInfo) {
+ let formatting = {};
+
+ if (columnInfo.segmentIndex !== null && data.segment_values)
+ formatting = data.segment_values[`seg_${columnInfo.segmentIndex}`] || {};
+ else formatting = data;
+
+ return formatting;
+ },
+
+ _format_custom_account_column: function (value, data, formatting, column, default_formatter, row) {
+ if (!value) return "";
+
+ // Link to open ledger
+ const should_link_to_ledger =
+ formatting.is_detail || (formatting.account_filters && formatting.child_accounts);
+
+ if (should_link_to_ledger) {
+ const glData = {
+ account: formatting.account_name || formatting.child_accounts || value,
+ from_date: formatting.from_date || formatting.period_start_date,
+ to_date: formatting.to_date || formatting.period_end_date,
+ account_type: formatting.account_type,
+ company: frappe.query_report.get_filter_value("company"),
+ };
+
+ column.link_onclick =
+ "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(glData) + ")";
+
+ value = default_formatter(value, row, column, data);
+ }
+
+ let formattedValue = String(value);
+
+ // Prefix
+ if (formatting.is_detail || formatting.prefix)
+ formattedValue = (formatting.prefix || "• ") + formattedValue;
+
+ // Indent
+ if (data._segment_info && data._segment_info.total_segments === 1) {
+ column.is_tree = true;
+ } else if (formatting.indent && formatting.indent > 0) {
+ const indent = " ".repeat(formatting.indent * 4);
+ formattedValue = indent + formattedValue;
+ }
+
+ // Style
+ return this._style_custom_value(formattedValue, formatting, null);
+ },
+
+ _format_custom_value_column: function (value, data, formatting, column, default_formatter, row) {
+ if (formatting.is_blank_line) return "";
+
+ const col = { ...column };
+ col.fieldtype = formatting.fieldtype || col.fieldtype;
+ // Avoid formatting as currency
+ if (col.fieldtype === "Float") col.options = null;
+
+ let formattedValue = default_formatter(value, row, col, data);
+ return this._style_custom_value(formattedValue, formatting, value);
+ },
+
+ _style_custom_value(formattedValue, formatting, value) {
+ let $element = $(`${formattedValue}`);
+
+ if (formatting.bold) $element.css("font-weight", "bold");
+ if (formatting.italic) $element.css("font-style", "italic");
+ if (formatting.warn_if_negative && typeof value === "number" && value < 0)
+ $element.addClass("text-danger");
+ if (formatting.color) $element.css("color", formatting.color);
+
+ return $element.wrap("").parent().html();
+ },
+
+ _format_special_view: function (value, row, column, data, default_formatter) {
+ const selectedView = frappe.query_report.get_filter_value("selected_view");
+
+ if (selectedView === "Growth") {
+ const growthPercent = data[column.fieldname];
+ if (growthPercent === undefined) return "NA";
+ if (growthPercent === "") return "";
if (column.fieldname === "total") {
value = $(`${growthPercent}`);
} else {
value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`);
-
if (growthPercent < 0) {
value = $(value).addClass("text-danger");
} else {
value = $(value).addClass("text-success");
}
}
- value = $(value).wrap("").parent().html();
+ return $(value).wrap("").parent().html();
+ } else {
+ const marginPercent = data[column.fieldname];
+ if (marginPercent === undefined) return "NA";
- return value;
- } else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) {
- if (column.colIndex >= 2) {
- const marginPercent = data[column.fieldname];
-
- if (marginPercent == undefined) return "NA"; //making this not applicable for undefined/null values
-
- value = $(`${marginPercent + "%"}`);
- if (marginPercent < 0) value = $(value).addClass("text-danger");
- else value = $(value).addClass("text-success");
- value = $(value).wrap("").parent().html();
- return value;
- }
+ value = $(`${marginPercent + "%"}`);
+ if (marginPercent < 0) value = $(value).addClass("text-danger");
+ else value = $(value).addClass("text-success");
+ return $(value).wrap("").parent().html();
}
+ },
+ _format_standard_report: function (value, row, column, data, default_formatter, filter) {
if (data && column.fieldname == this.name_field) {
- // first column
value = data.section_name || data.account_name || value;
if (filter && filter?.text && filter?.type == "contains") {
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 7d8a9002d30..9eebf21b1d0 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -15,6 +15,9 @@ from frappe.utils import add_months, cint, formatdate, get_first_day, get_link_t
from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
+from erpnext.accounts.doctype.financial_report_template.financial_report_template import (
+ sync_financial_report_templates,
+)
from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
@@ -294,6 +297,7 @@ class Company(NestedSet):
):
if not frappe.local.flags.ignore_chart_of_accounts:
frappe.flags.country_change = True
+ sync_financial_report_templates(self.chart_of_accounts, self.existing_company)
self.create_default_accounts()
self.create_default_warehouses()