Search on the Target model in O2M when it has an active field

When comparing a one2many to None we should search on the Target model in order
to ensure the correct handling of the active field. Otherwise there might be
records that wouldn't be found by the search yet their one2many fields come up
empty.

Closes #14370
This commit is contained in:
Nicolas Évrard
2025-11-14 18:11:20 +01:00
parent 64424d1a90
commit 7bdb724da2
5 changed files with 185 additions and 7 deletions
+7 -5
View File
@@ -355,9 +355,10 @@ class One2Many(Field):
where &= history_where
if origin_where:
where &= origin_where
if self.filter:
query = Target.search(
self.filter, order=[], query=True)
if self.filter or hasattr(Target, 'active'):
# Use an empty filter to apply the active test
filter_ = self.filter if self.filter else []
query = Target.search(filter_, order=[], query=True)
where &= target.id.in_(query)
query = target.select(origin, where=where)
expression = ~table.id.in_(query)
@@ -367,12 +368,13 @@ class One2Many(Field):
where &= history_where
if origin_where:
where &= origin_where
if self.filter:
if self.filter or hasattr(Target, 'active'):
target_tables = {
None: (target, None),
}
filter_ = self.filter if self.filter else []
target_tables, clause = Target.search_domain(
self.filter, tables=target_tables)
filter_, tables=target_tables)
where &= clause
query_table = convert_from(None, target_tables)
query = query_table.select(Literal(1), where=where)
+46 -1
View File
@@ -1,7 +1,7 @@
# 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 trytond.model import ModelSQL, fields
from trytond.model import DeactivableMixin, ModelSQL, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
@@ -197,6 +197,45 @@ class Many2ManyOrderTarget(ModelSQL):
__name__ = 'test.many2many_order.target'
class Many2ManyActive(ModelSQL):
__name__ = 'test.many2many_active'
targets = fields.Many2Many(
'test.many2many_active.relation', 'origin', 'target', 'Targets')
class Many2ManyTargetActive(DeactivableMixin, ModelSQL):
__name__ = 'test.many2many_active.target'
name = fields.Char('Name')
class Many2ManyRelationActive(ModelSQL):
__name__ = 'test.many2many_active.relation'
origin = fields.Many2One('test.many2many_active', 'Origin')
target = fields.Many2One('test.many2many_active.target', 'Target')
class Many2ManyReferenceActive(ModelSQL):
__name__ = 'test.many2many_reference.active'
targets = fields.Many2Many(
'test.many2many_reference.active.relation', 'origin', 'target',
"Targets")
class Many2ManyReferenceActiveTarget(DeactivableMixin, ModelSQL):
__name__ = 'test.many2many_reference.active.target'
name = fields.Char('Name')
class Many2ManyReferenceActiveRelation(ModelSQL):
__name__ = 'test.many2many_reference.active.relation'
origin = fields.Reference('Origin', [
(None, ''),
('test.many2many_reference.active', 'Many2Many Reference'),
])
target = fields.Many2One('test.many2many_reference.active.target',
'Reference Target')
def register(module):
Pool.register(
Many2Many,
@@ -228,4 +267,10 @@ def register(module):
Many2ManyOrder,
Many2ManyOrderRelation,
Many2ManyOrderTarget,
Many2ManyActive,
Many2ManyTargetActive,
Many2ManyRelationActive,
Many2ManyReferenceActive,
Many2ManyReferenceActiveRelation,
Many2ManyReferenceActiveTarget,
module=module, type_='model')
+32 -1
View File
@@ -1,7 +1,7 @@
# 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 trytond.model import ModelSQL, fields
from trytond.model import DeactivableMixin, ModelSQL, fields
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.transaction import Transaction
@@ -148,6 +148,33 @@ class One2ManyOrderTarget(ModelSQL):
cls._order = [('id', 'DESC')]
class One2ManyActive(ModelSQL):
__name__ = 'test.one2many.active'
targets = fields.One2Many(
'test.one2many.active_target', 'origin', "Targets")
class One2ManyActiveTarget(DeactivableMixin, ModelSQL):
__name__ = 'test.one2many.active_target'
name = fields.Char('Name')
origin = fields.Many2One('test.one2many.active', "Origin")
class One2ManyActiveReference(ModelSQL):
__name__ = 'test.one2many_reference.active'
targets = fields.One2Many(
'test.one2many_reference.active_target', 'origin', "Targets")
class One2ManyActiveReferenceTarget(DeactivableMixin, ModelSQL):
__name__ = 'test.one2many_reference.active_target'
name = fields.Char('Name')
origin = fields.Reference('Origin', [
(None, ''),
('test.one2many_reference.active', 'One2Many Reference'),
])
def register(module):
Pool.register(
One2Many,
@@ -170,4 +197,8 @@ def register(module):
One2ManyContextTarget,
One2ManyOrder,
One2ManyOrderTarget,
One2ManyActive,
One2ManyActiveTarget,
One2ManyActiveReference,
One2ManyActiveReferenceTarget,
module=module, type_='model')
@@ -100,6 +100,48 @@ class CommonTestCaseMixin:
self.assertListEqual(many2manys, [many2many1])
@with_transaction()
def test_search_equals_none_inactive(self):
"Test search many2many equals None when the target is inactive"
Many2Many = self.Many2ManyActive()
many2many1, many2many2, many2many3 = Many2Many.create([{
'targets': [('create', [{'name': "Target"}])],
}, {
'targets': [
('create', [{'name': "Target", 'active': False}])
],
}, {
'targets': None,
}])
many2manys = Many2Many.search([
('targets', '=', None),
])
self.assertListEqual(many2manys, [many2many3])
@with_transaction()
def test_search_non_equals_none_inactive(self):
"Test search many2many equals non None when the target is inactive"
Many2Many = self.Many2ManyActive()
many2many1, many2many2, many2many3 = Many2Many.create([{
'targets': [('create', [{'name': "Target"}])],
}, {
'targets': [
('create', [{'name': "Target", 'active': False}])
],
}, {
'targets': None,
}])
many2manys = Many2Many.search([
('targets', '!=', None),
])
self.assertListEqual(many2manys, [many2many1, many2many2])
@with_transaction()
def test_search_non_equals_no_link(self):
"Test search many2many non equals without link"
@@ -388,6 +430,9 @@ class FieldMany2ManyTestCase(TestCase, CommonTestCaseMixin):
def Many2ManyTarget(self):
return Pool().get('test.many2many.target')
def Many2ManyActive(self):
return Pool().get('test.many2many_active')
@with_transaction()
def test_create_required_with_value(self):
"Test create many2many required with value"
@@ -794,3 +839,6 @@ class FieldMany2ManyReferenceTestCase(TestCase, CommonTestCaseMixin):
def Many2ManyTarget(self):
return Pool().get('test.many2many_reference.target')
def Many2ManyActive(self):
return Pool().get('test.many2many_reference.active')
@@ -70,6 +70,46 @@ class SearchTestCaseMixin:
self.assertListEqual(one2manys, [one2many2])
@with_transaction()
def test_search_equals_none_inactive(self):
"Test search one2many equals None when the target is inactive"
One2Many = self.One2ManyActive()
one2many1, one2many2, one2many3 = One2Many.create([{
'targets': [('create', [{'name': "Target1"}])],
}, {
'targets': [
('create', [{'name': "Target2", 'active': False}]),
],
}, {
'targets': None,
}])
one2manys = One2Many.search([
('targets', '=', None),
])
self.assertEqual(one2manys, [one2many2, one2many3])
@with_transaction()
def test_search_not_equals_none_inactive(self):
"Test search one2many not equals None when the target is inactive"
One2Many = self.One2ManyActive()
one2many1, one2many2, one2many3 = One2Many.create([{
'targets': [('create', [{'name': "Target1"}])],
}, {
'targets': [
('create', [{'name': "Target2", 'active': False}]),
],
}, {
'targets': None,
}])
one2manys = One2Many.search([
('targets', '!=', None),
])
self.assertEqual(one2manys, [one2many1])
@with_transaction()
def test_search_non_equals_none(self):
"Test search one2many non equals None"
@@ -465,6 +505,9 @@ class FieldOne2ManyTestCase(
def One2ManyTarget(self):
return Pool().get('test.one2many.target')
def One2ManyActive(self):
return Pool().get('test.one2many.active')
@with_transaction()
def test_create_required_with_value(self):
"Test create one2many required with value"
@@ -750,6 +793,9 @@ class FieldOne2ManyReferenceTestCase(
def One2ManyTarget(self):
return Pool().get('test.one2many_reference.target')
def One2ManyActive(self):
return Pool().get('test.one2many_reference.active')
class FieldOne2ManyExistsTestCase(TestCase, SearchTestCaseMixin):
"Test Field One2Many when using EXISTS"
@@ -772,6 +818,9 @@ class FieldOne2ManyExistsTestCase(TestCase, SearchTestCaseMixin):
def One2ManyTarget(self):
return Pool().get('test.one2many.target')
def One2ManyActive(self):
return Pool().get('test.one2many.active')
class FieldOne2ManyReferenceExistsTestCase(
TestCase, SearchTestCaseMixin):
@@ -795,5 +844,8 @@ class FieldOne2ManyReferenceExistsTestCase(
def One2ManyTarget(self):
return Pool().get('test.one2many_reference.target')
def One2ManyActive(self):
return Pool().get('test.one2many_reference.active')
def assert_strategy(self, query):
self.assertIn('EXISTS', query)