details: https://code.tryton.org/tryton/commit/070a8b93efac
branch: default
user: Cédric Krier <[email protected]>
date: Thu Nov 06 18:15:38 2025 +0100
description:
Add support to domain parser to search on empty relation field
Closes #14106
diffstat:
sao/CHANGELOG | 1 +
sao/src/common.js | 23 ++++++++++++++---------
sao/tests/sao.js | 9 +++++++++
tryton/CHANGELOG | 1 +
tryton/tryton/common/domain_parser.py | 21 +++++++++++++--------
tryton/tryton/tests/test_common_domain_parser.py | 21 +++++++++++++++++++++
6 files changed, 59 insertions(+), 17 deletions(-)
diffs (254 lines):
diff -r 107d18d53edf -r 070a8b93efac sao/CHANGELOG
--- a/sao/CHANGELOG Wed Nov 05 14:38:52 2025 +0100
+++ b/sao/CHANGELOG Thu Nov 06 18:15:38 2025 +0100
@@ -1,3 +1,4 @@
+* Add search on empty relation field to domain parser
* Escape completion content with custom format (issue14363)
* Use sandboxed iframe to display document (issue14290)
* Add support for multiple button in the tree view
diff -r 107d18d53edf -r 070a8b93efac sao/src/common.js
--- a/sao/src/common.js Wed Nov 05 14:38:52 2025 +0100
+++ b/sao/src/common.js Thu Nov 06 18:15:38 2025 +0100
@@ -1171,8 +1171,7 @@
string_prefix = string_prefix || '';
for (var name in fields) {
var field = fields[name];
- if ((field.searchable || (field.searchable === undefined)) &&
- (name !== 'rec_name')) {
+ if (field.searchable || (field.searchable === undefined)) {
field = jQuery.extend({}, field);
var fullname = prefix ? prefix + '.' + name : name;
var string = string_prefix ?
@@ -1227,7 +1226,7 @@
}
var name = clause[0];
var value = clause[2];
- if (name.endsWith('.rec_name')) {
+ if (name.endsWith('.rec_name') && value) {
name = name.slice(0, -9);
}
if (name in this.fields) {
@@ -1276,11 +1275,11 @@
var name = clause[0];
var operator = clause[1];
var value = clause[2];
- if (name.endsWith('.rec_name')) {
+ if (name.endsWith('.rec_name') && value) {
name = name.slice(0, -9);
}
if (!(name in this.fields)) {
- if (this.is_full_text(value)) {
+ if ((value !== null) && this.is_full_text(value)) {
value = value.slice(1, -1);
}
return this.quote(value);
@@ -1434,7 +1433,7 @@
name = clause[0];
operator = clause[1];
value = clause[2];
- if (name.endsWith('.rec_name')) {
+ if (name.endsWith('.rec_name') && value) {
name = name.substring(0, name.length - 9);
}
}
@@ -1854,7 +1853,7 @@
var split = this.split_target_value(field, value);
target = split[0];
value = split[1];
- if (target) {
+ if (target && value) {
field_name += '.rec_name';
}
} else if (field.type == 'multiselection') {
@@ -1901,7 +1900,7 @@
}
}
if (['many2one', 'one2many', 'many2many', 'one2one',
- 'many2many', 'one2one'].includes(field.type)) {
+ 'many2many', 'one2one'].includes(field.type) && value)
{
field_name += '.rec_name';
}
if (value instanceof Array) {
@@ -2116,7 +2115,13 @@
},
'selection': convert_selection,
'multiselection': convert_selection,
- 'reference': convert_selection,
+ 'reference': function() {
+ if (value === '') {
+ return null;
+ } else {
+ return convert_selection();
+ }
+ },
'datetime': () => Sao.common.parse_datetime(
Sao.common.date_format(context.date_format) + ' ' +
this.time_format(field), value),
diff -r 107d18d53edf -r 070a8b93efac sao/tests/sao.js
--- a/sao/tests/sao.js Wed Nov 05 14:38:52 2025 +0100
+++ b/sao/tests/sao.js Thu Nov 06 18:15:38 2025 +0100
@@ -2051,6 +2051,8 @@
[['many2one.rec_name', 'ilike', '%John%']]],
[[c(['Many2One', null, ['John', 'Jane']])],
[['many2one.rec_name', 'in', ['John', 'Jane']]]],
+ [[c(['Many2One', '=', null])], [['many2one', '=', null]]],
+ [[c(['Many2One', '=', ''])], [['many2one', '=', null]]],
[[[c(['John'])]], [[['rec_name', 'ilike', '%John%']]]],
[[c(['Relation', null, 'John'])],
[['relation.rec_name', 'ilike', '%John%']]],
@@ -2308,6 +2310,10 @@
'string': "Name",
'type': 'char',
},
+ 'rec_name': {
+ 'string': "Record Name",
+ 'type': 'char',
+ },
},
},
});
@@ -2371,6 +2377,9 @@
[[['reference', 'in', ['foo', 'bar']]], 'Reference: foo;bar'],
[[['many2one', 'ilike', '%John%']], 'Many2One: John'],
[[['many2one.rec_name', 'in', ['John', 'Jane']]], 'Many2One:
John;Jane'],
+ [[['many2one.rec_name', '=', 'John']], 'Many2One: =John'],
+ [[['many2one.rec_name', '=', '']], '"Many2One.Record Name": =""'],
+ [[['many2one.rec_name', '=', null]], '"Many2One.Record Name": ='],
[[['many2one.name', 'ilike', '%Foo%']], 'Many2One.Name: Foo'],
].forEach(function(test) {
var value = test[0];
diff -r 107d18d53edf -r 070a8b93efac tryton/CHANGELOG
--- a/tryton/CHANGELOG Wed Nov 05 14:38:52 2025 +0100
+++ b/tryton/CHANGELOG Thu Nov 06 18:15:38 2025 +0100
@@ -1,3 +1,4 @@
+* Add search on empty relation field to domain parser
* Add support for multiple button in the tree view
Version 7.6.0 - 2025-04-28
diff -r 107d18d53edf -r 070a8b93efac tryton/tryton/common/domain_parser.py
--- a/tryton/tryton/common/domain_parser.py Wed Nov 05 14:38:52 2025 +0100
+++ b/tryton/tryton/common/domain_parser.py Thu Nov 06 18:15:38 2025 +0100
@@ -292,6 +292,11 @@
return None
return value
+ def convert_reference():
+ if value == '':
+ return None
+ return convert_selection()
+
converts = {
'boolean': convert_boolean,
'float': convert_float,
@@ -299,7 +304,7 @@
'numeric': convert_numeric,
'selection': convert_selection,
'multiselection': convert_selection,
- 'reference': convert_selection,
+ 'reference': convert_reference,
'datetime': convert_datetime,
'timestamp': convert_datetime,
'date': convert_date,
@@ -543,7 +548,7 @@
def update_fields(fields, prefix='', string_prefix=''):
for name, field in fields.items():
- if not field.get('searchable', True) or name == 'rec_name':
+ if not field.get('searchable', True):
continue
field = field.copy()
fullname = '.'.join(filter(None, [prefix, name]))
@@ -584,7 +589,7 @@
and all(isinstance(c, (list, tuple)) for c in clause[1:])):
return self.stringable(clause)
name, _, value = clause[:3]
- if name.endswith('.rec_name'):
+ if name.endswith('.rec_name') and value:
name = name[:-len('.rec_name')]
if name in self.fields:
field = self.fields[name]
@@ -619,10 +624,10 @@
or clause[0] in ('AND', 'OR')):
return '(%s)' % self.string(clause)
name, operator, value = clause[:3]
- if name.endswith('.rec_name'):
+ if name.endswith('.rec_name') and value:
name = name[:-9]
if name not in self.fields:
- if is_full_text(value):
+ if value is not None and is_full_text(value):
value = value[1:-1]
return quote(value)
field = self.fields[name]
@@ -724,7 +729,7 @@
name, operator, value = clause
else:
name, operator, value, target = clause
- if name.endswith('.rec_name'):
+ if name.endswith('.rec_name') and value:
name = name[:-9]
value = target
if name == 'rec_name':
@@ -846,7 +851,7 @@
target = None
if field['type'] == 'reference':
target, value = split_target_value(field, value)
- if target:
+ if target and value:
field_name += '.rec_name'
elif field['type'] == 'multiselection':
if value is not None and not isinstance(value, list):
@@ -881,7 +886,7 @@
continue
if field['type'] in {
'many2one', 'one2many', 'many2many', 'one2one',
- }:
+ } and value:
field_name += '.rec_name'
if isinstance(value, list):
value = [convert_value(field, v, self.context)
diff -r 107d18d53edf -r 070a8b93efac
tryton/tryton/tests/test_common_domain_parser.py
--- a/tryton/tryton/tests/test_common_domain_parser.py Wed Nov 05 14:38:52
2025 +0100
+++ b/tryton/tryton/tests/test_common_domain_parser.py Thu Nov 06 18:15:38
2025 +0100
@@ -663,6 +663,10 @@
'string': "Name",
'type': 'char',
},
+ 'rec_name': {
+ 'string': "Record Name",
+ 'type': 'char',
+ },
},
},
})
@@ -777,6 +781,15 @@
dom.string([('many2one.rec_name', 'in', ['John', 'Jane'])]),
'Many2One: John;Jane')
self.assertEqual(
+ dom.string([('many2one.rec_name', '=', 'John')]),
+ 'Many2One: =John')
+ self.assertEqual(
+ dom.string([('many2one.rec_name', '=', '')]),
+ '"Many2One.Record Name": =""')
+ self.assertEqual(
+ dom.string([('many2one.rec_name', '=', None)]),
+ '"Many2One.Record Name": =')
+ self.assertEqual(
dom.string([('many2one.name', 'ilike', '%Foo%')]),
"Many2One.Name: Foo")
@@ -1113,6 +1126,14 @@
('many2one.rec_name', 'in', ['John', 'Jane']),
])
self.assertEqual(
+ rlist(dom.parse_clause([('Many2One', '=', None)])), [
+ ('many2one', '=', None),
+ ])
+ self.assertEqual(
+ rlist(dom.parse_clause([('Many2One', '=', '')])), [
+ ('many2one', '=', None),
+ ])
+ self.assertEqual(
rlist(dom.parse_clause(iter([iter([['John']])]))), [
[('rec_name', 'ilike', '%John%')]])
self.assertEqual(