From 71fc2e55f9b9488f1d95656b50e8eeee80c729cf Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Mon, 6 Sep 2021 11:45:15 +0000 Subject: [PATCH] [FIX] core: inherited computed fields in onchange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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é --- odoo/addons/test_new_api/models/test_new_api.py | 15 +++++++++++++++ odoo/addons/test_new_api/tests/test_onchange.py | 15 +++++++++++++++ odoo/fields.py | 6 ++++-- odoo/models.py | 15 ++++++++++++--- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index 6cb379478cd..b55129e6ac5 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -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' diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py index 424a88c71f9..0d5b94495b0 100644 --- a/odoo/addons/test_new_api/tests/test_onchange.py +++ b/odoo/addons/test_new_api/tests/test_onchange.py @@ -548,6 +548,8 @@ class TestOnChange(SavepointCaseWithUserDemo): + + """, }) @@ -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): diff --git a/odoo/fields.py b/odoo/fields.py index f916a741b19..1ba2fb16d66 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -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 diff --git a/odoo/models.py b/odoo/models.py index dda5837c306..8c519fdc52f 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -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))