[FIX] web: handle 'domain' key returned by onchanges

Onchange RPCs return an object that may contain a 'domain' key.
When it does, its value is an object whose keys are field names and
values are the new domain for the corresponding field.

Before this rev., the 'domain' key was totally ignored by the
BasicModel. This feature is used for example in the account.payment
form view (go to an open invoice, click on 'Register payment'): the
domain of field payment_method_id is updated by the onchanges (e.g.
when changing the journal_id). As the domain was ignored, it always
displayed all possible payment methods.
This commit is contained in:
Aaron Bohy
2017-06-29 14:58:04 +02:00
parent b954cd6483
commit 8473bde80e
4 changed files with 238 additions and 40 deletions
@@ -1785,15 +1785,7 @@ var FieldSelection = AbstractField.extend({
*/
init: function () {
this._super.apply(this, arguments);
if (this.field.type === 'many2one') {
this.values = this.record.specialData[this.name];
this.formatType = 'many2one';
} else {
this.values = _.reject(this.field.selection, function (v) {
return v[0] === false && v[1] === '';
});
}
this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
this._setValues();
},
//--------------------------------------------------------------------------
@@ -1823,13 +1815,12 @@ var FieldSelection = AbstractField.extend({
* @private
*/
_renderEdit: function () {
if (!this.$el.children().length) {
for (var i = 0 ; i < this.values.length ; i++) {
this.$el.append($('<option/>', {
value: JSON.stringify(this.values[i][0]),
html: this.values[i][1]
}));
}
this.$el.empty();
for (var i = 0 ; i < this.values.length ; i++) {
this.$el.append($('<option/>', {
value: JSON.stringify(this.values[i][0]),
html: this.values[i][1]
}));
}
var value = this.value;
if (this.field.type === 'many2one' && value) {
@@ -1844,6 +1835,31 @@ var FieldSelection = AbstractField.extend({
_renderReadonly: function () {
this.$el.empty().text(this._formatValue(this.value));
},
/**
* @override
*/
_reset: function () {
this._super.apply(this, arguments);
this._setValues();
},
/**
* Sets the possible field values. If the field is a many2one, those values
* may change during the lifecycle of the widget if the domain change (an
* onchange may change the domain).
*
* @private
*/
_setValues: function () {
if (this.field.type === 'many2one') {
this.values = this.record.specialData[this.name];
this.formatType = 'many2one';
} else {
this.values = _.reject(this.field.selection, function (v) {
return v[0] === false && v[1] === '';
});
}
this.values = [[false, this.attrs.placeholder || '']].concat(this.values);
},
//--------------------------------------------------------------------------
// Handlers
@@ -1869,7 +1885,9 @@ var FieldSelection = AbstractField.extend({
});
var FieldRadio = FieldSelection.extend({
template: 'FieldRadio',
template: null,
className: 'o_field_radio',
tagName: 'span',
specialData: "_fetchSpecialMany2ones",
supportedFieldTypes: ['selection', 'many2one'],
events: _.extend({}, AbstractField.prototype.events, {
@@ -1880,14 +1898,12 @@ var FieldRadio = FieldSelection.extend({
*/
init: function () {
this._super.apply(this, arguments);
if (this.field.type === 'selection') {
this.values = this.field.selection || [];
} else if (this.field.type === 'many2one') {
this.values = _.map(this.record.specialData[this.name], function (val) {
return [val.id, val.display_name];
});
if (this.mode === 'edit') {
this.tagName = 'div';
this.className += this.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical';
}
this.unique_id = _.uniqueId("radio");
this._setValues();
},
//--------------------------------------------------------------------------
@@ -1911,17 +1927,45 @@ var FieldRadio = FieldSelection.extend({
* @override
*/
_renderEdit: function () {
var value;
var self = this;
var currentValue;
if (this.field.type === 'many2one') {
value = this.value && this.value.data.id;
currentValue = this.value && this.value.data.id;
} else {
value = this.value;
currentValue = this.value;
}
var index = _.findIndex(this.values, function (option) {
return option[0] === value;
this.$el.empty();
_.each(this.values, function (value, index) {
self.$el.append(qweb.render('FieldRadio.button', {
checked: value[0] === currentValue,
id: self.unique_id + '_' + value[0],
index: index,
value: value,
}));
});
this.$("input").prop("checked", false);
this.$('input[data-index="' + index + '"]').prop('checked', true);
},
/**
* @override
*/
_reset: function () {
this._super.apply(this, arguments);
this._setValues();
},
/**
* Sets the possible field values. If the field is a many2one, those values
* may change during the lifecycle of the widget if the domain change (an
* onchange may change the domain).
*
* @private
*/
_setValues: function () {
if (this.field.type === 'selection') {
this.values = this.field.selection || [];
} else if (this.field.type === 'many2one') {
this.values = _.map(this.record.specialData[this.name], function (val) {
return [val.id, val.display_name];
});
}
},
//--------------------------------------------------------------------------
@@ -1791,7 +1791,9 @@ var BasicModel = AbstractModel.extend({
var context = record.getContext({fieldName: fieldName});
var domain = record.getDomain({fieldName: fieldName});
if (domain.length) {
var localID = record._changes && record._changes[fieldName] || record.data[fieldName];
var localID = (record._changes && fieldName in record._changes) ?
record._changes[fieldName] :
record.data[fieldName];
if (localID) {
var element = this.localData[localID];
domain = ["|", ["id", "=", element.data.id]].concat(domain);
@@ -2994,6 +2996,12 @@ var BasicModel = AbstractModel.extend({
});
record._warning = true;
}
if (result.domain) {
var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
for (var fieldName in result.domain) {
fieldsInfo[fieldName].domain = result.domain[fieldName];
}
}
return self._applyOnChange(result.value, record).then(function () {
return result;
});
+5 -9
View File
@@ -724,15 +724,11 @@
t-att-autofocus="widget.attrs.autofocus"
t-att-id="widget.idForLabel"/>
</t>
<t t-name="FieldRadio">
<span t-if="widget.mode === 'readonly'"/>
<div t-if="widget.mode !== 'readonly'" t-attf-class="o_field_radio #{widget.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical'}">
<div t-foreach="widget.values" t-as="value" class="o_radio_item">
<input type="radio" class="o_radio_input" t-att-id="widget.unique_id + '_' + value[0]"
t-att-name="widget.unique_id" t-att-data-value="value[0]" t-att-data-index="value_index"/>
<label t-if="!widget.nodeOptions.no_radiolabel" class="o_form_label" t-att-for="widget.unique_id + '_' + value[0]"><t t-esc="value[1]"/></label>
</div>
<t t-name="FieldRadio.button">
<div class="o_radio_item">
<input type="radio" class="o_radio_input" t-att-checked="checked ? true : undefined"
t-att-data-value="value[0]" t-att-data-index="index" t-att-id="id"/>
<label class="o_form_label" t-att-for="id"><t t-esc="value[1]"/></label>
</div>
</t>
<t t-name="FieldMany2One">
@@ -1250,6 +1250,55 @@ QUnit.module('relational_fields', {
});
});
QUnit.test('many2one: domain updated by an onchange', function (assert) {
assert.expect(2);
this.data.partner.onchanges = {
int_field: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="int_field"/>' +
'<field name="trululu"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
domain = [['id', 'in', [10]]];
return $.when({
domain: {
trululu: domain,
}
});
}
if (args.method === 'name_search') {
assert.deepEqual(args.kwargs.args, domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
// trigger a name_search (domain should be [])
form.$('.o_field_widget[name=trululu] input').click();
// close the dropdown
form.$('.o_field_widget[name=trululu] input').click();
// trigger an onchange that will update the domain
form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
// trigger a name_search (domain should be [['id', 'in', [10]]])
form.$('.o_field_widget[name=trululu] input').click();
form.destroy();
});
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -5884,6 +5933,55 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('widget selection on a many2one: domain updated by an onchange', function (assert) {
assert.expect(4);
this.data.partner.onchanges = {
int_field: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="int_field"/>' +
'<field name="trululu" widget="selection"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
domain = [['id', 'in', [10]]];
return $.when({
domain: {
trululu: domain,
}
});
}
if (args.method === 'name_search') {
assert.deepEqual(args.args[1], domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 4,
"should be 4 options in the selection");
// trigger an onchange that will update the domain
form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
assert.strictEqual(form.$('.o_field_widget[name=trululu] option').length, 1,
"should be 1 option in the selection");
form.destroy();
});
QUnit.module('FieldMany2ManyTags');
QUnit.test('fieldmany2many tags: rendering and edition', function (assert) {
@@ -6298,6 +6396,58 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('widget radio on a many2one: domain updated by an onchange', function (assert) {
assert.expect(4);
this.data.partner.onchanges = {
int_field: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="int_field"/>' +
'<field name="trululu" widget="radio"/>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
domain = [['id', 'in', [10]]];
return $.when({
value: {
trululu: false,
},
domain: {
trululu: domain,
},
});
}
if (args.method === 'search_read') {
assert.deepEqual(args.kwargs.domain, domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 3,
"should be 3 radio buttons");
// trigger an onchange that will update the domain
form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
assert.strictEqual(form.$('.o_field_widget[name=trululu] .o_radio_item').length, 0,
"should be no more radio button");
form.destroy();
});
QUnit.module('FieldMany2ManyCheckBoxes');
QUnit.test('widget many2many_checkboxes', function (assert) {