[FIX] web: apply onchanges for sub fields for new records

Before this commit, when creating a new record, we only looked at the
main fields to determine if a call to onchange was needed.  This was not
enough, since sub fields (fields in a one2many for example) can have an
onchange.

This was an issue only for new records with onchanges in sub fields, but
no onchange on main fields.  For example, the merge contact wizard.

To solve this issue, we had to propagate the current view type from the
field to the basic model, because otherwise, the main viewtype was used
to compute the onchangeSpec, and that is not always correct.
This commit is contained in:
Géry Debongnie
2017-05-19 12:14:49 +02:00
parent 8af3d452f4
commit 92409cdfe5
5 changed files with 85 additions and 39 deletions
@@ -371,6 +371,7 @@ var AbstractField = Widget.extend({
this.trigger_up('field_changed', {
dataPointID: this.dataPointID,
changes: changes,
viewType: this.viewType,
});
},
@@ -615,10 +615,12 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} record_id
* @param {Object} changes a map field => new value
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
* @returns {string[]} list of changed fields
*/
notifyChanges: function (record_id, changes) {
return this.mutex.exec(this._applyChange.bind(this, record_id, changes));
notifyChanges: function (record_id, changes, viewType) {
return this.mutex.exec(this._applyChange.bind(this, record_id, changes, viewType));
},
/**
* Reload all data for a given resource
@@ -902,9 +904,11 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} recordID
* @param {Object} changes
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
* @returns {Deferred}
*/
_applyChange: function (recordID, changes) {
_applyChange: function (recordID, changes, viewType) {
var self = this;
var record = this.localData[recordID];
var field;
@@ -937,7 +941,7 @@ var BasicModel = AbstractModel.extend({
}
var onchangeDef;
if (onChangeFields.length) {
onchangeDef = self._performOnChange(record, onChangeFields).then(function (result) {
onchangeDef = self._performOnChange(record, onChangeFields, viewType).then(function (result) {
delete record._warning;
return _.keys(changes).concat(Object.keys(result && result.value || {}));
});
@@ -1237,22 +1241,34 @@ var BasicModel = AbstractModel.extend({
* @see _performOnChange
*
* @param {Object} record resource object of type 'record'
* @returns {Object} an onchange spec
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
* @returns {Object|false} an onchange spec, or false if no onchange should
* be applied
*/
_buildOnchangeSpecs: function (record) {
// TODO: replace this function by some generic tree function in utils
_buildOnchangeSpecs: function (record, viewType) {
var hasOnchange = false;
var specs = {};
_.each(record.getFieldNames(), function (name) {
var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
_.each(Object.keys(fieldsInfo), function (name) {
var field = record.fields[name];
var fieldInfo = record.fieldsInfo[record.viewType][name];
var fieldInfo = fieldsInfo[name];
specs[name] = (field.onChange) || "";
if (field.onChange) {
hasOnchange = true;
}
_.each(fieldInfo.views, function (view) {
_.each(view.fieldsInfo[view.type], function (field, subname) {
specs[name + '.' + subname] = (view.fields[subname].onChange) || "";
var onChange = view.fields[subname].onChange;
specs[name + '.' + subname] = onChange || "";
if (onChange) {
hasOnchange = true;
}
});
});
});
return specs;
return hasOnchange ? specs : false;
},
/**
* Fetch all name_gets for the many2ones in a group
@@ -2349,23 +2365,11 @@ var BasicModel = AbstractModel.extend({
});
return $.when.apply($, defs)
.then(function () {
var shouldApplyOnchange = false;
var field;
for (var field_name in record.data) {
field = record.fields[field_name];
if (field.onChange) {
shouldApplyOnchange = true;
return self._performOnChange(record, fields_key).then(function () {
if (record._warning) {
return $.Deferred().reject();
}
}
if (shouldApplyOnchange) {
return self._performOnChange(record, fields_key).then(function () {
if (record._warning) {
return $.Deferred().reject();
}
});
} else {
return $.when();
}
});
})
.then(function () {
return self._fetchRelationalData(record);
@@ -2449,12 +2453,17 @@ var BasicModel = AbstractModel.extend({
*
* @param {Object} record
* @param {string[]} fields changed fields
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
* @returns {Deferred} The returned deferred can fail, in which case the
* fail value will be the warning message received from the server
*/
_performOnChange: function (record, fields) {
_performOnChange: function (record, fields, viewType) {
var self = this;
var onchange_spec = this._buildOnchangeSpecs(record);
var onchangeSpec = this._buildOnchangeSpecs(record, viewType);
if (!onchangeSpec) {
return $.when();
}
var idList = record.data.id ? [record.data.id] : [];
var options = {
full: true,
@@ -2470,7 +2479,7 @@ var BasicModel = AbstractModel.extend({
return self._rpc({
model: record.model,
method: 'onchange',
args: [idList, currentData, fields, onchange_spec, context],
args: [idList, currentData, fields, onchangeSpec, context],
})
.then(function (result) {
if (!record._changes) {
@@ -43,7 +43,7 @@ var FieldManagerMixin = {
*/
_applyChanges: function (dataPointID, changes, event) {
var self = this;
return this.model.notifyChanges(dataPointID, changes)
return this.model.notifyChanges(dataPointID, changes, event.data.viewType)
.then(function (result) {
if (event.data.force_save) {
return self.model.save(dataPointID).then(function () {
@@ -20,7 +20,7 @@ QUnit.module('relational_fields', {
int_field: {string: "int_field", type: "integer", sortable: true},
qux: {string: "Qux", type: "float", digits: [16,1] },
p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'},
turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle'},
turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu'},
trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
product_id: {string: "Product", type: "many2one", relation: 'product'},
@@ -1336,12 +1336,14 @@ QUnit.module('relational_fields', {
QUnit.test('one2many list (editable): edition, part 4', function (assert) {
assert.expect(3);
var i = 0;
this.data.turtle.onchanges = {
turtle_trululu: function (obj) {
if (obj.turtle_trululu) {
if (i) {
obj.turtle_description = "Some Description";
}
i++;
},
};
@@ -2334,25 +2336,26 @@ QUnit.module('relational_fields', {
});
QUnit.test('parent data is properly sent on an onchange rpc, new record', function (assert) {
assert.expect(1);
assert.expect(6);
this.data.partner.onchanges = {bar: function () {}};
this.data.turtle.onchanges = {turtle_bar: function () {}};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="foo"/>' +
'<field name="p">' +
'<field name="turtles">' +
'<tree editable="top">' +
'<field name="bar"/>' +
'<field name="turtle_bar"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'onchange') {
assert.step(args.method);
if (args.method === 'onchange' && args.model === 'turtle') {
var fieldValues = args.args[1];
assert.strictEqual(fieldValues.trululu.foo, "My little Foo Value",
assert.strictEqual(fieldValues.turtle_trululu.foo, "My little Foo Value",
"should have properly sent the parent foo value");
}
return this._super.apply(this, arguments);
@@ -2361,6 +2364,7 @@ QUnit.module('relational_fields', {
form.$buttons.find('.o_form_button_edit').click();
form.$('tbody td.o_field_x2many_list_row_add a').click();
assert.verifySteps(['default_get', 'onchange', 'default_get', 'onchange']);
form.destroy();
});
@@ -1159,6 +1159,38 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('default record with a one2many and an onchange on sub field', function (assert) {
assert.expect(4);
this.data.partner.onchanges.foo = function () {};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="p">' +
'<tree>' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'onchange') {
assert.deepEqual(args.args[3], {
p: '',
'p.display_name': '',
'p.foo': '1'
}, "onchangeSpec should be correct (with sub fields)");
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['default_get', 'onchange']);
form.destroy();
});
QUnit.test('sidebar is hidden when switching to edit mode', function (assert) {
assert.expect(3);