Parse equality to a date as two clauses when handling datetime fields

Closes #14778
(grafted from 6952308a8b6b65ff583a2b04d9cd558b44c4e4a9)

--HG--
branch : 7.0
This commit is contained in:
Nicolas Évrard
2026-04-14 18:29:42 +02:00
parent b47794d54f
commit 92c2669764
4 changed files with 113 additions and 0 deletions
+26
View File
@@ -1930,6 +1930,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 (value instanceof Array) {
value = value.map(
+34
View File
@@ -1908,6 +1908,12 @@
'name': 'integer',
'type': 'integer'
},
'timestamp': {
'name': 'timestamp',
'string': 'Timestamp',
'type': 'timestamp',
'format': '"%H:%M:%S"',
},
'selection': {
'string': 'Selection',
'name': 'selection',
@@ -1996,6 +2002,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'])],
@@ -2027,6 +2034,33 @@
QUnit.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() {
+20
View File
@@ -880,6 +880,26 @@ class DomainParser(object):
(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 isinstance(value, list):
value = [convert_value(field, v, self.context)
for v in value]
@@ -930,6 +930,12 @@ class DomainParserTestCase(TestCase):
'name': 'integer',
'type': 'integer',
},
'timestamp': {
'name': 'timestamp',
'string': 'Timestamp',
'type': 'timestamp',
'format': '"%H:%M:%S"',
},
'selection': {
'string': 'Selection',
'name': 'selection',
@@ -1119,6 +1125,33 @@ class DomainParserTestCase(TestCase):
self.assertEqual(
rlist(dom.parse_clause(iter([['AND']]))),
[('rec_name', 'ilike', "%AND%")])
for search_text, domain in [
([('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)),
])]),
]:
with self.subTest(f'parse_clause({search_text})'):
self.assertEqual(rlist(dom.parse_clause(search_text)), domain)
def test_completion_char(self):
"Test completion char"