[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:
co-authored by
William André
parent
9f33d7dcac
commit
71fc2e55f9
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user