From a43de7ce95b151571024ab2850968474ed651277 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:26:49 +0530 Subject: [PATCH] fix: improve message formatting and translation for validation issues (#58425) (cherry picked from commit 6842ebb1861713db1b1f816c8f78f9411c71a899) --- .../financial_report_template.js | 16 ++-- .../financial_report_validation.py | 94 +++++++++++-------- 2 files changed, 63 insertions(+), 47 deletions(-) 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 304da47577b..fe04d11b2c4 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js @@ -163,7 +163,7 @@ function show_accounts_tree(template_rows, has_selection) { fieldname: "company", fieldtype: "Link", options: "Company", - label: "Company", + label: __("Company"), reqd: 1, default: frappe.defaults.get_user_default("Company"), onchange: () => { @@ -176,7 +176,7 @@ function show_accounts_tree(template_rows, has_selection) { fieldname: "view_type", fieldtype: "Select", options: ["Missing Accounts", "Filtered Accounts"], - label: "View", + label: __("View"), default: has_selection ? "Filtered Accounts" : "Missing Accounts", reqd: 1, onchange: () => { @@ -192,10 +192,10 @@ function show_accounts_tree(template_rows, has_selection) { { fieldname: "tip", fieldtype: "HTML", - label: "Tip", + label: __("Tip"), options: ` `, depends_on: has_selection ? "eval: false" : "eval: true", @@ -203,7 +203,7 @@ function show_accounts_tree(template_rows, has_selection) { { fieldname: "tree_area", fieldtype: "HTML", - label: "Chart of Accounts", + label: __("Chart of Accounts"), read_only: 1, depends_on: "eval: doc.company", }, @@ -288,14 +288,14 @@ function update_formula_label(frm, data_source) { if (!field) return; const labels = { - "Account Data": "Account Filter", - "Custom API": "API Method Path", + "Account Data": __("Account Filter"), + "Custom API": __("API Method Path"), }; grid.update_docfield_property( "calculation_formula", "label", - labels[data_source] || "Calculation Formula" + labels[data_source] || __("Calculation Formula") ); } 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 170225fa74d..2ccdf81d54c 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_validation.py @@ -15,13 +15,21 @@ from frappe.database.operator_map import OPERATOR_MAP from frappe.database.query import SQLFunctionParser +def get_formula_field_label(data_source: str) -> str: + # Must mirror the `labels` map in financial_report_template.js (update_formula_label), + labels = { + "Account Data": _("Account Filter"), + "Custom API": _("API Method Path"), + } + return labels.get(data_source, _("Calculation Formula")) + + @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): @@ -29,10 +37,9 @@ class ValidationIssue: 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) + if self.row_idx: + return _("Row {0}: {1}", context="Financial Report Template").format(self.row_idx, self.message) + return self.message @dataclass @@ -134,7 +141,9 @@ class TemplateStructureValidator(Validator): 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", + message=_( + "Invalid line reference format: '{0}'. Must start with letter and contain only letters, numbers, underscores, and hyphens" + ).format(ref_code), row_idx=row.idx, ) ) @@ -143,7 +152,7 @@ class TemplateStructureValidator(Validator): if ref_code in used_codes: result.add_error( ValidationIssue( - message=f"Duplicate line reference: '{ref_code}'", + message=_("Duplicate line reference: '{0}'").format(ref_code), row_idx=row.idx, ) ) @@ -159,7 +168,7 @@ class TemplateStructureValidator(Validator): if row.data_source == "Account Data" and not row.balance_type: result.add_error( ValidationIssue( - message="Balance Type is required for Account Data", + message=_("Balance Type is required for Account Data"), row_idx=row.idx, ) ) @@ -169,7 +178,9 @@ class TemplateStructureValidator(Validator): if not row.calculation_formula: result.add_error( ValidationIssue( - message=f"Formula is required for {row.data_source}", + message=_("{0} is required for {1}").format( + get_formula_field_label(row.data_source), row.data_source + ), row_idx=row.idx, ) ) @@ -226,7 +237,7 @@ class DependencyValidator(Validator): cycle = [*path[cycle_start:], node] result.add_error( ValidationIssue( - message=f"Circular dependency detected: {' → '.join(cycle)}", + message=_("Circular dependency detected: {0}").format(" → ".join(cycle)), ) ) return @@ -258,7 +269,7 @@ class DependencyValidator(Validator): row_idx = self._get_row_idx(ref_code) result.add_error( ValidationIssue( - message=f"Line References undefined in Formula: {', '.join(undefined)}", + message=_("Line References undefined in Formula: {0}").format(", ".join(undefined)), row_idx=row_idx, ) ) @@ -288,9 +299,10 @@ class CalculationFormulaValidator(Validator): if not row.calculation_formula: result.add_error( ValidationIssue( - message="Formula is required for Calculated Amount", + message=_("{0} is required for Calculated Amount").format( + get_formula_field_label(row.data_source) + ), row_idx=row.idx, - field="Formula", ) ) return result @@ -302,7 +314,7 @@ class CalculationFormulaValidator(Validator): if not self._are_parentheses_balanced(formula): result.add_error( ValidationIssue( - message="Formula has unbalanced parentheses", + message=_("Formula has unbalanced parentheses"), row_idx=row.idx, ) ) @@ -314,7 +326,7 @@ class CalculationFormulaValidator(Validator): if row.reference_code and row.reference_code in refs: result.add_error( ValidationIssue( - message=f"Formula references itself ('{row.reference_code}')", + message=_("Formula references itself ('{0}')").format(row.reference_code), row_idx=row.idx, ) ) @@ -324,7 +336,7 @@ class CalculationFormulaValidator(Validator): if undefined: result.add_error( ValidationIssue( - message=f"Formula references undefined codes: {', '.join(undefined)}", + message=_("Formula references undefined codes: {0}").format(", ".join(undefined)), row_idx=row.idx, ) ) @@ -334,7 +346,7 @@ class CalculationFormulaValidator(Validator): if eval_error: result.add_error( ValidationIssue( - message=f"Formula evaluation error: {eval_error}", + message=_("Formula evaluation error: {0}").format(eval_error), row_idx=row.idx, ) ) @@ -371,7 +383,7 @@ class CalculationFormulaValidator(Validator): 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 _("Formula must return a numeric value, got {0}").format(type(result).__name__) return None except Exception as e: @@ -394,9 +406,10 @@ class AccountFilterValidator(Validator): if not row.calculation_formula: result.add_error( ValidationIssue( - message="Account filter is required for Account Data", + message=_("{0} is required for Account Data").format( + get_formula_field_label(row.data_source) + ), row_idx=row.idx, - field="Formula", ) ) return result @@ -412,18 +425,18 @@ class AccountFilterValidator(Validator): if error: result.add_error( ValidationIssue( - message=error, + message=_("{0}: {1}").format(get_formula_field_label(row.data_source), error), row_idx=row.idx, - field="Account Filter", ) ) except json.JSONDecodeError as e: result.add_error( ValidationIssue( - message=f"Invalid JSON format: {e!s}", + message=_("{0}: Invalid JSON format: {1}").format( + get_formula_field_label(row.data_source), str(e) + ), row_idx=row.idx, - field="Account Filter", ) ) @@ -438,36 +451,36 @@ class AccountFilterValidator(Validator): # simple condition: [field, operator, value] if isinstance(filter_config, list): if len(filter_config) != 3: - return "Filter must be [field, operator, value]" + 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" + 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 '{display}' is not a valid Account field" + return _("Field '{0}' is not a valid Account field").format(display) if operator.casefold() not in OPERATOR_MAP: - return f"Invalid operator '{operator}'" + return _("Invalid operator '{0}'").format(operator) if operator in ["in", "not in"] and not isinstance(value, list): - return f"Operator '{operator}' requires a list value" + return _("Operator '{0}' requires a list value").format(operator) # logical condition: {"and": [condition1, condition2]} elif isinstance(filter_config, dict): if len(filter_config) != 1: - return "Logical condition must have exactly one operator" + 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'" + 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" + return _("Logical conditions need at least 1 sub-condition") # recursive for condition in conditions: @@ -475,7 +488,7 @@ class AccountFilterValidator(Validator): if error: return error else: - return "Filter must be a list or dict" + return _("Filter must be a list or dict") return None @@ -511,9 +524,10 @@ class FormulaValidator(Validator): if "." not in api_path: result.add_error( ValidationIssue( - message="Custom API path should be in format: app.module.method", + message=_("{0} should be in format: app.module.method").format( + get_formula_field_label(row.data_source) + ), row_idx=row.idx, - field="Formula", ) ) return result @@ -526,17 +540,19 @@ class FormulaValidator(Validator): 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)", + message=_( + "{0}: Method '{1}' not found in module '{2}' (might be environment-specific)" + ).format(get_formula_field_label(row.data_source), method_name, module_path), row_idx=row.idx, - field="Formula", ) ) except Exception as e: result.add_error( ValidationIssue( - message=f"Could not validate API path: {e!s}", + message=_("Could not validate {0}: {1}").format( + get_formula_field_label(row.data_source), str(e) + ), row_idx=row.idx, - field="Formula", ) )