From f65475d68cef09f5818841cf12fc5b22d295fe28 Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Wed, 15 Mar 2017 15:13:38 +0100 Subject: [PATCH] [FIX] fields, web: improve form onchange to x2many In a form view, when a field onchange lead to a change on a x2many, there was two different behavior: - if the x2many had an embedded view (eg. a tree view inside a form view) the onchange would notify that it expected the x2many field in this embedded view to be changed and handled the changes correctly. - if the x2many had a default view, the onchange ORM would not be aware the x2many could be modified and would not sent the changes back causing blank or not updated x2m lines and error on save. --- Two solutions were birthed to solve the second point: => PR #10557 = solving everything With this PR the onchange in the ORM is aware of every fields in the current view (even field in a x2m in a x2m in a x2m in a form view) and if any of these are change the javascript gets back the value of the fields present in the view. This PR has currently not been merged by fear of changing too much and anyway could only be done in master. => PR #12249 = if no field for x2many, send its form view fields With this change, if the ORM onchange is not aware of the fields in the x2many widget to returns, all the field in the x2m default form view are returned. This was merged in bbdf960 but introduced a number of other issue: - in most situation the x2many is represented by a list view, which may have fields missing of the form view, so the original is still present. - the view used may differ from the default form view in other way (depending on value in context or other possibilities). - the form view could have fields not present in the form view which could end up in `write` on fields which should not be written to. --- This commit reverts bbdf960 and adapts a small part of #10557 so the x2many with default view works as an embedded x2many. For more than one level (eg. a x2many in a x2many) this would still not work but it is only solvable by a PR such as #10557 which could only be targetted for master. With this commit: - the list of fields sent to ORM onchange is computed at the first onchange - the fields from a x2many field default view is sent for onchange - the initial onchange on record creation is delayed to when x2many are loaded closes #12249, closes #15336, closes #15890 fixes #11236, fixes #12249, fixes #15129, #15419 opw-705965 opw-716095 opw-715619 opw-710440 --- addons/web/static/src/js/views/form_view.js | 33 +++++++++++++++---- .../test_new_api/tests/test_onchange.py | 7 +--- odoo/fields.py | 2 +- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index 0330c5d95cb..640644b482a 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -55,7 +55,6 @@ var FormView = View.extend(common.FieldManagerMixin, { this.fields = {}; this.fields_order = []; this.datarecord = {}; - this._onchange_specs = {}; this.onchanges_mutex = new utils.Mutex(); this.default_focus_field = null; this.default_focus_button = null; @@ -75,7 +74,6 @@ var FormView = View.extend(common.FieldManagerMixin, { this.rendering_engine = new FormRenderingEngine(this); this.set({actual_mode: this.options.initial_mode}); this.has_been_loaded.done(function() { - self._build_onchange_specs(); self.on("change:actual_mode", self, self.toggle_buttons); self.on("change:actual_mode", self, self.toggle_sidebar); }); @@ -318,8 +316,11 @@ var FormView = View.extend(common.FieldManagerMixin, { this.update_pager(); // the mode must be actualized before updating the pager return $.when.apply(null, set_values).then(function() { if (!record.id) { - // trigger onchanges - self.do_onchange(null); + // trigger onchange for new record after x2many with non-embedded views are loaded + var fields_loaded = _.pluck(self.fields, 'is_loaded'); + $.when.apply(null, fields_loaded).done(function() { + self.do_onchange(null); + }); } self.on_form_changed(); self.rendering_engine.init_fields().then(function() { @@ -333,7 +334,7 @@ var FormView = View.extend(common.FieldManagerMixin, { } else { self.do_push_state({}); } - self.$el.removeClass('oe_form_dirty'); + self.$el.removeClass('oe_form_dirty'); }); }); }, @@ -381,7 +382,24 @@ var FormView = View.extend(common.FieldManagerMixin, { _.each(this.fields, function(field, name) { self._onchange_fields.push(name); self._onchange_specs[name] = find(name, field.node); - _.each(field.field.views, function(view) { + + // we get the list of first-level fields of x2many firstly by + // getting them from the field embedded views, then if no embedded + // view is present for a loaded view, we get them from the default + // view that has been loaded + + // gather embedded view objects + var views = _.clone(field.field.views); + // also gather default view objects + if (field.viewmanager) { + _.each(field.viewmanager.views, function(view, view_type) { + // add default view if it was not embedded and it is loaded + if (views[view_type] === undefined && view.controller) { + views[view_type] = view.controller.fields_view; + } + }); + } + _.each(views, function(view) { _.each(view.fields, function(_, subname) { self._onchange_specs[name + '.' + subname] = find(subname, view.arch); }); @@ -410,6 +428,9 @@ var FormView = View.extend(common.FieldManagerMixin, { do_onchange: function(widget) { var self = this; + if (self._onchange_specs === undefined) { + self._build_onchange_specs(); + } var onchange_specs = self._onchange_specs; try { var def = $.when({}); diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py index 0301ca2b3e4..a6e2bff1b35 100644 --- a/odoo/addons/test_new_api/tests/test_onchange.py +++ b/odoo/addons/test_new_api/tests/test_onchange.py @@ -307,12 +307,7 @@ class TestOnChange(common.TransactionCase): # When one2many domain contains non-computed field, things are ok self.assertEqual(result['value']['important_messages'], - [(5,)] + [(1, msg.id, { - 'name': msg.name, - 'body': msg.body, - 'author': (msg.author.id, msg.author.display_name), - 'size': msg.size - }) for msg in discussion.important_messages]) + [(5,)] + [(4, msg.id) for msg in discussion.important_messages]) # But here with commit 5676d81, we get value of: [(2, email.id)] self.assertEqual( diff --git a/odoo/fields.py b/odoo/fields.py index c004b640cd2..2f898221e99 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2133,7 +2133,7 @@ class One2many(_RelationalMulti): _description_relation_field = property(attrgetter('inverse_name')) def convert_to_onchange(self, value, record, fnames=()): - fnames = set(fnames or value.fields_view_get()['fields']) + fnames = set(fnames or ()) fnames.discard(self.inverse_name) return super(One2many, self).convert_to_onchange(value, record, fnames)