')
- .addClass('oe_view_nocontent')
- .html(this.noContentHelp);
- this.$el.html($msg);
- } else {
- if (!document.contains(this.renderer.el)) {
- this.$('div.oe_view_nocontent').remove();
- this.$el.append(this.renderer.$el);
- }
- }
- },
/**
* This method is called after each update or when the start method is
* completed.
diff --git a/addons/web/static/src/js/views/abstract_renderer.js b/addons/web/static/src/js/views/abstract_renderer.js
index 7d10f05eab8..d7eb5002f2d 100644
--- a/addons/web/static/src/js/views/abstract_renderer.js
+++ b/addons/web/static/src/js/views/abstract_renderer.js
@@ -15,11 +15,13 @@ return Widget.extend({
* @param {Widget} parent
* @param {any} state
* @param {Object} params
+ * @param {string} [params.noContentHelp]
*/
init: function (parent, state, params) {
this._super(parent);
this.state = state;
this.arch = params.arch;
+ this.noContentHelp = params.noContentHelp;
},
/**
* The rendering can be asynchronous (but it is not encouraged). The start
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 4c43a692601..8de118b29b7 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -78,6 +78,7 @@ var AbstractView = Class.extend({
init: function (viewInfo, params) {
this.rendererParams = {
arch: viewInfo.arch,
+ noContentHelp: params.action && params.action.help,
};
this.controllerParams = {
@@ -88,7 +89,6 @@ var AbstractView = Class.extend({
delete: viewInfo.arch.attrs.delete ? JSON.parse(viewInfo.arch.attrs.delete) : true,
duplicate: viewInfo.arch.attrs.duplicate ? JSON.parse(viewInfo.arch.attrs.duplicate) : true,
},
- noContentHelp: params.action && params.action.help,
};
this.loadParams = {
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 cc72816a85d..3a47f65ab03 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -8,7 +8,6 @@ odoo.define('web.BasicController', function (require) {
*/
var AbstractController = require('web.AbstractController');
-var concurrency = require('web.concurrency');
var core = require('web.core');
var Dialog = require('web.Dialog');
var FieldManagerMixin = require('web.FieldManagerMixin');
@@ -19,7 +18,6 @@ var _t = core._t;
var BasicController = AbstractController.extend(FieldManagerMixin, {
custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, {
discard_changes: '_onDiscardChanges',
- mutexify: '_onMutexify',
reload: '_onReload',
sidebar_data_asked: '_onSidebarDataAsked',
translate: '_onTranslate',
@@ -39,7 +37,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
FieldManagerMixin.init.call(this, this.model);
this.handle = params.initialState.id;
this.mode = params.mode || 'readonly';
- this.mutex = new concurrency.Mutex();
},
/**
* @override
@@ -238,7 +235,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var recordID = record.data.id;
this.trigger_up('execute_action', {
action_data: _.extend({}, attrs, {
- context: record.getContext({additionalContext: attrs.context}),
+ context: record.getContext({additionalContext: attrs.context || {}}),
}),
model: record.model,
res_ids: [recordID],
@@ -361,17 +358,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
model: this.modelName,
};
},
- /**
- * Used by list and kanban views to determine whether or not to display
- * the no content helper (if there is no data in the state to display)
- *
- * @private
- * @param {Object} state
- * @returns {boolean}
- */
- _hasContent: function (state) {
- return state.count !== 0;
- },
/**
* Helper function to display a warning that some fields have an invalid
* value. This is used when a save operation cannot be completed.
@@ -537,15 +523,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
}
FieldManagerMixin._onFieldChanged.apply(this, arguments);
},
- /**
- * @private
- * @param {OdooEvent} ev
- * @param {function} ev.data.action the function to execute in the mutex
- */
- _onMutexify: function (ev) {
- ev.stopPropagation(); // prevent other controllers from handling this request
- this.mutex.exec(ev.data.action);
- },
/**
* When a reload event triggers up, we need to reload the full view.
* For example, after a form view dialog saved some data.
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 8f679207f00..04fe384afdc 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -628,12 +628,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
+ * @param {Object} [options] will be transferred to the applyChange method
+ * @see _applyChange
* @returns {string[]} list of changed fields
*/
- notifyChanges: function (record_id, changes, viewType) {
- return this.mutex.exec(this._applyChange.bind(this, record_id, changes, viewType));
+ notifyChanges: function (record_id, changes, options) {
+ return this.mutex.exec(this._applyChange.bind(this, record_id, changes, options));
},
/**
* Reload all data for a given resource
@@ -956,23 +956,30 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} recordID
* @param {Object} changes
- * @param {string} [viewType] current viewType. If not set, we will assume
+ * @param {Object} [options]
+ * @param {boolean} [options.doNotSetDirty=false] if this flag is set to
+ * true, then we will not tag the record as dirty. This should be avoided
+ * for most situations.
+ * @param {string} [options.viewType] current viewType. If not set, we will assume
* main viewType from the record
* @returns {Deferred}
*/
- _applyChange: function (recordID, changes, viewType) {
+ _applyChange: function (recordID, changes, options) {
var self = this;
var record = this.localData[recordID];
var field;
var defs = [];
+ options = options || {};
record._changes = record._changes || {};
- record._isDirty = true;
+ if (!options.doNotSetDirty) {
+ record._isDirty = true;
+ }
// apply changes to local data
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
- defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], viewType));
+ defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options.viewType));
} else if (field.type === 'many2one' || field.type === 'reference') {
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
} else {
@@ -993,7 +1000,7 @@ var BasicModel = AbstractModel.extend({
}
var onchangeDef;
if (onChangeFields.length) {
- onchangeDef = self._performOnChange(record, onChangeFields, viewType).then(function (result) {
+ onchangeDef = self._performOnChange(record, onChangeFields, options.viewType).then(function (result) {
delete record._warning;
return _.keys(changes).concat(Object.keys(result && result.value || {}));
});
@@ -2196,7 +2203,7 @@ var BasicModel = AbstractModel.extend({
// remove readonly fields from the list of changes
if (!withReadonly && fieldName in changes || fieldName in commands) {
var editionViewType = record._editionViewType[fieldName] || viewType;
- if (this._isFieldReadonly(record, fieldName, editionViewType)) {
+ if (this._isFieldProtected(record, fieldName, editionViewType)) {
delete changes[fieldName];
continue;
}
@@ -2555,9 +2562,10 @@ 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).
+ * Returns true if the field is protected against changes, looking for a
+ * readonly modifier unless there is a force_save modifier (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
@@ -2566,12 +2574,12 @@ var BasicModel = AbstractModel.extend({
* main viewType from the record
* @returns {boolean}
*/
- _isFieldReadonly: function (record, fieldName, viewType) {
+ _isFieldProtected: function (record, fieldName, viewType) {
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
if (fieldInfo) {
var rawModifiers = JSON.parse(fieldInfo.modifiers || "{}");
var modifiers = this._evalModifiers(record, rawModifiers);
- return modifiers.readonly;
+ return modifiers.readonly && !fieldInfo.force_save;
} else {
return false;
}
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 8bbf9ae45a4..e040a5fa7f0 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -317,6 +317,16 @@ var BasicRenderer = AbstractRenderer.extend({
_getModifiersData: function (node) {
return _.findWhere(this.allModifiersData, {node: node});
},
+ /**
+ * Used by list and kanban renderers to determine whether or not to display
+ * the no content helper (if there is no data in the state to display)
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _hasContent: function () {
+ return this.state.count !== 0;
+ },
/**
* Registers or updates the modifiers data associated to the given node.
* This method is quiet complex as it handles all the needs of the basic
@@ -486,6 +496,20 @@ var BasicRenderer = AbstractRenderer.extend({
return widget;
},
+ /**
+ * Renders the nocontent helper.
+ *
+ * This method is a helper for renderers that want to display a help
+ * message when no content is available.
+ *
+ * @private
+ */
+ _renderNoContentHelper: function () {
+ var $msg = $('
')
+ .addClass('oe_view_nocontent')
+ .html(this.noContentHelp);
+ this.$el.html($msg);
+ },
/**
* Actual rendering. Supposed to be overridden by concrete renderers.
* The basic responsabilities of _renderView are:
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index 73868a93fdb..d2b0c02c524 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -302,44 +302,38 @@ return AbstractModel.extend({
// Private
//--------------------------------------------------------------------------
+ /**
+ * Converts this.data.filters into a domain
+ *
+ * @private
+ * @returns {Array}
+ */
_getFilterDomain: function () {
- var domain = [];
- _.each(this.data.filters, function (filter) {
- if (filter.all) return;
- var nb = {};
- var not = {};
- var contains = {};
- _.each(filter.filters, function (f) {
- if (!contains[filter.fieldName]) {
- contains[filter.fieldName] = [];
- not[filter.fieldName] = [];
- nb[filter.fieldName] = 0;
- }
- if (f.value === 'all' && f.active) {
- contains[filter.fieldName].all = true;
- } else {
- nb[filter.fieldName]++;
- }
+ // List authorized values for every field
+ // fields with an active 'all' filter are skipped
+ var authorizedValues = {};
- if (f.active && contains[filter.fieldName].indexOf(f.value) === -1) {
- contains[filter.fieldName].push(f.value);
- }
- if (!f.active && not[filter.fieldName].indexOf(f.value) === -1) {
- not[filter.fieldName].push(f.value);
+ _.each(this.data.filters, function (filter) {
+ // Skip 'all' filters because they do not affect the domain
+ if (filter.all) return;
+
+ // Loop over subfilters to complete authorizedValues
+ _.each(filter.filters, function (f) {
+ if (!authorizedValues[filter.fieldName])
+ authorizedValues[filter.fieldName] = [];
+
+ if (f.active) {
+ authorizedValues[filter.fieldName].push(f.value);
}
});
-
- for (var field in contains) {
- if (contains[field].all || contains[field].length === nb[field]) {
- continue;
- }
- if (contains[field].length) {
- domain.push([field, 'in', contains[field]]);
- } else if (not[field].length && not[field].length !== nb[field]) {
- domain.push([field, 'not in', not[field]]);
- }
- }
});
+
+ // Compute the domain
+ var domain = [];
+ for (var field in authorizedValues) {
+ domain.push([field, 'in', authorizedValues[field]]);
+ }
+
return domain;
},
/**
@@ -393,19 +387,16 @@ return AbstractModel.extend({
this.data.fc_options = this._getFullCalendarOptions();
var defs = _.map(this.data.filters, this._loadFilter.bind(this));
+
return $.when.apply($, defs).then(function () {
- var eventsDef = $.Deferred().resolve([]);
- var filterDomain = self._getFilterDomain();
- if (filterDomain.length || _.isEmpty(self.data.filters)) {
- eventsDef = self._rpc({
+ return self._rpc({
model: self.modelName,
method: 'search_read',
context: self.data.context,
fields: self.fieldNames,
- domain: self.data.domain.concat(self._getRangeDomain()).concat(filterDomain)
- });
- }
- return eventsDef.then(function (events) {
+ domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain())
+ })
+ .then(function (events) {
self._parseServerData(events);
self.data.data = _.map(events, self._recordToCalendarEvent.bind(self));
return $.when(
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index b61236131c6..4f5719063ff 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -330,11 +330,12 @@ return AbstractRenderer.extend({
}
element.find('.fc-content .fc-time').text(display_hour);
},
- height: 'auto',
+ height: 'parent',
unselectAuto: false,
});
this.$calendar.fullCalendar(fc_options);
+ window.dispatchEvent(new Event('resize'));
},
/**
* Initialize the mini calendar in the sidebar
diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js
index 422af121d96..1ee7bf4a501 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -9,11 +9,13 @@ odoo.define('web.FieldManagerMixin', function (require) {
*/
var BasicModel = require('web.BasicModel');
+var concurrency = require('web.concurrency');
var FieldManagerMixin = {
custom_events: {
field_changed: '_onFieldChanged',
load: '_onLoad',
+ mutexify: '_onMutexify',
},
/**
* A FieldManagerMixin can be initialized with an instance of a basicModel.
@@ -23,6 +25,7 @@ var FieldManagerMixin = {
*/
init: function (model) {
this.model = model || new BasicModel(this);
+ this.mutex = new concurrency.Mutex();
},
//--------------------------------------------------------------------------
@@ -43,7 +46,8 @@ var FieldManagerMixin = {
*/
_applyChanges: function (dataPointID, changes, event) {
var self = this;
- return this.model.notifyChanges(dataPointID, changes, event.data.viewType)
+ var options = _.pick(event.data, 'viewType', 'doNotSetDirty');
+ return this.model.notifyChanges(dataPointID, changes, options)
.then(function (result) {
if (event.data.force_save) {
return self.model.save(dataPointID).then(function () {
@@ -129,6 +133,15 @@ var FieldManagerMixin = {
data.on_success(self.model.get(db_id));
});
},
+ /**
+ * @private
+ * @param {OdooEvent} ev
+ * @param {function} ev.data.action the function to execute in the mutex
+ */
+ _onMutexify: function (ev) {
+ ev.stopPropagation(); // prevent other field managers from handling this request
+ this.mutex.exec(ev.data.action);
+ },
};
return FieldManagerMixin;
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 35194b313ad..1d788ac2c05 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -115,18 +115,20 @@ var FormRenderer = BasicRenderer.extend({
});
},
/**
- * Disable stat buttons so that they can't be clicked anymore
+ * Disable statusbar buttons and stat buttons so that they can't be clicked anymore
*
*/
disableButtons: function () {
- this.$('.oe_button_box button').attr('disabled', true);
+ this.$('.o_statusbar_buttons button, .oe_button_box button')
+ .attr('disabled', true);
},
/**
- * Enable stat buttons so they can be clicked again
+ * Enable statusbar buttons and stat buttons so they can be clicked again
*
*/
enableButtons: function () {
- this.$('.oe_button_box button').removeAttr('disabled');
+ this.$('.o_statusbar_buttons button, .oe_button_box button')
+ .removeAttr('disabled');
},
/**
* returns the active tab pages for each notebook
diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js
index 8a7f5da6d5d..50e9f224697 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -114,13 +114,13 @@ return AbstractModel.extend({
* @returns {Deferred}
*/
_loadGraph: function () {
- var fields = _.map(this.chart.groupedBy, function (groupBy) {
+ var groupedBy = this.chart.groupedBy.length ? this.chart.groupedBy : this.initialGroupBys;
+ var fields = _.map(groupedBy, function (groupBy) {
return groupBy.split(':')[0];
});
if (this.chart.measure !== '__count__') {
fields = fields.concat(this.chart.measure);
}
- var groupedBy = this.chart.groupedBy.length ? this.chart.groupedBy : this.initialGroupBys;
return this._rpc({
model: this.modelName,
method: 'read_group',
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 2592e3d96ec..2cf6a94285e 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -80,19 +80,6 @@ var KanbanController = BasicController.extend({
_confirmSave: function (id) {
return this.renderer.updateRecord(this.model.get(id));
},
- /**
- * The nocontent helper should be displayed in kanban:
- * - ungrouped: if there is no records
- * - grouped: if there is no groups and no column quick create
- *
- * @override
- * @private
- */
- _hasContent: function (state) {
- return this._super.apply(this, arguments) ||
- this.createColumnEnabled ||
- (state.groupedBy.length && state.data.length);
- },
/**
* The column quick create should be displayed in kanban iff grouped by an
* m2o field and group_create action enabled.
@@ -138,17 +125,6 @@ var KanbanController = BasicController.extend({
.toggleClass('btn-default', createMuted);
}
},
- /**
- * @override
- * @param {Object} state
- * @returns {Deferred}
- */
- _update: function (state) {
- var hasNoContent = !this._hasContent(state);
- this.$el.toggleClass('o_kanban_nocontent', hasNoContent);
- this._toggleNoContentHelper(hasNoContent);
- return this._super.apply(this, arguments);
- },
//--------------------------------------------------------------------------
// Handlers
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 ea2f0270017..0bbc7aa2698 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -100,6 +100,8 @@ var KanbanRenderer = BasicRenderer.extend({
viewType: 'kanban',
});
this.columnOptions = _.extend({}, params.column_options, { qweb: this.qweb });
+
+ this._setState(state);
},
//--------------------------------------------------------------------------
@@ -167,11 +169,31 @@ var KanbanRenderer = BasicRenderer.extend({
record.update(recordState);
}
},
+ /**
+ * @override
+ */
+ updateState: function (state) {
+ this._setState(state);
+ return this._super.apply(this, arguments);
+ },
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
+ /**
+ * The nocontent helper should be displayed in kanban:
+ * - ungrouped: if there is no records
+ * - grouped: if there is no groups and no column quick create
+ *
+ * @override
+ * @private
+ */
+ _hasContent: function () {
+ return this._super.apply(this, arguments) ||
+ this.createColumnEnabled ||
+ (this.state.groupedBy.length && this.state.data.length);
+ },
/**
* Renders empty invisible divs in a document fragment.
*
@@ -193,28 +215,6 @@ 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
- var draggable = true;
- if (groupByFieldAttrs) {
- if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") {
- draggable = false;
- } else if (groupByFieldAttrs.readonly !== undefined) {
- draggable = !(groupByFieldAttrs.readonly);
- }
- }
- 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,
- });
// Render columns
_.each(this.state.data, function (group) {
@@ -228,7 +228,7 @@ var KanbanRenderer = BasicRenderer.extend({
}
});
- if (groupedByM2O) {
+ if (this.groupedByM2O) {
// Enable column sorting
this.$el.sortable({
axis: 'x',
@@ -249,7 +249,7 @@ var KanbanRenderer = BasicRenderer.extend({
});
// Enable column quickcreate
- if (this.columnOptions.group_creatable) {
+ if (this.createColumnEnabled) {
this.quickCreate = new ColumnQuickCreate(this);
this.quickCreate.appendTo(fragment);
}
@@ -280,20 +280,62 @@ var KanbanRenderer = BasicRenderer.extend({
_renderView: function () {
var oldWidgets = this.widgets;
this.widgets = [];
- var isGrouped = !!this.state.groupedBy.length;
- this.$el.toggleClass('o_kanban_grouped', isGrouped);
- this.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
this.$el.empty();
- var fragment = document.createDocumentFragment();
- if (isGrouped) {
- this._renderGrouped(fragment);
+ var displayNoContentHelper = !this._hasContent() && !!this.noContentHelp;
+ this.$el.toggleClass('o_kanban_nocontent', displayNoContentHelper);
+ if (displayNoContentHelper) {
+ // display the no content helper if there is no data to display
+ this._renderNoContentHelper();
} else {
- this._renderUngrouped(fragment);
+ var isGrouped = !!this.state.groupedBy.length;
+ this.$el.toggleClass('o_kanban_grouped', isGrouped);
+ this.$el.toggleClass('o_kanban_ungrouped', !isGrouped);
+ var fragment = document.createDocumentFragment();
+ // render the kanban view
+ if (isGrouped) {
+ this._renderGrouped(fragment);
+ } else {
+ this._renderUngrouped(fragment);
+ }
+ this.$el.append(fragment);
}
- this.$el.append(fragment);
+
return this._super.apply(this, arguments).then(_.invoke.bind(_, oldWidgets, 'destroy'));
},
+ /**
+ * Sets the current state and updates some internal attributes accordingly.
+ *
+ * @private
+ * @param {Object} state
+ */
+ _setState: function (state) {
+ this.state = state;
+
+ var groupByFieldAttrs = state.fields[state.groupedBy[0]];
+ var groupByFieldInfo = state.fieldsInfo.kanban[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
+ var draggable = true;
+ if (groupByFieldAttrs) {
+ if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") {
+ draggable = false;
+ } else if (groupByFieldAttrs.readonly !== undefined) {
+ draggable = !(groupByFieldAttrs.readonly);
+ }
+ }
+ this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
+ var grouped_by_field = this.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: this.groupedByM2O,
+ relation: grouped_by_field,
+ });
+ this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
+ },
});
return KanbanRenderer;
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 2ec9cf3181e..3b74f140e4d 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -188,7 +188,6 @@ var ListController = BasicController.extend({
position: self.editable,
});
}).then(function (recordID) {
- self._toggleNoContentHelper(false);
var state = self.model.get(self.handle);
self.renderer.updateState(state, {});
self.renderer.editRecord(recordID);
@@ -283,12 +282,10 @@ var ListController = BasicController.extend({
},
/**
* @override
- * @param {Object} state
* @returns {Deferred}
*/
- _update: function (state) {
+ _update: function () {
this.selectedRecords = [];
- this._toggleNoContentHelper(!this._hasContent(state));
this._toggleSidebar();
return this._super.apply(this, arguments);
},
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 08b9e7f1602..3cd93bb6e8b 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -13,6 +13,7 @@ odoo.define('web.EditableListRenderer', function (require) {
*/
var core = require('web.core');
var ListRenderer = require('web.ListRenderer');
+var utils = require('web.utils');
var _t = core._t;
@@ -153,12 +154,32 @@ ListRenderer.include({
*/
setRowMode: function (recordID, mode) {
var self = this;
- var rowIndex = _.findIndex(this.state.data, {id: recordID});
+
+ // find the record and its row index (handles ungrouped and grouped cases
+ // as even if the grouped list doesn't support edition, it may contain
+ // a widget allowing the edition in readonly (e.g. priority), so it
+ // should be able to update a record as well)
+ var record;
+ var rowIndex;
+ if (this.state.groupedBy.length) {
+ rowIndex = -1;
+ var count = 0;
+ utils.traverse_records(this.state, function (r) {
+ if (r.id === recordID) {
+ record = r;
+ rowIndex = count;
+ }
+ count++;
+ });
+ } else {
+ rowIndex = _.findIndex(this.state.data, {id: recordID});
+ record = this.state.data[rowIndex];
+ }
+
if (rowIndex < 0) {
return $.when();
}
var editMode = (mode === 'edit');
- var record = this.state.data[rowIndex];
this.currentRow = editMode ? rowIndex : null;
var $row = this.$('.o_data_row:nth(' + rowIndex + ')');
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 802f2f03a09..295266db276 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -30,7 +30,6 @@ var FIELD_CLASSES = {
};
var ListRenderer = BasicRenderer.extend({
- className: 'table-responsive',
events: {
'click tbody tr': '_onRowClicked',
'click tbody .o_list_record_selector': '_onSelectRecord',
@@ -577,12 +576,24 @@ var ListRenderer = BasicRenderer.extend({
_renderView: function () {
var self = this;
+ this.$el
+ .removeClass('table-responsive')
+ .empty();
+
// destroy the previously instantiated pagers, if any
_.invoke(this.pagers, 'destroy');
this.pagers = [];
+ // display the no content helper if there is no data to display
+ if (!this._hasContent() && this.noContentHelp) {
+ this._renderNoContentHelper();
+ return this._super();
+ }
+
var $table = $('
').addClass('o_list_view table table-condensed table-striped');
- this.$el.empty().append($table);
+ this.$el
+ .addClass('table-responsive')
+ .append($table);
var is_grouped = !!this.state.groupedBy.length;
this._computeAggregates();
$table.toggleClass('o_list_view_grouped', is_grouped);
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 78640a97124..731183e7efe 100644
--- a/addons/web/static/src/js/views/pivot/pivot_controller.js
+++ b/addons/web/static/src/js/views/pivot/pivot_controller.js
@@ -170,21 +170,8 @@ var PivotController = AbstractController.extend({
/**
* @private
*/
- _update: function (state) {
+ _update: function () {
this._updateButtons();
- var hasNoContent = !state.has_data || !state.measures.length;
-
- if (hasNoContent) {
- if (!this.$('.oe_view_nocontent').length) {
- this.renderer.$el.detach();
- this.$el.append(QWeb.render('PivotView.nodata'));
- }
- } else {
- if (!document.contains(this.renderer.el)) {
- this.$('div.oe_view_nocontent').remove();
- this.$el.append(this.renderer.$el);
- }
- }
return this._super.apply(this, arguments);
},
/**
diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js
index a37e65b104b..d279d59d4b1 100644
--- a/addons/web/static/src/js/views/pivot/pivot_renderer.js
+++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js
@@ -2,8 +2,11 @@ odoo.define('web.PivotRenderer', function (require) {
"use strict";
var AbstractRenderer = require('web.AbstractRenderer');
+var core = require('web.core');
var field_utils = require('web.field_utils');
+var QWeb = core.qweb;
+
var PivotRenderer = AbstractRenderer.extend({
tagName: 'table',
className: 'table-hover table-condensed table-bordered',
@@ -15,15 +18,32 @@ var PivotRenderer = AbstractRenderer.extend({
// Private
//--------------------------------------------------------------------------
+ /**
+ * Used to determine whether or not to display the no content helper.
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _hasContent: function () {
+ return this.state.has_data && this.state.measures.length;
+ },
/**
* @override
* @private
* @returns {Deferred}
*/
_render: function () {
- if (!this.state.has_data) {
+ if (!this._hasContent()) {
+ // display the nocontent helper
+ this.replaceElement(QWeb.render('PivotView.nodata'));
return this._super.apply(this, arguments);
}
+
+ if (!this.$el.is('table')) {
+ // coming from the no content helper, so the root element has to be
+ // re-rendered before rendering and appending its content
+ this.renderElement();
+ }
var $fragment = $(document.createDocumentFragment());
var $table = $('').appendTo($fragment);
var $thead = $('').appendTo($table);
diff --git a/addons/web/static/src/less/views.less b/addons/web/static/src/less/views.less
index 102d50e7590..876ad01a6e7 100644
--- a/addons/web/static/src/less/views.less
+++ b/addons/web/static/src/less/views.less
@@ -4,6 +4,9 @@
*/
// No content helper
+.o_view_manager_content > div {
+ height: 100%;
+}
.oe_view_nocontent {
a {
cursor: pointer;
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 173f49080ea..508e86d9cd3 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -849,6 +849,122 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('input field: change value before pending onchange returns', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ product_id: function () {},
+ };
+
+ var def;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === "onchange") {
+ return $.when(def).then(function () {
+ return result;
+ });
+ } else {
+ return result;
+ }
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$('.o_field_x2many_list_row_add a').click();
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'My little Foo Value',
+ 'should contain the default value');
+
+ def = $.Deferred();
+ form.$('.o_field_many2one input').click();
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ $dropdown.find('li:first()').click();
+
+ // set foo before onchange
+ form.$('input[name="foo"]').val("tralala").trigger('input');
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain tralala');
+
+ // complete the onchange
+ def.resolve();
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain the same value as before onchange');
+
+ form.destroy();
+ });
+
+ QUnit.test('input field: change value before pending onchange renaming', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ obj.foo = 'on change value';
+ },
+ };
+
+ var def = $.Deferred();
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === "onchange") {
+ return $.when(def).then(function () {
+ return result;
+ });
+ } else {
+ return result;
+ }
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'yop',
+ 'should contain the correct value');
+
+ form.$('.o_field_many2one input').click();
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ $dropdown.find('li:first()').click();
+
+ // set foo before onchange
+ form.$('input[name="foo"]').val("tralala").trigger('input');
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain tralala');
+
+ // complete the onchange
+ def.resolve();
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain the same value as before onchange');
+
+ form.destroy();
+ });
+
QUnit.module('UrlWidget');
QUnit.test('url widget in form view', function (assert) {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 68a0257b24c..0ad9aa2a416 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -6,6 +6,7 @@ var concurrency = require('web.concurrency');
var FormView = require('web.FormView');
var ListView = require('web.ListView');
var relationalFields = require('web.relational_fields');
+var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
@@ -565,7 +566,7 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('many2one field with option standalone', function (assert) {
+ QUnit.test('standalone many2one field', function (assert) {
assert.expect(3);
var done = assert.async();
@@ -593,7 +594,14 @@ QUnit.module('relational_fields', {
},
}).then(function (recordID) {
var record = model.get(recordID);
- var parent = new Widget();
+ // create a new widget that uses the StandaloneFieldManagerMixin
+ var StandaloneWidget = Widget.extend(StandaloneFieldManagerMixin, {
+ init: function (parent) {
+ this._super.apply(this, arguments);
+ StandaloneFieldManagerMixin.init.call(this, parent);
+ },
+ });
+ var parent = new StandaloneWidget(model);
testUtils.addMockEnvironment(parent, {
data: self.data,
mockRPC: function (route, args) {
@@ -607,7 +615,6 @@ QUnit.module('relational_fields', {
record,
{
mode: 'edit',
- standalone: true,
});
relField.appendTo(fixture);
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 9e8e64edde6..53577d90e40 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -1475,6 +1475,71 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('dont write on readonly fields unless save attribute is set', function (assert) {
+ assert.expect(6);
+
+ this.params.fieldNames = ['foo', 'bar'];
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.bar = obj.foo.length;
+ };
+ this.params.fieldsInfo = {
+ default: {
+ foo: {},
+ bar: {
+ modifiers:"{\"readonly\": true}",
+ force_save: true,
+ },
+ }
+ };
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1], {bar: 14, foo: "verylongstring"},
+ "should only save foo field");
+ }
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {bar: 21, foo: "anotherverylongstring"},
+ "should only save foo field");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.bar, 2,
+ "should be initialized with correct value");
+
+ model.notifyChanges(resultID, {foo: "verylongstring"});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.bar, 14,
+ "should be changed with correct value");
+
+ model.save(resultID);
+ });
+
+ // start again, but with a new record
+ delete this.params.res_id;
+ model.load(this.params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.bar, 0,
+ "should be initialized with correct value (0 as integer)");
+
+ model.notifyChanges(resultID, {foo: "anotherverylongstring"});
+
+ record = model.get(resultID);
+ assert.strictEqual(record.data.bar, 21,
+ "should be changed with correct value");
+
+ model.save(resultID);
+ });
+ model.destroy();
+ });
+
QUnit.test('default_get with one2many values', function (assert) {
assert.expect(1);
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index dfc42c6f746..31b1babc9b3 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -32,7 +32,7 @@ QUnit.module('Views', {
fields: {
id: {string: "ID", type: "integer"},
user_id: {string: "user", type: "many2one", relation: 'user'},
- parnter_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.parnter_id'},
+ partner_id: {string: "user", type: "many2one", relation: 'partner', related: 'user_id.partner_id'},
name: {string: "name", type: "char"},
start_date: {string: "start date", type: "date"},
stop_date: {string: "stop date", type: "date"},
@@ -58,7 +58,7 @@ QUnit.module('Views', {
fields: {
id: {string: "ID", type: "integer"},
display_name: {string: "Displayed name", type: "char"},
- parnter_id: {string: "partner", type: "many2one", relation: 'partner'},
+ partner_id: {string: "partner", type: "many2one", relation: 'partner'},
image: {string: "image", type: "integer"},
},
records: [
@@ -158,7 +158,7 @@ QUnit.module('Views', {
'all_day="allday" '+
'mode="week" '+
'attendee="partner_ids" '+
- 'color="parnter_id">'+
+ 'color="partner_id">'+
''+
''+
''+
@@ -194,7 +194,7 @@ QUnit.module('Views', {
var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))');
assert.ok($typeFilter.length, "should display 'user' filter");
- assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 1, "should display 1 filter items for 'user'");
+ assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 2, "should display 2 filter items for 'user'");
var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))');
assert.ok($attendeesFilter.length, "should display 'attendees' filter");
@@ -940,6 +940,100 @@ QUnit.module('Views', {
assert.strictEqual($('#ui-datepicker-div:empty').length, 0, "should have a clean body");
});
+ QUnit.test('"all" filter', function (assert) {
+ assert.expect(3);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch:
+ ''+
+ ''+
+ ''+
+ ''+
+ '',
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ assert.strictEqual(calendar.$('.fc-event').length, 9,
+ "should display 9 events on the week");
+
+ // Select the events only associated with partner 2
+ calendar.$('.o_calendar_filter_item[data-id=2] input').click();
+ assert.strictEqual(calendar.$('.fc-event').length, 4,
+ "should display 4 events on the week");
+
+ // Click on the 'all' filter to reload all events
+ calendar.$('.o_calendar_filter_item[data-value=all] input').click();
+ assert.strictEqual(calendar.$('.fc-event').length, 9,
+ "should display 9 events on the week");
+
+ calendar.destroy();
+ });
+
+ QUnit.test('ensure calendar adapts to screen size', function (assert) {
+ assert.expect(2);
+
+ // We want to be able to restore changes on 'body'
+ var bodyStyle = $('body').attr('style');
+
+ // Force the view height to be smaller than the natural calendar height
+ // (FullCalendar requires that the height be modified on body not $target)
+ var height = 500;
+ $('body').height(height);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch: '',
+ });
+
+ var $scroller = calendar.$('.fc-scroller')
+ assert.strictEqual($scroller.height() < height, true,
+ "the scroller should be smaller than the view height");
+ assert.strictEqual($scroller.prop('scrollHeight') > height, true,
+ "the scroller should have a scrollbar");
+ calendar.destroy();
+
+ $('body').removeAttr('style');
+ if (bodyStyle) {
+ $('body').attr('style', bodyStyle);
+ }
+ });
+
+ QUnit.test('ensure events are still shown if filters give an empty domain', function (assert) {
+ assert.expect(2);
+
+ var calendar = createView({
+ View: CalendarView,
+ model: 'event',
+ data: this.data,
+ arch: '' +
+ '' +
+ '',
+ viewOptions: {
+ initialDate: initialDate,
+ },
+ });
+
+ assert.strictEqual(calendar.$('.fc-event').length, 5,
+ "should display 5 events");
+ calendar.$('.o_calendar_filter_item[data-value=all] input[type=checkbox]').click();
+ assert.strictEqual(calendar.$('.fc-event').length, 5,
+ "should display 5 events");
+ calendar.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 4b499fe1c6f..3055b6e08d2 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -3608,6 +3608,40 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('clicking on a stat button with no context', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '',
+ res_id: 2,
+ viewOptions: {
+ context: {some_context: true},
+ },
+ intercepts: {
+ execute_action: function (e) {
+ assert.deepEqual(e.data.action_data.context, {},
+ "button context should have been evaluated and given to the action, without previous context");
+ },
+ },
+ });
+
+ form.$('.oe_stat_button').click();
+
+ form.destroy();
+ });
+
QUnit.test('diplay a stat button outside a buttonbox', function (assert) {
assert.expect(3);
@@ -4946,8 +4980,8 @@ QUnit.module('Views', {
_t.database.multi_lang = multi_lang;
});
- QUnit.test('buttons are disabled until action is resolved', function (assert) {
- assert.expect(3);
+ QUnit.test('buttons are disabled until status bar action is resolved', function (assert) {
+ assert.expect(9);
var def = $.Deferred();
@@ -4956,6 +4990,10 @@ QUnit.module('Views', {
model: 'partner',
data: this.data,
arch: '