From 3b7fb6851a1d1d6eeb2a612fd303ca39b81a81ee Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Mon, 13 Jul 2026 16:51:46 +0530 Subject: [PATCH] fix(stock): read quality inspection readings in the user's number format readings are Data fields, so they are parsed server side. parse_float only swapped the separators for "#.###,##", so in the space grouped "# ###,##" (polish) a reading of 1,15 was read as 115, fell outside the acceptance range and silently rejected the inspection. strip whatever the group separator is and normalise whatever the decimal separator is instead. it also read the global number format, while the desk formats numbers with the user's own. a user whose locale differs from the site therefore typed readings in a format the server did not parse them with. read the user default, which falls back to the global one. a reading that is not a valid number in that format is now rejected with an error instead of being read as a different number. (cherry picked from commit e74c0a3cdbc644f2113d4e64519d51950e0c698d) --- .../quality_inspection/quality_inspection.py | 81 +++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 5e7a6ba307d..99d5d94f2da 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -82,6 +82,7 @@ class QualityInspection(Document): reading.status = "Accepted" if self.readings: + self.validate_reading_number_format() self.inspect_and_set_status() self.validate_inspection_required() @@ -280,6 +281,37 @@ class QualityInspection(Document): ) break + def validate_reading_number_format(self): + """Reject readings written in a different number format than the user's. + + They would otherwise be misread rather than refused, silently rejecting an + inspection whose readings are in fact within the acceptance range.""" + number_format = get_reading_number_format() + decimal_str, comma_str, _precision = get_number_format_info(number_format) + + for reading in self.readings: + if not cint(reading.numeric): + continue + + for i in range(1, 11): + value = reading.get("reading_" + str(i)) + if value is None or not value.strip(): + continue + + if not is_valid_number(value, decimal_str, comma_str): + frappe.throw( + _( + "Row #{0}: Reading {1} {2} is not a valid number in the {3} number format. Use {4} as the decimal separator." + ).format( + reading.idx, + i, + frappe.bold(value), + frappe.bold(number_format), + frappe.bold(decimal_str), + ), + title=_("Invalid Reading"), + ) + def set_status_based_on_acceptance_values(self, reading): if not cint(reading.numeric): reading_value = reading.get("reading_value") or "" @@ -507,17 +539,56 @@ def make_quality_inspection(source_name, target_doc=None): return doc +def get_reading_number_format() -> str: + """Number format the user enters readings in. + + User defaults fall back to the global default, so this is the same format the + user's desk formats numbers with.""" + return frappe.defaults.get_user_default("number_format") or "#,###.##" + + +def is_valid_number(num: str, decimal_str: str, comma_str: str) -> bool: + num = num.strip().lstrip("+-") + integer_part, fraction = num, "" + + if decimal_str: + if num.count(decimal_str) > 1: + return False + integer_part, _, fraction = num.partition(decimal_str) + + if fraction and not fraction.isdigit(): + return False + + if not integer_part: + return bool(fraction) + + if comma_str and comma_str in integer_part: + groups = integer_part.split(comma_str) + if any(not group.isdigit() for group in groups): + return False + + # the group just before the decimal separator is always 3 digits long + if not 1 <= len(groups[0]) <= 3 or len(groups[-1]) != 3: + return False + + # 3 digits per group, or 2 in the indian format + return all(len(group) in (2, 3) for group in groups[1:-1]) + + return integer_part.isdigit() + + def parse_float(num: str) -> float: """Since reading_# fields are `Data` field they might contain number which is representation in user's prefered number format instead of machine readable format. This function converts them to machine readable format.""" - number_format = frappe.db.get_default("number_format") or "#,###.##" + number_format = get_reading_number_format() decimal_str, comma_str, _number_format_precision = get_number_format_info(number_format) - if decimal_str == "," and comma_str == ".": - num = num.replace(",", "#$") - num = num.replace(".", ",") - num = num.replace("#$", ".") + if comma_str: + num = num.replace(comma_str, "") + + if decimal_str and decimal_str != ".": + num = num.replace(decimal_str, ".") return flt(num)