')
- .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..54c8ccfbf51 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -361,17 +361,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.
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..ec3ff8940a7 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;
},
/**
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..b4bf1ddbf94 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -43,7 +43,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 () {
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/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/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..8910316eb21 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,47 @@ 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();
+ });
+
});
});
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index 33b2fd8ffbd..c7d69b194ff 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -372,6 +372,30 @@ QUnit.module('Views', {
});
});
+ QUnit.test('reload graph with correct fields', function (assert) {
+ assert.expect(2);
+
+ var graph = createView({
+ View: GraphView,
+ model: 'foo',
+ data: this.data,
+ arch: '' +
+ '' +
+ '' +
+ '',
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.fields, ['product_id', 'foo'],
+ "should read the correct fields");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ graph.reload({groupBy: []});
+
+ graph.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index dd0a52868f6..7fc9343d82c 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -965,7 +965,7 @@ QUnit.module('Views', {
},
});
- assert.ok(kanban.$el.hasClass('o_kanban_nocontent'),
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_nocontent'),
"$el should have correct no content class");
assert.strictEqual(kanban.$('.oe_view_nocontent').length, 1,
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index e3d8acfe84a..f0370ac7cf8 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -2483,6 +2483,34 @@ QUnit.module('Views', {
list.destroy();
});
+
+ QUnit.test('grouped list edition with toggle_button widget', function (assert) {
+ assert.expect(3);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '',
+ groupBy: ['m2o'],
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1], {bar: false},
+ "should write the correct value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ list.$('.o_group_header:first').click(); // open the first group
+ assert.strictEqual(list.$('.o_data_row:first .o_toggle_button_success').length, 1,
+ "boolean value of the first record should be true");
+ list.$('.o_data_row:first .o_icon_button').click(); // toggle the value
+ assert.strictEqual(list.$('.o_data_row:first .text-muted:not(.o_toggle_button_success)').length, 1,
+ "boolean button should have been updated");
+
+ list.destroy();
+ });
});
});
diff --git a/addons/web_kanban_gauge/static/src/js/kanban_gauge.js b/addons/web_kanban_gauge/static/src/js/kanban_gauge.js
index 84f5ada2f85..93dc6d746f0 100644
--- a/addons/web_kanban_gauge/static/src/js/kanban_gauge.js
+++ b/addons/web_kanban_gauge/static/src/js/kanban_gauge.js
@@ -6,7 +6,7 @@ var field_registry = require('web.field_registry');
var utils = require('web.utils');
/**
- * Kanban widgets: GaugeWidget
+ * Field widget: GaugeWidget
* options
* - max_value: maximum value of the gauge [default: 100]
* - max_field: get the max_value from the field that must be present in the
@@ -26,7 +26,15 @@ var utils = require('web.utils');
var GaugeWidget = AbstractField.extend({
className: "oe_gauge",
- render: function() {
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _render: function () {
// parameters
var max_value = this.nodeOptions.max_value || 100;
if (this.nodeOptions.max_field) {
@@ -124,4 +132,6 @@ var GaugeWidget = AbstractField.extend({
field_registry.add("gauge", GaugeWidget);
+return GaugeWidget;
+
});
diff --git a/addons/web_kanban_gauge/static/tests/gauge_tests.js b/addons/web_kanban_gauge/static/tests/gauge_tests.js
new file mode 100644
index 00000000000..e494d5391e5
--- /dev/null
+++ b/addons/web_kanban_gauge/static/tests/gauge_tests.js
@@ -0,0 +1,49 @@
+odoo.define('web_kanban_gauge.gauge_tests', function (require) {
+"use strict";
+
+var KanbanView = require('web.KanbanView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('basic_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ int_field: {string: "int_field", type: "integer", sortable: true},
+ },
+ records: [
+ {id: 1, int_field: 10},
+ {id: 2, int_field: 4},
+ ]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('gauge widget');
+
+ QUnit.test('basic rendering', function (assert) {
+ assert.expect(1);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '
' +
+ '',
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_record:first .oe_gauge svg').length, 1,
+ "should render the gauge widget");
+
+ kanban.destroy();
+ });
+
+});
+});
+});
diff --git a/addons/web_kanban_gauge/views/web_kanban_gauge_templates.xml b/addons/web_kanban_gauge/views/web_kanban_gauge_templates.xml
index 93ac6cf2653..fbba1de789b 100644
--- a/addons/web_kanban_gauge/views/web_kanban_gauge_templates.xml
+++ b/addons/web_kanban_gauge/views/web_kanban_gauge_templates.xml
@@ -5,4 +5,10 @@
+
+
+
+
+
+
diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js
index 1999b6f6f0c..4dba132497a 100644
--- a/addons/web_tour/static/src/js/tour_manager.js
+++ b/addons/web_tour/static/src/js/tour_manager.js
@@ -119,10 +119,13 @@ var RunningTourActionHelper = core.Class.extend({
if (values.consume_event === "input") {
values.$element.trigger("keydown").val(text).trigger("keyup").trigger("input");
} else if (values.$element.is("select")) {
- values.$element.children("option")
- .prop("selected", false).removeProp("selected")
- .filter(function () { return $(this).val() === text; })
- .prop("selected", true);
+ var $options = values.$element.children("option");
+ $options.prop("selected", false).removeProp("selected");
+ var $selectedOption = $options.filter(function () { return $(this).val() === text; });
+ if ($selectedOption.length === 0) {
+ $selectedOption = $options.filter(function () { return $(this).text() === text; });
+ }
+ $selectedOption.prop("selected", true);
this._click(values);
} else {
values.$element.text(text);
diff --git a/addons/website_portal_sale/views/website_portal_sale_templates.xml b/addons/website_portal_sale/views/website_portal_sale_templates.xml
index 2f2f37f13f9..01884a10859 100644
--- a/addons/website_portal_sale/views/website_portal_sale_templates.xml
+++ b/addons/website_portal_sale/views/website_portal_sale_templates.xml
@@ -178,7 +178,7 @@
-
+
diff --git a/doc/cla/individual/vincentadriaensen.md b/doc/cla/individual/vincentadriaensen.md
new file mode 100644
index 00000000000..3fcaa1d1352
--- /dev/null
+++ b/doc/cla/individual/vincentadriaensen.md
@@ -0,0 +1,11 @@
+Belgium, 19/07/2017
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Vincent Adriaensen vincentadriaensen@users.noreply.github.com https://github.com/vincentadriaensen
diff --git a/odoo/addons/base/rng/view.rng b/odoo/addons/base/rng/view.rng
index 0fb5a0ca9fc..3225735ffad 100644
--- a/odoo/addons/base/rng/view.rng
+++ b/odoo/addons/base/rng/view.rng
@@ -556,6 +556,7 @@
+
diff --git a/odoo/addons/test_converter/models.py b/odoo/addons/test_converter/models.py
index b6251a5df3f..55938946b34 100644
--- a/odoo/addons/test_converter/models.py
+++ b/odoo/addons/test_converter/models.py
@@ -21,12 +21,12 @@ class test_model(models.Model):
(4, "réponse
"),
])
selection_str = fields.Selection([
- ('A', "Qu'il n'est pas arrivé à Toronto"),
- ('B', "Qu'il était supposé arriver à Toronto"),
- ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"),
- ('D', "La réponse D"),
- ], string="Lorsqu'un pancake prend l'avion à destination de Toronto et "
- "qu'il fait une escale technique à St Claude, on dit:")
+ ('A', u"Qu'il n'est pas arrivé à Toronto"),
+ ('B', u"Qu'il était supposé arriver à Toronto"),
+ ('C', u"Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"),
+ ('D', u"La réponse D"),
+ ], string=u"Lorsqu'un pancake prend l'avion à destination de Toronto et "
+ u"qu'il fait une escale technique à St Claude, on dit:")
html = fields.Html()
text = fields.Text()
diff --git a/odoo/addons/test_main_flows/__init__.py b/odoo/addons/test_main_flows/__init__.py
new file mode 100644
index 00000000000..ac45264fad4
--- /dev/null
+++ b/odoo/addons/test_main_flows/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+#from . import models
diff --git a/odoo/addons/test_main_flows/__manifest__.py b/odoo/addons/test_main_flows/__manifest__.py
new file mode 100644
index 00000000000..031e886e5c8
--- /dev/null
+++ b/odoo/addons/test_main_flows/__manifest__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'Test Main Flow',
+ 'version': '1.0',
+ 'category': 'Tools',
+ 'description': """
+This mobule will test the main workflow of Odoo.
+It will install some main apps and will try to execute the most important actions.
+""",
+ 'depends': ['web_tour', 'crm', 'sale_timesheet', 'purchase', 'mrp', 'account_accountant'],
+ 'data': [
+ 'views/templates.xml',
+ ],
+ 'installable': True,
+}
diff --git a/odoo/addons/test_main_flows/static/src/js/tour.js b/odoo/addons/test_main_flows/static/src/js/tour.js
new file mode 100644
index 00000000000..107c5d62c65
--- /dev/null
+++ b/odoo/addons/test_main_flows/static/src/js/tour.js
@@ -0,0 +1,691 @@
+odoo.define('test_main_flows.tour', function(require) {
+"use strict";
+
+var core = require('web.core');
+var tour = require('web_tour.tour');
+
+var _t = core._t;
+
+tour.register('main_flow_tour', {
+ test: true,
+ url: "/web",
+}, [tour.STEPS.MENU_MORE, {
+ trigger: '.o_app[data-menu-xmlid="sales_team.menu_base_partner"], .oe_menu_toggler[data-menu-xmlid="sales_team.menu_base_partner"]',
+ content: _t('Organize your sales activities with the Sales app.'),
+ position: 'bottom',
+}, {
+// Add Stockable product
+ edition: "enterprise",
+ trigger: ".o_menu_sections a:contains('Catalog')",
+ extra_trigger: '.o_main_navbar',
+ content: _t("Let\'s create products."),
+ position: "bottom",
+}, {
+ trigger: ".o_menu_sections a:has(span:contains('Products')), .oe_secondary_submenu .oe_menu_text:contains('Products'):first",
+ content: _t("Let\'s create products."),
+ position: "bottom"
+}, {
+ trigger: '.o-kanban-button-new',
+ extra_trigger: '.o_kanban_view',
+ content: _t('Let\'s create your first product.'),
+ position: 'right',
+}, {
+ trigger: 'input[name=name]',
+ extra_trigger: '.o_form_sheet',
+ content: _t('Let\'s enter the name.'),
+ position: 'left',
+ run: 'text the_flow.product',
+}, {
+ trigger: "select[name=type]",
+ content: _t('Let\'s enter the product type'),
+ position: 'left',
+ run: 'text "product"',
+}, {
+ trigger: '.o_notebook a:contains("Inventory")',
+ content: _t('Go to inventory tab'),
+ position: 'top',
+}, {
+ trigger: '.o_field_widget[name=route_ids] .o_checkbox + label:contains("Manufacture")',
+ content: _t('Check Manufacture'),
+ position: 'right',
+}, {
+ trigger: '.o_field_widget[name=route_ids] .o_checkbox + label:contains("Buy")',
+ content: _t('Uncheck Buy'),
+ position: 'right',
+}, {
+ trigger: '.o_field_widget[name=route_ids] .o_checkbox + label:contains("Make To Order")',
+ content: _t('Uncheck Make To Order'),
+ position: 'right',
+}, {
+ trigger: '.o_form_button_save',
+ content: _t('Save this product and the modifications you\'ve made to it.'),
+ position: 'bottom',
+}, {
+ trigger: ".oe_button_box .oe_stat_button:has(div[name=bom_count])",
+ extra_trigger: '.o_form_readonly',
+ content: _t('See Bill of material'),
+ position: 'bottom',
+}, {
+ trigger: ".o_list_button_add",
+ content: _t("Let's create a new bill of material"),
+ position: "right",
+}, {
+// Add first component
+ trigger: ".o_field_x2many_list_row_add > a",
+ extra_trigger: ".o_form_editable",
+ content: _t("Click here to add some lines."),
+ position: "bottom",
+}, {
+ trigger: ".o_selected_row .o_required_modifier[name=product_id] input",
+ content: _t("Select a product, or create a new one on the fly."),
+ position: "right",
+ run: "text the_flow.component1",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.component1')",
+ auto: true,
+}, {
+// Edit first component
+ trigger: ".o_selected_row .o_external_button",
+ content: _t("Click here to edit your component"),
+ position: "right",
+}, {
+ trigger: '.o_notebook a:contains("Inventory")',
+ content: _t('Go to inventory tab'),
+ position: 'top',
+}, {
+ trigger: '.o_field_widget[name=route_ids] .o_checkbox + label:contains("Make To Order")',
+ content: _t('Check Make To Order'),
+ position: 'right',
+}, {
+ trigger: ".o_field_widget[name=seller_ids] .o_field_x2many_list_row_add > a",
+ content: _t('Let\'s enter the cost price'),
+ position: 'right',
+}, {
+ trigger: ".o_field_widget[name=name] input",
+ extra_trigger: ".modal-dialog",
+ content: _t('Select a seller'),
+ position: 'top',
+ run: "text the_flow.vendor",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.vendor')",
+ in_modal: false,
+}, {
+ trigger: ".o_field_widget[name=price]",
+ content: _t('Set the cost price'),
+ position: 'right',
+ run: "text 1",
+}, {
+ trigger: ".modal-footer .btn-primary:first",
+ extra_trigger: ".o_field_widget[name=name] > .o_external_button", // Wait name_create
+ content: _t('Save & Close'),
+ position: 'bottom',
+}, {
+ trigger: ".modal-footer .btn-primary",
+ content: _t('Save'),
+ position: 'bottom',
+}, {
+// Add second component
+ trigger: ".o_field_x2many_list_row_add > a",
+ in_modal: false,
+ content: _t("Click here to add some lines."),
+ position: "bottom",
+}, {
+ trigger: ".o_selected_row .o_required_modifier[name=product_id] input",
+ extra_trigger: '.o_field_widget[name=bom_line_ids] .o_data_row:nth(1).o_selected_row',
+ content: _t("Select a product, or create a new one on the fly."),
+ position: "right",
+ run: "text the_flow.component2",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.component2')",
+ auto: true,
+}, {
+// Edit second component
+ trigger: ".o_selected_row .o_external_button",
+ content: _t("Click here to edit your component"),
+ position: "right",
+}, {
+ trigger: '.o_notebook a:contains("Inventory")',
+ content: _t('Go to inventory tab'),
+ position: 'top',
+}, {
+ trigger: ".o_field_widget[name=seller_ids] .o_field_x2many_list_row_add > a",
+ content: _t('Let\'s enter the cost price'),
+ position: 'right',
+}, {
+ trigger: ".o_field_widget[name=name] input",
+ extra_trigger: ".modal-dialog",
+ content: _t('Select a seller'),
+ position: 'top',
+ run: "text the_flow.vendor",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.vendor')",
+ auto: true,
+ in_modal: false,
+}, {
+ trigger: ".o_field_widget[name=price]",
+ content: _t('Set the cost price'),
+ position: 'right',
+ run: "text 1",
+}, {
+ trigger: ".modal-footer .btn-primary:first",
+ content: _t('Save & Close'),
+ position: 'bottom',
+}, {
+ trigger: ".modal-footer .btn-primary",
+ // Wait Save & Close and check value
+ extra_trigger: ".o_field_widget[name=seller_ids] .o_data_row td:nth-child(2):contains('the_flow.vendor')",
+ content: _t('Save'),
+ position: 'bottom',
+}, {
+ trigger: '.o_form_button_save',
+ extra_trigger: ".o_field_widget[name=bom_line_ids] .o_list_view tr:nth-child(3):has(.o_field_x2many_list_row_add)",
+ content: _t('Save the bom.'),
+ position: 'bottom',
+}, {
+ trigger: ".breadcrumb li:first",
+ extra_trigger: ".o_form_readonly", // FIXME: this is required due to an issue in tour_manager (see [*])
+ content: _t("Use the breadcrumbs to go back to products."),
+ position: "bottom"
+}, {
+// Add service product
+ trigger: '.o-kanban-button-new',
+ extra_trigger: '.o_kanban_view',
+ content: _t('Let\'s create your second product.'),
+ position: 'right',
+}, {
+ trigger: 'input[name=name]',
+ extra_trigger: '.o_form_sheet',
+ content: _t('Let\'s enter the name.'),
+ position: 'left',
+ run: 'text the_flow.service',
+}, {
+ trigger: '.o_field_widget[name=type]',
+ content: _t('Set to service'),
+ position: 'left',
+ run: 'text "service"',
+}, {
+ trigger: '.o_notebook a:contains("Invoicing")',
+ content: _t('Go to invoicing tab'),
+ position: 'bottom',
+}, {
+ trigger: '.o_field_widget[name=invoice_policy] .o_radio_input[data-value="delivery"]',
+ content: _t('Change invoicing policy'),
+ position: 'left',
+}, {
+ trigger: '.o_field_widget[name=track_service] input[data-value="task"]',
+ content: _t('Change track service'),
+ position: 'left',
+}, {
+ trigger: '.o_field_widget[name=project_id] input',
+ content: _t('Choose project'),
+ position: 'left',
+ run: 'text the_flow.project',
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.project')",
+ auto: true,
+}, {
+ trigger: '.o_form_button_save',
+ extra_trigger: '.o_field_widget[name=project_id] > .o_external_button', // Wait name_create
+ content: _t('Save this product and the modifications you\'ve made to it.'),
+ position: 'bottom',
+}, {
+// Create an opportunity
+ edition: "enterprise",
+ trigger: '.o_menu_toggle',
+ content: _t('Go back to the app switcher'),
+ position: 'bottom',
+}, {
+ trigger: '.o_app[data-menu-xmlid="sales_team.menu_base_partner"], .oe_menu_toggler[data-menu-xmlid="sales_team.menu_base_partner"]',
+ content: _t('Organize your sales activities with the Sales app.'),
+ position: 'bottom',
+}, {
+ trigger: ".o_sales_dashboard .o_dashboard_action[name='crm.action_your_pipeline']:last, .oe_secondary_submenu .oe_menu_text:contains('My Pipeline'):first",
+ content: _t("Let\'s have a look at your opportunities pipeline."),
+ position: "bottom"
+}, {
+ trigger: ".o-kanban-button-new",
+ extra_trigger: '.o_opportunity_kanban',
+ content: _t("Click here to create your first opportunity and add it to your pipeline."),
+ position: "right"
+}, {
+ trigger: ".modal-body input:first",
+ content: _t("Enter the opportunity title."),
+ position: "right",
+ run: "text the_flow.opportunity",
+}, {
+ trigger: ".o_field_widget[name=partner_id] input",
+ content: _t("Write the name of your customer to create one on the fly, or select an existing one."),
+ position: "left",
+ run: "text the_flow.customer",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.customer')",
+ in_modal: false,
+ auto: true,
+}, {
+ trigger: ".modal-footer .btn-primary",
+ extra_trigger: ".o_field_widget[name=partner_id] > .o_external_button", // Wait name_create
+ content: _t("Create"),
+ position: "bottom",
+}, {
+ trigger: ".o_kanban_group:first-child .o_kanban_record:last-child",
+ content: _t("Drag & drop opportunities between columns as you progress in your sales cycle."),
+ position: "right",
+ run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
+}, {
+ trigger: ".o_kanban_record:has(span:contains('the_flow.opportunity'))",
+ extra_trigger: ".o_kanban_group:eq(2) > .o_kanban_record:has(span:contains('the_flow.opportunity'))", // FIXME: this is required due to an issue in tour_manager (see [*])
+ content: _t("Click on an opportunity to zoom to it."),
+ position: "bottom",
+}, {
+// Create a quotation
+ trigger: ".o_statusbar_buttons > button:contains('New Quotation')",
+ content: _t('Create a quotation
'),
+ position: "right"
+}, {
+ trigger: ".o_field_widget[name=order_line] .o_field_x2many_list_row_add > a",
+ content: _t("Click here to add some lines to your quotations."),
+ position: "bottom",
+}, {
+ trigger: ".o_field_widget[name=product_id] input",
+ content: _t("Select a product, or create a new one on the fly. The product will define the default sales price (that you can change), taxes and description automatically."),
+ position: "right",
+ run: "text the_flow.product",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.product')",
+ auto: true,
+ in_modal: false,
+ run: function (actions) {
+ actions.auto();
+ // if the one2many isn't editable, we have to close the dialog
+ if ($(".modal-footer .btn-primary").length) {
+ actions.auto(".modal-footer .btn-primary");
+ }
+ },
+}, {
+ trigger: ".o_field_widget[name=order_line] .o_field_x2many_list_row_add > a",
+ content: _t("Click here to add some lines to your quotations."),
+ position: "bottom",
+}, {
+ trigger: ".o_field_widget[name=product_id] input",
+ // the one2many may be editable or not according to the modules installed, so
+ // we have to handle both cases
+ extra_trigger: '.o_field_widget[name=order_line] .o_data_row:nth(1).o_selected_row, .modal-dialog',
+ content: _t("Select a product"),
+ position: "right",
+ run: "text the_flow.service",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.service')",
+ auto: true,
+ in_modal: false,
+ run: function (actions) {
+ actions.auto();
+ // if the one2many isn't editable, we have to close the dialog
+ if ($(".modal-footer .btn-primary").length) {
+ actions.auto(".modal-footer .btn-primary");
+ }
+ },
+}, {
+ trigger: ".o_statusbar_buttons > button.o_sale_print",
+ content: _t("Print this quotation.
"),
+ position: "bottom"
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Send by Email')",
+ extra_trigger: ".o_statusbar_status .btn-primary:contains('Quotation Sent')",
+ content: _t("Try to send it to email"),
+ position: "bottom",
+}, {
+ trigger: ".o_field_widget[name=email]",
+ content: _t("Enter an email address"),
+ position: "right",
+ run: "text test@the_flow.com",
+}, {
+ trigger: ".modal-footer .btn-primary",
+ content: _t("Save your changes"),
+ position: "bottom",
+}, {
+ trigger: ".modal-footer .btn-primary span:contains('Send')",
+ content: _t("Try to send it to email"),
+ position: "bottom",
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Confirm Sale')",
+ content: _t("Confirm this quotation
"),
+ position: "bottom"
+}, {
+ trigger: ".o_form_button_save",
+ extra_trigger: ".o_statusbar_status .btn-primary:contains('Sales Order')",
+ content: _t("Save this quotation
"),
+ position: "bottom"
+// Reordering rule
+}, {
+ edition: "enterprise",
+ trigger: '.o_menu_toggle',
+ content: _t('Go back to the app switcher'),
+ position: 'bottom',
+}, {
+ trigger: '.o_app > div:contains("Inventory"), .oe_menu_toggler:contains("Inventory")',
+ content: _t('Go to Inventory'),
+ position: 'bottom',
+}, {
+ edition: "enterprise",
+ trigger: ".o_menu_sections a:contains('Inventory Control')",
+ content: _t("Go to Inventory Control"),
+ position: "bottom"
+}, {
+ trigger: ".o_menu_sections a:has(span:contains('Reordering Rules')), .oe_secondary_submenu .oe_menu_text:contains('Reordering Rules'):first",
+ content: _t("Reordering Rules"),
+ position: "bottom"
+}, {
+ trigger: ".o_list_button_add",
+ content: _t("Let's create a new reordering rule"),
+ position: "right",
+}, {
+ trigger: ".o_field_widget[name=product_id] input",
+ content: _t("Write the name of your product."),
+ position: "top",
+ run: "text the_flow.component2",
+}, {
+ trigger: ".ui-menu-item > a:contains('the_flow.component2')",
+ auto: true,
+}, {
+ trigger: ".o_field_widget[name=product_min_qty]",
+ content: _t("Set the minimum product quantity"),
+ position: "right",
+ run: 'text 1',
+}, {
+ trigger: ".o_field_widget[name=product_max_qty]",
+ content: _t("Set the maximum product quantity"),
+ position: "right",
+ run: 'text 10',
+}, {
+ trigger: ".o_form_button_save",
+ content: _t("Save this reordering rule
"),
+ position: "bottom"
+}, {
+// Run the schedulers
+ edition: "enterprise",
+ trigger: ".o_menu_sections a:contains('Schedulers')",
+ content: _t("Go to Schedulers"),
+ position: "bottom"
+}, {
+ trigger: ".o_menu_sections a:has(span:contains('Run Schedulers')), .oe_secondary_submenu .oe_menu_text:contains('Run Schedulers'):first",
+ content: _t("Run the schedulers"),
+ position: "bottom"
+}, {
+ trigger: ".modal-footer .btn-primary",
+ extra_trigger: ".modal-dialog",
+ content: _t("Run Schedulers"),
+ position: "bottom",
+}, {
+//Go to purchase:
+ edition: "enterprise",
+ trigger: '.o_menu_toggle',
+ content: _t('Go back to the app switcher'),
+ position: 'bottom',
+}, {
+ trigger: '.o_app > div:contains("Purchases"), .oe_menu_toggler:contains("Purchases")',
+ content: _t('Go to Purchase'),
+ position: 'bottom',
+}, {
+ trigger: '.o_data_row:has(.o_data_cell:contains("the_flow.vendor"))',
+ content: _t('Select the generated request for quotation'),
+ position: 'bottom',
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Confirm Order')",
+ content: _t("Confirm quotation"),
+ position: "bottom",
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Receive Products')",
+ content: _t("Receive Product"),
+ position: "bottom",
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Validate')",
+ content: _t("Validate"),
+ position: "bottom",
+}, {
+ trigger: ".modal-footer .btn-primary",
+ extra_trigger: ".modal-dialog",
+ content: _t("Apply"),
+ position: "bottom",
+}, {
+ trigger: ".o_back_button a, .breadcrumb li:not('.active'):last",
+ content: _t('go back to the purchase order'),
+ position: 'bottom',
+ }, {
+ trigger: ".oe_button_box .oe_stat_button:has(div[name=invoice_count])",
+ content: _t('go to Vendor Bills'),
+ position: 'bottom',
+}, {
+ trigger: ".o_list_button_add",
+ content: _t("Let's create a new vendor bill"),
+ position: "right",
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Validate')",
+ content: _t("Try to send it to email"),
+ position: "bottom",
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Register Payment')",
+ content: _t("Register Payment"),
+ position: "bottom",
+}, {
+ trigger: "select.o_field_widget[name=journal_id]",
+ extra_trigger: ".modal-dialog",
+ content: _t("Select Journal"),
+ position: "bottom",
+ run: 'text(Bank (USD))',
+}, {
+ trigger: ".modal-footer .btn-primary",
+ extra_trigger: ".o_field_widget[name=payment_method_id]", // FIXME: Wait onchange
+ content: _t("Validate"),
+ position: "bottom",
+}, {
+ edition: "enterprise",
+ trigger: '.o_menu_toggle',
+ content: _t('Go back to the app switcher'),
+ position: 'bottom',
+}, {
+ trigger: '.o_app > div:contains("Manufacturing"), .oe_menu_toggler:contains("Manufacturing")',
+ content: _t('Go to Manufacturing'),
+ position: 'bottom',
+}, {
+ trigger: '.o_data_row:has(.o_data_cell:contains("the_flow.product")):first',
+ content: _t('Select the generated manufacturing order'),
+ position: 'bottom',
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Check availability')",
+ content: _t("Check availability"),
+ position: "bottom",
+}, {
+ trigger: ".o_statusbar_buttons > button.oe_highlight:contains('Produce')",
+ content: _t("Produce"),
+ position: "bottom",
+}, {
+ trigger: ".modal-footer .btn-primary:first",
+ content: _t('Record Production'),
+ position: 'bottom',
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Mark as Done')",
+ content: _t("Mark as Done"),
+ position: "bottom",
+}, {
+ edition: "enterprise",
+ trigger: '.o_menu_toggle',
+ content: _t('Go back to the app switcher'),
+ position: 'bottom',
+}, {
+ trigger: '.o_app[data-menu-xmlid="sales_team.menu_base_partner"], .oe_menu_toggler[data-menu-xmlid="sales_team.menu_base_partner"]',
+ content: _t('Organize your sales activities with the Sales app.'),
+ position: 'bottom',
+}, {
+ edition: "enterprise",
+ trigger: ".o_menu_sections a[data-menu-xmlid='sales_team.menu_sales']",
+ content: _t("Go to Sales menu"),
+ position: "bottom"
+}, {
+ trigger: ".o_menu_sections a:has(span:contains('Sales Orders')), .oe_secondary_submenu .oe_menu_text:contains('Sales Orders'):first",
+ content: _t("Go to the sales orders"),
+ position: "bottom"
+}, {
+ trigger: ".o_data_row:first",
+ extra_trigger: '.o_control_panel > .breadcrumb:contains("Sales Orders")',
+ content: _t("Go to the last sale order"),
+ position: "right"
+}, {
+ trigger: '.oe_button_box .oe_stat_button:has(div[name=tasks_count])',
+ content: _t('See Tasks'),
+ position: 'right',
+}, {
+ trigger: '.o_field_widget[name=project_id]',
+ content: _t('See Project'),
+ position: 'right',
+}, {
+ trigger: '.oe_button_box .oe_stat_button:has(span:contains("Timesheets"))',
+ extra_trigger: '.o_form_readonly',
+ content: _t('See Timesheets'),
+ position: 'bottom',
+}, {
+ trigger: '.o_list_button_add',
+ content: _t('Add a Timesheet'),
+ position: 'bottom',
+}, {
+ trigger: '.o_selected_row input[name=name]',
+ content: _t('Set description'),
+ position: 'bottom',
+ run: 'text 10 hours',
+}, {
+ trigger: '.o_selected_row .o_field_widget[name=task_id] input',
+ content: _t('Choose a task'),
+ position: 'bottom',
+ run: 'click',
+}, {
+ trigger: ".ui-menu-item > a",
+ auto: true,
+}, {
+ trigger: '.o_selected_row input[name=unit_amount]',
+ content: _t('Set time'),
+ position: 'bottom',
+ run: 'text 10',
+}, {
+ trigger: '.o_list_button_save',
+ content: _t('Save'),
+ position: 'bottom',
+}, {
+ trigger: '.breadcrumb li:nth-child(2) a',
+ extra_trigger: '.o_list_button_add', // Waiting save
+ content: _t('Back to the sale order'),
+ position: 'bottom',
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Create Invoice')",
+ content: _t("Validate"),
+ position: "bottom",
+}, {
+ trigger: ".modal-footer .btn-primary",
+ content: _t("Create and View Invoices"),
+ position: "bottom",
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Validate')",
+ content: _t("Validate"),
+ position: "bottom",
+}, {
+ trigger: ".o_statusbar_buttons > button:contains('Register Payment')",
+ content: _t("Register Payment"),
+ position: "bottom",
+}, {
+ trigger: "select.o_field_widget[name=journal_id]",
+ extra_trigger: ".modal-dialog",
+ content: _t("Select Journal"),
+ position: "bottom",
+ run: 'text(Bank (USD))',
+}, {
+ trigger: ".modal-footer .btn-primary",
+ content: _t("Validate"),
+ position: "bottom",
+}, {
+ edition: "enterprise",
+ trigger: '.o_menu_toggle',
+ content: _t('Go back to the app switcher'),
+ position: 'bottom',
+}, {
+ edition: "enterprise",
+ trigger: '.o_app[data-menu-xmlid="account.menu_finance"], .oe_menu_toggler[data-menu-xmlid="account.menu_finance"]',
+ content: _t('Go to Accounting'),
+ position: 'bottom',
+}, {
+ edition: "enterprise",
+ trigger: 'div[name=bank_journal_default_cta] > a[data-name=create_bank_statement], div[name=bank_journal_cta] > a[data-name=create_bank_statement]',
+ content: _t('Create a new bank statement'),
+ position: 'bottom',
+}, {
+ edition: "enterprise",
+ trigger: 'input[name=name]',
+ content: _t('Let\'s enter the reference.'),
+ position: 'left',
+ run: 'text the_flow.statement',
+}, {
+ edition: "enterprise",
+ trigger: ".o_field_widget[name=balance_end_real] input",
+ content: _t('Let\'s calculate the ending balance.'),
+ position: 'right',
+ run: 'text 9010.85', // + 12.65
+}, {
+ edition: "enterprise",
+ trigger: ".o_field_widget[name=line_ids] .o_field_x2many_list_row_add > a",
+ content: _t("Click here to add some lines."),
+ position: "bottom",
+}, {
+ edition: "enterprise",
+ trigger: ".o_selected_row .o_field_widget[name=amount] input",
+ content: _t("Write the amount received."),
+ position: "bottom",
+ run: "text 12.65",
+}, {
+ edition: "enterprise",
+ trigger: ".o_selected_row .o_field_widget[name=partner_id] input",
+ content: _t("Write the name of your customer."),
+ position: "bottom",
+ run: "text the_flow.customer",
+}, {
+ edition: "enterprise",
+ trigger: ".ui-menu-item > a:contains('the_flow.customer')",
+ auto: true,
+}, {
+ edition: "enterprise",
+ trigger: ".o_selected_row .o_field_widget[name=name]",
+ extra_trigger: ".o_selected_row .o_field_widget[name=partner_id] .o_external_button", // FIXME: this is required due to an issue in tour_manager (see [*])
+ content: _t('Let\'s enter a name.'),
+ position: "bottom",
+ run: "text the_flow.statement.line",
+}, {
+ edition: "enterprise",
+ trigger: '.o_form_button_save',
+ content: _t('Save.'),
+ position: 'bottom',
+}, {
+ edition: "enterprise",
+ trigger: ".o_statusbar_buttons > button:contains('Reconcile')",
+ content: _t('Reconcile
'),
+ position: "bottom",
+}, {
+ edition: "enterprise",
+ trigger: "button.o_reconcile",
+ content: _t('Click on Reconcile
'),
+ position: "right",
+}, {
+ edition: "enterprise",
+ trigger: ".button_close_statement",
+ content: _t('Close this statement.
'),
+ position: "bottom",
+}]);
+});
+
+/*
+ * [*] FIXME: issue in tour_manager:
+ * The JQuery element of a step is registered as soon as its trigger and
+ * extra_trigger elements are visible in the DOM, but it's action is delayed
+ * to handle the 'running_step_delay' option (0 by default, but even with 0,
+ * the action is executed after finishing the current execution block, which
+ * may produce a re-rendering of the part of the DOM containing the element).
+ * When this happens, the action is executed on an jquery element not present
+ * in the DOM anymore.
+ * To avoid this, we add an 'extra_trigger' to wait for the updated jquery
+ * element before activating the step (and thus registering the JQuery element).
+ */
diff --git a/odoo/addons/test_main_flows/tests/__init__.py b/odoo/addons/test_main_flows/tests/__init__.py
new file mode 100644
index 00000000000..ad09d038804
--- /dev/null
+++ b/odoo/addons/test_main_flows/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import test_flow
diff --git a/odoo/addons/test_main_flows/tests/test_flow.py b/odoo/addons/test_main_flows/tests/test_flow.py
new file mode 100644
index 00000000000..bf9305efec5
--- /dev/null
+++ b/odoo/addons/test_main_flows/tests/test_flow.py
@@ -0,0 +1,11 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+
+
+@odoo.tests.common.at_install(False)
+@odoo.tests.common.post_install(True)
+class TestUi(odoo.tests.HttpCase):
+
+ def test_01_main_flow_tour(self):
+ self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('main_flow_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.main_flow_tour.ready", login="admin", timeout=180)
diff --git a/odoo/addons/test_main_flows/views/templates.xml b/odoo/addons/test_main_flows/views/templates.xml
new file mode 100644
index 00000000000..9c1bd81699f
--- /dev/null
+++ b/odoo/addons/test_main_flows/views/templates.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/odoo/netsvc.py b/odoo/netsvc.py
index a33e3bcaf27..7772d7d47cd 100644
--- a/odoo/netsvc.py
+++ b/odoo/netsvc.py
@@ -89,6 +89,7 @@ def init_logger():
_logger_init = True
logging.addLevelName(25, "INFO")
+ logging.captureWarnings(True)
from .tools.translate import resetlocale
resetlocale()
diff --git a/odoo/service/server.py b/odoo/service/server.py
index 6e4b05f3dcf..56acaaed901 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -75,12 +75,11 @@ class BaseWSGIServerNoBind(LoggingBaseWSGIServerMixIn, werkzeug.serving.BaseWSGI
use this class, sets the socket and calls the process_request() manually
"""
def __init__(self, app):
- werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app)
- def server_bind(self):
- # we dont bind beause we use the listen socket of PreforkServer#socket
- # instead we close the socket
+ werkzeug.serving.BaseWSGIServer.__init__(self, "127.0.0.1", 0, app)
+ # Directly close the socket. It will be replaced by WorkerHTTP when processing requests
if self.socket:
self.socket.close()
+
def server_activate(self):
# dont listen as we use PreforkServer#socket
pass
@@ -583,6 +582,7 @@ class PreforkServer(CommonServer):
if self.address:
# listen to socket
+ _logger.info('HTTP service (werkzeug) running on %s:%s', *self.address)
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.setblocking(0)