refactor: financial report template enhancements (backport #52687) (#54113)

Co-authored-by: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com>
This commit is contained in:
mergify[bot]
2026-04-07 17:10:42 +00:00
committed by GitHub
parent 4a6fe477d4
commit 7b91566435
6 changed files with 56 additions and 28 deletions

View File

@@ -26,8 +26,13 @@
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [
"modified": "2025-10-15 03:19:47.171349", {
"link_doctype": "Account",
"link_fieldname": "account_category"
}
],
"modified": "2026-02-23 01:19:49.589393",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Account Category", "name": "Account Category",

View File

@@ -3,6 +3,8 @@
frappe.ui.form.on("Financial Report Template", { frappe.ui.form.on("Financial Report Template", {
refresh(frm) { refresh(frm) {
if (frm.is_new() || frm.doc.rows.length === 0) return;
// add custom button to view missed accounts // add custom button to view missed accounts
frm.add_custom_button(__("View Account Coverage"), function () { frm.add_custom_button(__("View Account Coverage"), function () {
let selected_rows = frm.get_field("rows").grid.get_selected_children(); 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) { if (!frm.doc.rows || frm.doc.rows.length === 0) {
frappe.msgprint(__("At least one row is required for a financial report template")); 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_label(frm, row.data_source);
update_formula_description(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); 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 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 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 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 = ""; let description_html = "";
@@ -382,8 +378,13 @@ function update_formula_description(frm, data_source) {
<li><code>my_app.financial_reports.get_kpi_data</code></li> <li><code>my_app.financial_reports.get_kpi_data</code></li>
</ul> </ul>
<h6 ${subtitle_style}>Method Signature:</h6>
<div ${code_style}>
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br>&nbsp; # filters: dict — report filters (company, period, etc.) <br>&nbsp; # periods: list[dict] — period definitions <br>&nbsp; # row: dict — the current report row <br><br>&nbsp; return [1000.0, 1200.0, 1150.0] # one value per period</pre>
</div>
<h6 ${subtitle_style}>Return Format:</h6> <h6 ${subtitle_style}>Return Format:</h6>
<p ${text_style}>Numbers for each period: <code>[1000.0, 1200.0, 1150.0]</code></p> <p ${text_style}>A list of numbers, one for each period: <code>[1000.0, 1200.0, 1150.0]</code></p>
</div>`; </div>`;
} else if (data_source === "Blank Line") { } else if (data_source === "Blank Line") {
description_html = ` description_html = `

View File

@@ -1,6 +1,5 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1,
"autoname": "field:template_name", "autoname": "field:template_name",
"creation": "2025-08-02 04:44:15.184541", "creation": "2025-08-02 04:44:15.184541",
"doctype": "DocType", "doctype": "DocType",
@@ -31,7 +30,8 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Report Type", "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", "depends_on": "eval:frappe.boot.developer_mode",
@@ -66,7 +66,7 @@
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2025-11-14 00:11:03.508139", "modified": "2026-02-23 01:04:05.797161",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Financial Report Template", "name": "Financial Report Template",

View File

@@ -32,6 +32,19 @@ class FinancialReportTemplate(Document):
template_name: DF.Data template_name: DF.Data
# end: auto-generated types # 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): def validate(self):
validator = TemplateValidator(self) validator = TemplateValidator(self)
result = validator.validate() result = validator.validate()

View File

@@ -70,8 +70,8 @@ class ValidationResult:
self.warnings.append(issue) self.warnings.append(issue)
def notify_user(self) -> None: def notify_user(self) -> None:
warnings = "<br><br>".join(str(w) for w in self.warnings) warnings = "<br><br>".join(str(w) for w in self.warnings if w)
errors = "<br><br>".join(str(e) for e in self.issues) errors = "<br><br>".join(str(e) for e in self.issues if e)
if warnings: if warnings:
frappe.msgprint(warnings, title=_("Warnings"), indicator="orange") frappe.msgprint(warnings, title=_("Warnings"), indicator="orange")
@@ -99,9 +99,8 @@ class TemplateValidator:
result.merge(validator.validate(self.template)) result.merge(validator.validate(self.template))
# Run row-level validations # Run row-level validations
account_fields = {field.fieldname for field in frappe.get_meta("Account").fields}
for row in self.template.rows: for row in self.template.rows:
result.merge(self.formula_validator.validate(row, account_fields)) result.merge(self.formula_validator.validate(row))
return result return result
@@ -383,7 +382,8 @@ class AccountFilterValidator(Validator):
"""Validates account filter expressions used in Account Data rows""" """Validates account filter expressions used in Account Data rows"""
def __init__(self, account_fields: set | None = None): 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: def validate(self, row) -> ValidationResult:
result = ValidationResult() result = ValidationResult()
@@ -403,7 +403,11 @@ class AccountFilterValidator(Validator):
try: try:
filter_config = json.loads(row.calculation_formula) 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: if error:
result.add_error( result.add_error(
@@ -425,7 +429,12 @@ class AccountFilterValidator(Validator):
return result 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] # simple condition: [field, operator, value]
if isinstance(filter_config, list): if isinstance(filter_config, list):
if len(filter_config) != 3: if len(filter_config) != 3:
@@ -436,8 +445,10 @@ class AccountFilterValidator(Validator):
if not isinstance(field, str) or not isinstance(operator, str): if not isinstance(field, str) or not isinstance(operator, str):
return "Field and operator must be strings" 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: 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: if operator.casefold() not in OPERATOR_MAP:
return f"Invalid operator '{operator}'" return f"Invalid operator '{operator}'"
@@ -460,7 +471,7 @@ class AccountFilterValidator(Validator):
# recursive # recursive
for condition in conditions: for condition in conditions:
error = self._validate_filter_structure(condition, account_fields) error = self._validate_filter_structure(condition, account_fields, advanced_filtering)
if error: if error:
return error return error
else: else:
@@ -476,7 +487,7 @@ class FormulaValidator(Validator):
self.calculation_validator = CalculationFormulaValidator(reference_codes) self.calculation_validator = CalculationFormulaValidator(reference_codes)
self.account_filter_validator = AccountFilterValidator() self.account_filter_validator = AccountFilterValidator()
def validate(self, row, account_fields: set) -> ValidationResult: def validate(self, row) -> ValidationResult:
result = ValidationResult() result = ValidationResult()
if not row.calculation_formula: if not row.calculation_formula:
@@ -486,9 +497,6 @@ class FormulaValidator(Validator):
return self.calculation_validator.validate(row) return self.calculation_validator.validate(row)
elif row.data_source == "Account Data": 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) return self.account_filter_validator.validate(row)
elif row.data_source == "Custom API": elif row.data_source == "Custom API":

View File

@@ -1295,6 +1295,7 @@ class TestFilterExpressionParser(FinancialReportTemplateTestCase):
self.data_source = "Account Data" self.data_source = "Account Data"
self.idx = 1 self.idx = 1
self.reverse_sign = 0 self.reverse_sign = 0
self.advanced_filtering = True
return MockReportRow(formula, reference_code) return MockReportRow(formula, reference_code)