diff --git a/CHANGELOG b/CHANGELOG
index 6ffa3e82d7..5299d49c90 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+* Remove float_time widget
+* Add TimeDelta field
* search_global yields record instead of id
* Add ModelTestCase
* Add test for missing default model access
diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst
index 2e1de08af1..e0efe48431 100644
--- a/doc/ref/models/fields.rst
+++ b/doc/ref/models/fields.rst
@@ -364,6 +364,20 @@ A time, represented in Python by a ``datetime.time`` instance.
Same as :attr:`DateTime.format`
+TimeDelta
+---------
+
+.. class::TimeDelta(string[, converter[, \**options]])
+
+An interval, represented in Python by a ``datetime.timedelta`` instance.
+
+.. attribute:: TimeDelta.converter
+
+ The name of the context key containing the time converter.
+ A time converter is a dictionary with the keys: ``s`` (second), ``m``
+ (minute), ``h`` (hour), ``d`` (day), ``w`` (week), ``M`` (month), ``Y``
+ (year) and the value in second.
+
Binary
------
diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py
index 16811f685d..4426ec0236 100644
--- a/trytond/backend/postgresql/database.py
+++ b/trytond/backend/postgresql/database.py
@@ -9,9 +9,9 @@ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from psycopg2.extensions import register_type, register_adapter
from psycopg2.extensions import UNICODE, AsIs
try:
- from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME
+ from psycopg2.extensions import PYDATE, PYDATETIME, PYTIME, PYINTERVAL
except ImportError:
- PYDATE, PYDATETIME, PYTIME = None, None, None
+ PYDATE, PYDATETIME, PYTIME, PYINTERVAL = None, None, None, None
from psycopg2 import IntegrityError as DatabaseIntegrityError
from psycopg2 import OperationalError as DatabaseOperationalError
import time
@@ -379,5 +379,7 @@ if PYDATETIME:
register_type(PYDATETIME)
if PYTIME:
register_type(PYTIME)
+if PYINTERVAL:
+ register_type(PYINTERVAL)
register_adapter(float, lambda value: AsIs(repr(value)))
register_adapter(Decimal, lambda value: AsIs(str(value)))
diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py
index 83006faeeb..f117ee9798 100644
--- a/trytond/backend/sqlite/database.py
+++ b/trytond/backend/sqlite/database.py
@@ -419,3 +419,6 @@ sqlite.register_adapter(datetime.datetime, adapt_datetime)
sqlite.register_adapter(datetime.time, lambda val: val.isoformat())
sqlite.register_converter('TIME', lambda val: datetime.time(*map(int,
val.split(':'))))
+sqlite.register_adapter(datetime.timedelta, lambda val: val.total_seconds())
+sqlite.register_converter('INTERVAL', lambda val: datetime.timedelta(
+ seconds=float(val)))
diff --git a/trytond/ir/trigger.py b/trytond/ir/trigger.py
index 29359868a2..f83729512b 100644
--- a/trytond/ir/trigger.py
+++ b/trytond/ir/trigger.py
@@ -2,7 +2,7 @@
# this repository contains the full copyright notices and license terms.
import datetime
import time
-from sql import Literal
+from sql import Literal, Null
from sql.aggregate import Count, Max
from ..model import ModelView, ModelSQL, fields
@@ -45,9 +45,10 @@ class Trigger(ModelSQL, ModelView):
limit_number = fields.Integer('Limit Number', required=True,
help='Limit the number of call to "Action Function" by records.\n'
'0 for no limit.')
- minimum_delay = fields.Float('Minimum Delay', help='Set a minimum delay '
- 'in minutes between call to "Action Function" for the same record.\n'
- '0 for no delay.')
+ minimum_time_delay = fields.TimeDelta('Minimum Delay',
+ help='Set a minimum time delay between call to "Action Function" '
+ 'for the same record.\n'
+ 'empty for no delay.')
action_model = fields.Many2One('ir.model', 'Action Model', required=True)
action_function = fields.Char('Action Function', required=True)
_get_triggers_cache = Cache('ir_trigger.get_triggers')
@@ -66,6 +67,29 @@ class Trigger(ModelSQL, ModelView):
})
cls._order.insert(0, ('name', 'ASC'))
+ @classmethod
+ def __register__(cls, module_name):
+ TableHandler = backend.get('TableHandler')
+ cursor = Transaction().cursor
+ table = TableHandler(cursor, cls, module_name)
+ sql_table = cls.__table__()
+
+ super(Trigger, cls).__register__(module_name)
+
+ # Migration from 3.4:
+ # change minimum_delay into timedelta minimu_time_delay
+ if table.column_exist('minimum_delay'):
+ cursor.execute(*sql_table.select(
+ sql_table.id, sql_table.minimum_delay,
+ where=sql_table.minimum_delay != Null))
+ for id_, delay in cursor.fetchall():
+ delay = datetime.timedelta(hours=delay)
+ cursor.execute(*sql_table.update(
+ [sql_table.minimu_time_delay],
+ [delay],
+ where=sql_table.id == id_))
+ table.drop_column('minimum_delay')
+
@classmethod
def validate(cls, triggers):
super(Trigger, cls).validate(triggers)
@@ -182,8 +206,8 @@ class Trigger(ModelSQL, ModelView):
new_ids.append(record_id)
ids = new_ids
- # Filter on minimum_delay
- if trigger.minimum_delay:
+ # Filter on minimum_time_delay
+ if trigger.minimum_time_delay:
new_ids = []
for sub_ids in grouped_slice(ids):
sub_ids = list(sub_ids)
@@ -211,15 +235,14 @@ class Trigger(ModelSQL, ModelView):
delay[record_id] = datetime.datetime(year, month, day,
hours, minutes, seconds, microseconds)
if (datetime.datetime.now() - delay[record_id]
- >= datetime.timedelta(
- minutes=trigger.minimum_delay)):
+ >= trigger.minimum_time_delay):
new_ids.append(record_id)
ids = new_ids
records = Model.browse(ids)
if records:
getattr(ActionModel, trigger.action_function)(records, trigger)
- if trigger.limit_number or trigger.minimum_delay:
+ if trigger.limit_number or trigger.minimum_time_delay:
to_create = []
for record in records:
to_create.append({
diff --git a/trytond/ir/ui/form.rnc b/trytond/ir/ui/form.rnc
index dc953606e1..00fdc962a5 100644
--- a/trytond/ir/ui/form.rnc
+++ b/trytond/ir/ui/form.rnc
@@ -49,7 +49,7 @@ attlist.field &=
| "selection"
| "char"
| "password"
- | "float_time"
+ | "timedelta"
| "boolean"
| "reference"
| "binary"
@@ -98,7 +98,6 @@ attlist.field &=
| "top_to_bottom"
}?
attlist.field &= attribute spell { text }?
-attlist.field &= attribute float_time { text }?
attlist.field &=
[a:defaultValue = "0"] attribute filename_visible { "0" | "1" }?
attlist.field &= [a:defaultValue = "0"] attribute pre_validate { "0" | "1" }?
diff --git a/trytond/ir/ui/form.rng b/trytond/ir/ui/form.rng
index 171569b268..8b680158a0 100644
--- a/trytond/ir/ui/form.rng
+++ b/trytond/ir/ui/form.rng
@@ -148,7 +148,7 @@
selection
char
password
- float_time
+ timedelta
boolean
reference
binary
@@ -327,11 +327,6 @@
-
-
-
-
-
diff --git a/trytond/ir/ui/graph.rnc b/trytond/ir/ui/graph.rnc
index 3a74cfc563..9373be300a 100644
--- a/trytond/ir/ui/graph.rnc
+++ b/trytond/ir/ui/graph.rnc
@@ -23,8 +23,7 @@ attlist.field &= attribute domain { text }?
attlist.field &= attribute fill { "0" | "1" }?
attlist.field &= attribute empty { "0" | "1" }?
attlist.field &= attribute color { text }?
-attlist.field &= attribute widget { "float_time" }?
-attlist.field &= attribute float_time { text }?
+attlist.field &= attribute timedelta { text }?
attlist.field &=
[ a:defaultValue = "linear" ]
attribute interpolation { "constant-left" | "constant-right" | "constant-center" | "linear" }?
diff --git a/trytond/ir/ui/graph.rng b/trytond/ir/ui/graph.rng
index 125c49c339..eb55457bf0 100644
--- a/trytond/ir/ui/graph.rng
+++ b/trytond/ir/ui/graph.rng
@@ -115,14 +115,7 @@
-
- float_time
-
-
-
-
-
-
+
diff --git a/trytond/ir/ui/tree.rnc b/trytond/ir/ui/tree.rnc
index d24e8e54dd..e42ec0d214 100644
--- a/trytond/ir/ui/tree.rnc
+++ b/trytond/ir/ui/tree.rnc
@@ -27,7 +27,7 @@ attlist.field &=
| "selection"
| "float"
| "numeric"
- | "float_time"
+ | "timedelta"
| "integer"
| "datetime"
| "time"
@@ -58,7 +58,6 @@ attlist.field &=
| "bottom_to_top"
| "top_to_bottom"
}?
-attlist.field &= attribute float_time { text }?
attlist.field &= [a:defaultValue = "0"] attribute pre_validate { "0" | "1" }?
attlist.field &= [a:defaultValue = "1"] attribute completion { "0" | "1" }?
attlist.field &= attribute string { text }?
diff --git a/trytond/ir/ui/tree.rng b/trytond/ir/ui/tree.rng
index 117f89b1fc..44a5d3c96d 100644
--- a/trytond/ir/ui/tree.rng
+++ b/trytond/ir/ui/tree.rng
@@ -97,7 +97,7 @@
selection
float
numeric
- float_time
+ timedelta
integer
datetime
time
@@ -168,11 +168,6 @@
-
-
-
-
-
diff --git a/trytond/ir/view/trigger_form.xml b/trytond/ir/view/trigger_form.xml
index 5301e5953f..5077179304 100644
--- a/trytond/ir/view/trigger_form.xml
+++ b/trytond/ir/view/trigger_form.xml
@@ -22,8 +22,8 @@ this repository contains the full copyright notices and license terms. -->
-
-
+
+
diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py
index cfc50ffe9b..5eeff08e01 100644
--- a/trytond/model/fields/date.py
+++ b/trytond/model/fields/date.py
@@ -134,3 +134,36 @@ class Time(DateTime):
def sql_type(self):
return SQLType('TIME', 'TIME')
+
+
+class TimeDelta(Field):
+ '''
+ Define a timedelta field (``timedelta``).
+ '''
+ _type = 'timedelta'
+
+ def __init__(self, string='', converter=None, 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 converter: The name of the context key containing
+ the time converter.
+ '''
+ super(TimeDelta, self).__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.converter = converter
+
+ @staticmethod
+ def sql_format(value):
+ if isinstance(value, (Query, Expression)):
+ return value
+ if value is None:
+ return None
+ assert(isinstance(value, datetime.timedelta))
+ return value
+
+ def sql_type(self):
+ return SQLType('INTERVAL', 'INTERVAL')
diff --git a/trytond/model/model.py b/trytond/model/model.py
index 3ad1356b2d..47569097a0 100644
--- a/trytond/model/model.py
+++ b/trytond/model/model.py
@@ -212,6 +212,7 @@ class Model(WarningErrorMixin, URLMixin, PoolBase):
'filename',
'selection_change_with',
'domain',
+ 'converter',
):
if getattr(cls._fields[field], arg, None) is not None:
value = getattr(cls._fields[field], arg)
diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py
index 836ded3bbd..39dd50bd75 100644
--- a/trytond/protocols/jsonrpc.py
+++ b/trytond/protocols/jsonrpc.py
@@ -55,6 +55,8 @@ JSONDecoder.register('date',
JSONDecoder.register('time',
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
dct['microsecond']))
+JSONDecoder.register('timedelta',
+ lambda dct: datetime.timedelta(seconds=dct['seconds']))
JSONDecoder.register('buffer', lambda dct:
buffer(base64.decodestring(dct['base64'])))
JSONDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
@@ -105,6 +107,11 @@ JSONEncoder.register(datetime.time,
'second': o.second,
'microsecond': o.microsecond,
})
+JSONEncoder.register(datetime.timedelta,
+ lambda o: {
+ '__class__': 'timedelta',
+ 'seconds': o.total_seconds(),
+ })
JSONEncoder.register(buffer,
lambda o: {
'__class__': 'buffer',
diff --git a/trytond/protocols/xmlrpc.py b/trytond/protocols/xmlrpc.py
index 590bba5d45..928d340107 100644
--- a/trytond/protocols/xmlrpc.py
+++ b/trytond/protocols/xmlrpc.py
@@ -54,11 +54,19 @@ def dump_time(self, value, write):
}
self.dump_struct(value, write)
+
+def dump_timedelta(self, value, write):
+ value = {'__class__': 'timedelta',
+ 'seconds': value.total_seconds(),
+ }
+ self.dump_struct(value, write)
+
xmlrpclib.Marshaller.dispatch[Decimal] = dump_decimal
xmlrpclib.Marshaller.dispatch[type(None)] = \
lambda self, value, write: write("")
xmlrpclib.Marshaller.dispatch[datetime.date] = dump_date
xmlrpclib.Marshaller.dispatch[datetime.time] = dump_time
+xmlrpclib.Marshaller.dispatch[datetime.timedelta] = dump_timedelta
xmlrpclib.Marshaller.dispatch[buffer] = dump_buffer
@@ -94,6 +102,8 @@ XMLRPCDecoder.register('date',
XMLRPCDecoder.register('time',
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
dct['microsecond']))
+XMLRPCDecoder.register('timedelta',
+ lambda dct: datetime.timedelta(seconds=dct['seconds']))
XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
diff --git a/trytond/tests/__init__.py b/trytond/tests/__init__.py
index 9eb4c5bcac..100d972254 100644
--- a/trytond/tests/__init__.py
+++ b/trytond/tests/__init__.py
@@ -54,6 +54,9 @@ def register():
TimeDefault,
TimeRequired,
TimeFormat,
+ TimeDelta,
+ TimeDeltaDefault,
+ TimeDeltaRequired,
One2One,
One2OneTarget,
One2OneRelation,
diff --git a/trytond/tests/test.py b/trytond/tests/test.py
index cd73456283..3eb21cc4c4 100644
--- a/trytond/tests/test.py
+++ b/trytond/tests/test.py
@@ -16,6 +16,7 @@ __all__ = [
'Date', 'DateDefault', 'DateRequired',
'DateTime', 'DateTimeDefault', 'DateTimeRequired', 'DateTimeFormat',
'Time', 'TimeDefault', 'TimeRequired', 'TimeFormat',
+ 'TimeDelta', 'TimeDeltaDefault', 'TimeDeltaRequired',
'One2One', 'One2OneTarget', 'One2OneRelation', 'One2OneRequired',
'One2OneRequiredRelation', 'One2OneDomain', 'One2OneDomainRelation',
'One2Many', 'One2ManyTarget',
@@ -315,6 +316,31 @@ class TimeFormat(ModelSQL):
time = fields.Time(string='Time', format='%H:%M')
+class TimeDelta(ModelSQL):
+ 'TimeDelta'
+ __name__ = 'test.timedelta'
+ timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
+ required=False)
+
+
+class TimeDeltaDefault(ModelSQL):
+ 'TimeDelta Default'
+ __name__ = 'test.timedelta_default'
+ timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
+ required=False)
+
+ @staticmethod
+ def default_timedelta():
+ return datetime.timedelta(seconds=3600)
+
+
+class TimeDeltaRequired(ModelSQL):
+ 'TimeDelta Required'
+ __name__ = 'test.timedelta_required'
+ timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta',
+ required=True)
+
+
class One2One(ModelSQL):
'One2One'
__name__ = 'test.one2one'
diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py
index 2c6afeb4de..8ff8ca9920 100644
--- a/trytond/tests/test_fields.py
+++ b/trytond/tests/test_fields.py
@@ -67,6 +67,10 @@ class FieldsTestCase(unittest.TestCase):
self.time_required = POOL.get('test.time_required')
self.time_format = POOL.get('test.time_format')
+ self.timedelta = POOL.get('test.timedelta')
+ self.timedelta_default = POOL.get('test.timedelta_default')
+ self.timedelta_required = POOL.get('test.timedelta_required')
+
self.one2one = POOL.get('test.one2one')
self.one2one_target = POOL.get('test.one2one.target')
self.one2one_required = POOL.get('test.one2one_required')
@@ -3258,6 +3262,222 @@ class FieldsTestCase(unittest.TestCase):
transaction.cursor.rollback()
+ def test0200timedelta(self):
+ 'Test timedelta'
+ with Transaction().start(DB_NAME, USER,
+ context=CONTEXT) as transaction:
+
+ minute = datetime.timedelta(minutes=1)
+ hour = datetime.timedelta(hours=1)
+ day = datetime.timedelta(days=1)
+ default_timedelta = datetime.timedelta(seconds=3600)
+
+ timedelta1, = self.timedelta.create([{
+ 'timedelta': hour,
+ }])
+ self.assert_(timedelta1)
+ self.assertEqual(timedelta1.timedelta, hour)
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', hour),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', day),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', None),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '!=', day),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '!=', None),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [hour]),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [day]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [minute]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [None]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', []),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [hour]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [day]),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [None]),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', []),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<', day),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<', minute),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<', hour),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<=', hour),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<=', minute),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '<=', day),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>', day),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>', minute),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>', hour),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>=', day),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>=', minute),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '>=', hour),
+ ])
+ self.assertEqual(timedelta, [timedelta1])
+
+ timedelta2, = self.timedelta.create([{
+ 'timedelta': minute,
+ }])
+ self.assert_(timedelta2)
+ self.assertEqual(timedelta2.timedelta, minute)
+
+ timedelta = self.timedelta.search([
+ ('timedelta', '=', minute),
+ ])
+ self.assertEqual(timedelta, [timedelta2])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'in', [minute, hour]),
+ ])
+ self.assertEqual(timedelta, [timedelta1, timedelta2])
+
+ timedelta = self.timedelta.search([
+ ('timedelta', 'not in', [minute, hour]),
+ ])
+ self.assertEqual(timedelta, [])
+
+ timedelta3, = self.timedelta.create([{}])
+ self.assert_(timedelta3)
+ self.assertEqual(timedelta3.timedelta, None)
+
+ timedelta4, = self.timedelta_default.create([{}])
+ self.assert_(timedelta4)
+ self.assertEqual(timedelta4.timedelta, default_timedelta)
+
+ self.timedelta.write([timedelta1], {
+ 'timedelta': minute,
+ })
+ self.assertEqual(timedelta1.timedelta, minute)
+
+ self.timedelta.write([timedelta2], {
+ 'timedelta': day,
+ })
+ self.assertEqual(timedelta2.timedelta, day)
+
+ self.assertRaises(Exception, self.timedelta.create, [{
+ 'timedelta': 'test',
+ }])
+
+ self.assertRaises(Exception, self.timedelta.write, [timedelta1], {
+ 'timedelta': 'test',
+ })
+
+ self.assertRaises(Exception, self.timedelta.create, [{
+ 'timedelta': 1,
+ }])
+
+ self.assertRaises(Exception, self.timedelta.write, [timedelta1], {
+ 'timedelta': 1,
+ })
+
+ self.assertRaises(UserError, self.timedelta_required.create, [{}])
+ transaction.cursor.rollback()
+
+ timedelta6, = self.timedelta_required.create([{
+ 'timedelta': day,
+ }])
+ self.assert_(timedelta6)
+
+ timedelta7, = self.timedelta.create([{
+ 'timedelta': None,
+ }])
+ self.assert_(timedelta7)
+
+ transaction.cursor.rollback()
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(FieldsTestCase)