diff --git a/trytond/doc/topics/domain.rst b/trytond/doc/topics/domain.rst index ba991ff298..e65a166f18 100644 --- a/trytond/doc/topics/domain.rst +++ b/trytond/doc/topics/domain.rst @@ -37,6 +37,10 @@ by this pattern:: The number of *dots* in a clause is not limited. + .. note:: + Negative operators match records for which any of the field before the + last dot is empty. + .. warning:: For :class:`trytond.model.fields.Reference`, an extra ending clause is needed to define the target model to join, for example:: diff --git a/trytond/trytond/model/fields/many2one.py b/trytond/trytond/model/fields/many2one.py index 0e3a9a1816..eb73d46921 100644 --- a/trytond/trytond/model/fields/many2one.py +++ b/trytond/trytond/model/fields/many2one.py @@ -1,6 +1,6 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. -from sql import As, Column, Expression, Literal, Query, With +from sql import As, Column, Expression, Literal, Null, Query, With from sql.aggregate import Max from sql.conditionals import Coalesce from sql.operators import Or @@ -266,7 +266,7 @@ class Many2One(Field): target_domain = [(target_name,) + tuple(domain[1:])] if Target.estimated_count() < _subquery_threshold: query = Target.search(target_domain, order=[], query=True) - return column.in_(query) + expression = column.in_(query) else: rule_domain = Rule.domain_get(Target.__name__, mode='read') if rule_domain: @@ -279,7 +279,9 @@ class Many2One(Field): target_table, _ = target_tables[None] _, expression = Target.search_domain( target_domain, tables=target_tables) - return expression + if operator.startswith('not') or operator == '!=': + expression |= column == Null + return expression @order_method def convert_order(self, name, tables, Model): diff --git a/trytond/trytond/model/fields/reference.py b/trytond/trytond/model/fields/reference.py index 42865c69fd..520b4cf38e 100644 --- a/trytond/trytond/model/fields/reference.py +++ b/trytond/trytond/model/fields/reference.py @@ -2,7 +2,7 @@ # this repository contains the full copyright notices and license terms. import warnings -from sql import Cast, Expression, Literal, Query +from sql import Cast, Expression, Literal, Null, Query from sql.functions import Position, Substring from trytond.pool import Pool @@ -214,9 +214,12 @@ class Reference(SelectionMixin, Field): if 'active' in Target._fields: target_domain.append(('active', 'in', [True, False])) query = Target.search(target_domain, order=[], query=True) - return ( + expression = ( self.sql_id(column, Model).in_(query) & column.like(target + ',%')) + if operator.startswith('not') or operator == '!=': + expression |= column == Null + return expression def definition(self, model, language): encoder = PYSONEncoder() diff --git a/trytond/trytond/tests/test_field_many2one.py b/trytond/trytond/tests/test_field_many2one.py index 61a258f00f..047a0240a7 100644 --- a/trytond/trytond/tests/test_field_many2one.py +++ b/trytond/trytond/tests/test_field_many2one.py @@ -200,6 +200,24 @@ class FieldMany2OneTestCase(unittest.TestCase): query = self._test_search_join() self.assertNotIsInstance(query.from_[0], Join) + @with_transaction() + def test_search_nested_null(self): + "Test search by null many2one" + pool = Pool() + Many2One = pool.get('test.many2one') + + record, = Many2One.create([{'many2one': None}]) + + result = Many2One.search([ + ('many2one.value', '=', 1), + ]) + self.assertListEqual(result, []) + + result = Many2One.search([ + ('many2one.value', '!=', 1), + ]) + self.assertListEqual(result, [record]) + @with_transaction() def test_context_attribute(self): "Test context on many2one attribute" diff --git a/trytond/trytond/tests/test_field_reference.py b/trytond/trytond/tests/test_field_reference.py index 204af19c07..faf8572257 100644 --- a/trytond/trytond/tests/test_field_reference.py +++ b/trytond/trytond/tests/test_field_reference.py @@ -352,6 +352,28 @@ class FieldReferenceTestCase(unittest.TestCase): self.assertListEqual(references, [reference]) + @with_transaction() + def test_search_join_null(self): + "Test search by null reference join" + pool = Pool() + Target = pool.get('test.reference.target') + Reference = pool.get('test.reference') + reference, = Reference.create([{ + 'reference': None, + }]) + + result = Reference.search([ + ('reference.name', '=', "Target", Target.__name__), + ]) + + self.assertListEqual(result, []) + + result = Reference.search([ + ('reference.name', '!=', "Target", Target.__name__), + ]) + + self.assertListEqual(result, [reference]) + @with_transaction() def test_write_string(self): "Test write reference string"