[FIX] core: inherited computed fields in onchange

Assume that model A has a computed field G that depends on both fields
F1 and F2.  Assume that model B inherits from A (_inherits).  When G is
computed during an onchange, it must use the form values for all its
dependencies F1 and F2.  Before this commit, only the field triggering
the onchange was assigned on the parent record (see (*) below.)

                      record        parent
    --- ------------+-------------+------------
     initial state  | F1=0, F2=0  | F1=0, F2=0
     assign F1=1    | F1=1, F2=0  | F1=1, F2=0
     assign F2=2    | F1=1, F2=1  | F1=0, F2=1 (*)

The commit also includes another patch: when assigning the parent
record, the ORM determines the dependent records (in this case, the main
record in the onchange).  If the delegate field (many2one from B to A)
has an inverse one2many field, the ORM uses that field to determine
dependent records.  However, in the case of a new record, that field is
not in cache, and its value is determined to be... empty!  The patch
consists in "fixing" the value of that field when we determine the
parent record (when the delegate field is accessed.)

Part-of: odoo/odoo#76042
Co-authored-by: William André <wan@odoo.com>
This commit is contained in:
Raphael Collet
2021-09-06 18:29:03 +00:00
co-authored by William André
parent 9f33d7dcac
commit 71fc2e55f9
4 changed files with 46 additions and 5 deletions
@@ -457,12 +457,27 @@ class Move(models.Model):
quantity = fields.Integer(compute='_compute_quantity', store=True)
tag_id = fields.Many2one('test_new_api.multi.tag')
tag_name = fields.Char(related='tag_id.name')
tag_repeat = fields.Integer()
tag_string = fields.Char(compute='_compute_tag_string')
# This field can fool the ORM during onchanges! When editing a payment
# record, modified fields are assigned to the parent record. When
# determining the dependent records, the ORM looks for the payments related
# to this record by the field `move_id`. As this field is an inverse of
# `move_id`, it uses it. If that field was not initialized properly, the
# ORM determines its value to be... empty (instead of the payment record.)
payment_ids = fields.One2many('test_new_api.payment', 'move_id')
@api.depends('line_ids.quantity')
def _compute_quantity(self):
for record in self:
record.quantity = sum(line.quantity for line in record.line_ids)
@api.depends('tag_name', 'tag_repeat')
def _compute_tag_string(self):
for record in self:
record.tag_string = (record.tag_name or "") * record.tag_repeat
class MoveLine(models.Model):
_name = 'test_new_api.move_line'
@@ -548,6 +548,8 @@ class TestOnChange(SavepointCaseWithUserDemo):
<field name="move_id" readonly="1" required="0"/>
<field name="tag_id"/>
<field name="tag_name"/>
<field name="tag_repeat"/>
<field name="tag_string"/>
</form>
""",
})
@@ -556,19 +558,32 @@ class TestOnChange(SavepointCaseWithUserDemo):
# assigning 'tag_id' should modify 'move_id.tag_id' accordingly, which
# should in turn recompute `move.tag_name` and `tag_name`
form = Form(self.env['test_new_api.payment'], view)
self.assertEqual(form.tag_name, False)
form.tag_id = foo
self.assertEqual(form.tag_name, 'Foo')
self.assertEqual(form.tag_string, '')
form.tag_repeat = 2
self.assertEqual(form.tag_name, 'Foo')
self.assertEqual(form.tag_string, 'FooFoo')
payment = form.save()
self.assertEqual(payment.tag_id, foo)
self.assertEqual(payment.tag_name, 'Foo')
self.assertEqual(payment.tag_repeat, 2)
self.assertEqual(payment.tag_string, 'FooFoo')
with Form(payment, view) as form:
form.tag_id = bar
self.assertEqual(form.tag_name, 'Bar')
self.assertEqual(form.tag_string, 'BarBar')
form.tag_repeat = 3
self.assertEqual(form.tag_name, 'Bar')
self.assertEqual(form.tag_string, 'BarBarBar')
self.assertEqual(payment.tag_id, bar)
self.assertEqual(payment.tag_name, 'Bar')
self.assertEqual(payment.tag_repeat, 3)
self.assertEqual(payment.tag_string, 'BarBarBar')
class TestComputeOnchange(common.TransactionCase):
+4 -2
View File
@@ -1122,8 +1122,10 @@ class Field(MetaField('DummyField', (object,), {})):
for name, value in record._cache.items()
if is_inherited_field(name)
})
value = self.convert_to_cache(parent, record)
env.cache.set(record, self, value)
# in case the delegate field has inverse one2many fields, this
# updates the inverse fields as well
record._update_cache({self.name: parent}, validate=False)
value = env.cache.get(record, self)
else:
# non-stored field or stored field on new record: default value
+12 -3
View File
@@ -6395,13 +6395,22 @@ Fields:
initial_values = dict(values, **dict.fromkeys(names, False))
# do not force delegate fields to False
for name in self._inherits.values():
if not initial_values.get(name, True):
initial_values.pop(name)
for parent_name in self._inherits.values():
if not initial_values.get(parent_name, True):
initial_values.pop(parent_name)
# create a new record with values
record = self.new(initial_values, origin=self)
# make parent records match with the form values; this ensures that
# computed fields on parent records have all their dependencies at
# their expected value
for name in initial_values:
field = self._fields.get(name)
if field and field.inherited:
parent_name, name = field.related.split('.', 1)
record[parent_name]._update_cache({name: record[name]})
# make a snapshot based on the initial values of record
snapshot0 = Snapshot(record, nametree, fetch=(not first_call))