diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 9213277c116e9a0973c43bddd195602dff285090_dHJ5dG9uZC9DSEFOR0VMT0c=..cf8f1399bc1b398fd976e6ca4a035ecca6e4ec43_dHJ5dG9uZC9DSEFOR0VMT0c= 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Do not copy readonly relational fields * Support list of single value in sortable_values tools * Allow resetting password of user via RPC * Support cached_property on object with __slots__ diff --git a/trytond/doc/ref/models.rst b/trytond/doc/ref/models.rst index 9213277c116e9a0973c43bddd195602dff285090_dHJ5dG9uZC9kb2MvcmVmL21vZGVscy5yc3Q=..cf8f1399bc1b398fd976e6ca4a035ecca6e4ec43_dHJ5dG9uZC9kb2MvcmVmL21vZGVscy5yc3Q= 100644 --- a/trytond/doc/ref/models.rst +++ b/trytond/doc/ref/models.rst @@ -390,6 +390,13 @@ New records are returned following the input order. + .. note:: + + The :class:`~fields.One2Many` and :class:`~fields.Many2Many` relation + fields are not copied if their :attr:`~fields.Field.readonly` attribute + is ``True`` or if their relationional :class:`Model` is not a + :class:`ModelStorage` or if it has a :meth:`ModelSQL.table_query`. + .. classmethod:: ModelStorage.search(domain[, offset[, limit[, order[, count]]]]) Return a list of records that match the :ref:`domain <topics-domain>`. diff --git a/trytond/trytond/model/modelstorage.py b/trytond/trytond/model/modelstorage.py index 9213277c116e9a0973c43bddd195602dff285090_dHJ5dG9uZC90cnl0b25kL21vZGVsL21vZGVsc3RvcmFnZS5weQ==..cf8f1399bc1b398fd976e6ca4a035ecca6e4ec43_dHJ5dG9uZC90cnl0b25kL21vZGVsL21vZGVsc3RvcmFnZS5weQ== 100644 --- a/trytond/trytond/model/modelstorage.py +++ b/trytond/trytond/model/modelstorage.py @@ -382,7 +382,15 @@ else: default = default.copy() - def is_readonly(Model): + def is_readonly(field): + if field.readonly: + return True + if hasattr(field, 'get_relation'): + Model = field.get_relation() + elif hasattr(field, 'get_target'): + Model = field.get_target() + else: + return False return (not issubclass(Model, ModelStorage) or callable(getattr(Model, 'table_query', None))) @@ -424,10 +432,10 @@ except Exception: pass elif ftype in ('one2many',): - if is_readonly(field.get_target()): + if is_readonly(field): del data[field_name] elif data[field_name]: data[field_name] = [( 'copy', data[field_name], get_default(field_name))] elif ftype == 'many2many': @@ -428,10 +436,10 @@ del data[field_name] elif data[field_name]: data[field_name] = [( 'copy', data[field_name], get_default(field_name))] elif ftype == 'many2many': - if is_readonly(pool.get(field.relation_name)): + if is_readonly(field): del data[field_name] elif data[field_name]: data[field_name] = [('add', data[field_name])] diff --git a/trytond/trytond/tests/test_copy.py b/trytond/trytond/tests/test_copy.py index 9213277c116e9a0973c43bddd195602dff285090_dHJ5dG9uZC90cnl0b25kL3Rlc3RzL3Rlc3RfY29weS5weQ==..cf8f1399bc1b398fd976e6ca4a035ecca6e4ec43_dHJ5dG9uZC90cnl0b25kL3Rlc3RzL3Rlc3RfY29weS5weQ== 100644 --- a/trytond/trytond/tests/test_copy.py +++ b/trytond/trytond/tests/test_copy.py @@ -2,6 +2,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. import unittest +from unittest.mock import patch from trytond.model import fields from trytond.model.exceptions import AccessError @@ -93,6 +94,22 @@ [x.name for x in one2many_copy.one2many]) @with_transaction() + def test_one2many_readonly(self): + "Test copy one2many readonly" + pool = Pool() + Model = pool.get('test.copy.one2many') + Target = pool.get('test.copy.one2many.target') + + record = Model(name="Test") + record.one2many = [Target(name="Target")] + record.save() + + with patch.object(Model.one2many, 'readonly', True): + copy, = Model.copy([record]) + + self.assertEqual(copy.one2many, ()) + + @with_transaction() def test_one2many_default(self): "Test copy one2many with default" pool = Pool() @@ -158,6 +175,22 @@ [x.name for x in many2many_copy.many2many]) @with_transaction() + def test_many2many_readonly(self): + "test copy many2many readonly" + pool = Pool() + Model = pool.get('test.copy.many2many') + Target = pool.get('test.copy.many2many.target') + + record = Model(name="Test") + record.many2many = [Target(name="Target")] + record.save() + + with patch.object(Model.many2many, 'readonly', True): + copy, = Model.copy([record]) + + self.assertEqual(copy.many2many, ()) + + @with_transaction() def test_many2many_default(self): "Test copy many2many with default" pool = Pool()