From 0ed69e3a5bdabc6cc54558687d507effa0ad6658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Fri, 14 Nov 2025 18:11:20 +0100 Subject: [PATCH] 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) --HG-- branch : 6.0 --- trytond/trytond/model/fields/one2many.py | 6 ++- 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 | 46 ++++++++++++++++ 5 files changed, 186 insertions(+), 4 deletions(-) diff --git a/trytond/trytond/model/fields/one2many.py b/trytond/trytond/model/fields/one2many.py index 72e7425e3f..aa4cb055a9 100644 --- a/trytond/trytond/model/fields/one2many.py +++ b/trytond/trytond/model/fields/one2many.py @@ -316,8 +316,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) diff --git a/trytond/trytond/tests/field_many2many.py b/trytond/trytond/tests/field_many2many.py index d70c793839..815bbe7134 100644 --- a/trytond/trytond/tests/field_many2many.py +++ b/trytond/trytond/tests/field_many2many.py @@ -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 @@ -180,6 +180,51 @@ class Many2ManyContextTarget(ModelSQL): return context.get('test') +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, @@ -205,4 +250,10 @@ def register(module): Many2ManyContext, Many2ManyContextTarget, Many2ManyContextRelation, + Many2ManyActive, + Many2ManyTargetActive, + Many2ManyRelationActive, + Many2ManyReferenceActive, + Many2ManyReferenceActiveRelation, + Many2ManyReferenceActiveTarget, module=module, type_='model') diff --git a/trytond/trytond/tests/field_one2many.py b/trytond/trytond/tests/field_one2many.py index a1848cf8a7..9fe9d9552d 100644 --- a/trytond/trytond/tests/field_one2many.py +++ b/trytond/trytond/tests/field_one2many.py @@ -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 @@ -130,6 +130,37 @@ class One2ManyContextTarget(ModelSQL): return context.get('test') +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, @@ -148,4 +179,8 @@ def register(module): One2ManyFilterDomainTarget, One2ManyContext, One2ManyContextTarget, + One2ManyActive, + One2ManyActiveTarget, + One2ManyActiveReference, + One2ManyActiveReferenceTarget, module=module, type_='model') diff --git a/trytond/trytond/tests/test_field_many2many.py b/trytond/trytond/tests/test_field_many2many.py index 0844e3b86a..50af506725 100644 --- a/trytond/trytond/tests/test_field_many2many.py +++ b/trytond/trytond/tests/test_field_many2many.py @@ -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" @@ -387,6 +429,9 @@ class FieldMany2ManyTestCase(unittest.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" @@ -740,6 +785,9 @@ class FieldMany2ManyReferenceTestCase(unittest.TestCase, CommonTestCaseMixin): def Many2ManyTarget(self): return Pool().get('test.many2many_reference.target') + def Many2ManyActive(self): + return Pool().get('test.many2many_reference.active') + def suite(): suite_ = unittest.TestSuite() diff --git a/trytond/trytond/tests/test_field_one2many.py b/trytond/trytond/tests/test_field_one2many.py index 4ec9cafdf7..31f3b7f097 100644 --- a/trytond/trytond/tests/test_field_one2many.py +++ b/trytond/trytond/tests/test_field_one2many.py @@ -146,6 +146,46 @@ class CommonTestCaseMixin: 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" @@ -446,6 +486,9 @@ class FieldOne2ManyTestCase(unittest.TestCase, CommonTestCaseMixin): 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" @@ -661,6 +704,9 @@ class FieldOne2ManyReferenceTestCase(unittest.TestCase, CommonTestCaseMixin): def One2ManyTarget(self): return Pool().get('test.one2many_reference.target') + def One2ManyActive(self): + return Pool().get('test.one2many_reference.active') + def suite(): suite_ = unittest.TestSuite()