mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 08:24:47 +00:00
Co-authored-by: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com>
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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> # filters: dict — report filters (company, period, etc.) <br> # periods: list[dict] — period definitions <br> # row: dict — the current report row <br><br> 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 = `
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user