From 5c4485d182b6ab68c7d5c97161f25e67a2fa60d2 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 8 Jun 2017 14:57:09 +0200 Subject: [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. --- .../web/static/src/js/fields/relational_fields.js | 43 ++++++++++++---------- 1 file changed, 24 insertions(+), 19 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index ae3b06d3a02..3b47b8c64b0 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -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 -- cgit v1.2.3 From ebd17217c4e7e9ff06d97028de64fb2fc44d6266 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 8 Jun 2017 15:37:04 +0200 Subject: [FIX] web: BasicModel: don't write on readonly fields Ultimately, it seems that sending the value of readonly fields when saving is not such a good idea. Suppose that there is a computed field displayed as readonly in a form view. This field is computed from other records, e.g. from a one2many field which is editable in the form view. Finally, the inverse function of the computed field creates the one2many records. If the user adds some records to the one2many, then saves, the readonly computed field's value is sent to the server as well, and thus the inverse function is executed, which isn't what we want. This commit essentially reverts 0494d61274, except that we now take into account the readonly modifier to determine whether or not a value is sent to the server, and not only the readonly attribute of the field (which can be seen as a default). Before rev. 0494d61274, we made a difference between write and create RPCs, for an unclear reason. We don't do that anymore: readonly fields are never sent to the server (same behavior as before the new views). --- .../static/src/js/views/basic/basic_controller.js | 1 + .../web/static/src/js/views/basic/basic_model.js | 61 ++++++++++++++++++++-- addons/web/static/src/js/views/view_dialogs.js | 1 + 3 files changed, 58 insertions(+), 5 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 16a066db495..af4ca7d7a08 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -419,6 +419,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { var saveDef = this.model.save(recordID, { // Save then leave edit mode reload: options.reload, savePoint: options.savePoint, + viewType: options.viewType, }); if (!options.stayInEdit) { saveDef = saveDef.then(function (fieldNames) { 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 61c285d6581..20066b858f1 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -704,6 +704,8 @@ var BasicModel = AbstractModel.extend({ * @param {boolean} [options.savePoint=false] if true, the record will only * be 'locally' saved: its changes written in a _savePoint key that can * be restored later by call discardChanges with option rollback to true + * @param {string} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record * @returns {Deferred} * Resolved with the list of field names (whose value has been modified) */ @@ -718,6 +720,11 @@ var BasicModel = AbstractModel.extend({ if (newValue instanceof Array) { rec._savePoint = newValue.slice(0); } else { + // save the viewType of edition, so that the correct readonly modifiers + // can be evaluated when the record will be saved + for (var fieldName in (rec._changes || {})) { + rec._editionViewType[fieldName] = options.viewType; + } rec._savePoint = _.extend({}, newValue); } }); @@ -728,7 +735,7 @@ var BasicModel = AbstractModel.extend({ // id never changes, and should not be written delete record._changes.id; } - var changes = self._generateChanges(record); + var changes = self._generateChanges(record, options.viewType); if (method === 'create') { var fieldNames = record.getFieldNames(); @@ -1869,12 +1876,26 @@ var BasicModel = AbstractModel.extend({ * * @private * @param {Object} record + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record. Note that if an editionViewType is + * specified for a field, it will take the priority over the viewType arg. * @returns {Object} a map from changed fields to their new value */ - _generateChanges: function (record) { + _generateChanges: function (record, viewType) { + viewType = viewType || record.viewType; var changes = _.extend({}, record._changes); var commands = this._generateX2ManyCommands(record, true); for (var fieldName in record.fields) { + // remove readonly fields from the list of changes + if (fieldName in changes || fieldName in commands) { + var editionViewType = record._editionViewType[fieldName] || viewType; + if (this._isFieldReadonly(record, fieldName, editionViewType)) { + delete changes[fieldName]; + continue; + } + } + + // process relational fields and handle the null case var type = record.fields[fieldName].type; if (type === 'one2many' || type === 'many2many') { if (commands[fieldName].length) { // replace localId by commands @@ -2199,6 +2220,31 @@ var BasicModel = AbstractModel.extend({ } return context; }, + /** + * Returns true if the field is readonly (checking first in the modifiers, + * and if there is no readonly modifier, checking the readonly attribute of + * the field). + * + * @private + * @param {Object} record an element from the localData + * @param {string} fieldName + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record + * @returns {boolean} + */ + _isFieldReadonly: function (record, fieldName, viewType) { + var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName]; + var modifiers; + if (fieldInfo) { + var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}"); + modifiers = this._evalModifiers(record, rawModifiers); + } + if (modifiers && 'readonly' in modifiers) { + return modifiers.readonly; + } else { + return record.fields[fieldName].readonly; + } + }, /** * Returns true iff value is considered to be set for the given field's type. * @@ -2330,6 +2376,12 @@ var BasicModel = AbstractModel.extend({ viewType: params.viewType, }; + // _editionViewType is a dict whose keys are field names and which is populated when a field + // is edited with the viewType as value. This is useful for one2manys to determine whether + // or not a field is readonly (using the readonly modifiers of the view in which the field + // has been edited) + dataPoint._editionViewType = {}; + dataPoint.evalModifiers = this._evalModifiers.bind(this, dataPoint); dataPoint.getContext = this._getContext.bind(this, dataPoint); dataPoint.getDomain = this._getDomain.bind(this, dataPoint); @@ -2507,7 +2559,7 @@ var BasicModel = AbstractModel.extend({ .then(function () { // save initial changes, so they can be restored later, // if we need to discard. - self.save(record.id, {savePoint: true}) + self.save(record.id, {savePoint: true}); return record.id; }); @@ -2582,8 +2634,7 @@ var BasicModel = AbstractModel.extend({ * @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 + * @returns {Deferred} */ _performOnChange: function (record, fields, viewType) { var self = this; diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index a19e440913e..4035ea6e63e 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -222,6 +222,7 @@ var FormViewDialog = ViewDialog.extend({ stayInEdit: true, reload: false, savePoint: this.shouldSaveLocally, + viewType: 'form', }); } return $.when(def).then(function () { -- cgit v1.2.3 From 708c1c2915563a40a1e4c736b4fd11ffd89280f8 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 13 Jun 2017 08:46:15 +0200 Subject: [FIX] web: false id should remain false in evalContext 'id' is a particular field that should not be considered like other integer fields. For instance, in the evalContext, its value should not fallback to 0 if it is false. This would break domains in attrs like ['id', '=', False], which is supposed to be true on new (not saved yet) records. This was for example the case in the Product form view: go to Sales Orders form view, add a line, on the product field choose 'Create and Edit', try to save the product -> error: field Product Template is required but not set (actually it is invisible), as its required modifier says ['id', '!=', False], which was always the case. --- addons/web/static/src/js/views/basic/basic_model.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/web/static/src') 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 20066b858f1..6c23550ba2e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2176,7 +2176,7 @@ var BasicModel = AbstractModel.extend({ if (context[fieldName] === null) { context[fieldName] = false; } - if (!field) { + if (!field || field.name === 'id') { continue; } if (field.type === 'float' || -- cgit v1.2.3 From 83115b2a108f3444468c6883f9df3b7540f706bb Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Thu, 1 Jun 2017 15:21:03 +0200 Subject: [FIX] web: SelectCreateDialog uses the domain of the view when searching before this fix: the domain is used when opening the dialog but it is not used if the user update the filter or the group_by. --- addons/web/static/src/js/views/view_dialogs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index 4035ea6e63e..d38400c2e84 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -327,7 +327,6 @@ var SelectCreateDialog = ViewDialog.extend({ var searchview = new SearchView(this, this.dataset, fields_views.search, options); searchview.prependTo($header).done(function () { var d = searchview.build_search_data(); - d.domains = d.domains.concat([self.domain]); if (self.initial_ids) { d.domains.push([["id", "in", self.initial_ids]]); self.initial_ids = undefined; @@ -388,8 +387,9 @@ var SelectCreateDialog = ViewDialog.extend({ contexts: [this.context].concat(contexts), group_by_seq: groupbys || [] }); + var context = _.omit(results.context, function (value, key) { return key.indexOf('search_default_') === 0; }); return { - context: results.context, + context: context, domain: results.domain, groupBy: results.group_by, }; -- cgit v1.2.3 From 70624772b2dd6a885a9cc8578e0e451063e18ad4 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 13 Jun 2017 09:35:30 +0200 Subject: [FIX] web: Pivot: allow to click on a cell Before this rev., when the user clicked on a data cell of a pivot view, it crashed. This was due to several problems, fixed by this commit. --- .../static/src/js/views/pivot/pivot_controller.js | 34 +++++++++------------- .../web/static/src/js/views/pivot/pivot_model.js | 7 +++-- addons/web/static/src/js/views/pivot/pivot_view.js | 11 +++++++ 3 files changed, 29 insertions(+), 23 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index fb175aa1934..78640a97124 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -45,13 +45,15 @@ var PivotController = AbstractController.extend({ this.groupableFields = params.groupableFields; this.title = params.title; this.enableLinking = params.enableLinking; + // views to use in the action triggered when a data cell is clicked + this.views = params.views; this.lastHeaderSelected = null; }, /** * @override */ start: function () { - this.$el.toggleClass('o_enableLinking', this.enableLinking); + this.$el.toggleClass('o_enable_linking', this.enableLinking); this.$fieldSelection = this.$('.o_field_selection'); core.bus.on('click', this, function () { this.$fieldSelection.empty(); @@ -253,33 +255,23 @@ var PivotController = AbstractController.extend({ !this.enableLinking) { return; } - var row_id = $target.data('id'); - var col_id = $target.data('col_id'); var state = this.model.get(this.handle); - var row_domain = state.data.headers[row_id].domain; - var col_domain = state.data.headers[col_id].domain; - var context = _.omit(_.clone(state.context), 'group_by'); - - function _find_view_info (views, view_type) { - return _.find(views, function (view) { - return view[1] === view_type; - }) || [false, view_type]; - } - var views = [ - _find_view_info(this.options.action.views, "list"), - _find_view_info(this.options.action.views, "form"), - ]; + var colDomain = this.model.getHeader($target.data('col_id')).domain; + var rowDomain = this.model.getHeader($target.data('id')).domain; + var context = _.omit(state.context, function (val, key) { + return key === 'group_by' || _.str.startsWith(key, 'search_default_'); + }); this.do_action({ type: 'ir.actions.act_window', name: this.title, - res_model: this.model, - views: views, - view_type : "list", - view_mode : "list", + res_model: this.modelName, + views: this.views, + view_type: 'list', + view_mode: 'list', target: 'current', context: context, - domain: state.domain.concat(row_domain, col_domain), + domain: state.domain.concat(rowDomain, colDomain), }); }, /** diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 6bbadd56ed8..49c00663d47 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -16,9 +16,10 @@ odoo.define('web.PivotModel', function (require) { * @todo add a full description/specification of the data layout */ +var AbstractModel = require('web.AbstractModel'); var core = require('web.core'); +var session = require('web.session'); var utils = require('web.utils'); -var AbstractModel = require('web.AbstractModel'); var _t = core._t; @@ -189,6 +190,8 @@ var PivotModel = AbstractModel.extend({ } return { colGroupBys: this.data.main_col.groupbys, + context: this.data.context, + domain: this.data.domain, fields: this.fields, headers: !isRaw && this._computeHeaders(), has_data: true, @@ -225,7 +228,7 @@ var PivotModel = AbstractModel.extend({ this.modelName = params.modelName; this.data = { domain: params.domain, - context: params.context, + context: _.extend({}, session.user_context, params.context), groupedBy: params.rowGroupBys, colGroupBys: params.colGroupBys, measures: params.measures, diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js index d90c0d21780..356672081d9 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -96,6 +96,17 @@ var PivotView = AbstractView.extend({ this.controllerParams.enableLinking = !arch.attrs.disable_linking; this.controllerParams.measures = measures; this.controllerParams.groupableFields = groupableFields; + // retrieve form and list view ids from the action to open those views + // when a data cell of the pivot view is clicked + this.controllerParams.views = [ + _findView(params.action && params.action.views, 'list'), + _findView(params.action && params.action.views, 'form'), + ]; + function _findView(views, viewType) { + return _.find(views, function (view) { + return view[1] === viewType; + }) || [false, viewType]; + } }, }); -- cgit v1.2.3 From a3bea9a62bdaf205c0a9cd75945f6f6e9b023d6d Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 13 Jun 2017 15:18:27 +0200 Subject: [FIX] web: Form: delete the correct record When the user duplicated a record 'A', and deleted the new record 'A (copy)' directly, the wrong record was deleted: 'A' was deleted instead of 'A (copy)', because de id of 'A' was bound to the delete handler, and it wasn't updated after the duplication. Also correctly updates the record's offset after the deletion, so that it doesn't produce a warning when deleting the last record of the dataset. --- addons/web/static/src/js/views/basic/basic_model.js | 1 + addons/web/static/src/js/views/form/form_controller.js | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'addons/web/static/src') 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 6c23550ba2e..ae48a98dd13 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -208,6 +208,7 @@ var BasicModel = AbstractModel.extend({ delete self.localData[record.id]; } else { record.res_ids.splice(record.offset, 1); + record.offset = Math.min(record.offset, record.res_ids.length - 1); record.res_id = record.res_ids[record.offset]; record.count--; } diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index d22a4095d1a..8a42168a098 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -151,7 +151,7 @@ var FormController = BasicController.extend({ if (this.is_action_enabled('delete')) { otherItems.push({ label: _t('Delete'), - callback: this._deleteRecords.bind(this, [this.handle]), + callback: this._onDeleteRecord.bind(this), }); } if (this.is_action_enabled('create') && this.is_action_enabled('duplicate')) { @@ -394,6 +394,14 @@ var FormController = BasicController.extend({ _onCreate: function () { this.createRecord(); }, + /** + * Deletes the current record + * + * @private + */ + _onDeleteRecord: function () { + this._deleteRecords([this.handle]); + }, /** * Called when the user wants to discard the changes made to the current * record -> @see discardChanges -- cgit v1.2.3 From f6525c6b6ce04534e7fb2c282191243b8d3c4707 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 14 Jun 2017 13:42:38 +0200 Subject: [FIX] web: BasicModel: onchanges: send all values Since commit ebd17217c4, values of readonly fields inside x2many fields weren't sent for onchanges, whereas they should. This was for example an issue on the stock.picking form view (which is in state 'draft' by default). If the user added a stock.move, the state field on the move wasn't sent for the onchange, and thus the state field on the picking was set to false. --- .../web/static/src/js/views/basic/basic_model.js | 42 ++++++++++++++-------- 1 file changed, 28 insertions(+), 14 deletions(-) (limited to 'addons/web/static/src') 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 ae48a98dd13..d897a74c696 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -736,7 +736,7 @@ var BasicModel = AbstractModel.extend({ // id never changes, and should not be written delete record._changes.id; } - var changes = self._generateChanges(record, options.viewType); + var changes = self._generateChanges(record, {viewType: options.viewType}); if (method === 'create') { var fieldNames = record.getFieldNames(); @@ -1877,18 +1877,28 @@ var BasicModel = AbstractModel.extend({ * * @private * @param {Object} record - * @param {string} [viewType] current viewType. If not set, we will assume - * main viewType from the record. Note that if an editionViewType is + * @param {Object} [options] + * @param {boolean} [options.changesOnly=true] if true, only generates + * commands for fields that have changed (concerns x2many fields only) + * @param {boolean} [options.withReadonly=false] if false, doesn't generate + * changes for readonly fields + * @param {string} [options.viewType] current viewType. If not set, we will + * assume main viewType from the record. Note that if an editionViewType is * specified for a field, it will take the priority over the viewType arg. * @returns {Object} a map from changed fields to their new value */ - _generateChanges: function (record, viewType) { - viewType = viewType || record.viewType; + _generateChanges: function (record, options) { + options = options || {}; + var viewType = options.viewType || record.viewType; var changes = _.extend({}, record._changes); - var commands = this._generateX2ManyCommands(record, true); + var withReadonly = options.withReadonly || false; + var commands = this._generateX2ManyCommands(record, { + changesOnly: 'changesOnly' in options ? options.changesOnly : true, + withReadonly: withReadonly, + }); for (var fieldName in record.fields) { // remove readonly fields from the list of changes - if (fieldName in changes || fieldName in commands) { + if (!withReadonly && fieldName in changes || fieldName in commands) { var editionViewType = record._editionViewType[fieldName] || viewType; if (this._isFieldReadonly(record, fieldName, editionViewType)) { delete changes[fieldName]; @@ -1923,7 +1933,7 @@ var BasicModel = AbstractModel.extend({ * @returns {Object} the data */ _generateOnChangeData: function (record) { - var commands = this._generateX2ManyCommands(record, false); + var commands = this._generateX2ManyCommands(record, {withReadonly: true}); var data = _.extend(this.get(record.id, {raw: true}).data, commands); // one2many records have a parentID @@ -1946,12 +1956,16 @@ var BasicModel = AbstractModel.extend({ * or write them... * * @param {Object} record + * @param {Object} [options] * @param {boolean} [changesOnly=false] if true, only generates commands for * fields that have changed + * @param {boolean} [options.withReadonly=false] if false, doesn't generate + * changes for readonly fields in commands * @returns {Object} a map from some field names to commands */ - _generateX2ManyCommands: function (record, changesOnly) { + _generateX2ManyCommands: function (record, options) { var self = this; + options = options || {}; var commands = {}; var data = _.extend({}, record.data, record._changes); var type; @@ -1965,7 +1979,7 @@ var BasicModel = AbstractModel.extend({ continue; } var list = this.localData[data[fieldName]]; - if (changesOnly && !list._changes) { + if (options.changesOnly && !list._changes) { // if only changes are requested, skip if there is no change continue; } @@ -1983,7 +1997,7 @@ var BasicModel = AbstractModel.extend({ // generate update commands for records that have been // updated (it may happen with editable lists) _.each(relData, function (relRecord) { - var changes = self._generateChanges(relRecord); + var changes = self._generateChanges(relRecord, options); if (!_.isEmpty(changes)) { var command = x2ManyCommands.update(relRecord.res_id, changes); commands[fieldName].push(command); @@ -2002,7 +2016,7 @@ var BasicModel = AbstractModel.extend({ if (_.contains(keptIds, relIds[i])) { // this is an id that already existed relRecord = _.findWhere(relData, {res_id: relIds[i]}); - changes = this._generateChanges(relRecord); + changes = this._generateChanges(relRecord, options); if (!_.isEmpty(changes)) { command = x2ManyCommands.update(relRecord.res_id, changes); didChange = true; @@ -2013,11 +2027,11 @@ var BasicModel = AbstractModel.extend({ } else if (_.contains(addedIds, relIds[i])) { // this is a new id relRecord = _.findWhere(relData, {res_id: relIds[i]}); - changes = this._generateChanges(relRecord); + changes = this._generateChanges(relRecord, options); commands[fieldName].push(x2ManyCommands.create(changes)); } } - if (changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) { + if (options.changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) { // in this situation, we have no changed ids, no added // ids and no removed ids, so we can safely ignore the // last changes -- cgit v1.2.3 From 46751de5069960fdc282446212998f82cede646b Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Wed, 14 Jun 2017 16:37:22 +0200 Subject: [FIX] web: correctly style `o_status` class The `o_status` style has been moved in a previous rev. in `fields.less` which is only for `o_field_widget` nodes. This class however is used in other context than a field (e.g. on a label in the project.task.type form view) so its style needs to be placed at an upper level. --- addons/web/static/src/less/fields.less | 18 ------------------ addons/web/static/src/less/views.less | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 18 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less index b070a8f4cc8..1afe611ee9e 100644 --- a/addons/web/static/src/less/fields.less +++ b/addons/web/static/src/less/fields.less @@ -180,24 +180,6 @@ } } - // Status - &.o_selection .o_status { - display: inline-block; - height: 12px; - width: 12px; - border-radius: 6px; - vertical-align: middle; - - background-color: @odoo-brand-secondary; - - &.o_status_green { - background-color: @brand-success; - } - &.o_status_red { - background-color: @brand-danger; - } - } - // Specific success color for toggle_button widget .fa.o_toggle_button_success { color: @brand-success; diff --git a/addons/web/static/src/less/views.less b/addons/web/static/src/less/views.less index db5fe8f814c..102d50e7590 100644 --- a/addons/web/static/src/less/views.less +++ b/addons/web/static/src/less/views.less @@ -21,3 +21,23 @@ .o_invisible_modifier { display: none!important; } + +// Status +// This should normally be put in fields.less but these classes are used outside +// of `.o_field_widget` so it needs to be placed at an upper level. +.o_status { + display: inline-block; + height: 12px; + width: 12px; + border-radius: 6px; + vertical-align: middle; + + background-color: @odoo-brand-secondary; + + &.o_status_green { + background-color: @brand-success; + } + &.o_status_red { + background-color: @brand-danger; + } +} -- cgit v1.2.3 From d88d1abcf07f6f644f2039baf4b53c218cb95f84 Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Wed, 14 Jun 2017 16:56:48 +0200 Subject: [FIX] web: reimplement `group_by_tooltip` option in kanban This option hadn't been implemented in the new views. It allows to display informative explanations as tooltips on column over. It can be set on a many2one field in the kanban view, e.g.: ``` ``` An extra RPC needs to be done on the many2one relation to fetch these additional fields and the data are stored in the datapoint under the key `tooltipData`. --- .../static/src/js/views/kanban/kanban_column.js | 12 ++--- .../web/static/src/js/views/kanban/kanban_model.js | 62 +++++++++++++++++++++- .../static/src/js/views/kanban/kanban_renderer.js | 3 ++ 3 files changed, 69 insertions(+), 8 deletions(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js index 642ab2e8aeb..d999ee74025 100644 --- a/addons/web/static/src/js/views/kanban/kanban_column.js +++ b/addons/web/static/src/js/views/kanban/kanban_column.js @@ -44,7 +44,6 @@ var KanbanColumn = Widget.extend({ this.folded = !data.isOpen; this.has_active_field = 'active' in data.fields; this.size = data.count; - this.values = data.values; this.fields = data.fields; this.records = []; this.modelName = data.model; @@ -62,13 +61,12 @@ var KanbanColumn = Widget.extend({ this.record_options = _.clone(recordOptions); - if (data.options && data.options.group_by_tooltip) { - var self = this; - this.tooltip_info = _.map(data.options.group_by_tooltip, function (key, value) { - return (self.values && self.values[value] && "
" +key + "
" + self.values[value] + "
") || ''; + if (options.group_by_tooltip) { + this.tooltipInfo = _.map(options.group_by_tooltip, function (help, field) { + return (data.tooltipData && data.tooltipData[field] && "
" + help + "
" + data.tooltipData[field] + "
") || ''; }).join(''); } else { - this.tooltip_info = ""; + this.tooltipInfo = ""; } }, /** @@ -216,7 +214,7 @@ var KanbanColumn = Widget.extend({ this.$el.toggleClass('o_column_folded', this.folded); var tooltip = this.size + _t(' records'); - tooltip = '

' + tooltip + '

' + this.tooltip_info; + tooltip = '

' + tooltip + '

' + this.tooltipInfo; this.$header.tooltip({html: true}).attr('data-original-title', tooltip); if (!this.remaining) { this.$('.o_kanban_load_more').remove(); diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index c606776ccc0..cd69889bda9 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -89,6 +89,21 @@ var KanbanModel = BasicModel.extend({ return newGroup.id; }); }, + /** + * Add the key `tooltipData` (kanban specific) when performing a `geĊ§`. + * + * @override + * @see _readTooltipFields + * @returns {Object} + */ + get: function () { + var result = this._super.apply(this, arguments); + var dp = result && this.localData[result.id]; + if (dp && dp.tooltipData) { + result.tooltipData = $.extend(true, {}, dp.tooltipData); + } + return result; + }, /** * @override */ @@ -203,10 +218,55 @@ var KanbanModel = BasicModel.extend({ * @param {Object} list valid resource object */ _readGroup: function (list) { + var self = this; if (list.groupedBy.length > 1) { list.groupedBy = [list.groupedBy[0]]; } - return this._super.apply(this, arguments); + return this._super.apply(this, arguments).then(function (result) { + return self._readTooltipFields(list).then(_.constant(result)); + }); + }, + /** + * Fetches tooltip specific fields on the group by relation and stores it in + * the column datapoint in a special key `tooltipData`. + * Data for the tooltips (group_by_tooltip) are fetched in batch for all + * groups, to avoid doing multiple calls. + * Data are stored in a special key `tooltipData` on the datapoint. + * Note that the option `group_by_tooltip` is only for m2o fields. + * + * @private + * @param {Object} list a list of groups + * @returns {Deferred} + */ + _readTooltipFields: function (list) { + var self = this; + var groupedByField = list.fields[list.groupedBy[0]]; + if (groupedByField.type !== 'many2one') { + return $.when(); + } + var groupIds = _.map(list.data, function (id) { + return self.get(id, {raw: true}).res_id; + }); + var tooltipFields = []; + var groupedByFieldInfo = list.fieldsInfo.kanban[list.groupedBy[0]]; + if (groupedByFieldInfo && groupedByFieldInfo.options) { + tooltipFields = Object.keys(groupedByFieldInfo.options.group_by_tooltip || {}); + } + if (groupIds.length && tooltipFields.length) { + var fieldNames = _.union(['display_name'], tooltipFields); + return this._rpc({ + model: groupedByField.relation, + method: 'read', + args: [groupIds, fieldNames], + context: list.context, + }).then(function (result) { + _.each(list.data, function (id) { + var dp = self.localData[id]; + dp.tooltipData = _.findWhere(result, {id: dp.res_id}); + }); + }); + } + return $.when(); }, }); diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js index 7bfe21e66a9..87275260b01 100644 --- a/addons/web/static/src/js/views/kanban/kanban_renderer.js +++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js @@ -188,6 +188,7 @@ var KanbanRenderer = BasicRenderer.extend({ _renderGrouped: function (fragment) { var self = this; var groupByFieldAttrs = this.state.fields[this.state.groupedBy[0]]; + var groupByFieldInfo = this.state.fieldsInfo.kanban[this.state.groupedBy[0]]; // Deactivate the drag'n'drop if the groupedBy field: // - is a date or datetime since we group by month or // - is readonly @@ -201,8 +202,10 @@ var KanbanRenderer = BasicRenderer.extend({ } var groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one'); var grouped_by_field = groupedByM2O && groupByFieldAttrs.relation; + var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip; this.columnOptions = _.extend(this.columnOptions, { draggable: draggable, + group_by_tooltip: groupByTooltip, grouped_by_m2o: groupedByM2O, relation: grouped_by_field, }); -- cgit v1.2.3 From 0c2e018d4c416d0f47a3b64f1c28ce067dbab291 Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Wed, 14 Jun 2017 17:57:30 +0200 Subject: [FIX] web: display dashboard graphs in full-width With the new views, the class `o_field_widget` is added on every field widget and is styled with `display: inline-block`. The dashboards used to display graphs in full-width so this behaviour is restored. --- addons/web/static/src/less/fields.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'addons/web/static/src') diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less index 1afe611ee9e..02885ef3514 100644 --- a/addons/web/static/src/less/fields.less +++ b/addons/web/static/src/less/fields.less @@ -33,7 +33,7 @@ } // Block fields - &.note-editor, &.oe_form_field_html, &.oe_form_field_html_text, &.oe_form_field_text, &.o_view_manager_content, &.o_field_domain { + &.note-editor, &.oe_form_field_html, &.oe_form_field_html_text, &.oe_form_field_text, &.o_view_manager_content, &.o_field_domain, &.o_graph_linechart, &.o_graph_barchart { display: block; } -- cgit v1.2.3