diff --git a/CHANGELOG b/CHANGELOG index d7c57eee7a..822e37383a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Allow SQL expression as value of fields * Allow customizing Dict keys order with a sequence field * Add invalid domain in DomainValidationError * Add domain inversion tools diff --git a/trytond/model/fields/boolean.py b/trytond/model/fields/boolean.py index 40c00297c9..070f0e4980 100644 --- a/trytond/model/fields/boolean.py +++ b/trytond/model/fields/boolean.py @@ -10,6 +10,7 @@ class Boolean(Field): ''' _type = 'boolean' _sql_type = 'BOOL' + _py_type = bool def __init__(self, string='', help='', readonly=False, domain=None, states=None, select=False, on_change=None, on_change_with=None, diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py index 97374f3ca2..fedf452a3f 100644 --- a/trytond/model/fields/char.py +++ b/trytond/model/fields/char.py @@ -12,6 +12,7 @@ class Char(FieldTranslate): Define a char field (``unicode``). ''' _type = 'char' + _py_type = str def __init__(self, string='', size=None, help='', required=False, readonly=False, domain=None, states=None, translate=False, @@ -47,12 +48,6 @@ class Char(FieldTranslate): size = property(_get_size, _set_size) - def sql_format(self, value): - if value is None: - return None - assert isinstance(value, str) - return value - @property def _sql_type(self): return 'VARCHAR(%s)' % self.size if self.size else 'VARCHAR' diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py index ac1386b246..595ffb525f 100644 --- a/trytond/model/fields/date.py +++ b/trytond/model/fields/date.py @@ -1,6 +1,7 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import datetime + from sql.functions import Function from trytond.pyson import PYSONEncoder @@ -29,23 +30,16 @@ class Date(Field): ''' _type = 'date' _sql_type = 'DATE' + _py_type = datetime.date def sql_format(self, value): - if value is None: - return None if isinstance(value, str): year, month, day = list(map(int, value.split("-", 2))) - return datetime.date(year, month, day) - - if (not isinstance(value, datetime.date) - # Allow datetime with min time for XML-RPC - # datetime must be tested separately because datetime is a - # subclass of date - or (isinstance(value, datetime.datetime) - and value.time() != datetime.time())): - raise ValueError("invalid type '%s' for %s" - % (type(value), type(self))) - return value + value = datetime.date(year, month, day) + elif isinstance(value, datetime.datetime): + if value.time() != datetime.time(): + raise ValueError("Date field can not have time") + return super().sql_format(value) def sql_cast(self, expression): if backend.name() == 'sqlite': @@ -68,11 +62,10 @@ class Timestamp(FormatMixin, Field): ''' _type = 'timestamp' _sql_type = 'TIMESTAMP' + _py_type = datetime.datetime format = '%H:%M:%S.%f' def sql_format(self, value): - if value is None: - return None if isinstance(value, str): datepart, timepart = value.split(" ") year, month, day = map(int, datepart.split("-", 2)) @@ -82,12 +75,9 @@ class Timestamp(FormatMixin, Field): microseconds = int(timepart_full[1]) else: microseconds = 0 - return datetime.datetime(year, month, day, hours, minutes, seconds, - microseconds) - if not isinstance(value, datetime.datetime): - raise ValueError("invalid type '%s' for %s" - % (type(value), type(self))) - return value + value = datetime.datetime( + year, month, day, hours, minutes, seconds, microseconds) + return super().sql_format(value) def sql_cast(self, expression): if backend.name() == 'sqlite': @@ -119,28 +109,40 @@ class DateTime(Timestamp): def sql_format(self, value): value = super().sql_format(value) - if value: + if isinstance(value, datetime.datetime): value = value.replace(microsecond=0) return value -class Time(DateTime): +class Time(FormatMixin, Field): ''' Define a time field (``time``). ''' _type = 'time' _sql_type = 'TIME' + _py_type = datetime.time + + def __init__(self, string='', format='%H:%M:%S', help='', required=False, + readonly=False, domain=None, states=None, select=False, + on_change=None, on_change_with=None, depends=None, + context=None, loading='eager'): + ''' + :param format: The validation format as used by strftime. + ''' + super().__init__(string=string, help=help, + required=required, readonly=readonly, domain=domain, states=states, + select=select, on_change=on_change, on_change_with=on_change_with, + depends=depends, context=context, loading=loading) + self.format = format def sql_format(self, value): - if value is None: - return None if isinstance(value, str): hours, minutes, seconds = map(int, value.split(":")) - return datetime.time(hours, minutes, seconds) - if not isinstance(value, datetime.time): - raise ValueError("invalid type '%s' for %s" - % (type(value), type(self))) - return value.replace(microsecond=0) + value = datetime.time(hours, minutes, seconds) + value = super().sql_format(value) + if isinstance(value, datetime.time): + value = value.replace(microsecond=0) + return value def sql_cast(self, expression): if backend.name() == 'sqlite': @@ -154,6 +156,7 @@ class TimeDelta(Field): ''' _type = 'timedelta' _sql_type = 'INTERVAL' + _py_type = datetime.timedelta def __init__(self, string='', converter=None, help='', required=False, readonly=False, domain=None, states=None, select=False, @@ -170,12 +173,9 @@ class TimeDelta(Field): self.converter = converter def sql_format(self, value): - if value is None: - return None - if not isinstance(value, datetime.timedelta): - raise ValueError("invalid type '%s' for %s" - % (type(value), type(self))) - return super(TimeDelta, self).sql_format(value) + if isinstance(value, (int, float)): + raise TypeError("TimeDelta requires a timedelta") + return super().sql_format(value) @classmethod def get(cls, ids, model, name, values=None): diff --git a/trytond/model/fields/dict.py b/trytond/model/fields/dict.py index 0502081a0b..37d8575479 100644 --- a/trytond/model/fields/dict.py +++ b/trytond/model/fields/dict.py @@ -21,6 +21,7 @@ class Dict(Field): 'Define dict field.' _type = 'dict' _sql_type = 'TEXT' + _py_type = dict def __init__(self, schema_model, string='', help='', required=False, readonly=False, domain=None, states=None, select=False, @@ -44,11 +45,10 @@ class Dict(Field): return dicts def sql_format(self, value): - if value is None: - return None - assert isinstance(value, dict) - value = {k: v for k, v in value.items() if v is not None} - return dumps(value) + value = super().sql_format(value) + if isinstance(value, dict): + value = dumps({k: v for k, v in value.items() if v is not None}) + return value def translated(self, name=None, type_='values'): "Return a descriptor for the translated value of the field" diff --git a/trytond/model/fields/field.py b/trytond/model/fields/field.py index 9b11f0d59b..705ff300ab 100644 --- a/trytond/model/fields/field.py +++ b/trytond/model/fields/field.py @@ -210,6 +210,7 @@ SQL_OPERATORS = { class Field(object): _type = None _sql_type = None + _py_type = None def __init__(self, string='', help='', required=False, readonly=False, domain=None, states=None, select=False, on_change=None, @@ -343,6 +344,9 @@ class Field(object): assert self._sql_type is not None database = Transaction().database + if (self._py_type and value is not None + and not isinstance(value, self._py_type)): + value = self._py_type(value) return database.sql_format(self._sql_type, value) def sql_type(self): diff --git a/trytond/model/fields/float.py b/trytond/model/fields/float.py index 4ae2ce3ce7..30fff4dc41 100644 --- a/trytond/model/fields/float.py +++ b/trytond/model/fields/float.py @@ -22,6 +22,7 @@ class Float(Field): ''' _type = 'float' _sql_type = 'FLOAT' + _py_type = float def __init__(self, string='', digits=None, help='', required=False, readonly=False, domain=None, states=None, select=False, @@ -49,11 +50,6 @@ class Float(Field): digits = property(_get_digits, _set_digits) - def sql_format(self, value): - if value is None: - return None - return float(value) - def definition(self, model, language): encoder = PYSONEncoder() definition = super().definition(model, language) diff --git a/trytond/model/fields/integer.py b/trytond/model/fields/integer.py index fb66c921ea..1739d932da 100644 --- a/trytond/model/fields/integer.py +++ b/trytond/model/fields/integer.py @@ -9,11 +9,7 @@ class Integer(Field): ''' _type = 'integer' _sql_type = 'INTEGER' - - def sql_format(self, value): - if value is None: - return None - return int(str(value)) + _py_type = int class BigInteger(Integer): diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py index 92ecf69779..623442693d 100644 --- a/trytond/model/fields/many2one.py +++ b/trytond/model/fields/many2one.py @@ -19,6 +19,7 @@ class Many2One(Field): ''' _type = 'many2one' _sql_type = 'INTEGER' + _py_type = int def __init__(self, model_name, string='', left=None, right=None, ondelete='SET NULL', datetime_field=None, target_search='join', @@ -111,10 +112,8 @@ class Many2One(Field): super(Many2One, self).__set__(inst, value) def sql_format(self, value): - if value is None: - return None assert value is not False - return int(value) + return super().sql_format(value) def convert_domain_mptt(self, domain, tables): cursor = Transaction().connection.cursor() diff --git a/trytond/model/fields/numeric.py b/trytond/model/fields/numeric.py index 988b587c99..b9e250cb97 100644 --- a/trytond/model/fields/numeric.py +++ b/trytond/model/fields/numeric.py @@ -20,14 +20,7 @@ class Numeric(Float): ''' _type = 'numeric' _sql_type = 'NUMERIC' - - def sql_format(self, value): - if value is None: - return None - if isinstance(value, int): - value = Decimal(str(value)) - value = Decimal(value) - return value + _py_type = Decimal def sql_column(self, table): column = super(Numeric, self).sql_column(table) diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index 114ea40db4..78f2006807 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -20,6 +20,7 @@ class Reference(SelectionMixin, Field): ''' _type = 'reference' _sql_type = 'VARCHAR' + _py_type = str def __init__(self, string='', selection=None, sort=True, selection_change_with=None, translate=True, search_order=None, diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py index 6a3a8d7826..8a92a624eb 100644 --- a/trytond/model/fields/selection.py +++ b/trytond/model/fields/selection.py @@ -59,6 +59,7 @@ class Selection(SelectionMixin, Field): ''' _type = 'selection' _sql_type = 'VARCHAR' + _py_type = str def __init__(self, selection, string='', sort=True, selection_change_with=None, translate=True, help='', diff --git a/trytond/tests/field_time.py b/trytond/tests/field_time.py index 92ec5bdf93..29d8c0b272 100644 --- a/trytond/tests/field_time.py +++ b/trytond/tests/field_time.py @@ -35,10 +35,17 @@ class TimeFormat(ModelSQL): time = fields.Time(string='Time', format='%H:%M') +class TimePrecision(ModelSQL): + "Time precision" + __name__ = 'test.time_precision' + time = fields.Time(string="Time", format="%H:%M:%S.%f") + + def register(module): Pool.register( Time, TimeDefault, TimeRequired, TimeFormat, + TimePrecision, module=module, type_='model') diff --git a/trytond/tests/test_field_binary.py b/trytond/tests/test_field_binary.py index 60cd2d4d80..0b7b228ad5 100644 --- a/trytond/tests/test_field_binary.py +++ b/trytond/tests/test_field_binary.py @@ -4,6 +4,8 @@ import shutil import tempfile import unittest +from sql import Literal + from trytond.config import config from trytond.model.exceptions import RequiredValidationError from trytond.model import fields @@ -110,6 +112,17 @@ class FieldBinaryTestCase(unittest.TestCase): self.assertEqual(binary.binary, None) self.assertEqual(binary.binary_id, None) + @with_transaction() + def test_create_with_sql_value(self): + "Test create binary with SQL value" + Binary = Pool().get('test.binary') + + binary, = Binary.create([{ + 'binary': Literal('foo'), + }]) + + self.assertEqual(binary.binary, cast(b'foo')) + @with_transaction() def test_read_size(self): "Test read binary size" diff --git a/trytond/tests/test_field_boolean.py b/trytond/tests/test_field_boolean.py index 08637851fd..edc2969021 100644 --- a/trytond/tests/test_field_boolean.py +++ b/trytond/tests/test_field_boolean.py @@ -2,6 +2,8 @@ # this repository contains the full copyright notices and license terms. import unittest +from sql import Literal + from trytond.pool import Pool from trytond.tests.test_tryton import activate_module, with_transaction @@ -48,6 +50,15 @@ class FieldBooleanTestCase(unittest.TestCase): self.assertEqual(boolean.boolean, True) + @with_transaction() + def test_create_with_sql_value(self): + "Test create boolean with SQL value" + Boolean = Pool().get('test.boolean') + + boolean, = Boolean.create([{'boolean': Literal(True)}]) + + self.assertEqual(boolean.boolean, True) + @with_transaction() def test_search_equals_true(self): "Test search boolean equals True" diff --git a/trytond/tests/test_field_char.py b/trytond/tests/test_field_char.py index 4497575c35..c751ef6795 100644 --- a/trytond/tests/test_field_char.py +++ b/trytond/tests/test_field_char.py @@ -3,6 +3,8 @@ # this repository contains the full copyright notices and license terms. import unittest +from sql import Literal + from trytond import backend from trytond.model.exceptions import RequiredValidationError from trytond.pool import Pool @@ -37,6 +39,15 @@ class CommonTestCaseMixin: self.assertEqual(char.char, "é") + @with_transaction() + def test_create_with_sql_value(self): + "Test create with SQL value" + Char = self.Char() + + char, = Char.create([{'char': Literal('Foo')}]) + + self.assertEqual(char.char, "Foo") + @with_transaction() def test_search_equals(self): "Test search char equals" diff --git a/trytond/tests/test_field_date.py b/trytond/tests/test_field_date.py index 07ef06fb6a..ffd4f17b41 100644 --- a/trytond/tests/test_field_date.py +++ b/trytond/tests/test_field_date.py @@ -3,6 +3,8 @@ import datetime import unittest +from sql.functions import CurrentDate + from trytond.model.exceptions import RequiredValidationError from trytond.pool import Pool from trytond.tests.test_tryton import activate_module, with_transaction @@ -73,6 +75,15 @@ class FieldDateTestCase(unittest.TestCase): self.assertEqual(date.date, default_date) + @with_transaction() + def test_create_with_sql_value(self): + "Test create date with SQL value" + Date = Pool().get('test.date') + + date, = Date.create([{'date': CurrentDate()}]) + + self.assert_(date.date) + @with_transaction() def test_create_non_date(self): "Test create date with non date" @@ -88,7 +99,7 @@ class FieldDateTestCase(unittest.TestCase): "Test create date with integer" Date = Pool().get('test.date') - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Date.create([{ 'date': 42, }]) @@ -413,7 +424,7 @@ class FieldDateTestCase(unittest.TestCase): 'date': today, }]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Date.write([date], { 'date': 42, }) diff --git a/trytond/tests/test_field_datetime.py b/trytond/tests/test_field_datetime.py index 23d0e82c76..335499d151 100644 --- a/trytond/tests/test_field_datetime.py +++ b/trytond/tests/test_field_datetime.py @@ -3,6 +3,8 @@ import datetime import unittest +from sql.functions import CurrentTimestamp + from trytond.model.exceptions import ( RequiredValidationError, TimeFormatValidationError) from trytond.pool import Pool @@ -74,6 +76,17 @@ class FieldDateTimeTestCase(unittest.TestCase): self.assertEqual(datetime.datetime, default_datetime) + @with_transaction() + def test_create_with_sql_value(self): + "Test create datetime with SQL value" + DateTime = Pool().get('test.datetime') + + datetime, = DateTime.create([{ + 'datetime': DateTime.datetime.sql_cast(CurrentTimestamp()), + }]) + + self.assert_(datetime.datetime) + @with_transaction() def test_create_non_datetime(self): "Test create datetime with non datetime" @@ -89,7 +102,7 @@ class FieldDateTimeTestCase(unittest.TestCase): "Test create datetime with integer" DateTime = Pool().get('test.datetime') - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): DateTime.create([{ 'datetime': 42, }]) @@ -99,7 +112,7 @@ class FieldDateTimeTestCase(unittest.TestCase): "Test create datetime with date" DateTime = Pool().get('test.datetime') - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): DateTime.create([{ 'datetime': datetime.date(2009, 1, 1), }]) @@ -447,7 +460,7 @@ class FieldDateTimeTestCase(unittest.TestCase): 'datetime': today, }]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): DateTime.write([datetime], { 'datetime': 42, }) @@ -460,7 +473,7 @@ class FieldDateTimeTestCase(unittest.TestCase): 'datetime': today, }]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): DateTime.write([datetime_], { 'datetime': datetime.date(2009, 1, 1), }) diff --git a/trytond/tests/test_field_float.py b/trytond/tests/test_field_float.py index c92e6e3d23..a027c20876 100644 --- a/trytond/tests/test_field_float.py +++ b/trytond/tests/test_field_float.py @@ -2,6 +2,8 @@ # this repository contains the full copyright notices and license terms. import unittest +from sql import Literal + from trytond.model.exceptions import ( RequiredValidationError, DigitsValidationError) from trytond.pool import Pool @@ -58,6 +60,15 @@ class FieldFloatTestCase(unittest.TestCase): self.assertEqual(float_.float, 5.5) + @with_transaction() + def test_create_with_sql_value(self): + "Test create float with SQL value" + Float = Pool().get('test.float') + + float_, = Float.create([{'float': Literal(5.) / Literal(2.)}]) + + self.assertEqual(float_.float, 2.5) + @with_transaction() def test_create_non_float(self): "Test create float with non float" diff --git a/trytond/tests/test_field_integer.py b/trytond/tests/test_field_integer.py index ad00490b1a..48078691d6 100644 --- a/trytond/tests/test_field_integer.py +++ b/trytond/tests/test_field_integer.py @@ -2,6 +2,8 @@ # this repository contains the full copyright notices and license terms. import unittest +from sql import Literal + from trytond.model.exceptions import ( RequiredValidationError, DomainValidationError) from trytond.pool import Pool @@ -47,6 +49,15 @@ class FieldIntegerTestCase(unittest.TestCase): self.assertEqual(integer.integer, 5) + @with_transaction() + def test_create_with_sql_value(self): + "Test create integer with SQL value" + Integer = Pool().get('test.integer_default') + + integer, = Integer.create([{'integer': Literal(5)}]) + + self.assertEqual(integer.integer, 5) + @with_transaction() def test_create_non_integer(self): "Test create integer with non integer" diff --git a/trytond/tests/test_field_numeric.py b/trytond/tests/test_field_numeric.py index 5f411afe36..4e9f79c266 100644 --- a/trytond/tests/test_field_numeric.py +++ b/trytond/tests/test_field_numeric.py @@ -3,6 +3,8 @@ import unittest from decimal import Decimal, InvalidOperation +from sql import Literal + from trytond.model.exceptions import ( RequiredValidationError, DigitsValidationError) from trytond.pool import Pool @@ -59,6 +61,15 @@ class FieldNumericTestCase(unittest.TestCase): self.assertEqual(numeric.numeric, Decimal('5.5')) + @with_transaction() + def test_create_with_sql_value(self): + "Test create numeric with SQL value" + Numeric = Pool().get('test.numeric') + + numeric, = Numeric.create([{'numeric': Literal(5.) / Literal(2.)}]) + + self.assertEqual(numeric.numeric, Decimal('2.5')) + @with_transaction() def test_create_non_numeric(self): "Test create numeric with non numeric" diff --git a/trytond/tests/test_field_time.py b/trytond/tests/test_field_time.py index ffb566d590..c838de1cb9 100644 --- a/trytond/tests/test_field_time.py +++ b/trytond/tests/test_field_time.py @@ -3,6 +3,8 @@ import datetime import unittest +from sql.functions import CurrentTimestamp + from trytond.model.exceptions import ( RequiredValidationError, TimeFormatValidationError) from trytond.pool import Pool @@ -74,6 +76,15 @@ class FieldTimeTestCase(unittest.TestCase): self.assertEqual(time.time, default_time) + @with_transaction() + def test_create_with_sql_value(self): + "Test create time with SQL value" + Time = Pool().get('test.time_precision') + + time, = Time.create([{'time': Time.time.sql_cast(CurrentTimestamp())}]) + + self.assert_(time.time) + @with_transaction() def test_create_non_time(self): "Test create time with non time" @@ -99,7 +110,7 @@ class FieldTimeTestCase(unittest.TestCase): "Test create time with date" Time = Pool().get('test.time') - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Time.create([{ 'time': datetime.date(2009, 1, 1), }]) @@ -460,7 +471,7 @@ class FieldTimeTestCase(unittest.TestCase): 'time': evening, }]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): Time.write([time], { 'time': datetime.date(2009, 1, 1), }) diff --git a/trytond/tests/test_field_timedelta.py b/trytond/tests/test_field_timedelta.py index b6b9d2dd55..238ed4f5c2 100644 --- a/trytond/tests/test_field_timedelta.py +++ b/trytond/tests/test_field_timedelta.py @@ -57,7 +57,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase): "Test create timedelta with non timedelta" TimeDelta = Pool().get('test.timedelta') - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): TimeDelta.create([{ 'timedelta': 'non timedelta', }]) @@ -67,7 +67,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase): "Test create timedelta with integer" TimeDelta = Pool().get('test.timedelta') - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): TimeDelta.create([{ 'timedelta': 42, }]) @@ -369,7 +369,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase): 'timedelta': hour, }]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): TimeDelta.write([timedelta], { 'timedelta': 'non timedelta', }) @@ -382,7 +382,7 @@ class FieldTimeDeltaTestCase(unittest.TestCase): 'timedelta': hour, }]) - with self.assertRaises(ValueError): + with self.assertRaises(TypeError): TimeDelta.write([timedelta], { 'timedelta': 42, })