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"