[FIX] core: invalidation of computed editable field in one2many

This fixes the issue introduced by revision fd50ba9fb8

The way computed fields are invalidated depends on the order of the dict
passed as a parameter to method onchange().  This causes some unexpected
behavior when dealing with computed editable fields: the user loses the
value in that field because of the onchange.

Explanation: during the onchange, the modified field is cached by

    record._update_cache(changed_values, validate=True)

If the field is a one2many, 'value' contains an update command for each
modified line.  Because of 'validate=True', the assignment triggers
field computations on the lines, which are already handled by another
call to onchange().  In case anything on the parent model modifies some
line, the whole one2many values are returned to the user, and
accidentally recomputed fields show up in the interface.

closes odoo/odoo#62748

Signed-off-by: Raphael Collet (rco) <rco@openerp.com>
This commit is contained in:
Laurent Smet
2020-12-02 15:36:51 +00:00
committed by Raphael Collet
parent e3b27f2670
commit 6f2a6f7f2e
7 changed files with 102 additions and 5 deletions
@@ -1147,3 +1147,35 @@ class ComputeMember(models.Model):
container = self.env['test_new_api.compute.container']
for member in self:
member.container_id = container.search([('name', '=', member.name)], limit=1)
class ComputeEditable(models.Model):
_name = _description = 'test_new_api.compute_editable'
line_ids = fields.One2many('test_new_api.compute_editable.line', 'parent_id')
@api.onchange('line_ids')
def _onchange_line_ids(self):
for line in self.line_ids:
# even if 'same' is not in the view, it should be the same as 'value'
line.count += line.same
class ComputeEditableLine(models.Model):
_name = _description = 'test_new_api.compute_editable.line'
parent_id = fields.Many2one('test_new_api.compute_editable')
value = fields.Integer()
same = fields.Integer(compute='_compute_same', store=True)
edit = fields.Integer(compute='_compute_edit', store=True, readonly=False)
count = fields.Integer()
@api.depends('value')
def _compute_same(self):
for line in self:
line.same = line.value
@api.depends('value')
def _compute_edit(self):
for line in self:
line.edit = line.value
@@ -66,3 +66,5 @@ access_test_new_api_model_shared_cache_compute_parent,access_test_new_api.model_
access_test_new_api_model_shared_cache_compute_line,access_test_new_api.model_shared_cache_compute_line,model_test_new_api_model_shared_cache_compute_line,,1,1,1,1
access_test_new_api_compute_container,access_test_new_api_compute_container,model_test_new_api_compute_container,,1,1,1,1
access_test_new_api_compute_member,access_test_new_api_compute_member,model_test_new_api_compute_member,,1,1,1,1
access_test_new_api_compute_editable,access_test_new_api_compute_editable,model_test_new_api_compute_editable,,1,1,1,1
access_test_new_api_compute_editable_line,access_test_new_api_compute_editable_line,model_test_new_api_compute_editable_line,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
66 access_test_new_api_model_shared_cache_compute_line access_test_new_api.model_shared_cache_compute_line model_test_new_api_model_shared_cache_compute_line 1 1 1 1
67 access_test_new_api_compute_container access_test_new_api_compute_container model_test_new_api_compute_container 1 1 1 1
68 access_test_new_api_compute_member access_test_new_api_compute_member model_test_new_api_compute_member 1 1 1 1
69 access_test_new_api_compute_editable access_test_new_api_compute_editable model_test_new_api_compute_editable 1 1 1 1
70 access_test_new_api_compute_editable_line access_test_new_api_compute_editable_line model_test_new_api_compute_editable_line 1 1 1 1
@@ -1338,7 +1338,7 @@ class TestFields(TransactionCaseWithUserDemo):
new_origin = Model.new({'name': 'Bar'}, origin=real_record)
new_record = Model.new({'name': 'Baz'})
self.assertEqual(real_record.display_name, 'Foo')
self.assertEqual(new_origin.display_name, 'Foo')
self.assertEqual(new_origin.display_name, 'Bar')
self.assertEqual(new_record.display_name, 'Baz')
# computed stored field with recomputation: always computed
@@ -4,7 +4,7 @@
from unittest.mock import patch
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
from odoo.tests import common
from odoo.tests import common, Form
from odoo import Command
def strip_prefix(prefix, names):
@@ -752,3 +752,39 @@ class TestComputeOnchange(common.TransactionCase):
with form.child_ids.edit(2) as line:
line.cost = 30
self.assertEqual(form.cost, 61)
def test_onchange_editable_compute_one2many(self):
# create a record with a computed editable field ('edit') on lines
record = self.env['test_new_api.compute_editable'].create({'line_ids': [(0, 0, {'value': 7})]})
record.flush()
line = record.line_ids
self.assertRecordValues(line, [{'value': 7, 'edit': 7, 'count': 0}])
# retrieve the onchange spec for calling 'onchange'
spec = Form(record)._view['onchange']
# The onchange on 'line_ids' should increment 'count' and keep the value
# of 'edit' (this field should not be recomputed), whatever the order of
# the fields in the dictionary. This ensures that the value set by the
# user on a computed editable field on a line is not lost.
line_ids = [
Command.update(line.id, {'value': 8, 'edit': 9, 'count': 0}),
Command.create({'value': 8, 'edit': 9, 'count': 0}),
]
result = record.onchange({'line_ids': line_ids}, 'line_ids', spec)
expected = {'value': {
'line_ids': [
Command.clear(),
Command.update(line.id, {'value': 8, 'edit': 9, 'count': 8}),
Command.create({'value': 8, 'edit': 9, 'count': 8}),
],
}}
self.assertEqual(result, expected)
# change dict order in lines, and try again
line_ids = [
(op, id_, dict(reversed(list(vals.items()))))
for op, id_, vals in line_ids
]
result = record.onchange({'line_ids': line_ids}, 'line_ids', spec)
self.assertEqual(result, expected)
@@ -341,5 +341,31 @@
</form>
</field>
</record>
<!-- compute_editable view -->
<record id="compute_editable_form" model="ir.ui.view">
<field name="name">test_new_api.compute_editable.form</field>
<field name="model">test_new_api.compute_editable</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="line_ids" mode="tree">
<tree editable="bottom">
<field name="value"/>
<field name="edit"/>
<field name="count"/>
</tree>
<form>
<!-- this view makes sure that field "same" is not considered -->
<field name="value"/>
<field name="edit"/>
<field name="count"/>
</form>
</field>
</sheet>
</form>
</field>
</record>
</data>
</odoo>
+3 -2
View File
@@ -961,7 +961,8 @@ class Field(MetaField('DummyField', (object,), {})):
# not stored and not computed -> default
#
# on a new record w/ origin:
# stored -> fetch from origin (computation done above)
# stored and not (computed and readonly) -> fetch from origin
# stored and computed and readonly -> compute
# not stored and computed -> compute
# not stored and not computed -> default
#
@@ -987,7 +988,7 @@ class Field(MetaField('DummyField', (object,), {})):
]))
value = env.cache.get(record, self)
elif self.store and record._origin:
elif self.store and record._origin and not (self.compute and self.readonly):
# new record with origin: fetch from origin
value = self.convert_to_cache(record._origin[self.name], record)
env.cache.set(record, self, value)
+1 -1
View File
@@ -6132,7 +6132,7 @@ Fields:
# store changed values in cache; also trigger recomputations based on
# subfields (e.g., line.a has been modified, line.b is computed stored
# and depends on line.a, but line.b is not in the form view)
record._update_cache(changed_values, validate=True)
record._update_cache(changed_values, validate=False)
# update snapshot0 with changed values
for name in names: