[FIX] core: computed inversed fields partly assigned

This fixes inconsistencies when dealing with fields that are computed
and inversed by the same methods.

Consider two fields F1, F2 with the same compute and inverse methods.
Consider a record where we wrote on a dependency of the common compute
method.  At this point, both fields F1 and F2 are marked to be computed.

Now let us write on F1 only.  Here is what happens:
 - the write discards the computation of F1, but not F2
 - the inverse method of F1 is called:
    - the method accesses F2
       -> this calls the compute method, which assigns both F1 and F2
    - the method accesses F1
       -> the value of F1 has been replaced by the computation above

The issue comes from a combination of factors:
 - the value of F2 must be determined by the computation;
 - the computation assigns both F1 and F2;
 - the computation is done while inversing F1 (and F2).

The solution is to force the computation before actually writing on the
fields and calling their inverse methods.  Note that this is necessary
only when part of the fields computed by a common method are updated.
When all fields computed by a common method are updated, the computation
will automatically be cancelled.

closes odoo/odoo#81172

X-original-commit: fb4d6ee4ba8bcb5cd8030105ac57e9e02850bfc4
Signed-off-by: Raphael Collet <rco@odoo.com>
This commit is contained in:
Raphael Collet
2021-12-09 20:26:52 +00:00
parent e2624c719c
commit 1616d47efa
2 changed files with 35 additions and 0 deletions
@@ -621,6 +621,33 @@ class TestFields(TransactionCaseWithUserDemo):
self.assertEqual(record.bar3, 'C')
self.assertCountEqual(log, ['compute'])
# corner case: write on a field that is marked to compute
log.clear()
# writing on 'foo' marks 'bar1', 'bar2', 'bar3' to compute
record.write({'foo': '1/2/3'})
self.assertCountEqual(log, [])
# writing on 'bar3' must force the computation before updating
record.write({'bar3': 'X'})
self.assertCountEqual(log, ['compute', 'inverse23'])
self.assertEqual(record.foo, '1/2/X')
self.assertEqual(record.bar1, '1')
self.assertEqual(record.bar2, '2')
self.assertEqual(record.bar3, 'X')
self.assertCountEqual(log, ['compute', 'inverse23'])
log.clear()
# writing on 'foo' marks 'bar1', 'bar2', 'bar3' to compute
record.write({'foo': 'A/B/C'})
self.assertCountEqual(log, [])
# writing on 'bar1', 'bar2', 'bar3' discards the computation
record.write({'bar1': 'X', 'bar2': 'Y', 'bar3': 'Z'})
self.assertCountEqual(log, ['inverse1', 'inverse23'])
self.assertEqual(record.foo, 'X/Y/Z')
self.assertEqual(record.bar1, 'X')
self.assertEqual(record.bar2, 'Y')
self.assertEqual(record.bar3, 'Z')
self.assertCountEqual(log, ['inverse1', 'inverse23'])
def test_13_inverse_access(self):
""" test access rights on inverse fields """
foo = self.env['test_new_api.category'].create({'name': 'Foo'})
+8
View File
@@ -3855,6 +3855,14 @@ Fields:
if fname == 'company_id' or (field.relational and field.check_company):
check_company = True
# force the computation of fields that are computed with some assigned
# fields, but are not assigned themselves
to_compute = [field.name
for field in protected
if field.compute and field.name not in vals]
if to_compute:
self.recompute(to_compute, self)
# protect fields being written against recomputation
with env.protecting(protected, self):
# Determine records depending on values. When modifying a relational