diff --git a/erpnext/accounts/doctype/account_category/account_category.json b/erpnext/accounts/doctype/account_category/account_category.json
index d69d37bd78b..cc8f4103f21 100644
--- a/erpnext/accounts/doctype/account_category/account_category.json
+++ b/erpnext/accounts/doctype/account_category/account_category.json
@@ -26,8 +26,13 @@
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2025-10-15 03:19:47.171349",
+ "links": [
+ {
+ "link_doctype": "Account",
+ "link_fieldname": "account_category"
+ }
+ ],
+ "modified": "2026-02-23 01:19:49.589393",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Category",
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js
index 739956631fd..304da47577b 100644
--- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js
@@ -3,6 +3,8 @@
frappe.ui.form.on("Financial Report Template", {
refresh(frm) {
+ if (frm.is_new() || frm.doc.rows.length === 0) return;
+
// 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();
@@ -20,7 +22,7 @@ frappe.ui.form.on("Financial Report Template", {
});
},
- validate(frm) {
+ after_save(frm) {
if (!frm.doc.rows || frm.doc.rows.length === 0) {
frappe.msgprint(__("At least one row is required for a financial report template"));
}
@@ -34,14 +36,6 @@ frappe.ui.form.on("Financial Report Row", {
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);
},
@@ -322,6 +316,8 @@ function update_formula_description(frm, data_source) {
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;"`;
+ const code_style = `style="background: var(--bg-light-gray); padding: var(--padding-xs); border-radius: var(--border-radius); font-size: 0.85em; width: max-content; margin-bottom: var(--margin-sm);"`;
+ const pre_style = `style="margin: 0; border-radius: var(--border-radius)"`;
let description_html = "";
@@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
my_app.financial_reports.get_kpi_data
+ Method Signature:
+
+
def get_custom_data(filters, periods, row):
# filters: dict — report filters (company, period, etc.)
# periods: list[dict] — period definitions
# row: dict — the current report row
return [1000.0, 1200.0, 1150.0] # one value per period
+
+
Return Format:
- Numbers for each period: [1000.0, 1200.0, 1150.0]
+ A list of numbers, one for each period: [1000.0, 1200.0, 1150.0]
`;
} else if (data_source === "Blank Line") {
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
index 7383306f332..5bfd56810db 100644
--- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.json
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "allow_rename": 1,
"autoname": "field:template_name",
"creation": "2025-08-02 04:44:15.184541",
"doctype": "DocType",
@@ -31,7 +30,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Report Type",
- "options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement"
+ "options": "\nProfit and Loss Statement\nBalance Sheet\nCash Flow\nCustom Financial Statement",
+ "reqd": 1
},
{
"depends_on": "eval:frappe.boot.developer_mode",
@@ -66,7 +66,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-11-14 00:11:03.508139",
+ "modified": "2026-02-23 01:04:05.797161",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Financial Report Template",
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_template.py b/erpnext/accounts/doctype/financial_report_template/financial_report_template.py
index 69ee7e4f7dd..f30ca7b1249 100644
--- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.py
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.py
@@ -32,6 +32,19 @@ class FinancialReportTemplate(Document):
template_name: DF.Data
# end: auto-generated types
+ def before_validate(self):
+ self.clear_hidden_fields()
+
+ def clear_hidden_fields(self):
+ style_data_sources = {"Blank Line", "Column Break", "Section Break"}
+
+ for row in self.rows:
+ if row.data_source != "Account Data":
+ row.balance_type = None
+
+ if row.data_source in style_data_sources:
+ row.calculation_formula = None
+
def validate(self):
validator = TemplateValidator(self)
result = validator.validate()
diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py b/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py
index 306fb562585..170225fa74d 100644
--- a/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py
+++ b/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py
@@ -70,8 +70,8 @@ class ValidationResult:
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)
+ warnings = "
".join(str(w) for w in self.warnings if w)
+ errors = "
".join(str(e) for e in self.issues if e)
if warnings:
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
@@ -99,9 +99,8 @@ class TemplateValidator:
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))
+ result.merge(self.formula_validator.validate(row))
return result
@@ -383,7 +382,8 @@ 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)
+ self.account_meta = frappe.get_meta("Account")
+ self.account_fields = account_fields or set(self.account_meta._valid_columns)
def validate(self, row) -> ValidationResult:
result = ValidationResult()
@@ -403,7 +403,11 @@ class AccountFilterValidator(Validator):
try:
filter_config = json.loads(row.calculation_formula)
- error = self._validate_filter_structure(filter_config, self.account_fields)
+ error = self._validate_filter_structure(
+ filter_config,
+ self.account_fields,
+ row.advanced_filtering,
+ )
if error:
result.add_error(
@@ -425,7 +429,12 @@ class AccountFilterValidator(Validator):
return result
- def _validate_filter_structure(self, filter_config, account_fields: set) -> str | None:
+ def _validate_filter_structure(
+ self,
+ filter_config,
+ account_fields: set,
+ advanced_filtering: bool = False,
+ ) -> str | None:
# simple condition: [field, operator, value]
if isinstance(filter_config, list):
if len(filter_config) != 3:
@@ -436,8 +445,10 @@ class AccountFilterValidator(Validator):
if not isinstance(field, str) or not isinstance(operator, str):
return "Field and operator must be strings"
+ display = (field if advanced_filtering else self.account_meta.get_label(field)) or field
+
if field not in account_fields:
- return f"Field '{field}' is not a valid account field"
+ return f"Field '{display}' is not a valid Account field"
if operator.casefold() not in OPERATOR_MAP:
return f"Invalid operator '{operator}'"
@@ -460,7 +471,7 @@ class AccountFilterValidator(Validator):
# recursive
for condition in conditions:
- error = self._validate_filter_structure(condition, account_fields)
+ error = self._validate_filter_structure(condition, account_fields, advanced_filtering)
if error:
return error
else:
@@ -476,7 +487,7 @@ class FormulaValidator(Validator):
self.calculation_validator = CalculationFormulaValidator(reference_codes)
self.account_filter_validator = AccountFilterValidator()
- def validate(self, row, account_fields: set) -> ValidationResult:
+ def validate(self, row) -> ValidationResult:
result = ValidationResult()
if not row.calculation_formula:
@@ -486,9 +497,6 @@ class FormulaValidator(Validator):
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":
diff --git a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py
index a23c6bb6883..ef6f2785184 100644
--- a/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py
+++ b/erpnext/accounts/doctype/financial_report_template/test_financial_report_engine.py
@@ -1295,6 +1295,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
self.data_source = "Account Data"
self.idx = 1
self.reverse_sign = 0
+ self.advanced_filtering = True
return MockReportRow(formula, reference_code)