Validate integer digits

Closes #12975
This commit is contained in:
Cédric Krier
2024-10-20 18:22:26 +02:00
parent d85f0b4456
commit 6cff740d3c
14 changed files with 187 additions and 61 deletions
+11 -8
View File
@@ -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];
}
+33 -11
View File
@@ -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);
},
});
+12 -6
View File
@@ -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
@@ -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
@@ -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)
@@ -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()
+1
View File
@@ -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
+1 -1
View File
@@ -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`.
+1 -1
View File
@@ -185,7 +185,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="text">The value "%(value)s" for field "%(field)s" in record "%(record)s" of "%(model)s" is too long (%(size)i > %(max_size)i).</field>
</record>
<record model="ir.message" id="msg_digits_validation_record">
<field name="text">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".</field>
<field name="text">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".</field>
</record>
<record model="ir.message" id="msg_forbidden_char_validation_record">
<field name="text">The value "%(value)s" for field "%(field)s" in record "%(record)s" of "%(model)s" contains some invalid chars "%(chars)s".</field>
+1 -1
View File
@@ -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):
+4 -4
View File
@@ -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):
+14 -9
View File
@@ -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):
+46
View File
@@ -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"
+47 -1
View File
@@ -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"