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

Closes #14778
(grafted from 6952308a8b6b65ff583a2b04d9cd558b44c4e4a9)

--HG--
branch : 8.0
This commit is contained in:
Nicolas Évrard
2026-04-14 18:29:42 +02:00
parent 2ff149fde6
commit 6a4c1aa5e3
4 changed files with 113 additions and 0 deletions
+26
View File
@@ -1951,6 +1951,32 @@
]); ]);
return; 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', if (['many2one', 'one2many', 'many2many', 'one2one',
'many2many', 'one2one'].includes(field.type) && value) { 'many2many', 'one2one'].includes(field.type) && value) {
+34
View File
@@ -1933,6 +1933,12 @@
'name': 'integer', 'name': 'integer',
'type': 'integer' 'type': 'integer'
}, },
'timestamp': {
'name': 'timestamp',
'string': 'Timestamp',
'type': 'timestamp',
'format': '"%H:%M:%S"',
},
'selection': { 'selection': {
'string': 'Selection', 'string': 'Selection',
'name': 'selection', 'name': 'selection',
@@ -2033,6 +2039,7 @@
c(['integer', '>=', 3]), c(['integer', '>=', 3]),
c(['integer', '<=', 5]) c(['integer', '<=', 5])
]]], ]]],
[[c(['Timestamp', null, null])], [c(['timestamp', '=', null])]],
[[c(['Reference', null, 'foo'])], [[c(['Reference', null, 'foo'])],
[c(['reference', 'ilike', '%foo%'])]], [c(['reference', 'ilike', '%foo%'])]],
[[c(['Reference', null, 'Spam'])], [[c(['Reference', null, 'Spam'])],
@@ -2073,6 +2080,33 @@
QUnit.assert.deepEqual(parser.parse_clause(value), result, QUnit.assert.deepEqual(parser.parse_clause(value), result,
'parse_clause(' + JSON.stringify(value) + ')'); '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() { QUnit.test('DomainParser.format_value', function() {
+20
View File
@@ -885,6 +885,26 @@ class DomainParser(object):
(field_name, '<=', rvalue), (field_name, '<=', rvalue),
]) ])
continue 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 { if field['type'] in {
'many2one', 'one2many', 'many2many', 'one2one', 'many2one', 'one2many', 'many2many', 'one2one',
} and value: } and value:
@@ -943,6 +943,12 @@ class DomainParserTestCase(TestCase):
'name': 'integer', 'name': 'integer',
'type': 'integer', 'type': 'integer',
}, },
'timestamp': {
'name': 'timestamp',
'string': 'Timestamp',
'type': 'timestamp',
'format': '"%H:%M:%S"',
},
'selection': { 'selection': {
'string': 'Selection', 'string': 'Selection',
'name': 'selection', 'name': 'selection',
@@ -1161,6 +1167,33 @@ class DomainParserTestCase(TestCase):
self.assertEqual( self.assertEqual(
rlist(dom.parse_clause(iter([['AND']]))), rlist(dom.parse_clause(iter([['AND']]))),
[('rec_name', 'ilike', "%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): def test_completion_char(self):
"Test completion char" "Test completion char"