From 6cff740d3c4904242693a8e7098bf43a3f287da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 20 Oct 2024 18:22:26 +0200 Subject: [PATCH] Validate integer digits Closes #12975 --- sao/src/model.js | 19 ++++---- sao/src/view/form.js | 44 ++++++++++++----- tryton/tryton/common/number_entry.py | 18 ++++--- .../gui/window/view_form/model/field.py | 2 +- .../view_form/view/form_gtk/dictionary.py | 25 +++++----- .../window/view_form/view/form_gtk/float.py | 8 +--- trytond/CHANGELOG | 1 + trytond/doc/ref/fields.rst | 2 +- trytond/trytond/ir/message.xml | 2 +- trytond/trytond/model/digits.py | 2 +- trytond/trytond/model/fields/float.py | 8 ++-- trytond/trytond/model/modelstorage.py | 23 +++++---- trytond/trytond/tests/test_field_float.py | 46 ++++++++++++++++++ trytond/trytond/tests/test_field_numeric.py | 48 ++++++++++++++++++- 14 files changed, 187 insertions(+), 61 deletions(-) diff --git a/sao/src/model.js b/sao/src/model.js index 3fb8230c20..0a06498c3d 100644 --- a/sao/src/model.js +++ b/sao/src/model.js @@ -1976,13 +1976,16 @@ return; } } - if (!digits || !digits.every(function(e) { - return e !== null; - })) { - return; - } var shift = Math.round(Math.log(Math.abs(factor)) / Math.LN10); - return [digits[0] + shift, digits[1] - shift]; + var int_size = digits[0]; + if (int_size !== null) { + int_size += shift; + } + var dec_size = digits[1]; + if (dec_size !== null) { + dec_size += shift; + } + return [int_size, dec_size]; }, get_symbol: function(record, symbol) { if (record && (symbol in record.model.fields)) { @@ -2040,7 +2043,7 @@ if (value !== null) { value /= factor; var digits = this.digits(record); - if (digits) { + if (digits && (digits[1] !== null)) { // Round to avoid float precision error // after the division by factor value = value.toFixed(digits[1]); @@ -2061,7 +2064,7 @@ useGrouping: grouping, }; var digits = this.digits(record, factor); - if (digits) { + if (digits && (digits[1] !== null)) { options.minimumFractionDigits = digits[1]; options.maximumFractionDigits = digits[1]; } diff --git a/sao/src/view/form.js b/sao/src/view/form.js index 91d0db7944..ef04fe56d0 100644 --- a/sao/src/view/form.js +++ b/sao/src/view/form.js @@ -2180,13 +2180,27 @@ function eval_pyson(value){ display: function() { var record = this.record; var step = 'any'; + var max, min; if (record) { var digits = this.digits; if (digits) { - step = Math.pow(10, -digits[1]).toFixed(digits[1]); + if (digits[1] !== null) { + step = Math.pow(10, -digits[1]).toFixed(digits[1]); + } + if (digits[0] !== null) { + max = '9'.repeat(digits[0]); + if (digits[1] !== null) { + max += '.' + '9'.repeat(digits[1]); + } else { + max += 1; + } + min = '-' + max; + } } } this.input.attr('step', step); + this.input.attr('max', max); + this.input.attr('min', min); Sao.View.Form.Float._super.display.call(this); } }); @@ -5567,13 +5581,7 @@ function eval_pyson(value){ get digits() { var record = this.parent_widget.record; if (record) { - var digits = record.expr_eval(this.definition.digits); - if (!digits || !digits.every(function(e) { - return e !== null; - })) { - return null; - } - return digits; + return record.expr_eval(this.definition.digits); } else { return null; } @@ -5592,13 +5600,27 @@ function eval_pyson(value){ set_value: function(value) { var step = 'any', options = {}; + var max, min; var digits = this.digits; if (digits) { - step = Math.pow(10, -digits[1]).toFixed(digits[1]); - options.minimumFractionDigits = digits[1]; - options.maximumFractionDigits = digits[1]; + if (digits[1] !== null) { + step = Math.pow(10, -digits[1]).toFixed(digits[1]); + options.minimumFractionDigits = digits[1]; + options.maximumFractionDigits = digits[1]; + } + if (digits[0] !== null) { + max = '9'.repeat(digits[0]); + if (digits[1] !== null) { + max += '.' + '9'.repeat(digits[1]); + } else { + max += 1; + } + min = '-' + max; + } } this.input.attr('step', step); + this.input.attr('max', max); + this.input.attr('min', min); Sao.View.Form.Dict.Float._super.set_value.call(this, value, options); }, }); diff --git a/tryton/tryton/common/number_entry.py b/tryton/tryton/common/number_entry.py index 4787d5385e..f590129cfd 100644 --- a/tryton/tryton/common/number_entry.py +++ b/tryton/tryton/common/number_entry.py @@ -2,6 +2,7 @@ # this repository contains the full copyright notices and license terms. import gettext import locale +import math from decimal import Decimal, InvalidOperation from gi.repository import Gdk, GObject, Gtk @@ -64,13 +65,18 @@ class NumberEntry(Gtk.Entry, Gtk.Editable): value = Decimal(locale.delocalize(text, self.monetary)) except (ValueError, InvalidOperation): return position - try: - if (value and self.__digits is not None - and (round(value, self.__digits) != value - or value.as_tuple().exponent < -self.__digits)): + if self.__digits: + int_size, dec_size = self.__digits + try: + if int_size is not None and value: + if math.ceil(math.log10(abs(value))) > int_size: + return position + if dec_size is not None: + if (round(value, dec_size) != value + or value.as_tuple().exponent < -dec_size): + return position + except InvalidOperation: return position - except InvalidOperation: - return position length = len(new_text.encode('utf-8')) buffer_.insert_text(position, new_text, length) return position + length diff --git a/tryton/tryton/gui/window/view_form/model/field.py b/tryton/tryton/gui/window/view_form/model/field.py index 479b668b04..06247c5f08 100644 --- a/tryton/tryton/gui/window/view_form/model/field.py +++ b/tryton/tryton/gui/window/view_form/model/field.py @@ -472,7 +472,7 @@ class FloatField(Field): d = value * factor if not isinstance(d, Decimal): d = Decimal(repr(d)) - if digits: + if digits and digits[1] is not None: p = int(digits[1]) elif d == d.to_integral_value(): p = 0 diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py b/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py index ad5bcc9e88..a2d56cb7a7 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/dictionary.py @@ -301,15 +301,12 @@ class DictFloatEntry(DictIntegerEntry): def digits(self): record = self.parent_widget.record if record: - digits = record.expr_eval(self.definition.get('digits')) - if not digits or any(d is None for d in digits): - return - return digits + return record.expr_eval(self.definition.get('digits')) @property def width(self): digits = self.digits - if digits: + if digits and all(digits): return sum(digits) else: return 18 @@ -318,15 +315,19 @@ class DictFloatEntry(DictIntegerEntry): return self.widget.value def set_value(self, value): - digits = self.digits - if digits: - self.widget.digits = digits[1] - else: - self.widget.digits = None + self.widget.digits = digits = self.digits self.widget.set_width_chars(self.width) if isinstance(value, (int, float, Decimal)): - txt_val = locale.localize( - '{0:.{1}f}'.format(value, digits[1]), True) + d = value + if not isinstance(d, Decimal): + d = Decimal(repr(d)) + if digits and digits[1] is not None: + p = int(digits[1]) + elif d == d.to_integral_value(): + p = 0 + else: + p = -int(d.as_tuple().exponent) + txt_val = locale.localize('{0:.{1}f}'.format(d, p), True) else: txt_val = '' self.widget.set_text(txt_val) diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/float.py b/tryton/tryton/gui/window/view_form/view/form_gtk/float.py index f709518826..cc34742487 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/float.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/float.py @@ -14,15 +14,11 @@ class Float(Integer): @property def width(self): digits = self.digits - if digits: + if digits and all(digits): return sum(digits) else: return self.attrs.get('width', 18) def display(self): - digits = self.digits - if digits: - self.entry.digits = digits[1] - else: - self.entry.digits = None + self.entry.digits = self.digits super(Float, self).display() diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index db7ffc34c5..53b82475ab 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Validate integer digits * Add support for DATE_TRUNC from INTERVAL to SQLite backend * Add support for EXTRACT from INTERVAL to SQLite backend * Add msg_gettext and msg_ngettext to Report context diff --git a/trytond/doc/ref/fields.rst b/trytond/doc/ref/fields.rst index b651806e02..e51d3fb1e4 100644 --- a/trytond/doc/ref/fields.rst +++ b/trytond/doc/ref/fields.rst @@ -430,7 +430,7 @@ Float Integers can be replaced by a :class:`~trytond.pyson.PYSON` statement. If digits is ``None`` or any values of the tuple is ``None``, no validation - on the numbers is done. + on this part of the numbers is done. The tuple can be replaced by a string containing the name of a :class:`Many2One` pointing to a :class:`~trytond.model.DigitsMixin`. diff --git a/trytond/trytond/ir/message.xml b/trytond/trytond/ir/message.xml index 80a1059968..340de6d46c 100644 --- a/trytond/trytond/ir/message.xml +++ b/trytond/trytond/ir/message.xml @@ -185,7 +185,7 @@ this repository contains the full copyright notices and license terms. --> The value "%(value)s" for field "%(field)s" in record "%(record)s" of "%(model)s" is too long (%(size)i > %(max_size)i). - The number of digits in the value "%(value)r" for field "%(field)s" in record "%(record)s" of "%(model)s" exceeds the limit of "%(digits)i". + The number of digits in the value "%(value)r" for field "%(field)s" in record "%(record)s" of "%(model)s" exceeds the limit of "%(digits)r". The value "%(value)s" for field "%(field)s" in record "%(record)s" of "%(model)s" contains some invalid chars "%(chars)s". diff --git a/trytond/trytond/model/digits.py b/trytond/trytond/model/digits.py index 4761efc90e..16c7c3392e 100644 --- a/trytond/trytond/model/digits.py +++ b/trytond/trytond/model/digits.py @@ -26,7 +26,7 @@ class DigitsMixin: return digits def _get_digits(self): - return (16, self.digits) + return (None, self.digits) @classmethod def write(cls, *args): diff --git a/trytond/trytond/model/fields/float.py b/trytond/trytond/model/fields/float.py index 2887c24170..23a3a8a90b 100644 --- a/trytond/trytond/model/fields/float.py +++ b/trytond/trytond/model/fields/float.py @@ -12,11 +12,11 @@ def digits_validate(value): 'digits must be a tuple or a string' if isinstance(value, tuple): for i in value: - assert isinstance(i, (int, PYSON)), \ - 'digits must be tuple of integers or PYSON' + assert isinstance(i, (type(None), int, PYSON)), \ + "digits must be tuple of integers, PYSON or None" if isinstance(i, PYSON): - assert i.types() == {int}, \ - 'PYSON digits must return an integer' + assert i.types() <= {int, type(None)}, \ + "PYSON digits must return an integer or None" def _get_digits_depends(field): diff --git a/trytond/trytond/model/modelstorage.py b/trytond/trytond/model/modelstorage.py index ab5532810e..5aa8e6a080 100644 --- a/trytond/trytond/model/modelstorage.py +++ b/trytond/trytond/model/modelstorage.py @@ -5,6 +5,7 @@ import base64 import csv import datetime import decimal +import math import random import time import warnings @@ -1455,7 +1456,7 @@ class ModelStorage(Model): def digits_test(record, digits, field_name): def raise_error(value): error_args = cls.__names__(field_name, record) - error_args['digits'] = digits[1] + error_args['digits'] = digits raise DigitsValidationError( gettext('ir.msg_digits_validation_record', **error_args)) @@ -1467,16 +1468,20 @@ class ModelStorage(Model): digits = digit_record.get_digits() else: digits = None - if (value is None - or not digits - or any(d is None for d in digits)): + if value is None or not digits: return - if isinstance(value, Decimal): - if value.as_tuple().exponent < -digits[1]: - raise_error(value) - else: - if round(value, digits[1]) != value: + + int_size, dec_size = digits + if int_size is not None and value: + if math.ceil(math.log10(abs(value))) > int_size: raise_error(value) + if dec_size is not None: + if isinstance(value, Decimal): + if value.as_tuple().exponent < -dec_size: + raise_error(value) + else: + if round(value, dec_size) != value: + raise_error(value) # validate digits if getattr(field, 'digits', None): if is_pyson(field.digits): diff --git a/trytond/trytond/tests/test_field_float.py b/trytond/trytond/tests/test_field_float.py index 7a2321c865..13db0e3abe 100644 --- a/trytond/trytond/tests/test_field_float.py +++ b/trytond/trytond/tests/test_field_float.py @@ -143,6 +143,52 @@ class FieldFloatTestCase(TestCase): self.assertEqual(record.float, 0.123456789012345) + @with_transaction() + def test_float_int_digits_valid(self): + "Test create float with limit integer digits" + pool = Pool() + Float = pool.get('test.float_digits') + + record, = Float.create([{ + 'float': 1234567890123456, + }]) + + self.assertEqual(record.float, 1234567890123456) + + @with_transaction() + def test_float_negative_int_digits_valid(self): + "Test create negative float with limit integer digits" + pool = Pool() + Float = pool.get('test.float_digits') + + record, = Float.create([{ + 'float': -1234567890123456, + }]) + + self.assertEqual(record.float, -1234567890123456) + + @with_transaction() + def test_float_int_digits_invalid(self): + "Test create float with invalid integer digits" + pool = Pool() + Float = pool.get('test.float_digits') + + with self.assertRaises(DigitsValidationError): + Float.create([{ + 'float': '12345678901234567', + }]) + + @with_transaction() + def test_float_negative_int_digits_invalid(self): + "Test create negative float with invalid integer digits" + pool = Pool() + Float = pool.get('test.float_digits') + + with self.assertRaises(DigitsValidationError): + Float.create([{ + 'float': -12345678901234567, + }]) + @with_transaction() def test_search_equals(self): "Test search float equals" diff --git a/trytond/trytond/tests/test_field_numeric.py b/trytond/trytond/tests/test_field_numeric.py index 8497008904..a1a58a1f85 100644 --- a/trytond/trytond/tests/test_field_numeric.py +++ b/trytond/trytond/tests/test_field_numeric.py @@ -175,7 +175,7 @@ class FieldNumericTestCase(TestCase): Numeric.create([{ 'digits': 2, - 'numeric': Decimal('10e25'), + 'numeric': Decimal('10e15'), }]) @with_transaction() @@ -189,6 +189,52 @@ class FieldNumericTestCase(TestCase): 'numeric': Decimal('1.10'), }]) + @with_transaction() + def test_create_int_digits_valid(self): + "Test create numeric with limit integer digits" + pool = Pool() + Numeric = pool.get('test.numeric_digits') + + record, = Numeric.create([{ + 'numeric': Decimal('10e15'), + }]) + + self.assertEqual(record.numeric, Decimal('10e15')) + + @with_transaction() + def test_create_negative_int_digits_valid(self): + "Test create negative numeric with limit integer digits" + pool = Pool() + Numeric = pool.get('test.numeric_digits') + + record, = Numeric.create([{ + 'numeric': Decimal('-10e15'), + }]) + + self.assertEqual(record.numeric, Decimal('-10e15')) + + @with_transaction() + def test_create_int_digits_invalid(self): + "Test create numeric with invalid integer digits" + pool = Pool() + Numeric = pool.get('test.numeric_digits') + + with self.assertRaises(DigitsValidationError): + Numeric.create([{ + 'numeric': Decimal('10e16'), + }]) + + @with_transaction() + def test_create_negative_int_digits_invalid(self): + "Test create negative numeric with invalid integer digits" + pool = Pool() + Numeric = pool.get('test.numeric_digits') + + with self.assertRaises(DigitsValidationError): + Numeric.create([{ + 'numeric': Decimal('-10e16'), + }]) + @with_transaction() def test_search_equals(self): "Test search numeric equals"