details: https://code.tryton.org/tryton/commit/6952308a8b6b
branch: default
user: Nicolas Évrard <[email protected]>
date: Tue Apr 14 18:29:42 2026 +0200
description:
Parse equality to a date as two clauses when handling datetime fields
Closes #14778
diffstat:
sao/src/common.js | 26 ++++++++++++++++++
sao/tests/sao.js | 34 ++++++++++++++++++++++++
tryton/tryton/common/domain_parser.py | 20 ++++++++++++++
tryton/tryton/tests/test_common_domain_parser.py | 29 ++++++++++++++++++++
4 files changed, 109 insertions(+), 0 deletions(-)
diffs (170 lines):
diff -r a28800bfd0f1 -r 6952308a8b6b sao/src/common.js
--- a/sao/src/common.js Tue Apr 14 18:20:45 2026 +0200
+++ b/sao/src/common.js Tue Apr 14 18:29:42 2026 +0200
@@ -1951,6 +1951,32 @@
]);
return;
}
+ if ((typeof value == 'string') &&
+ ['datetime', 'timestamp'].includes(field.type) &&
+ (operator == '=')) {
+ let ctx, format_, parsed_date;
+ if (this.context &&
Object.keys(this.context).length) {
+ ctx = this.context;
+ } else {
+ ctx = {};
+ }
+ format_ = Sao.common.date_format(ctx.date_format);
+ parsed_date = Sao.common.parse_date(format_,
value);
+ if (parsed_date &&
+ (Sao.common.format_date(format_, parsed_date)
== value)) {
+ let date = Sao.DateTime.combine(parsed_date,
Sao.Time());
+ let next_day = Sao.DateTime(
+ date.year(), date.month(), date.date(),
+ date.hour(), date.minute(), date.second(),
+ date.millisecond())
+ next_day.add(1, 'day');
+ result.push(this._clausify([
+ [field_name, '>=', date],
+ [field_name, '<', next_day]
+ ]));
+ return;
+ }
+ }
}
if (['many2one', 'one2many', 'many2many', 'one2one',
'many2many', 'one2one'].includes(field.type) && value)
{
diff -r a28800bfd0f1 -r 6952308a8b6b sao/tests/sao.js
--- a/sao/tests/sao.js Tue Apr 14 18:20:45 2026 +0200
+++ b/sao/tests/sao.js Tue Apr 14 18:29:42 2026 +0200
@@ -1933,6 +1933,12 @@
'name': 'integer',
'type': 'integer'
},
+ 'timestamp': {
+ 'name': 'timestamp',
+ 'string': 'Timestamp',
+ 'type': 'timestamp',
+ 'format': '"%H:%M:%S"',
+ },
'selection': {
'string': 'Selection',
'name': 'selection',
@@ -2033,6 +2039,7 @@
c(['integer', '>=', 3]),
c(['integer', '<=', 5])
]]],
+ [[c(['Timestamp', null, null])], [c(['timestamp', '=', null])]],
[[c(['Reference', null, 'foo'])],
[c(['reference', 'ilike', '%foo%'])]],
[[c(['Reference', null, 'Spam'])],
@@ -2073,6 +2080,33 @@
QUnit.assert.deepEqual(parser.parse_clause(value), result,
'parse_clause(' + JSON.stringify(value) + ')');
});
+
+ let clause = parser.parse_clause([c(
+ ['Timestamp', '=', Sao.common.format_date('%x', Sao.Date(2002, 12,
4))])]);
+ QUnit.assert.strictEqual(clause[0].length, 2);
+ let [ , operator, value] = clause[0][0];
+ QUnit.assert.strictEqual(operator, '>=');
+ QUnit.assert.ok(value.isSame(Sao.Date(2002, 12, 4)));
+ [ , operator, value] = clause[0][1];
+ QUnit.assert.strictEqual(operator, '<');
+ QUnit.assert.ok(value.isSame(Sao.Date(2002, 12, 5)));
+
+ clause = parser.parse_clause([c(
+ ['Timestamp', '=',
+ Sao.common.format_datetime('%x %X', Sao.DateTime(2002, 12, 4,
12, 30))])]);
+ [ , operator, value] = clause[0];
+ QUnit.assert.strictEqual(operator, '=');
+ QUnit.assert.ok(value.isSame(Sao.DateTime(2002, 12, 4, 12, 30)));
+
+ clause = parser.parse_clause([c(
+ ['Timestamp', null, [
+ `${Sao.common.format_date('%x', Sao.Date(2002, 12,
4))}`,
+ `${Sao.common.format_date('%x', Sao.Date(2002, 12,
5))}`,
+ ]])]);
+ [ , operator, value] = clause[0];
+ QUnit.assert.strictEqual(operator, 'in');
+ QUnit.assert.ok(value[0].isSame(Sao.DateTime(2002, 12, 4)));
+ QUnit.assert.ok(value[1].isSame(Sao.DateTime(2002, 12, 5)));
});
QUnit.test('DomainParser.format_value', function() {
diff -r a28800bfd0f1 -r 6952308a8b6b tryton/tryton/common/domain_parser.py
--- a/tryton/tryton/common/domain_parser.py Tue Apr 14 18:20:45 2026 +0200
+++ b/tryton/tryton/common/domain_parser.py Tue Apr 14 18:29:42 2026 +0200
@@ -885,6 +885,26 @@
(field_name, '<=', rvalue),
])
continue
+ if (isinstance(value, str)
+ and field['type'] in {'datetime', 'timestamp'}
+ and operator == '='):
+ ctx = self.context if self.context else {}
+ format_ = date_format(ctx.get('date_format'))
+ try:
+ dt = datetime.datetime.strptime(
+ value, format_).date()
+ except (ValueError, TypeError):
+ dt = None
+ if dt:
+ date = untimezoned_date(
+ datetime.datetime.combine(
+ dt, datetime.time()))
+ next_date = date + datetime.timedelta(days=1)
+ yield iter([
+ (field_name, '>=', date),
+ (field_name, '<', next_date),
+ ])
+ continue
if field['type'] in {
'many2one', 'one2many', 'many2many', 'one2one',
} and value:
diff -r a28800bfd0f1 -r 6952308a8b6b
tryton/tryton/tests/test_common_domain_parser.py
--- a/tryton/tryton/tests/test_common_domain_parser.py Tue Apr 14 18:20:45
2026 +0200
+++ b/tryton/tryton/tests/test_common_domain_parser.py Tue Apr 14 18:29:42
2026 +0200
@@ -943,6 +943,12 @@
'name': 'integer',
'type': 'integer',
},
+ 'timestamp': {
+ 'name': 'timestamp',
+ 'string': 'Timestamp',
+ 'type': 'timestamp',
+ 'format': '"%H:%M:%S"',
+ },
'selection': {
'string': 'Selection',
'name': 'selection',
@@ -1051,6 +1057,29 @@
(
[('Integer', None, '3..5')],
[[('integer', '>=', 3), ('integer', '<=', 5)]]),
+ ([('Timestamp', None, None)], [('timestamp', '=', None)]),
+ (
+ [('Timestamp', '=', dt.date(2002, 12, 4).strftime('%x'))],
+ [[
+ ('timestamp', '>=',
+ untimezoned_date(dt.datetime(2002, 12, 4))),
+ ('timestamp', '<',
+ untimezoned_date(dt.datetime(2002, 12, 5)))]]),
+ (
+ [('Timestamp', '=',
+ dt.datetime(2002, 12, 4, 12, 30).strftime('%x %X'))
+ ],
+ [('timestamp', '=', untimezoned_date(
+ dt.datetime(2002, 12, 4, 12, 30)))]),
+ (
+ [('Timestamp', None, [
+ f"{dt.date(2002, 12, 4).strftime('%x')}",
+ f"{dt.date(2002, 12, 5).strftime('%x')}",
+ ])],
+ [('timestamp', 'in', [
+ untimezoned_date(dt.datetime(2002, 12, 4)),
+ untimezoned_date(dt.datetime(2002, 12, 5)),
+ ])]),
(
[('Reference', None, 'foo')],
[('reference', 'ilike', '%foo%')]),