diff --git a/sao/CHANGELOG b/sao/CHANGELOG index d5641078b1..6ad15f6486 100644 --- a/sao/CHANGELOG +++ b/sao/CHANGELOG @@ -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 --git a/sao/src/common.js b/sao/src/common.js index e52f06e2df..7c0e308294 100644 --- a/sao/src/common.js +++ b/sao/src/common.js @@ -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 --git a/sao/tests/sao.js b/sao/tests/sao.js index 6d74026bc1..03c61b3bf7 100644 --- a/sao/tests/sao.js +++ b/sao/tests/sao.js @@ -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 --git a/tryton/CHANGELOG b/tryton/CHANGELOG index 07ee15f51e..768860e499 100644 --- a/tryton/CHANGELOG +++ b/tryton/CHANGELOG @@ -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 --git a/tryton/tryton/common/domain_parser.py b/tryton/tryton/common/domain_parser.py index 304545c63e..c0569c2e0b 100644 --- a/tryton/tryton/common/domain_parser.py +++ b/tryton/tryton/common/domain_parser.py @@ -292,6 +292,11 @@ def convert_value(field, value, context=None): 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 @@ def convert_value(field, value, context=None): '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 @@ class DomainParser(object): 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 @@ class DomainParser(object): 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 @@ class DomainParser(object): 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 @@ class DomainParser(object): 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 @@ class DomainParser(object): 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 @@ class DomainParser(object): 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 --git a/tryton/tryton/tests/test_common_domain_parser.py b/tryton/tryton/tests/test_common_domain_parser.py index c5e18fb502..b7dba8dc49 100644 --- a/tryton/tryton/tests/test_common_domain_parser.py +++ b/tryton/tryton/tests/test_common_domain_parser.py @@ -663,6 +663,10 @@ class DomainParserTestCase(TestCase): 'string': "Name", 'type': 'char', }, + 'rec_name': { + 'string': "Record Name", + 'type': 'char', + }, }, }, }) @@ -776,6 +780,15 @@ class DomainParserTestCase(TestCase): self.assertEqual( 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") @@ -1112,6 +1125,14 @@ class DomainParserTestCase(TestCase): rlist(dom.parse_clause([('Many2One', None, ['John', 'Jane'])])), [ ('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%')]])