From fdc52473c526b85e2500864a5d913bd05e42e4a9 Mon Sep 17 00:00:00 2001 From: Devansh Shetty Date: Tue, 23 Sep 2025 08:34:02 +0000 Subject: [PATCH 1/2] fix(filters): robust localized numeric parsing & correct null-check in set_values --- frappe/public/js/frappe/ui/filters/filter.js | 78 +++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index a0431999e1ef..0828033492f9 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -208,7 +208,7 @@ frappe.ui.Filter = class { if (Array.isArray(value)) { this._filter_value_set = this.field.set_value(value); - } else if (value !== undefined || value !== null) { + } else if (value !== undefined && value !== null) { this._filter_value_set = this.field.set_value((value + "").trim()); } return this._filter_value_set; @@ -447,6 +447,55 @@ frappe.ui.Filter = class { }; frappe.ui.filter_utils = { + // Parse localized number strings like "100,00" or "1.000,50" into a JS number + parseLocalizedNumber(input) { + if (input === null || input === undefined) return NaN; + let s = String(input).trim(); + if (!s) return NaN; + // Keep digits, comma, dot, and minus + s = s.replace(/[^0-9,\.\-]/g, ""); + const lastComma = s.lastIndexOf(","); + const lastDot = s.lastIndexOf("."); + let decSep = null; + if (lastComma !== -1 && lastDot !== -1) { + decSep = lastComma > lastDot ? "," : "."; + } else if (lastComma !== -1) { + decSep = ","; + } else if (lastDot !== -1) { + decSep = "."; + } + if (decSep) { + const thouSep = decSep === "," ? "." : ","; + // remove thousands separators + s = s.replace(new RegExp("\\" + thouSep, "g"), ""); + // convert decimal sep to dot + if (decSep === ",") s = s.replace(/,/g, "."); + } else { + // no clear decimal sep: drop any stray separators + s = s.replace(/[,.]/g, ""); + } + const n = parseFloat(s); + return isNaN(n) ? NaN : n; + }, + + + + // Normalize numeric according to field semantics (percent, int, rating) + normalizeNumeric(field, n) { + if (isNaN(n)) return n; + let out = n; + if (field?.df?.fieldtype === "Percent") { + // Convert 0..1 to 0..100, clamp and round + if (out > 0 && out < 1) out = out * 100; + out = Math.round(out); + if (out < 0) out = 0; + if (out > 100) out = 100; + } else if (field?.df?.fieldtype === "Int" || field?.df?.fieldtype === "Rating") { + out = Math.round(out); + } + return out; + }, + get_formatted_value(field, value) { if (field.df.fieldname === "docstatus") { value = { 0: "Draft", 1: "Submitted", 2: "Cancelled" }[value] || value; @@ -471,6 +520,18 @@ frappe.ui.filter_utils = { val = strip(val); } + const isNumericFieldEarly = + ["Int", "Float", "Currency", "Rating", "Percent"].includes(field.df.fieldtype); + + // For numeric fields with non list conditions, normalize from raw input + if (isNumericFieldEarly && !["in", "not in"].includes(condition)) { + const raw = field.$input && typeof field.$input.val === "function" ? field.$input.val() : null; + const numFromRaw = this.parseLocalizedNumber(raw ?? val); + if (!isNaN(numFromRaw)) { + val = this.normalizeNumeric(field, numFromRaw); + } + } + if (condition == "is" && !val) { val = field.df.options[0].value; } @@ -486,9 +547,20 @@ frappe.ui.filter_utils = { } } else if (["in", "not in"].includes(condition)) { if (val) { - val = val.split(",").map((v) => strip(v)); + let parts = val.split(",").map((v) => strip(v)); + const isNumericField = ["Int", "Float", "Currency", "Rating", "Percent"].includes(field.df.fieldtype); + if (isNumericField) { + parts = parts.map((p) => { + const num = this.parseLocalizedNumber(p); + return isNaN(num) ? p : this.normalizeNumeric(field, num); + }); + } + val = parts; } - } else if (frappe.boot.additional_filters_config[condition]) { + } else if ( + frappe.boot.additional_filters_config && + frappe.boot.additional_filters_config[condition] + ) { val = field.value || val; } if (val === "%") { From 00fa2d0b25702f299f43bab96c25039bd2e4aa3f Mon Sep 17 00:00:00 2001 From: Devansh Shetty Date: Tue, 30 Sep 2025 09:28:44 +0000 Subject: [PATCH 2/2] fix: formatting/lint issues flagged by pre-commit. --- frappe/public/js/frappe/ui/filters/filter.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 0828033492f9..39575b1325d9 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -478,8 +478,6 @@ frappe.ui.filter_utils = { return isNaN(n) ? NaN : n; }, - - // Normalize numeric according to field semantics (percent, int, rating) normalizeNumeric(field, n) { if (isNaN(n)) return n; @@ -520,12 +518,14 @@ frappe.ui.filter_utils = { val = strip(val); } - const isNumericFieldEarly = - ["Int", "Float", "Currency", "Rating", "Percent"].includes(field.df.fieldtype); + const isNumericFieldEarly = ["Int", "Float", "Currency", "Rating", "Percent"].includes( + field.df.fieldtype + ); // For numeric fields with non list conditions, normalize from raw input if (isNumericFieldEarly && !["in", "not in"].includes(condition)) { - const raw = field.$input && typeof field.$input.val === "function" ? field.$input.val() : null; + const raw = + field.$input && typeof field.$input.val === "function" ? field.$input.val() : null; const numFromRaw = this.parseLocalizedNumber(raw ?? val); if (!isNaN(numFromRaw)) { val = this.normalizeNumeric(field, numFromRaw); @@ -548,7 +548,9 @@ frappe.ui.filter_utils = { } else if (["in", "not in"].includes(condition)) { if (val) { let parts = val.split(",").map((v) => strip(v)); - const isNumericField = ["Int", "Float", "Currency", "Rating", "Percent"].includes(field.df.fieldtype); + const isNumericField = ["Int", "Float", "Currency", "Rating", "Percent"].includes( + field.df.fieldtype + ); if (isNumericField) { parts = parts.map((p) => { const num = this.parseLocalizedNumber(p);