[FIX] core: fix search domain containing empty False None
For non-translated field, when search(['name', 'in', list_right]) The behavior of False and the behavior of None in list_right should be the same. Translated fields need customized process for 'in' operator X-original-commit: a9a3c666f08c5612432dba40a69c7cfdc54ba96e Part-of: odoo/odoo#103031
This commit is contained in:
@@ -1171,7 +1171,7 @@ class TestQueries(TransactionCase):
|
||||
with self.assertQueries(['''
|
||||
SELECT "res_partner_title".id
|
||||
FROM "res_partner_title"
|
||||
WHERE COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US') like %s
|
||||
WHERE (COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US') like %s)
|
||||
ORDER BY COALESCE("res_partner_title"."name"->>'fr_FR', "res_partner_title"."name"->>'en_US')
|
||||
''']):
|
||||
Model.search([('name', 'like', 'foo')])
|
||||
@@ -1221,7 +1221,7 @@ class TestQueries(TransactionCase):
|
||||
SELECT "ir_model".id
|
||||
FROM "ir_model"
|
||||
WHERE (
|
||||
"ir_model"."name"->>'en_US' ILIKE %s
|
||||
("ir_model"."name"->>'en_US' ILIKE %s)
|
||||
OR ("ir_model"."model"::text ILIKE %s)
|
||||
)
|
||||
ORDER BY "ir_model"."model"
|
||||
@@ -1234,7 +1234,7 @@ class TestQueries(TransactionCase):
|
||||
SELECT "ir_model".id
|
||||
FROM "ir_model"
|
||||
WHERE (
|
||||
"ir_model"."name"->>'en_US' NOT ILIKE %s
|
||||
("ir_model"."name" is NULL OR "ir_model"."name"->>'en_US' not ilike %s)
|
||||
AND (("ir_model"."model"::text NOT ILIKE %s) OR "ir_model"."model" IS NULL)
|
||||
)
|
||||
ORDER BY "ir_model"."model"
|
||||
|
||||
@@ -1737,3 +1737,9 @@ class IndexedTranslation(models.Model):
|
||||
_description = 'A model to indexed translated fields'
|
||||
|
||||
name = fields.Text('Name trigram', translate=True, index='trigram')
|
||||
|
||||
class EmptyChar(models.Model):
|
||||
_name = 'test_new_api.empty_char'
|
||||
_description = 'A model to test emtpy char'
|
||||
|
||||
name = fields.Char('Name')
|
||||
|
||||
@@ -100,3 +100,4 @@ access_test_new_api_related_translation_1,access_test_new_api_related_translatio
|
||||
access_test_new_api_related_translation_2,access_test_new_api_related_translation_2,model_test_new_api_related_translation_2,,1,1,1,1
|
||||
access_test_new_api_related_translation_3,access_test_new_api_related_translation_3,model_test_new_api_related_translation_3,,1,1,1,1
|
||||
access_test_new_api_indexed_translation,access_test_new_api_indexed_translation,model_test_new_api_indexed_translation,,1,1,1,1
|
||||
access_test_new_api_empty_char,access_test_new_api_empty_char,model_test_new_api_empty_char,,1,1,1,1
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from itertools import combinations
|
||||
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
@@ -39,3 +41,69 @@ class test_domain(common.TransactionCase):
|
||||
|
||||
self.assertEqual(eq_1+eq_2, all_bool, 'True + False != all')
|
||||
self.assertEqual(neq_1+neq_2, all_bool, 'not True + not False != all')
|
||||
|
||||
def test_empty_char(self):
|
||||
EmptyChar = self.env['test_new_api.empty_char']
|
||||
EmptyChar.create([
|
||||
{'name': 'name'},
|
||||
{'name': ''},
|
||||
{'name': False},
|
||||
])
|
||||
|
||||
self.assertListEqual(EmptyChar.search([('name', '=', 'name')]).mapped('name'), ['name'])
|
||||
self.assertListEqual(EmptyChar.search([('name', '!=', 'name')]).mapped('name'), ['', False])
|
||||
self.assertListEqual(EmptyChar.search([('name', 'ilike', 'name')]).mapped('name'), ['name'])
|
||||
self.assertListEqual(EmptyChar.search([('name', 'not ilike', 'name')]).mapped('name'), ['', False])
|
||||
|
||||
self.assertListEqual(EmptyChar.search([('name', '=', '')]).mapped('name'), [''])
|
||||
self.assertListEqual(EmptyChar.search([('name', '!=', '')]).mapped('name'), ['name'])
|
||||
self.assertListEqual(EmptyChar.search([('name', 'ilike', '')]).mapped('name'), ['name', '', False])
|
||||
self.assertListEqual(EmptyChar.search([('name', 'not ilike', '')]).mapped('name'), [False])
|
||||
|
||||
self.assertListEqual(EmptyChar.search([('name', '=', False)]).mapped('name'), [False])
|
||||
self.assertListEqual(EmptyChar.search([('name', '!=', False)]).mapped('name'), ['name', ''])
|
||||
self.assertListEqual(EmptyChar.search([('name', 'ilike', False)]).mapped('name'), ['name', '', False])
|
||||
self.assertListEqual(EmptyChar.search([('name', 'not ilike', False)]).mapped('name'), [False])
|
||||
|
||||
values = ['name', '', False]
|
||||
for length in range(len(values) + 1):
|
||||
for subset in combinations(values, length):
|
||||
sublist = list(subset)
|
||||
self.assertListEqual(EmptyChar.search([('name', 'in', sublist)]).mapped('name'), sublist)
|
||||
sublist_remained = [v for v in values if v not in subset]
|
||||
self.assertListEqual(EmptyChar.search([('name', 'not in', sublist)]).mapped('name'), sublist_remained)
|
||||
|
||||
def test_empty_translation(self):
|
||||
records_en = self.env['test_new_api.indexed_translation'].with_context(lang='en_US').create([
|
||||
{'name': 'English'},
|
||||
{'name': 'English'},
|
||||
{'name': 'English'},
|
||||
])
|
||||
self.env['res.lang']._activate_lang('fr_FR')
|
||||
records_fr = records_en.with_context(lang='fr_FR')
|
||||
records_fr[0].name = 'name'
|
||||
records_fr[1].name = ''
|
||||
records_fr[2].name = False
|
||||
|
||||
self.assertListEqual(records_fr.search([('name', '=', 'name')]).mapped('name'), ['name'])
|
||||
self.assertListEqual(records_fr.search([('name', '!=', 'name')]).mapped('name'), ['', False])
|
||||
self.assertListEqual(records_fr.search([('name', 'ilike', 'name')]).mapped('name'), ['name'])
|
||||
self.assertListEqual(records_fr.search([('name', 'not ilike', 'name')]).mapped('name'), ['', False])
|
||||
|
||||
self.assertListEqual(records_fr.search([('name', '=', '')]).mapped('name'), [''])
|
||||
self.assertListEqual(records_fr.search([('name', '!=', '')]).mapped('name'), ['name'])
|
||||
self.assertListEqual(records_fr.search([('name', 'ilike', '')]).mapped('name'), ['name', '', False])
|
||||
self.assertListEqual(records_fr.search([('name', 'not ilike', '')]).mapped('name'), [False])
|
||||
|
||||
self.assertListEqual(records_fr.search([('name', '=', False)]).mapped('name'), [False])
|
||||
self.assertListEqual(records_fr.search([('name', '!=', False)]).mapped('name'), ['name', ''])
|
||||
self.assertListEqual(records_fr.search([('name', 'ilike', False)]).mapped('name'), ['name', '', False])
|
||||
self.assertListEqual(records_fr.search([('name', 'not ilike', False)]).mapped('name'), [False])
|
||||
|
||||
values = ['name', '', False]
|
||||
for length in range(len(values) + 1):
|
||||
for subset in combinations(values, length):
|
||||
sublist = list(subset)
|
||||
self.assertListEqual(records_fr.search([('name', 'in', sublist)]).mapped('name'), sublist)
|
||||
sublist_remained = [v for v in values if v not in subset]
|
||||
self.assertListEqual(records_fr.search([('name', 'not in', sublist)]).mapped('name'), sublist_remained)
|
||||
|
||||
+31
-8
@@ -960,11 +960,18 @@ class expression(object):
|
||||
expr, params = self.__leaf_to_sql(leaf, model, alias)
|
||||
push_result(expr, params)
|
||||
|
||||
elif field.translate and right:
|
||||
elif field.translate and isinstance(right, str):
|
||||
sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
|
||||
expr = ''
|
||||
params = []
|
||||
|
||||
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
|
||||
if not need_wildcard:
|
||||
right = field.convert_to_column(right, model, validate=False).adapted['en_US']
|
||||
|
||||
if (need_wildcard and not right) or (right and sql_operator in NEGATIVE_TERM_OPERATORS):
|
||||
expr += f'"{alias}"."{left}" is NULL OR '
|
||||
|
||||
if self._has_trigram and field.index == 'trigram' and sql_operator in ('=', 'like', 'ilike'):
|
||||
# a prefilter using trigram index to speed up '=', 'like', 'ilike'
|
||||
# '!=', '<=', '<', '>', '>=', 'in', 'not in', 'not like', 'not ilike' cannot use this trick
|
||||
@@ -977,7 +984,7 @@ class expression(object):
|
||||
_unaccent = self._unaccent(field)
|
||||
_left = _unaccent(f'''jsonb_path_query_array("{alias}"."{left}", '$.*')::text''')
|
||||
_sql_operator = 'like' if sql_operator == '=' else sql_operator
|
||||
expr = f"{_left} {_sql_operator} {_unaccent('%s')} AND "
|
||||
expr += f"{_left} {_sql_operator} {_unaccent('%s')} AND "
|
||||
params.append(_right)
|
||||
|
||||
unaccent = self._unaccent(field) if sql_operator.endswith('like') else lambda x: x
|
||||
@@ -987,16 +994,32 @@ class expression(object):
|
||||
else:
|
||||
left = unaccent(f'''COALESCE("{alias}"."{left}"->>'{lang}', "{alias}"."{left}"->>'en_US')''')
|
||||
|
||||
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
|
||||
if need_wildcard:
|
||||
right = '%%%s%%' % right
|
||||
if sql_operator in ('in', 'not in'):
|
||||
right = tuple(right)
|
||||
right = f'%{right}%'
|
||||
|
||||
expr += f"{left} {sql_operator} {unaccent('%s')}"
|
||||
params.append(right)
|
||||
push_result(expr, params)
|
||||
push_result(f'({expr})', params)
|
||||
|
||||
elif field.translate and operator in ['in', 'not in'] and isinstance(right, (list, tuple)):
|
||||
params = [it for it in right if it is not False and it is not None]
|
||||
check_null = len(params) < len(right)
|
||||
if params:
|
||||
params = [field.convert_to_column(p, model, validate=False).adapted['en_US'] for p in params]
|
||||
lang = model.env.lang or 'en_US'
|
||||
if lang == 'en_US':
|
||||
query = f'''("{alias}"."{left}"->>'en_US' {operator} %s)'''
|
||||
else:
|
||||
query = f'''(COALESCE("{alias}"."{left}"->>'{lang}', "{alias}"."{left}"->>'en_US') {operator} %s)'''
|
||||
params = [tuple(params)]
|
||||
else:
|
||||
# The case for (left, 'in', []) or (left, 'not in', []).
|
||||
query = 'FALSE' if operator == 'in' else 'TRUE'
|
||||
if (operator == 'in' and check_null) or (operator == 'not in' and not check_null):
|
||||
query = '(%s OR %s."%s" IS NULL)' % (query, alias, left)
|
||||
elif operator == 'not in' and check_null:
|
||||
query = '(%s AND %s."%s" IS NOT NULL)' % (query, alias, left) # needed only for TRUE.
|
||||
push_result(query, params)
|
||||
else:
|
||||
expr, params = self.__leaf_to_sql(leaf, model, alias)
|
||||
push_result(expr, params)
|
||||
@@ -1058,7 +1081,7 @@ class expression(object):
|
||||
params = [it for it in (True, False) if it in right]
|
||||
check_null = False in right
|
||||
else:
|
||||
params = [it for it in right if it != False]
|
||||
params = [it for it in right if it is not False and it is not None]
|
||||
check_null = len(params) < len(right)
|
||||
if params:
|
||||
if left != 'id':
|
||||
|
||||
Reference in New Issue
Block a user