diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index ffed8a9caa0..225e193b783 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -1351,6 +1351,12 @@ class ComputeContainer(models.Model): name = fields.Char() member_ids = fields.One2many('test_new_api.compute.member', 'container_id') + member_count = fields.Integer(compute='_compute_member_count', store=True) + + @api.depends('member_ids') + def _compute_member_count(self): + for record in self: + record.member_count = len(record.member_ids) class ComputeMember(models.Model): diff --git a/odoo/addons/test_new_api/tests/test_one2many.py b/odoo/addons/test_new_api/tests/test_one2many.py index 21e25bd7e79..8dfa81c18b6 100644 --- a/odoo/addons/test_new_api/tests/test_one2many.py +++ b/odoo/addons/test_new_api/tests/test_one2many.py @@ -256,6 +256,18 @@ class One2manyCase(TransactionCase): # at this point, member.container_id must be computed for member to # appear in container.member_ids self.assertEqual(container.member_ids, member) + self.assertEqual(container.member_count, 1) + + # Changing member.name will trigger recomputing member.container_id, + # container.member_ids and container.member_count. Since we are setting + # the name to bar, it will be detached from container, resulting in a + # member_count of zero on the container. + member.name = 'Bar' + self.assertEqual(container.member_count, 0) + + # Reattach member to container again + member.name = 'Foo' + self.assertEqual(container.member_count, 1) def test_reward_line_delete(self): order = self.env['test_new_api.order'].create({ diff --git a/odoo/models.py b/odoo/models.py index 9d68c790aed..a7d8567ed18 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -4134,7 +4134,7 @@ class BaseModel(metaclass=MetaModel): field_values = [] # [(field, value)] determine_inverses = defaultdict(list) # {inverse: fields} - relational_names = [] + fnames_modifying_relations = [] protected = set() check_company = False for fname, value in vals.items(): @@ -4152,8 +4152,8 @@ class BaseModel(metaclass=MetaModel): # order to avoid an inconsistent update. self[fname] determine_inverses[field.inverse].append(field) - if field.relational or self.pool.field_inverses[field]: - relational_names.append(fname) + if field in self.pool.fields_modifying_relations: + fnames_modifying_relations.append(fname) if field.inverse or (field.compute and not field.readonly): if field.store or field.type not in ('one2many', 'many2many'): # Protect the field from being recomputed while being @@ -4197,7 +4197,7 @@ class BaseModel(metaclass=MetaModel): # In this situation, the total amount must be recomputed on *both* # sales order: the line's order before the modification, and the # line's order after the modification. - self.modified(relational_names, before=True) + self.modified(fnames_modifying_relations, before=True) real_recs = self.filtered('id') @@ -6649,7 +6649,8 @@ class BaseModel(metaclass=MetaModel): # Generic onchange method # - def _dependent_fields(self, field): + @classmethod + def _dependent_fields(cls, field): """ Return an iterator on the fields that depend on ``field``. """ def traverse(node): for key, val in node.items(): @@ -6657,7 +6658,7 @@ class BaseModel(metaclass=MetaModel): yield from val else: yield from traverse(val) - return traverse(self.pool.field_triggers.get(field, {})) + return traverse(cls.pool.field_triggers.get(field, {})) def _has_onchange(self, field, other_fields): """ Return whether ``field`` should trigger an onchange event in the diff --git a/odoo/modules/registry.py b/odoo/modules/registry.py index d286c3c0494..74137c17dc2 100644 --- a/odoo/modules/registry.py +++ b/odoo/modules/registry.py @@ -367,6 +367,30 @@ class Registry(Mapping): return triggers + @lazy_property + def fields_modifying_relations(self): + ''' + Return the union of the set of relational fields that are dependencies + of other fields, with the set of non-relational fields that are + dependencies of relational fields. + ''' + result = set() + + for field in self.field_triggers: + # If the field is itself a relational field, it is also considered + # as triggering relational fields. + if field.relational or self.field_inverses[field]: + result.add(field) + continue + + Model = self.models[field.model_name] + for dep in Model._dependent_fields(field): + if dep.relational or self.field_inverses[dep]: + result.add(field) + break + + return result + def post_init(self, func, *args, **kwargs): """ Register a function to call at the end of :meth:`~.init_models`. """ self._post_init_queue.append(partial(func, *args, **kwargs))