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(

Reply via email to