details:   https://code.tryton.org/tryton/commit/6b8e5ff7a275
branch:    default
user:      Cédric Krier <[email protected]>
date:      Wed Jun 10 19:10:53 2026 +0200
description:
        Complete selection value in domain parser using contains matcher

        Closes #14886
diffstat:

 sao/src/common.js                                |  25 +++++++++++++----------
 sao/tests/index.html                             |   1 +
 sao/tests/sao.js                                 |  23 ++++++++++++++++++++-
 tryton/tryton/common/domain_parser.py            |  17 +++++++++++++--
 tryton/tryton/tests/test_common_domain_parser.py |  24 +++++++++++++++++++---
 5 files changed, 70 insertions(+), 20 deletions(-)

diffs (217 lines):

diff -r a1a73f478a9e -r 6b8e5ff7a275 sao/src/common.js
--- a/sao/src/common.js Thu Jun 18 16:28:03 2026 +0200
+++ b/sao/src/common.js Wed Jun 10 19:10:53 2026 +0200
@@ -1588,6 +1588,15 @@
             }
             return results;
         },
+        contains_without_diacritics: function(main, sub) {
+            main = main.normalize('NFD').toLowerCase();
+            sub = sub.normalize('NFD').toLowerCase();
+
+            main = main.replace(/[\u0300-\u036f]/g, '');
+            sub = sub.replace(/[\u0300-\u036f]/g, '');
+
+            return main.includes(sub);
+        },
         complete_value: function(field, value) {
             var complete_boolean = function() {
                 if ((value === null) || (value === undefined)) {
@@ -1599,18 +1608,15 @@
                 }
             };
 
-            var complete_selection = function() {
+            var complete_selection = () => {
                 var results = [];
                 var test_value = value !== null ? value : '';
                 if (value instanceof Array) {
                     test_value = value[value.length - 1] || '';
                 }
                 test_value = test_value.replace(/^%*|%*$/g, '');
-                var i, len, svalue, test;
-                for (i=0, len=field.selection.length; i<len; i++) {
-                    svalue = field.selection[i][0];
-                    test = field.selection[i][1].toLowerCase();
-                    if (test.startsWith(test_value.toLowerCase())) {
+                for(let [svalue, test] of field.selection) {
+                    if (this.contains_without_diacritics(test, test_value)) {
                         if (value instanceof Array) {
                             results.push(value.slice(0, -1).concat([svalue]));
                         } else {
@@ -1628,11 +1634,8 @@
                     test_value = value[value.length - 1];
                 }
                 test_value = test_value.replace(/^%*|%*$/g, '');
-                var i, len, svalue, test;
-                for (i=0, len=field.selection.length; i<len; i++) {
-                    svalue = field.selection[i][0];
-                    test = field.selection[i][1].toLowerCase();
-                    if (test.startsWith(test_value.toLowerCase())) {
+                for (let [svalue, test] of field.selection) {
+                    if (this.contains_without_diacritics(test, test_value)) {
                         if (value instanceof Array) {
                             results.push(value.slice(0, -1).concat([svalue]));
                         } else {
diff -r a1a73f478a9e -r 6b8e5ff7a275 sao/tests/index.html
--- a/sao/tests/index.html      Thu Jun 18 16:28:03 2026 +0200
+++ b/sao/tests/index.html      Wed Jun 10 19:10:53 2026 +0200
@@ -3,6 +3,7 @@
 this repository contains the full copyright notices and license terms. -->
 <html>
     <head>
+        <meta charset="UTF-8">
         <title>Sao Test Suite</title>
         <script type="text/javascript" 
src="../node_modules/jquery/dist/jquery.js"></script>
         <script type="text/javascript" 
src="../node_modules/bootstrap/dist/js/bootstrap.js"></script>
diff -r a1a73f478a9e -r 6b8e5ff7a275 sao/tests/sao.js
--- a/sao/tests/sao.js  Thu Jun 18 16:28:03 2026 +0200
+++ b/sao/tests/sao.js  Wed Jun 10 19:10:53 2026 +0200
@@ -2427,6 +2427,25 @@
         });
     });
 
+    QUnit.test('DomainParser.contains_without_diacritics', function() {
+        let parser = new Sao.common.DomainParser();
+        for (let [main, sub, expected] of [
+            ["Café au lait", "cafe", true],
+            ["Café au lait", "café", true],
+            ["Café au lait", "lait", true],
+            ["Café au lait", "the", false],
+            ["Héllò Wörld", "hello", true],
+            ["Héllò Wörld", "world", true],
+            ["Héllò Wörld", "hallo", false],
+        ]) {
+            QUnit.assert.strictEqual(
+                parser.contains_without_diacritics(main, sub), expected,
+                'contains_without_diacritics('
+                + JSON.stringify(main) + ',' + JSON.stringify(sub) + ')');
+        }
+
+    });
+
     QUnit.test('DomainParser.complete_value', function () {
         var parser = new Sao.common.DomainParser();
         var field;
@@ -2456,7 +2475,7 @@
                 ],
         };
         [
-            ['m', ['male']],
+            ['f', ['female']],
             ['test', []],
             ['', ['male', 'female']],
             [null, ['male', 'female']],
@@ -2473,7 +2492,7 @@
                 ],
         };
         [
-            ['m', ['male']],
+            ['f', ['female']],
             ['test', []],
             ['', ['male', 'female', '']],
             [null, ['male', 'female', '']],
diff -r a1a73f478a9e -r 6b8e5ff7a275 tryton/tryton/common/domain_parser.py
--- a/tryton/tryton/common/domain_parser.py     Thu Jun 18 16:28:03 2026 +0200
+++ b/tryton/tryton/common/domain_parser.py     Wed Jun 10 19:10:53 2026 +0200
@@ -6,6 +6,7 @@
 import io
 import locale
 import math
+import unicodedata
 from collections import OrderedDict
 from decimal import Decimal
 from shlex import shlex
@@ -426,6 +427,16 @@
             lambda: value if value is not None else '')(), empty=_quote_empty)
 
 
+def contains_without_diacritics(main, sub):
+    main = unicodedata.normalize('NFD', main.lower())
+    sub = unicodedata.normalize('NFD', sub.lower())
+
+    main = ''.join(c for c in main if not unicodedata.combining(c))
+    sub = ''.join(c for c in sub if not unicodedata.combining(c))
+
+    return sub in main
+
+
 def complete_value(field, value):
     "Complete value for field"
 
@@ -444,7 +455,7 @@
             test_value = value[-1] or ''
         test_value = test_value.strip('%')
         for svalue, test in field['selection']:
-            if test.lower().startswith(test_value.lower()):
+            if contains_without_diacritics(test, test_value):
                 if isinstance(value, list):
                     yield value[:-1] + [svalue]
                 else:
@@ -454,9 +465,9 @@
         test_value = value if value is not None else ''
         if isinstance(value, list):
             test_value = value[-1]
-        test_value = test_value.strip('%')
+        test_value = test_value.strip('%').lower()
         for svalue, test in field['selection']:
-            if test.lower().startswith(test_value.lower()):
+            if contains_without_diacritics(test, test_value):
                 if isinstance(value, list):
                     yield value[:-1] + [svalue]
                 else:
diff -r a1a73f478a9e -r 6b8e5ff7a275 
tryton/tryton/tests/test_common_domain_parser.py
--- a/tryton/tryton/tests/test_common_domain_parser.py  Thu Jun 18 16:28:03 
2026 +0200
+++ b/tryton/tryton/tests/test_common_domain_parser.py  Wed Jun 10 19:10:53 
2026 +0200
@@ -6,8 +6,9 @@
 
 from tryton.common import untimezoned_date
 from tryton.common.domain_parser import (
-    DomainParser, complete_value, convert_value, format_value, group_operator,
-    likify, operatorize, parenthesize, quote, rlist, split_target_value, udlex)
+    DomainParser, complete_value, contains_without_diacritics, convert_value,
+    format_value, group_operator, likify, operatorize, parenthesize, quote,
+    rlist, split_target_value, udlex)
 
 
 class DomainParserTestCase(TestCase):
@@ -433,6 +434,21 @@
                 format_value(field, value), result,
                 msg="format_value(%r, %r)" % (field, value))
 
+    def test_contains_without_diacritics(self):
+        "Test contains without diacritics"
+        for main, sub, expected in [
+                ("Café au lait", "cafe", True),
+                ("Café au lait", "café", True),
+                ("Café au lait", "lait", True),
+                ("Café au lait", "the", False),
+                ("Héllò Wörld", "hello", True),
+                ("Héllò Wörld", "world", True),
+                ("Héllò Wörld", "hallo", False),
+                ]:
+            with self.subTest(main=main, sub=sub):
+                self.assertEqual(
+                    contains_without_diacritics(main, sub), expected)
+
     def test_complete_boolean(self):
         "Test complete boolean"
         field = {
@@ -457,7 +473,7 @@
                 ],
             }
         for value, result in (
-                ('m', ['male']),
+                ('f', ['female']),
                 ('test', []),
                 ('', ['male', 'female']),
                 (None, ['male', 'female']),
@@ -472,7 +488,7 @@
         field_with_empty['selection'] = (field_with_empty['selection']
             + [('', '')])
         for value, result in (
-                ('m', ['male']),
+                ('f', ['female']),
                 ('test', []),
                 ('', ['male', 'female', '']),
                 (None, ['male', 'female', '']),

Reply via email to