diff --git a/CHANGELOG b/CHANGELOG
index 5911fc8595..c87d99f6b7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,4 @@
+* Add time format validation
* Remove ModelSQL.default_sequence
Version 2.4.0 - 2012-04-23
diff --git a/doc/ref/models/fields.rst b/doc/ref/models/fields.rst
index f66c4a8539..82695ec1b3 100644
--- a/doc/ref/models/fields.rst
+++ b/doc/ref/models/fields.rst
@@ -272,17 +272,27 @@ A date, represented in Python by a ``datetime.date`` instance.
DateTime
--------
-.. class:: DateTime(string[, \**options])
+.. class:: DateTime(string[, format, \**options])
A date and time, represented in Python by a ``datetime.datetime`` instance.
+.. attribute:: DateTime.format
+
+ A string format as used by strftime. This format will be used to display
+ the time part of the field. The default value is `%H:%M:%S`.
+ The value can be replaced by a :class:`~trytond.pyson.PYSON` statement.
+
Time
----
-.. class:: Time(string[, \**options])
+.. class:: Time(string[, format, \**options])
A time, represented in Python by a ``datetime.time`` instance.
+.. attribute:: Time.format
+
+ Same as :attr:`DateTime.format`
+
Binary
------
diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml
index d9ea0627af..fc852bccf4 100644
--- a/trytond/ir/translation.xml
+++ b/trytond/ir/translation.xml
@@ -177,6 +177,15 @@ this repository contains the full copyright notices and license terms. -->
ir
+
+ time_format_validation_record
+ en_US
+ error
+ The time value of field "%s" on "%s" is not valid.
+ The time value of field "%s" on "%s" is not valid.
+ ir
+
+
access_error
en_US
diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py
index 5301fcc3ac..7102d38f0f 100644
--- a/trytond/model/fields/date.py
+++ b/trytond/model/fields/date.py
@@ -17,8 +17,24 @@ class DateTime(Field):
'''
_type = 'datetime'
+ 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,
+ order_field=None, context=None, loading='eager'):
+ '''
+ :param format: The validation format as used by strftime.
+ '''
+ super(DateTime, 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, order_field=order_field, context=context,
+ loading=loading)
+ self.format = format
-class Time(Field):
+ __init__.__doc__ += Field.__init__.__doc__
+
+
+class Time(DateTime):
'''
Define a time field (``time``).
'''
diff --git a/trytond/model/model.py b/trytond/model/model.py
index 9ee98eb040..5d2df7a37d 100644
--- a/trytond/model/model.py
+++ b/trytond/model/model.py
@@ -554,10 +554,12 @@ class Model(WarningErrorMixin, URLMixin):
and hasattr(self._columns[field], 'field'):
res[field]['relation_field'] = copy.copy(
self._columns[field].field)
+ if res[field]['type'] in ('datetime', 'time'):
+ res[field]['format'] = copy.copy(self._columns[field].format)
# convert attributes into pyson
for attr in ('states', 'domain', 'context', 'digits',
- 'add_remove'):
+ 'add_remove', 'format'):
if attr in res[field]:
res[field][attr] = encoder.encode(res[field][attr])
diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py
index 6a09b4a765..8c8e1d19c3 100644
--- a/trytond/model/modelstorage.py
+++ b/trytond/model/modelstorage.py
@@ -19,7 +19,7 @@ from trytond.model import fields
from trytond.model.browse import BrowseRecordList, BrowseRecord, \
BrowseRecordNull
from trytond.model.browse import EvalEnvironment
-from trytond.tools import safe_eval, reduce_domain
+from trytond.tools import safe_eval, reduce_domain, datetime_strftime
from trytond.pyson import PYSONEncoder, PYSONDecoder, PYSON
from trytond.const import OPERATORS, RECORD_CACHE_SIZE
from trytond.transaction import Transaction
@@ -1132,6 +1132,35 @@ class ModelStorage(Model):
digits_test(record[field_name], field.digits,
field_name)
+ def format_test(value, format, field_name):
+ if not value:
+ return
+ if not isinstance(value, datetime.time):
+ value = value.time()
+ if value != datetime.datetime.strptime(
+ value.strftime(format), format).time():
+ self.raise_user_error('time_format_validation_record',
+ error_args=self._get_error_args(field_name))
+
+ # validate time format
+ if (field._type in ('datetime', 'time')
+ and field_name not in ('create_date', 'write_date')):
+ if is_pyson(field.format):
+ pyson_format = PYSONDecoder().encode(field.format)
+ for record in records:
+ env = EvalEnvironment(record, self)
+ env.update(Transaction().context)
+ env['current_date'] = datetime.datetime.today()
+ env['time'] = time
+ env['context'] = Transaction().context
+ env['active_id'] = record.id
+ format = PYSONDecoder(env).decode(pyson_format)
+ format_test(record[field_name], format, field_name)
+ else:
+ for record in records:
+ format_test(record[field_name], field.format,
+ field_name)
+
def _clean_defaults(self, defaults):
pool = Pool()
vals = {}
diff --git a/trytond/test/test.py b/trytond/test/test.py
index bb83747de3..b7961247ee 100644
--- a/trytond/test/test.py
+++ b/trytond/test/test.py
@@ -356,6 +356,15 @@ class DateTimeRequired(ModelSQL):
DateTimeRequired()
+class DateTimeFormat(ModelSQL):
+ 'DateTime Format'
+ _name = 'test.datetime_format'
+ _description = __doc__
+ datetime = fields.DateTime(string='DateTime', format='%H:%M')
+
+DateTimeFormat()
+
+
class Time(ModelSQL):
'Time'
_name = 'test.time'
@@ -386,6 +395,15 @@ class TimeRequired(ModelSQL):
TimeRequired()
+class TimeFormat(ModelSQL):
+ 'Time Format'
+ _name = 'test.time_format'
+ _description = __doc__
+ time = fields.Time(string='Time', format='%H:%M')
+
+TimeFormat()
+
+
class One2One(ModelSQL):
'One2One'
_name = 'test.one2one'
diff --git a/trytond/tests/test_fields.py b/trytond/tests/test_fields.py
index 8412fff41d..9fb25511c1 100644
--- a/trytond/tests/test_fields.py
+++ b/trytond/tests/test_fields.py
@@ -65,10 +65,12 @@ class FieldsTestCase(unittest.TestCase):
self.datetime = POOL.get('test.datetime')
self.datetime_default = POOL.get('test.datetime_default')
self.datetime_required = POOL.get('test.datetime_required')
+ self.datetime_format = POOL.get('test.datetime_format')
self.time = POOL.get('test.time')
self.time_default = POOL.get('test.time_default')
self.time_required = POOL.get('test.time_required')
+ self.time_format = POOL.get('test.time_format')
self.one2one = POOL.get('test.one2one')
self.one2one_target = POOL.get('test.one2one.target')
@@ -2059,6 +2061,14 @@ class FieldsTestCase(unittest.TestCase):
datetime9 = self.datetime.read(datetime9_id, ['datetime'])
self.assert_(datetime9['datetime'] == today)
+ # Test format
+ self.assert_(self.datetime_format.create({
+ 'datetime': datetime.datetime(2009, 1, 1, 12, 30),
+ }))
+ self.failUnlessRaises(Exception, self.datetime_format.create, {
+ 'datetime': datetime.datetime(2009, 1, 1, 12, 30, 25),
+ })
+
transaction.cursor.rollback()
def test0100time(self):
@@ -2312,6 +2322,14 @@ class FieldsTestCase(unittest.TestCase):
time9 = self.time.read(time9_id, ['time'])
self.assert_(time9['time'] == evening)
+ # Test format
+ self.assert_(self.time_format.create({
+ 'time': datetime.time(12, 30),
+ }))
+ self.failUnlessRaises(Exception, self.time_format.create, {
+ 'time': datetime.time(12, 30, 25),
+ })
+
transaction.cursor.rollback()
def test0110one2one(self):