diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index e1ff2e8cca9..1d45402ee29 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -45,18 +45,24 @@ var AbstractField = Widget.extend({ * skip the widget when pressing tab */ readonly: false, - /** * If this flag is set to true, the field widget will be reset on every * change which is made in the view (if the view supports it). This is * currently a form view feature. */ resetOnAnyFieldChange: false, - /** * if true, the widget will replace a cell in an editable list view */ replace_element: false, + /** + * If this flag is given a string, the related BasicModel will be used to + * initialize specialData the field might need. This data will be available + * through this.record.specialData[this.name]. + * + * @see BasicModel._fetchSpecialData + */ + specialData: false, /** * to override to indicate which field types are supported by the widget */ diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 4d3aaf14e7d..68bd7a52343 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1107,10 +1107,11 @@ var FieldMany2ManyCheckBoxes = AbstractField.extend({ events: _.extend({}, AbstractField.prototype.events, { change: '_onChange', }), + specialData: "_fetchSpecialRelation", supportedFieldTypes: ['many2many'], init: function () { this._super.apply(this, arguments); - this.m2mValues = this.field.__many2many_information; + this.m2mValues = this.record.specialData[this.name]; }, //-------------------------------------------------------------------------- @@ -1170,6 +1171,7 @@ var FieldStatus = AbstractField.extend({ events: { 'click .o_arrow_button': '_onClickStage', }, + specialData: "_fetchSpecialStatus", supportedFieldTypes: ['selection', 'many2one'], init: function () { this._super.apply(this, arguments); @@ -1192,7 +1194,7 @@ var FieldStatus = AbstractField.extend({ */ _setState: function () { if (this.field.type === 'many2one') { - this.status_information = this.field.__status_information; + this.status_information = this.record.specialData[this.name]; var status = _.findWhere(this.status_information, {id: this.value.res_id}); if (status) { status.selected = true; @@ -1260,6 +1262,7 @@ var FieldStatus = AbstractField.extend({ */ var FieldSelection = AbstractField.extend({ template: 'FieldSelection', + specialData: "_fetchSpecialRelation", supportedFieldTypes: ['selection', 'many2one'], events: _.extend({}, AbstractField.prototype.events, { 'change': '_onChange', @@ -1272,7 +1275,7 @@ var FieldSelection = AbstractField.extend({ this._super.apply(this, arguments); this.values = []; if (this.field.type === 'many2one') { - this.values = this.field.__selection_information; + this.values = this.record.specialData[this.name]; } else { this.values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; @@ -1357,6 +1360,7 @@ var FieldSelection = AbstractField.extend({ var FieldRadio = FieldSelection.extend({ template: 'FieldRadio', + specialData: "_fetchSpecialMany2ones", supportedFieldTypes: ['selection', 'many2one'], events: _.extend({}, AbstractField.prototype.events, { 'click input': '_onInputClick', @@ -1369,7 +1373,7 @@ var FieldRadio = FieldSelection.extend({ if (this.field.type === 'selection') { this.values = this.field.selection || []; } else if (this.field.type === 'many2one') { - this.values = _.map(this.field.__status_information, function (val) { + this.values = _.map(this.record.specialData[this.name], function (val) { return [val.id, val.display_name]; }); } diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index f6c2021f2e8..cdb8bb61f7c 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -41,6 +41,8 @@ odoo.define('web.BasicModel', function (require) { * relationField: {string}, * res_id: {integer|null}, * res_ids: {integer[]}, + * specialData: {Object}, + * _specialDataCache: {Object}, * static: {boolean}, * type: {string} 'record' | 'list' * value: ?, @@ -133,14 +135,6 @@ var BasicModel = AbstractModel.extend({ // save is performed. this.mutex = new concurrency.Mutex(); - // this dictionary is used to hold all many2one informations for widget - // selection. - this.many2ones = {}; - - // this dictionary is used to hold all many2many informations for widget - // many2many_checkboxes. - this.many2manys = {}; - this.localData = Object.create(null); this._super.apply(this, arguments); }, @@ -330,16 +324,6 @@ var BasicModel = AbstractModel.extend({ element.data[fieldName] = this.get(element.data[fieldName]) || []; } } - - // weird way to get out many2ones for widget=selection - if (field.__fetch_selection) { - field.__selection_information = self.many2ones[field.relation]; - } - - // weird way to get out many2manys for widget=many2many_checkboxes - if (field.__fetch_many2manys) { - field.__many2many_information = self.many2manys[field.relation]; - } } // this is not strictly necessary, but it hides some implementation @@ -697,7 +681,11 @@ var BasicModel = AbstractModel.extend({ delete record._changes[name]; } }); - return fieldNames; + return self._fetchSpecialData(record).then(function (fieldNames2) { + // Return the names of the fields that changed (onchange or + // associated special data change) + return _.union(fieldNames, fieldNames2); + }); }); }); }, @@ -1371,6 +1359,147 @@ var BasicModel = AbstractModel.extend({ return $.when.apply($, _.map(groups, this._fetchMany2OneGroup.bind(this))); }, + /** + * Check the AbstractField specializations that are (will be) used by the + * given record and fetch the special data they will need. Special data are + * data that the rendering of the record won't need if it was not using + * particular widgets (example of these can be found at the methods which + * start with _fetchSpecial). + * + * @param {Object} record - an element from the localData + * @returns {Deferred} + * The deferred is resolved with an array containing the names of + * the field whose special data has been changed. + */ + _fetchSpecialData: function (record) { + var self = this; + var fieldNames = []; + return $.when.apply($, _.map(record.fieldNames, function (name) { + var attrs = record.fieldAttrs[name] || {}; + var Widget = attrs.Widget; + if (Widget && Widget.prototype.specialData) { + return self[Widget.prototype.specialData](record, name).then(function (data) { + if (data === undefined) { + return; + } + record.specialData[name] = data; + fieldNames.push(name); + }); + } + })).then(function () { + return fieldNames; + }); + }, + /** + * Fetches all the m2o records associated to the given fieldName. If the + * given fieldName is not a m2o field, nothing is done. + * + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @param {string[]} [fieldsToRead] - the m2os fields to read (id and + * display_name are automatic). + * @returns {Deferred} + * The deferred is resolved with the fetched special data. If this + * data is the same as the previously fetched one (for the given + * parameters), no RPC is done and the deferred is resolved with + * the undefined value. + */ + _fetchSpecialMany2ones: function (record, fieldName, fieldsToRead) { + var field = record.fields[fieldName]; + if (field.type !== "many2one") { + return $.when(); + } + + var context = record.getContext({fieldName: fieldName}); + var domain = record.getDomain({fieldName: fieldName}); + if (domain.length) { + var localID = record.data[fieldName]; + var element = this.localData[localID]; + domain = ["|", ["id", "=", element.data.id]].concat(domain); + } + + // avoid rpc if not necessary + var specialDataCache = {context: context, domain: domain}; + if (_.isEqual(record._specialDataCache[fieldName], specialDataCache)) { + return $.when(); + } + record._specialDataCache[fieldName] = specialDataCache; + + var self = this; + return this._rpc(field.relation, 'search_read') + .withFields(["id"].concat(fieldsToRead || [])) + .withContext(context) + .withDomain(domain) + .exec() + .then(function (result) { + var ids = _.pluck(result.records, 'id'); + return self._rpc(field.relation, 'name_get') + .args([ids]) + .withContext(context) + .exec() + .then(function (name_gets) { + _.each(result.records, function (rec) { + var name_get = _.find(name_gets, function (n) { + return n[0] === rec.id; + }); + rec.display_name = name_get[1]; + }); + return result.records; + }); + }); + }, + /** + * Fetches all the relation records associated to the given fieldName. If + * the given fieldName is not a relational field, nothing is done. + * + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @returns {Deferred} + * The deferred is resolved with the fetched special data. If this + * data is the same as the previously fetched one (for the given + * parameters), no RPC is done and the deferred is resolved with + * the undefined value. + */ + _fetchSpecialRelation: function (record, fieldName) { + var field = record.fields[fieldName]; + if (!_.contains(["many2one", "many2many", "one2many"], field.type)) { + return $.when(); + } + + var context = record.getContext({fieldName: fieldName}); + var domain = record.getDomain({fieldName: fieldName}); + + // avoid rpc if not necessary + var specialDataCache = {context: context, domain: domain}; + if (_.isEqual(record._specialDataCache[fieldName], specialDataCache)) { + return $.when(); + } + record._specialDataCache[fieldName] = specialDataCache; + + return this._rpc(field.relation, "name_search") + .args(["", domain]) + .withContext(context) + .exec(); + }, + /** + * Fetches all the m2o records associated to the given fieldName. If the + * given fieldName is not a m2o field, nothing is done. The difference with + * _fetchSpecialMany2ones is that the field given by options.fold_field is + * also fetched. + * + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @returns {Deferred} + * The deferred is resolved with the fetched special data. If this + * data is the same as the previously fetched one (for the given + * parameters), no RPC is done and the deferred is resolved with + * the undefined value. + */ + _fetchSpecialStatus: function (record, fieldName) { + var foldField = record.fieldAttrs[fieldName].options.fold_field; + var fieldsToRead = foldField ? [foldField] : []; + return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead); + }, /** * Fetch all data in a ungrouped list * @@ -1767,6 +1896,8 @@ var BasicModel = AbstractModel.extend({ relationField: params.relationField, res_id: res_id, res_ids: res_ids, + specialData: {}, + _specialDataCache: {}, static: params.static || false, type: type, // 'record' | 'list' value: value, @@ -1833,38 +1964,6 @@ var BasicModel = AbstractModel.extend({ var field = record.fields[name]; var attrs = record.fieldAttrs[name] || {}; var options = attrs.options || {}; - if (field.__fetch_status && !field.__status_information) { - var field_values = _.mapObject(record.data, function (val, key) { - var fieldType = record.fields[key].type; - if (fieldType === 'many2one') { - var element = self.localData[val]; - return element && element.data.id || false; - } - return val; - }); - var domain = Domain.prototype.stringToArray(field.domain || [], field_values); - var fetch_status_information = self._rpc(field.relation, 'search_read') - .withFields(['id'].concat(options.fold_field ? [options.fold_field] : [])) - .withDomain(domain) - .exec() - .then(function (result) { - var ids = _.pluck(result.records, 'id'); - return self._rpc(field.relation, 'name_get') - .args([ids]) - .withContext(self._getContext(record, {fieldName: name})) - .exec() - .then(function (name_gets) { - _.each(result.records, function (record) { - var name_get = _.find(name_gets, function (n) { - return n[0] === record.id; - }); - record.display_name = name_get[1]; - }); - field.__status_information = result.records; - }); - }); - defs.push(fetch_status_information); - } if (options.always_reload) { if (record.fields[name].type === 'many2one' && record.data[name]) { var element = self.localData[record.data[name]]; @@ -1877,26 +1976,10 @@ var BasicModel = AbstractModel.extend({ })); } } - if (field.__fetch_selection && !self.many2ones[field.relation]) { - var fetchSelection = self._rpc(field.relation, 'name_search') - .args(['', field.domain]) - .exec() - .then(function (result) { - self.many2ones[field.relation] = result; - }); - defs.push(fetchSelection); - } - if (field.__fetch_many2manys) { - var fetchMany2Manys = self._rpc(field.relation, 'name_search') - .args(['', field.domain]) - .exec() - .then(function (result) { - self.many2manys[field.relation] = result; - }); - defs.push(fetchMany2Manys); - } }); + defs.push(this._fetchSpecialData(record)); + return $.when.apply($, defs).then(function () { return record; }); diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 8fbff881bd1..939d7dabee9 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -170,16 +170,6 @@ var BasicView = AbstractView.extend({ attrs.options = attrs.options ? pyeval.py_eval(attrs.options) : {}; } - if (field.type === 'many2one') { - if (attrs.widget === 'statusbar' || attrs.widget === 'radio') { - field.__fetch_status = true; - } else if (attrs.widget === 'selection') { - field.__fetch_selection = true; - } - } - if (attrs.widget === 'many2many_checkboxes') { - field.__fetch_many2manys = true; - } if (attrs.on_change) { field.onChange = "1"; } diff --git a/addons/web/static/test/fields/relational_fields_tests.js b/addons/web/static/test/fields/relational_fields_tests.js index ca8c984e54e..f3d4ef5708c 100644 --- a/addons/web/static/test/fields/relational_fields_tests.js +++ b/addons/web/static/test/fields/relational_fields_tests.js @@ -2355,7 +2355,7 @@ QUnit.module('relational_fields', { "all status should be disabled"); assert.ok(form.$('.o_statusbar_status button[data-value="4"]').hasClass('btn-primary'), "selected status should be btn-primary"); - // TODO: add a test to check that we can change the status + form.destroy(); }); @@ -2400,6 +2400,45 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('statusbar with dynamic domain', function (assert) { + assert.expect(5); + + this.data.partner.fields.trululu.domain = "[('int_field', '>', qux)]"; + this.data.partner.records[2].int_field = 0; + + var rpcCount = 0; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '
' + + '' + + '' + + '' + + '', + mockRPC: function (route, args) { + if (route === '/web/dataset/search_read') { + rpcCount++; + } + return this._super.apply(this, arguments); + }, + res_id: 1, + }); + + form.$buttons.find('.o_form_button_edit').click(); + + assert.strictEqual(form.$('.o_statusbar_status button.disabled').length, 3, "should have 3 status"); + assert.strictEqual(rpcCount, 1, "should have done 1 search_read rpc"); + form.$('input:first').val(9.5).trigger("input").trigger("change"); + assert.strictEqual(form.$('.o_statusbar_status button.disabled').length, 2, "should have 2 status"); + assert.strictEqual(rpcCount, 2, "should have done 1 more search_read rpc"); + form.$('input:last').val("hey").trigger("input").trigger("change"); + assert.strictEqual(rpcCount, 2, "should not have done 1 more search_read rpc"); + + form.destroy(); + }); + QUnit.module('FieldSelection'); QUnit.test('widget selection in a list view', function (assert) {