details: https://code.tryton.org/tryton/commit/1d29cd5b5121
branch: 7.4
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
(grafted from 715161fe8b3351072d7a52e698c95e75269b06a9)
diffstat:
trytond/trytond/model/fields/one2many.py | 12 +++--
trytond/trytond/tests/field_many2many.py | 53 ++++++++++++++++++++++++++-
trytond/trytond/tests/field_one2many.py | 37 ++++++++++++++++++-
trytond/trytond/tests/test_field_many2many.py | 48 ++++++++++++++++++++++++
trytond/trytond/tests/test_field_one2many.py | 52 ++++++++++++++++++++++++++
5 files changed, 195 insertions(+), 7 deletions(-)
diffs (325 lines):
diff -r a1579c166454 -r 1d29cd5b5121 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 a1579c166454 -r 1d29cd5b5121 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
@@ -226,6 +226,51 @@
__name__ = 'test.many2many_order.target'
+class Many2ManyActive(ModelSQL):
+ "Many2Many Active"
+ __name__ = 'test.many2many_active'
+ targets = fields.Many2Many(
+ 'test.many2many_active.relation', 'origin', 'target', 'Targets')
+
+
+class Many2ManyTargetActive(DeactivableMixin, ModelSQL):
+ "Many2Many Target Active"
+ __name__ = 'test.many2many_active.target'
+ name = fields.Char('Name')
+
+
+class Many2ManyRelationActive(ModelSQL):
+ "Many2Many Relation Active"
+ __name__ = 'test.many2many_active.relation'
+ origin = fields.Many2One('test.many2many_active', 'Origin')
+ target = fields.Many2One('test.many2many_active.target', 'Target')
+
+
+class Many2ManyReferenceActive(ModelSQL):
+ "Many2Many Reference Active"
+ __name__ = 'test.many2many_reference.active'
+ targets = fields.Many2Many(
+ 'test.many2many_reference.active.relation', 'origin', 'target',
+ "Targets")
+
+
+class Many2ManyReferenceActiveTarget(DeactivableMixin, ModelSQL):
+ "Many2Many Reference Active Target"
+ __name__ = 'test.many2many_reference.active.target'
+ name = fields.Char('Name')
+
+
+class Many2ManyReferenceActiveRelation(ModelSQL):
+ "Many2Many Reference Active Relation"
+ __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,
@@ -257,4 +302,10 @@
Many2ManyOrder,
Many2ManyOrderRelation,
Many2ManyOrderTarget,
+ Many2ManyActive,
+ Many2ManyTargetActive,
+ Many2ManyRelationActive,
+ Many2ManyReferenceActive,
+ Many2ManyReferenceActiveRelation,
+ Many2ManyReferenceActiveTarget,
module=module, type_='model')
diff -r a1579c166454 -r 1d29cd5b5121 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
@@ -168,6 +168,37 @@
cls._order = [('id', 'DESC')]
+class One2ManyActive(ModelSQL):
+ "One2Many Active"
+ __name__ = 'test.one2many.active'
+ targets = fields.One2Many(
+ 'test.one2many.active_target', 'origin', "Targets")
+
+
+class One2ManyActiveTarget(DeactivableMixin, ModelSQL):
+ "One2Many Active Target"
+ __name__ = 'test.one2many.active_target'
+ name = fields.Char('Name')
+ origin = fields.Many2One('test.one2many.active', "Origin")
+
+
+class One2ManyActiveReference(ModelSQL):
+ "One2Many Active Reference"
+ __name__ = 'test.one2many_reference.active'
+ targets = fields.One2Many(
+ 'test.one2many_reference.active_target', 'origin', "Targets")
+
+
+class One2ManyActiveReferenceTarget(DeactivableMixin, ModelSQL):
+ "One2Many Active Reference Target"
+ __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,
@@ -190,4 +221,8 @@
One2ManyContextTarget,
One2ManyOrder,
One2ManyOrderTarget,
+ One2ManyActive,
+ One2ManyActiveTarget,
+ One2ManyActiveReference,
+ One2ManyActiveReferenceTarget,
module=module, type_='model')
diff -r a1579c166454 -r 1d29cd5b5121
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()
@@ -387,6 +429,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"
@@ -792,3 +837,6 @@
def Many2ManyTarget(self):
return Pool().get('test.many2many_reference.target')
+
+ def Many2ManyActive(self):
+ return Pool().get('test.many2many_reference.active')
diff -r a1579c166454 -r 1d29cd5b5121
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()
@@ -464,6 +504,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"
@@ -748,6 +791,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"
@@ -771,6 +817,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)