details: https://code.tryton.org/tryton/commit/715161fe8b33
branch: default
user: Nicolas Évrard <[email protected]>
date: Fri Nov 14 18:11:20 2025 +0100
description:
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
diffstat:
trytond/trytond/model/fields/one2many.py | 12 +++--
trytond/trytond/tests/field_many2many.py | 47 +++++++++++++++++++++++-
trytond/trytond/tests/field_one2many.py | 33 ++++++++++++++++-
trytond/trytond/tests/test_field_many2many.py | 48 ++++++++++++++++++++++++
trytond/trytond/tests/test_field_one2many.py | 52 +++++++++++++++++++++++++++
5 files changed, 185 insertions(+), 7 deletions(-)
diffs (315 lines):
diff -r 6b5e31b69ae6 -r 715161fe8b33 trytond/trytond/model/fields/one2many.py
--- a/trytond/trytond/model/fields/one2many.py Sat Dec 13 14:57:52 2025 +0100
+++ b/trytond/trytond/model/fields/one2many.py Fri Nov 14 18:11:20 2025 +0100
@@ -355,9 +355,10 @@
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 @@
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)
diff -r 6b5e31b69ae6 -r 715161fe8b33 trytond/trytond/tests/field_many2many.py
--- a/trytond/trytond/tests/field_many2many.py Sat Dec 13 14:57:52 2025 +0100
+++ b/trytond/trytond/tests/field_many2many.py Fri Nov 14 18:11:20 2025 +0100
@@ -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 @@
__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 @@
Many2ManyOrder,
Many2ManyOrderRelation,
Many2ManyOrderTarget,
+ Many2ManyActive,
+ Many2ManyTargetActive,
+ Many2ManyRelationActive,
+ Many2ManyReferenceActive,
+ Many2ManyReferenceActiveRelation,
+ Many2ManyReferenceActiveTarget,
module=module, type_='model')
diff -r 6b5e31b69ae6 -r 715161fe8b33 trytond/trytond/tests/field_one2many.py
--- a/trytond/trytond/tests/field_one2many.py Sat Dec 13 14:57:52 2025 +0100
+++ b/trytond/trytond/tests/field_one2many.py Fri Nov 14 18:11:20 2025 +0100
@@ -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 @@
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 @@
One2ManyContextTarget,
One2ManyOrder,
One2ManyOrderTarget,
+ One2ManyActive,
+ One2ManyActiveTarget,
+ One2ManyActiveReference,
+ One2ManyActiveReferenceTarget,
module=module, type_='model')
diff -r 6b5e31b69ae6 -r 715161fe8b33
trytond/trytond/tests/test_field_many2many.py
--- a/trytond/trytond/tests/test_field_many2many.py Sat Dec 13 14:57:52
2025 +0100
+++ b/trytond/trytond/tests/test_field_many2many.py Fri Nov 14 18:11:20
2025 +0100
@@ -101,6 +101,48 @@
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"
Many2Many = self.Many2Many()
@@ -388,6 +430,9 @@
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 @@
def Many2ManyTarget(self):
return Pool().get('test.many2many_reference.target')
+
+ def Many2ManyActive(self):
+ return Pool().get('test.many2many_reference.active')
diff -r 6b5e31b69ae6 -r 715161fe8b33
trytond/trytond/tests/test_field_one2many.py
--- a/trytond/trytond/tests/test_field_one2many.py Sat Dec 13 14:57:52
2025 +0100
+++ b/trytond/trytond/tests/test_field_one2many.py Fri Nov 14 18:11:20
2025 +0100
@@ -71,6 +71,46 @@
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"
One2Many = self.One2Many()
@@ -465,6 +505,9 @@
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 @@
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 @@
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 @@
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)