[FIX] fields: setting an integer field invalidates its inverse one2many

In particular, creating a `mail.message` automatically invalidates the
corresponding `record.message_ids`.

closes odoo/odoo#30737
This commit is contained in:
XavierDo
2019-02-06 10:18:24 +00:00
committed by Raphael Collet
parent e1838a9e27
commit e2180e5a2e
4 changed files with 75 additions and 8 deletions
@@ -29,3 +29,5 @@ access_test_new_api_monetary_custom,access_test_new_api_monetary_custom,model_te
access_test_new_api_monetary_inherits,access_test_new_api_monetary_inherits,model_test_new_api_monetary_inherits,,1,1,1,1
access_test_new_api_field_with_caps,access_test_new_api_field_with_caps,model_test_new_api_field_with_caps,,1,1,1,1
access_test_new_api_req_m2o,access_test_new_api_req_m2o,model_test_new_api_req_m2o,,1,1,1,1
access_test_new_api_attachment,access_test_new_api_attachment,model_test_new_api_attachment,,1,1,1,1
access_test_new_api_attachment_host,access_test_new_api_attachment_host,model_test_new_api_attachment_host,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
29 access_test_new_api_monetary_inherits access_test_new_api_monetary_inherits model_test_new_api_monetary_inherits 1 1 1 1
30 access_test_new_api_field_with_caps access_test_new_api_field_with_caps model_test_new_api_field_with_caps 1 1 1 1
31 access_test_new_api_req_m2o access_test_new_api_req_m2o model_test_new_api_req_m2o 1 1 1 1
32 access_test_new_api_attachment access_test_new_api_attachment model_test_new_api_attachment 1 1 1 1
33 access_test_new_api_attachment_host access_test_new_api_attachment_host model_test_new_api_attachment_host 1 1 1 1
+24
View File
@@ -524,3 +524,27 @@ class RequiredM2O(models.Model):
foo = fields.Many2one('res.currency', required=True, ondelete='cascade')
bar = fields.Many2one('res.country', required=True)
class Attachment(models.Model):
_name = 'test_new_api.attachment'
_description = 'Attachment'
res_model = fields.Char(required=True)
res_id = fields.Integer(required=True)
name = fields.Char(compute='_compute_name', compute_sudo=True, store=True)
@api.depends('res_model', 'res_id')
def _compute_name(self):
for rec in self:
rec.name = self.env[rec.res_model].browse(rec.res_id).display_name
class AttachmentHost(models.Model):
_name = 'test_new_api.attachment.host'
_description = 'Attachment Host'
attachment_ids = fields.One2many(
'test_new_api.attachment', 'res_id', auto_join=True,
domain=lambda self: [('res_model', '=', self._name)],
)
@@ -149,3 +149,37 @@ class One2manyCase(TransactionCase):
self.assertEqual(model1.pArTneR_321_id, p1)
self.assertTrue(model2.exists())
self.assertEqual(model2.pArTneR_321_id, p1)
def test_cache_invalidation(self):
""" Cache invalidation for one2many with integer inverse. """
record = self.env['test_new_api.attachment.host'].create({})
with self.assertQueryCount(2):
self.assertFalse(record.attachment_ids, "inconsistent cache")
# creating attachment must compute name and invalidate attachment_ids
attachment = self.env['test_new_api.attachment'].create({
'res_model': record._name,
'res_id': record.id,
})
with self.assertQueryCount(1):
self.assertEqual(attachment.name, record.display_name,
"field should be computed")
with self.assertQueryCount(2):
self.assertEqual(record.attachment_ids, attachment, "inconsistent cache")
# creating a host should not attempt to recompute attachment.name
with self.assertQueryCount(1):
record = self.env['test_new_api.attachment.host'].create({})
with self.assertQueryCount(0):
# field res_id should not have been invalidated
attachment.res_id
with self.assertQueryCount(2):
self.assertFalse(record.attachment_ids, "inconsistent cache")
# writing on res_id must recompute name and invalidate attachment_ids
attachment.res_id = record.id
with self.assertQueryCount(1):
self.assertEqual(attachment.name, record.display_name,
"field should be recomputed")
with self.assertQueryCount(2):
self.assertEqual(record.attachment_ids, attachment, "inconsistent cache")
+15 -8
View File
@@ -697,10 +697,13 @@ class Field(MetaField('DummyField', (object,), {})):
# add indirect dependencies from the dependencies found above
seen = seen.union([self])
for model, field, path in list(result):
for inv_field in model._field_inverses[field]:
inv_model = model0.env[inv_field.model_name]
inv_path = None if path is None else path + [field.name]
result.append((inv_model, inv_field, inv_path))
# limitation: setting the inverse of an integer field F does not
# trigger the recomputation of fields that depend on F
if field.relational:
for inv_field in model._field_inverses[field]:
inv_model = model0.env[inv_field.model_name]
inv_path = None if path is None else path + [field.name]
result.append((inv_model, inv_field, inv_path))
if not field.store and field not in seen:
result += field.resolve_deps(model, path, seen)
@@ -2198,6 +2201,11 @@ class _RelationalMulti(_Relational):
def _update(self, records, value):
""" Update the cached value of ``self`` for ``records`` with ``value``. """
if not isinstance(records, BaseModel):
# the inverse of self is a non-relational field; do not update in
# this case, as we do not know whether the records are the ones that
# value makes reference to (via a res_model/res_id pair)
return
cache = records.env.cache
for record in records:
special = cache.get_special(record, self)
@@ -2377,12 +2385,11 @@ class One2many(_RelationalMulti):
# link self to its inverse field and vice-versa
comodel = model.env[self.comodel_name]
invf = comodel._fields[self.inverse_name]
# In some rare cases, a ``One2many`` field can link to ``Int`` field
# (res_model/res_id pattern). Only inverse the field if this is
# a ``Many2one`` field.
if isinstance(invf, Many2one):
# setting one2many fields only invalidates many2one inverses;
# integer inverses (res_model/res_id pairs) are not supported
model._field_inverses.add(self, invf)
comodel._field_inverses.add(invf, self)
comodel._field_inverses.add(invf, self)
_description_relation_field = property(attrgetter('inverse_name'))