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 1/4] 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", ) ) From d6f9dde3cb83ed2231127ed3c36c651bb3c5657c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:28:17 +0530 Subject: [PATCH 2/4] fix: filter cancelled BOMs in BOM Stock Analysis (backport #58647) (#58693) --- .../report/bom_stock_analysis/bom_stock_analysis.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js index 7629c102d7c..3d11a7d7263 100644 --- a/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js +++ b/erpnext/manufacturing/report/bom_stock_analysis/bom_stock_analysis.js @@ -9,6 +9,7 @@ frappe.query_reports["BOM Stock Analysis"] = { fieldtype: "Link", options: "BOM", reqd: 1, + get_query: () => ({ filters: { docstatus: 1 } }), }, { fieldname: "warehouse", From 41374019baf2ca82767d3aed918781d07f8a7854 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:00:42 +0530 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20include=20payment=20deductions=20in?= =?UTF-8?q?=20sales/purchase=20register=20ledger=20bal=E2=80=A6=20(backpor?= =?UTF-8?q?t=20#58437)=20(#58680)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- erpnext/accounts/report/utils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 3661e787f41..189ac56c874 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -304,6 +304,9 @@ def get_payment_entries(filters, args): pe.mode_of_payment, pe.project, pe.cost_center, + pe.payment_type, + pe.source_exchange_rate, + pe.target_exchange_rate, ) .where( (pe.docstatus == 1) @@ -314,6 +317,22 @@ def get_payment_entries(filters, args): ) query = apply_common_conditions(filters, query, doctype="Payment Entry", payments=True) payment_entries = query.run(as_dict=True) + + if payment_entries: + ded = frappe.qb.DocType("Payment Entry Deduction") + deduction_totals = frappe._dict( + frappe.qb.from_(ded) + .select(ded.parent, Sum(ded.amount)) + .where(ded.parent.isin([d.name for d in payment_entries]) & (ded.is_exchange_gain_loss == 0)) + .groupby(ded.parent) + .run() + ) + for d in payment_entries: + exchange_rate = ( + d.source_exchange_rate if d.payment_type == "Receive" else d.target_exchange_rate + ) or 1 + d.base_grand_total = flt(d.base_grand_total) + flt(deduction_totals.get(d.name)) / exchange_rate + return payment_entries From 7aad59b129711e9bba17b25665428d1fc57bf37c Mon Sep 17 00:00:00 2001 From: Sagar Vora <16315650+sagarvora@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:31:36 +0530 Subject: [PATCH 4/4] fix!: improve validation in financial report template --- .../financial_report_engine.py | 7 +- .../financial_report_template.js | 5 +- .../financial_report_validation.py | 51 +++++----- .../test_financial_report_template.py | 92 +++++++++++++++++++ 4 files changed, 130 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py index c79cbfe1448..1137b79a964 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_engine.py @@ -31,6 +31,7 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_validat AccountFilterValidator, CalculationFormulaValidator, DependencyValidator, + get_valid_api_method, ) from erpnext.accounts.report.financial_statements import ( get_columns, @@ -1164,10 +1165,12 @@ class RowProcessor: def _process_api_row(self, row) -> RowData: api_path = row.calculation_formula - # TODO + + method = get_valid_api_method(api_path) try: - values = frappe.call(api_path, filters=self.context.filters, periods=self.period_list, row=row) + # nosemgrep: frappe-semgrep-rules.rules.security.frappe-codeinjection-eval + values = frappe.call(method, filters=self.context.filters, periods=self.period_list, row=row) if row.reverse_sign: values = [-1 * v for v in values] 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 fe04d11b2c4..71da3e17635 100644 --- a/erpnext/accounts/doctype/financial_report_template/financial_report_template.js +++ b/erpnext/accounts/doctype/financial_report_template/financial_report_template.js @@ -370,7 +370,7 @@ function update_formula_description(frm, data_source) { description_html = `
Custom API Setup
-

Path to your custom method that returns financial data.

+

Path to your custom whitelisted method that returns financial data. It must permit GET requests.

Format: