[FIX] web: Many2one in X2Manys: quick create then save line

A problem occured with a required Many2One inside an X2Many (e.g.
order_line field in sale.order form view). If the user clicked on
'Add an item' to add a row, then typed some text in the Many2One,
then clicked on 'Create ...' to quick create a new product, and
then directly clicked on 'Add an item' to add another row (before
the name_create returned), the first row was discarded.

This was because before the name_create returns, the Many2one still
has no value, and as it is required, the row was considered invalid
and then discarded.

This rev. ensures to wait for the name_create to return before
trying to save the row and to create a new one.
This commit is contained in:
Aaron Bohy
2017-06-13 08:24:59 +02:00
parent 76262f2043
commit 5c4485d182
2 changed files with 105 additions and 19 deletions
@@ -619,12 +619,15 @@ var FieldX2Many = AbstractField.extend({
* @returns {Deferred}
*/
commitChanges: function () {
var self = this;
var inEditionRecordID =
this.renderer
&& this.renderer.viewType === "list"
&& this.renderer.getEditableRecordID();
if (inEditionRecordID) {
return this._saveLine(inEditionRecordID);
return this.renderer.commitChanges(inEditionRecordID).then(function () {
return self._saveLine(inEditionRecordID);
});
}
return this._super.apply(this, arguments);
},
@@ -767,21 +770,17 @@ var FieldX2Many = AbstractField.extend({
* did not agree to discard.
*/
_saveLine: function (recordID) {
var self = this;
var def = $.Deferred();
this.renderer.commitChanges(recordID).then(function () { // TODO wrong as no mutex protection
var fieldNames = self.renderer.canBeSaved(recordID);
if (fieldNames.length) {
self.trigger_up('discard_changes', {
recordID: recordID,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
});
} else {
self.renderer.setRowMode(recordID, 'readonly')
.done(def.resolve.bind(def));
}
});
var fieldNames = this.renderer.canBeSaved(recordID);
if (fieldNames.length) {
this.trigger_up('discard_changes', {
recordID: recordID,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
});
} else {
this.renderer.setRowMode(recordID, 'readonly').done(def.resolve.bind(def));
}
return def;
},
@@ -900,9 +899,15 @@ var FieldX2Many = AbstractField.extend({
ev.stopPropagation();
this.trigger_up('mutexify', {
action: function () {
return self._saveLine(ev.data.recordID)
.done(ev.data.onSuccess)
.fail(ev.data.onFailure);
return self.renderer.commitChanges(ev.data.recordID).then(function () {
self.trigger_up('mutexify', {
action: function () {
return self._saveLine(ev.data.recordID)
.done(ev.data.onSuccess)
.fail(ev.data.onFailure);
},
});
});
},
});
},
@@ -922,7 +927,7 @@ var FieldX2Many = AbstractField.extend({
id: rowID,
data: data,
});
})
});
},
/**
* Adds field name information to the event, so that the view upstream is
@@ -659,6 +659,87 @@ QUnit.module('relational_fields', {
});
});
QUnit.test('list in form: quick create then add a new line directly', function (assert) {
// required many2one inside a one2many list: directly after quick creating
// a new many2one value (before the name_create returns), click on add an item:
// at this moment, the many2one has still no value, and as it is required,
// the row is discarded if a saveLine is requested. However, it should
// wait for the name_create to return before trying to save the line.
var done = assert.async();
assert.expect(8);
this.data.partner.onchanges = {
trululu: function () {},
};
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
var def = $.Deferred();
var newRecordID;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="trululu" required="1"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'name_create') {
return def.then(_.constant(result)).then(function (nameGet) {
newRecordID = nameGet[0];
return nameGet;
});
}
if (args.method === 'create') {
assert.deepEqual(args.args[0].p[0][2].trululu, newRecordID);
}
return result;
},
});
form.$('.o_field_x2many_list_row_add a').click();
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
form.$('.o_field_many2one input').val('b').trigger('keydown');
concurrency.delay(0).then(function () {
$dropdown.find('li:first()').click(); // quick create 'b'
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('.o_data_row').length, 1,
"there should still be only one row");
assert.ok(form.$('.o_data_row').hasClass('o_selected_row'),
"the row should still be in edition");
def.resolve();
assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b',
"first row should have the correct m2o value");
assert.strictEqual(form.$('.o_data_row').length, 2,
"there should now be 2 rows");
assert.ok(form.$('.o_data_row:nth(1)').hasClass('o_selected_row'),
"the second row should be in edition");
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_data_row').length, 1,
"there should be 1 row saved (the second one was empty and invalid)");
assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'b',
"should have the correct m2o value");
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
form.destroy();
done();
});
});
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);