summaryrefslogtreecommitdiff
path: root/addons/web
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-01-03 12:28:52 +0100
committerChristophe Simonis <chs@odoo.com>2018-01-03 12:28:52 +0100
commit362f0489d07d5bcd3673278e93848d0ea3d177a8 (patch)
treece164613ca633d736bf41c5b3d02b8ee541baa89 /addons/web
parenta39491d459f7a70af21bce1f45434bab05f1c284 (diff)
parentfe22a0f9ca92ef0e76bb2f730a2b22da500db5dd (diff)
[MERGE] forward port branch 11.0 up to fe22a0f9ca9
Diffstat (limited to 'addons/web')
-rw-r--r--addons/web/controllers/main.py2
-rw-r--r--addons/web/models/models.py37
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js3
-rw-r--r--addons/web/static/src/js/core/qweb.js15
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js4
-rw-r--r--addons/web/static/src/js/fields/field_utils.js2
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js19
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js47
-rw-r--r--addons/web/static/src/js/views/graph/graph_model.js5
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_column.js2
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer.js11
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js19
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js57
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js3
-rw-r--r--addons/web/static/src/js/views/list/list_view.js2
-rw-r--r--addons/web/static/src/less/search_view.less9
-rw-r--r--addons/web/static/src/xml/kanban.xml4
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js57
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js197
-rw-r--r--addons/web/static/tests/helpers/test_utils.js70
-rw-r--r--addons/web/static/tests/views/form_benchmarks.js95
-rw-r--r--addons/web/static/tests/views/graph_tests.js40
-rw-r--r--addons/web/static/tests/views/kanban_mobile_tests.js63
-rw-r--r--addons/web/static/tests/views/kanban_tests.js48
-rw-r--r--addons/web/static/tests/views/list_tests.js19
-rw-r--r--addons/web/views/webclient_templates.xml1
26 files changed, 723 insertions, 108 deletions
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 438270ecfe6..a8a32347882 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -608,7 +608,7 @@ class WebClient(http.Controller):
def version_info(self):
return odoo.service.common.exp_version()
- @http.route('/web/tests', type='http', auth="none")
+ @http.route('/web/tests', type='http', auth="user")
def test_suite(self, mod=None, **kwargs):
return request.render('web.qunit_suite')
diff --git a/addons/web/models/models.py b/addons/web/models/models.py
index 09d56349048..edf167df6c9 100644
--- a/addons/web/models/models.py
+++ b/addons/web/models/models.py
@@ -1,7 +1,13 @@
# -*- coding: utf-8 -*-
+from datetime import datetime
+import babel.dates
+import pytz
+from odoo.tools import pycompat
+from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
from odoo import _, api, fields, models
+
class Base(models.AbstractModel):
_inherit = 'base'
@@ -19,12 +25,43 @@ class Base(models.AbstractModel):
:return a dictionnary mapping group_by values to dictionnaries mapping
progress bar field values to the related number of records
"""
+
+ # Workaround to match read_group's infrastructure
+ # TO DO in master: harmonize this function and readgroup to allow factorization
+ group_by_modifier = group_by.partition(':')[2] or 'month'
+ group_by = group_by.partition(':')[0]
+ display_date_formats = {
+ 'day': 'dd MMM yyyy',
+ 'week': "'W'w YYYY",
+ 'month': 'MMMM yyyy',
+ 'quarter': 'QQQ yyyy',
+ 'year': 'yyyy'}
+
fields = [progress_bar['field'], group_by]
records_values = self.search_read(domain or [], fields)
data = {}
for record_values in records_values:
group_by_value = record_values[group_by]
+
+ # Again, imitating what _read_group_format_result and _read_group_prepare_data do
+ field_type = self._fields[group_by].type
+ if field_type in ['date', 'datetime'] and isinstance(group_by_value, pycompat.string_types):
+ locale = self._context.get('lang') or 'en_US'
+ dt_format = DEFAULT_SERVER_DATETIME_FORMAT if field_type == 'datetime' else DEFAULT_SERVER_DATE_FORMAT
+ group_by_value = datetime.strptime(group_by_value, dt_format)
+ group_by_value = pytz.timezone('UTC').localize(group_by_value)
+ tz_info = None
+ if field_type == 'datetime' and self._context.get('tz') in pytz.all_timezones:
+ tz_info = self._context.get('tz')
+ group_by_value = babel.dates.format_datetime(
+ group_by_value, format=display_date_formats[group_by_modifier],
+ tzinfo=tz_info, locale=locale)
+ else:
+ group_by_value = babel.dates.format_date(
+ group_by_value, format=display_date_formats[group_by_modifier],
+ locale=locale)
+
if type(group_by_value) == tuple:
group_by_value = group_by_value[1] # FIXME should use technical value (0)
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index c9eed9b2e64..e72db2c3a0c 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -941,7 +941,7 @@ var ActionManager = Widget.extend({
});
});
},
- ir_actions_act_url: function (action) {
+ ir_actions_act_url: function (action, options) {
var url = action.url;
if (session.debug && url && url.length && url[0] === '/') {
url = $.param.querystring(url, {debug: session.debug});
@@ -952,6 +952,7 @@ var ActionManager = Widget.extend({
return $.Deferred(); // The action is finished only when the redirection is done
} else {
window.open(url, '_blank');
+ options.on_close();
}
return $.when();
},
diff --git a/addons/web/static/src/js/core/qweb.js b/addons/web/static/src/js/core/qweb.js
index 50742075e05..253e948d8ac 100644
--- a/addons/web/static/src/js/core/qweb.js
+++ b/addons/web/static/src/js/core/qweb.js
@@ -5,7 +5,18 @@ var translation = require('web.translation');
var _t = translation._t;
-function QWeb(debug, default_dict) {
+/**
+ * @param {boolean} debug
+ * @param {Object} default_dict
+ * @param {boolean} [enableTranslation=true] if true (this is the default),
+ * the rendering will translate all strings that are not marked with
+ * t-translation=off. This is useful for the kanban view, which uses a
+ * template which is already translated by the server
+ */
+function QWeb(debug, default_dict, enableTranslation) {
+ if (enableTranslation === undefined) {
+ enableTranslation = true;
+ }
var qweb = new QWeb2.Engine();
qweb.default_dict = _.extend({}, default_dict || {}, {
'_' : _,
@@ -16,7 +27,7 @@ function QWeb(debug, default_dict) {
'csrf_token': odoo.csrf_token,
});
qweb.debug = debug;
- qweb.preprocess_node = preprocess_node;
+ qweb.preprocess_node = enableTranslation ? preprocess_node : function () {};
return qweb;
}
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index de5f80b0e92..eb56844de14 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -576,8 +576,8 @@ var FieldMonetary = InputField.extend({
this.tagName = 'div';
this.className += ' o_input';
- // use the formatFloat function in edit
- this.formatType = 'float';
+ // do not display currency symbol in edit
+ this.formatOptions.noSymbol = true;
}
this.formatOptions.currency = this.currency;
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index bca74acc088..932ea997e6a 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -290,7 +290,7 @@ function formatMonetary(value, field, options) {
digits: (currency && currency.digits) || options.digits,
});
- if (!currency) {
+ if (!currency || options.noSymbol) {
return formatted_value;
}
if (currency.position === "after") {
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 1c4c072726b..c4697cdafe1 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -824,12 +824,9 @@ var FieldX2Many = AbstractField.extend({
return this._super();
}
if (this.renderer) {
- this.renderer.updateState(this.value, {});
this.currentColInvisibleFields = this._evalColumnInvisibleFields();
- this.pager.updateState({
- size: this.value.count,
- columnInvisibleFields: this.currentColInvisibleFields,
- });
+ this.renderer.updateState(this.value, {'columnInvisibleFields': this.currentColInvisibleFields});
+ this.pager.updateState({ size: this.value.count });
return $.when();
}
var arch = this.view.arch;
@@ -839,7 +836,7 @@ var FieldX2Many = AbstractField.extend({
this.currentColInvisibleFields = this._evalColumnInvisibleFields();
this.renderer = new ListRenderer(this, this.value, {
arch: arch,
- mode: this.mode,
+ editable: this.mode === 'edit' && arch.attrs.editable,
addCreateLine: !this.isReadonly && this.activeActions.create,
addTrashIcon: !this.isReadonly && this.activeActions.delete,
viewType: viewType,
@@ -1380,12 +1377,12 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
throw _.str.sprintf(msg, this.field.string);
}
+ this.uploadedFiles = {};
this.uploadingFiles = [];
this.fileupload_id = _.uniqueId('oe_fileupload_temp');
$(window).on(this.fileupload_id, this._onFileLoaded.bind(this));
this.metadata = {};
- this._generatedMetadata();
},
destroy: function () {
@@ -1415,10 +1412,10 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
_generatedMetadata: function () {
var self = this;
_.each(this.value.data, function (record) {
- // attachments are tagged `allowUnlink` because only new attachments
- // will be unlinked after deletion
+ // tagging `allowUnlink` ascertains if the attachment was user
+ // uploaded or was an existing or system generated attachment
self.metadata[record.id] = {
- allowUnlink: false,
+ allowUnlink: self.uploadedFiles[record.data.id] || false,
url: self._getFileUrl(record.data),
};
});
@@ -1430,6 +1427,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
_render: function () {
// render the attachments ; as the attachments will changes after each
// _setValue, we put the rendering here to ensure they will be updated
+ this._generatedMetadata();
this.$('.oe_placeholder_files, .oe_attachments')
.replaceWith($(qweb.render('FieldBinaryFileUploader.files', {
widget: this,
@@ -1537,6 +1535,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
self.do_warn(_t('Uploading Error'), file.error);
} else {
attachment_ids.push(file.id);
+ self.uploadedFiles[file.id] = true;
}
});
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 a6d5172bfe1..042d85d414a 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -251,13 +251,12 @@ var BasicRenderer = AbstractRenderer.extend({
function _apply(element) {
// If the view is in edit mode and that a widget have to switch
// its "readonly" state, we have to re-render it completely
- if ('readonly' in modifiers
- && self.mode === "edit"
- && element.widget
- && (element.widget.mode === 'readonly') !== modifiers.readonly)
- {
- self._rerenderFieldWidget(element.widget, record);
- return; // Rerendering already applied the modifiers, no need to go further
+ if ('readonly' in modifiers && element.widget) {
+ var mode = modifiers.readonly ? 'readonly' : modifiersData.baseMode;
+ if (mode !== element.widget.mode) {
+ self._rerenderFieldWidget(element.widget, record, mode);
+ return; // Rerendering already applied the modifiers, no need to go further
+ }
}
// Toggle modifiers CSS classes if necessary
@@ -429,6 +428,12 @@ var BasicRenderer = AbstractRenderer.extend({
this.allModifiersData.push(modifiersData);
}
}
+ // we register here the base mode of the node. This is a field widget
+ // specific settings which represents the generic mode for the widget,
+ // regardless of its modifiers. The interesting case is the list view:
+ // all widgets are supposed to be in the baseMode 'readonly', except the
+ // ones that are in the line that is currently being edited.
+ modifiersData.baseMode = (options && options.mode) || this.mode;
// Evaluate if necessary
if (!modifiersData.evaluatedModifiers[record.id]) {
@@ -454,7 +459,7 @@ var BasicRenderer = AbstractRenderer.extend({
}
modifiersData.elementsByRecord[record.id].push(newElement);
- this._applyModifiers(modifiersData, record, newElement);
+ this._applyModifiers(modifiersData, record, newElement, options);
}
return modifiersData.evaluatedModifiers[record.id];
@@ -493,22 +498,20 @@ var BasicRenderer = AbstractRenderer.extend({
* @param {Object} node
* @param {Object} record
* @param {Object} [options]
- * @param {Object} [modifiersOptions]
* @returns {jQueryElement}
*/
- _renderFieldWidget: function (node, record, options, modifiersOptions) {
+ _renderFieldWidget: function (node, record, options) {
var fieldName = node.attrs.name;
-
// Register the node-associated modifiers
- var modifiers = this._registerModifiers(node, record);
-
+ var mode = options && options.mode || this.mode;
+ var modifiers = this._registerModifiers(node, record, null, options);
// Initialize and register the widget
// Readonly status is known as the modifiers have just been registered
var Widget = record.fieldsInfo[this.viewType][fieldName].Widget;
- var widget = new Widget(this, fieldName, record, _.extend({
- mode: modifiers.readonly ? 'readonly' : this.mode,
+ var widget = new Widget(this, fieldName, record, {
+ mode: modifiers.readonly ? 'readonly' : mode,
viewType: this.viewType,
- }, options || {}));
+ });
// Register the widget so that it can easily be found again
if (this.allFieldWidgets[record.id] === undefined) {
@@ -534,15 +537,16 @@ var BasicRenderer = AbstractRenderer.extend({
if (async) {
$el.replaceWith(widget.$el);
}
- self._registerModifiers(node, record, widget, _.extend({
+ self._registerModifiers(node, record, widget, {
callback: function (element, modifiers, record) {
element.$el.toggleClass('o_field_empty', !!(
record.data.id
- && (modifiers.readonly || self.mode === 'readonly')
+ && (modifiers.readonly || mode === 'readonly')
&& !element.widget.isSet()
));
},
- }, modifiersOptions || {}));
+ mode: mode
+ });
self._postProcessField(widget, node);
});
@@ -610,10 +614,11 @@ var BasicRenderer = AbstractRenderer.extend({
* @private
* @param {Widget} widget
* @param {Object} record
+ * @param {string} mode either 'readonly' or 'edit'
*/
- _rerenderFieldWidget: function (widget, record) {
+ _rerenderFieldWidget: function (widget, record, mode) {
// Render the new field widget
- var $el = this._renderFieldWidget(widget.__node, record);
+ var $el = this._renderFieldWidget(widget.__node, record, {mode: mode});
widget.$el.replaceWith($el);
// Destroy the old widget and position the new one at the old one's
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 a6d2b057d0e..0518e279637 100644
--- a/addons/web/static/src/js/views/graph/graph_model.js
+++ b/addons/web/static/src/js/views/graph/graph_model.js
@@ -67,7 +67,6 @@ return AbstractModel.extend({
domain: params.domain,
context: params.context,
};
- this.defaultGroupedBy = params.groupedBy;
return this._loadGraph();
},
/**
@@ -96,7 +95,7 @@ return AbstractModel.extend({
this.chart.domain = params.domain;
}
if ('groupBy' in params) {
- this.chart.groupedBy = params.groupBy.length ? params.groupBy : this.defaultGroupedBy;
+ this.chart.groupedBy = params.groupBy.length ? params.groupBy : this.initialGroupBys;
}
if ('measure' in params) {
this.chart.measure = params.measure;
@@ -121,7 +120,7 @@ return AbstractModel.extend({
* @returns {Deferred}
*/
_loadGraph: function () {
- var groupedBy = this.chart.groupedBy.length ? this.chart.groupedBy : this.initialGroupBys;
+ var groupedBy = this.chart.groupedBy;
var fields = _.map(groupedBy, function (groupBy) {
return groupBy.split(':')[0];
});
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index d39a3b280e1..0e50e37194f 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -41,7 +41,7 @@ var KanbanColumn = Widget.extend({
this.data = data;
var value = data.value;
- this.id = data.res_id || value;
+ this.id = data.res_id;
this.folded = !data.isOpen;
this.has_active_field = 'active' in data.fields;
this.fields = data.fields;
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 029cef7cf0b..0f91fb2ae14 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -94,7 +94,7 @@ var KanbanRenderer = BasicRenderer.extend({
this._super.apply(this, arguments);
this.widgets = [];
- this.qweb = new QWeb(session.debug, {_s: session.origin});
+ this.qweb = new QWeb(session.debug, {_s: session.origin}, false);
var templates = findInNode(this.arch, function (n) { return n.tag === 'templates';});
transformQwebTemplate(templates, state.fields);
this.qweb.add_template(utils.json_node_to_xml(templates));
@@ -235,6 +235,10 @@ var KanbanRenderer = BasicRenderer.extend({
}
});
+ // remove previous sorting
+ if(this.$el.sortable('instance') !== undefined) {
+ this.$el.sortable('destroy');
+ }
if (this.groupedByM2O) {
// Enable column sorting
this.$el.sortable({
@@ -249,7 +253,10 @@ var KanbanRenderer = BasicRenderer.extend({
stop: function () {
var ids = [];
self.$('.o_kanban_group').each(function (index, u) {
- ids.push($(u).data('id'));
+ // Ignore 'Undefined' column
+ if (_.isNumber($(u).data('id'))) {
+ ids.push($(u).data('id'));
+ }
});
self.trigger_up('resequence_columns', {ids: ids});
},
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
index e2b08fc0da9..7614d6bca95 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer_mobile.js
@@ -12,6 +12,7 @@ var config = require('web.config');
var core = require('web.core');
var KanbanRenderer = require('web.KanbanRenderer');
+var _t = core._t;
var qweb = core.qweb;
if (!config.device.isMobile) {
@@ -107,8 +108,9 @@ KanbanRenderer.include({
var updateFunc = animate ? 'animate' : 'css';
self.$('.o_kanban_mobile_tab').removeClass('o_current');
_.each(self.widgets, function (column, index) {
- var $column = self.$('.o_kanban_group[data-id=' + column.id + ']');
- var $tab = self.$('.o_kanban_mobile_tab[data-id=' + column.id + ']');
+ var columnID = column.id || column.db_id;
+ var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]');
+ var $tab = self.$('.o_kanban_mobile_tab[data-id="' + columnID + '"]');
if (index === moveToIndex - 1) {
$column[updateFunc]({left: '-100%'});
$tab[updateFunc]({left: '0%'});
@@ -137,8 +139,19 @@ KanbanRenderer.include({
* @private
*/
_renderGrouped: function (fragment) {
+ var data = [];
+ _.each(this.state.data, function (group) {
+ if (!group.value) {
+ group = _.extend({}, group, {value: _t('Undefined')});
+ data.unshift(group);
+ }
+ else {
+ data.push(group);
+ }
+ });
+
var $tabs = $(qweb.render('KanbanView.MobileTabs', {
- data: this.state.data,
+ data: data,
}));
$tabs.appendTo(fragment);
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 c1ef23b6b5e..b57d44e7de2 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
@@ -54,7 +54,7 @@ ListRenderer.include({
* @returns {Deferred}
*/
start: function () {
- if (this.mode === 'edit') {
+ if (this._isEditable()) {
this.$el.css({height: '100%'});
core.bus.on('click', this, this._onWindowClicked.bind(this));
}
@@ -295,12 +295,7 @@ ListRenderer.include({
renderInvisible: editMode,
renderWidgets: editMode,
};
- if (!editMode) {
- // Force 'readonly' mode for widgets in readonly rows as
- // otherwise they default to the view mode which is 'edit' for
- // an editable list view
- options.mode = 'readonly';
- }
+ options.mode = editMode ? 'edit' : 'readonly';
// Switch each cell to the new mode; note: the '_renderBodyCell'
// function might fill the 'this.defs' variables with multiple deferred
@@ -417,7 +412,7 @@ ListRenderer.include({
* @returns {boolean}
*/
_isEditable: function () {
- return this.mode === 'edit' && !this.state.groupedBy.length && this.arch.attrs.editable;
+ return !this.state.groupedBy.length && this.editable;
},
/**
* Move the cursor on the end of the previous line, if possible.
@@ -547,21 +542,49 @@ ListRenderer.include({
var row = _.findWhere(rows, {id: movedRecordID});
var index0 = rows.indexOf(row);
var index1 = ui.item.index();
- rows = rows.slice(Math.min(index0, index1), Math.max(index0, index1) + 1);
- rows = _.without(rows, row);
- if (index0 > index1) {
- rows.unshift(row);
+ var lower = Math.min(index0, index1);
+ var upper = Math.max(index0, index1) + 1;
+
+ var order = _.findWhere(self.state.orderedBy, {name: self.handleField});
+ var asc = !order || order.asc;
+ var reorderAll = false;
+ var sequence = (asc ? -1 : 1) * Infinity;
+
+ // determine if we need to reorder all lines
+ _.each(rows, function (row, index) {
+ if ((index < lower || index >= upper) &&
+ ((asc && sequence >= row.data[self.handleField]) ||
+ (!asc && sequence <= row.data[self.handleField]))) {
+ reorderAll = true;
+ }
+ sequence = row.data[self.handleField];
+ });
+
+ if (reorderAll) {
+ rows = _.without(rows, row);
+ rows.splice(index1, 0, row);
} else {
- rows.push(row);
+ rows = rows.slice(lower, upper);
+ rows = _.without(rows, row);
+ if (index0 > index1) {
+ rows.unshift(row);
+ } else {
+ rows.push(row);
+ }
}
var sequences = _.pluck(_.pluck(rows, 'data'), self.handleField);
var rowIDs = _.pluck(rows, 'id');
- this.trigger_up('resequence', {
- rowIDs: rowIDs,
- offset: _.min(sequences),
- handleField: this.handleField,
+ if (!asc) {
+ rowIDs.reverse();
+ }
+ this.unselectRow().then(function () {
+ self.trigger_up('resequence', {
+ rowIDs: rowIDs,
+ offset: _.min(sequences),
+ handleField: self.handleField,
+ });
});
},
/**
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 b836da253c3..6b9e4aebe56 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -59,6 +59,7 @@ var ListRenderer = BasicRenderer.extend({
this.hasSelectors = params.hasSelectors;
this.selection = [];
this.pagers = []; // instantiated pagers (only for grouped lists)
+ this.editable = params.editable;
},
//--------------------------------------------------------------------------
@@ -260,7 +261,7 @@ var ListRenderer = BasicRenderer.extend({
// We register modifiers on the <td> element so that it gets the correct
// modifiers classes (for styling)
- var modifiers = this._registerModifiers(node, record, $td);
+ var modifiers = this._registerModifiers(node, record, $td, _.pick(options, 'mode'));
// If the invisible modifiers is true, the <td> element is left empty.
// Indeed, if the modifiers was to change the whole cell would be
// rerendered anyway.
diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js
index b2295affb98..8a989dad35e 100644
--- a/addons/web/static/src/js/views/list/list_view.js
+++ b/addons/web/static/src/js/views/list/list_view.js
@@ -48,7 +48,7 @@ var ListView = BasicView.extend({
this.rendererParams.arch = arch;
this.rendererParams.hasSelectors =
'hasSelectors' in params ? params.hasSelectors : true;
- this.rendererParams.mode = mode;
+ this.rendererParams.editable = params.readonly ? false : arch.attrs.editable;
this.loadParams.limit = this.loadParams.limit || 80;
this.loadParams.type = 'list';
diff --git a/addons/web/static/src/less/search_view.less b/addons/web/static/src/less/search_view.less
index a9d6d28e9e4..0261806631a 100644
--- a/addons/web/static/src/less/search_view.less
+++ b/addons/web/static/src/less/search_view.less
@@ -139,6 +139,9 @@
// Filters
.o_filters_menu {
.o-selected-li;
+
+ max-height: ~"calc(100vh - 125px)";
+ overflow: auto;
.o_filter_condition {
.o-search-options-dropdown-custom-li;
@@ -175,6 +178,9 @@
// Group By
.o_group_by_menu {
.o-selected-li;
+
+ max-height: ~"calc(100vh - 125px)";
+ overflow: auto;
.divider {display: none;} // hide the divider by default (no group)
.o_add_group { // add button
@@ -188,6 +194,9 @@
max-height: 75vh;
overflow-y: auto;
.o-selected-li;
+
+ max-height: ~"calc(100vh - 125px)";
+ overflow: auto;
.divider {display: none;} // hide the divider by default (no favorite)
.o-searchview-custom-private, .o-searchview-custom-public {
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index 6cbc4d55942..7afc640ccfa 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -9,7 +9,7 @@
</t>
<t t-name="KanbanView.Group">
- <div t-attf-class="o_kanban_group#{widget.data_records.length == 0 ? ' o_kanban_no_records' : ''}" t-att-data-id="widget.id">
+ <div t-attf-class="o_kanban_group#{widget.data_records.length == 0 ? ' o_kanban_no_records' : ''}" t-att-data-id="widget.id or widget.db_id">
<div class="o_kanban_header">
<div class="o_kanban_header_title" t-att-title="widget.data_records.length + ' records'" data-delay="500">
<span class="o_column_title"><t t-esc="widget.title"/></span>
@@ -107,7 +107,7 @@
<t t-name="KanbanView.MobileTabs">
<div class="o_kanban_mobile_tabs">
<t t-foreach="data" t-as="group">
- <div class="o_kanban_mobile_tab" t-att-data-id="group.res_id">
+ <div class="o_kanban_mobile_tab" t-att-data-id="group.res_id or group.id">
<span class="o_column_title"><t t-esc="group.value"/></span>
</div>
</t>
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 08e255777eb..6a67cc43736 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -2457,6 +2457,63 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('monetary field with currency digits != 2', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records = [{
+ id: 1,
+ bar: false,
+ foo: "pouet",
+ int_field: 68,
+ qux: 99.1234,
+ currency_id: 1,
+ }];
+ this.data.currency.records = [{
+ id: 1,
+ display_name: "VEF",
+ symbol: "Bs.F",
+ position: "after",
+ digits: [16, 4],
+ }];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1234\u00a0Bs.F',
+ 'The value should be displayed properly.');
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input').first().val(), '99.1234',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('input').parent().children().eq(1).text(), 'Bs.F',
+ 'The input should be followed by a span containing the currency symbol.');
+
+ form.$('input').first().val('99.111111111').trigger('input');
+ assert.strictEqual(form.$('input').first().val(), '99.111111111',
+ 'The value should not be formated yet.');
+
+ form.$buttons.find('.o_form_button_save').click();
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1111\u00a0Bs.F',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
QUnit.test('monetary field in editable list view', function (assert) {
assert.expect(9);
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index df9e7d4994b..84d095660cb 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -2274,7 +2274,7 @@ QUnit.module('relational_fields', {
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
"should still have the 3 rows in the correct order");
- // Drag and drop the fourth line in second position
+ // Drag and drop the second line in first position
testUtils.dragAndDrop(
form.$('.ui-sortable-handle').eq(1),
form.$('tbody tr').first(),
@@ -2334,7 +2334,7 @@ QUnit.module('relational_fields', {
form.$buttons.find('.o_form_button_edit').click();
- // Drag and drop the fourth line in second position
+ // Drag and drop the second line in first position
testUtils.dragAndDrop(
form.$('.ui-sortable-handle').eq(1),
form.$('tbody tr').first(),
@@ -2347,6 +2347,120 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('onchange for embedded one2many with handle widget using same sequence', function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.records[0].turtle_int = 1;
+ this.data.turtle.records[1].turtle_int = 1;
+ this.data.turtle.records[2].turtle_int = 1;
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var turtleOnchange = 0;
+ this.data.turtle.onchanges = {
+ turtle_int: function () {
+ turtleOnchange++;
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="turtles">' +
+ '<tree default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].turtles, [[1,1,{"turtle_int":2}],[4,2,false],[1,3,{"turtle_int":3}]],
+ "should change all lines that have changed (the first one doesn't change because it has the same sequence)");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
+ "should have the 3 rows in the correct order");
+
+ // Drag and drop the second line in first position
+ testUtils.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(1),
+ form.$('tbody tr').first(),
+ {position: 'top'}
+ );
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa",
+ "should still have the 3 rows in the correct order");
+ assert.strictEqual(turtleOnchange, 3, "should update all lines");
+
+ form.$buttons.find('.o_form_button_save').click();
+ form.destroy();
+ });
+
+ QUnit.test('onchange (with command 5) for embedded one2many with handle widget', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
+ "should have the 3 rows in the correct order");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click();
+ form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input');
+
+ // Drag and drop the third line in second position
+ testUtils.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(2),
+ form.$('.o_field_one2many tbody tr').eq(1),
+ {position: 'top'}
+ );
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip",
+ "should display to record in 'turtle_int' order");
+
+ form.destroy();
+ });
+
QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) {
var done = assert.async();
assert.expect(20);
@@ -4065,16 +4179,16 @@ QUnit.module('relational_fields', {
assert.expect(5);
var delta = 0;
- var oldInit = AbstractField.prototype.init;
- var oldDestroy = AbstractField.prototype.destroy;
- AbstractField.prototype.init = function () {
- delta++;
- oldInit.apply(this, arguments);
- };
- AbstractField.prototype.destroy = function () {
- delta--;
- oldDestroy.apply(this, arguments);
- };
+ testUtils.patch(AbstractField, {
+ init: function () {
+ delta++;
+ this._super.apply(this, arguments);
+ },
+ destroy: function () {
+ delta--;
+ this._super.apply(this, arguments);
+ },
+ });
var deactiveOnchange = true;
@@ -4161,8 +4275,49 @@ QUnit.module('relational_fields', {
"should correctly refresh the records after save");
form.destroy();
- AbstractField.prototype.init = oldInit;
- AbstractField.prototype.destroy = oldDestroy;
+ testUtils.unpatch(AbstractField);
+ });
+
+ QUnit.test('editable one2many with sub widgets are rendered in readonly', function (assert) {
+ assert.expect(2);
+
+ var editableWidgets = 0;
+ testUtils.patch(AbstractField, {
+ init: function () {
+ this._super.apply(this, arguments);
+ if (this.mode === 'edit') {
+ editableWidgets++;
+ }
+ },
+ });
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo" widget="char" attrs="{\'readonly\': [(\'turtle_int\', \'==\', 11111)]}"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(editableWidgets, 1,
+ "o2m is only widget in edit mode");
+ form.$('tbody td.o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(editableWidgets, 3,
+ "3 widgets currently in edit mode");
+
+ form.destroy();
+ testUtils.unpatch(AbstractField);
});
QUnit.test('one2many editable list with onchange keeps the order', function (assert) {
@@ -9538,7 +9693,7 @@ QUnit.module('relational_fields', {
});
QUnit.test('one2many invisible depends on parent field', function (assert) {
- assert.expect(2);
+ assert.expect(4);
this.data.partner.records[0].p = [2];
var form = createView({
@@ -9547,12 +9702,15 @@ QUnit.module('relational_fields', {
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
'<notebook>' +
'<page string="Partner page">' +
'<field name="bar"/>' +
'<field name="p">' +
'<tree>' +
- '<field name="foo"/>' +
+ '<field name="foo" attrs="{\'column_invisible\': [(\'parent.product_id\', \'!=\', False)]}"/>' +
'<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
'</tree>' +
'</field>' +
@@ -9565,6 +9723,13 @@ QUnit.module('relational_fields', {
assert.strictEqual(form.$('th').length, 2,
"should be 2 columns in the one2many");
form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_field_many2one[name="product_id"] input').click();
+ $('li.ui-menu-item a:contains(xpad)').trigger('mouseenter').click();
+ assert.strictEqual(form.$('th').length, 1,
+ "should be 1 column when the product_id is set");
+ form.$('.o_field_many2one[name="product_id"] input').val('').trigger('keyup');
+ assert.strictEqual(form.$('th').length, 2,
+ "should be 2 columns in the one2many when product_id is not set");
form.$('.o_field_boolean[name="bar"] input').click();
assert.strictEqual(form.$('th').length, 1,
"should be 1 column after the value change");
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index 37a98de1746..e049ba968b2 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -434,11 +434,16 @@ function dragAndDrop($el, $to, options) {
elementCenter.top += $el.outerHeight()/2;
var toOffset = $to.offset();
+ toOffset.top += $to.outerHeight()/2;
toOffset.left += $to.outerWidth()/2;
- if (position === 'center') {
- toOffset.top += $to.outerHeight()/2;
+ if (position === 'top') {
+ toOffset.top -= $to.outerHeight()/2;
} else if (position === 'bottom') {
- toOffset.top += $to.outerHeight();
+ toOffset.top += $to.outerHeight()/2;
+ } else if (position === 'left') {
+ toOffset.left -= $to.outerWidth()/2;
+ } else if (position === 'right') {
+ toOffset.left += $to.outerWidth()/2;
}
$el.trigger($.Event("mousedown", {
@@ -551,6 +556,51 @@ function removeSrcAttribute($el, widget) {
});
}
+var patches = {};
+/**
+ * Patches a given Class with the given properties.
+ *
+ * @param {Class} Klass
+ * @param {Object} props
+ */
+function patch (Klass, props) {
+ var patchID = _.uniqueId('patch_');
+ Klass.__patchID = patchID;
+ patches[patchID] = {
+ Klass: Klass,
+ otherPatchedProps: [],
+ ownPatchedProps: [],
+ };
+ _.each(props, function (value, key) {
+ if (Klass.prototype.hasOwnProperty(key)) {
+ patches[patchID].ownPatchedProps.push({
+ key: key,
+ initialValue: Klass.prototype[key],
+ });
+ } else {
+ patches[patchID].otherPatchedProps.push(key);
+ }
+ });
+ Klass.include(props);
+}
+/**
+ * Unpatches a given Class.
+ *
+ * @param {Class} Klass
+ */
+function unpatch(Klass) {
+ var patchID = Klass.__patchID;
+ var patch = patches[patchID];
+ _.each(patch.ownPatchedProps, function (p) {
+ Klass[p.key] = p.initialValue;
+ });
+ _.each(patch.otherPatchedProps, function (key) {
+ delete Klass.prototype[key];
+ });
+ delete patches[patchID];
+ delete Klass.__patchID;
+}
+
// Loading static files cannot be properly simulated when their real content is
// really needed. This is the case for static XML files so we load them here,
// before starting the qunit test suite.
@@ -566,18 +616,20 @@ return $.when(
QUnit.start();
}, 0);
return {
- intercept: intercept,
- observe: observe,
- createView: createView,
+ addMockEnvironment: addMockEnvironment,
createAsyncView: createAsyncView,
createModel: createModel,
createParent: createParent,
- addMockEnvironment: addMockEnvironment,
+ createView: createView,
dragAndDrop: dragAndDrop,
+ intercept: intercept,
+ observe: observe,
+ patch: patch,
+ removeSrcAttribute: removeSrcAttribute,
+ triggerKeypressEvent: triggerKeypressEvent,
triggerMouseEvent: triggerMouseEvent,
triggerPositionalMouseEvent: triggerPositionalMouseEvent,
- triggerKeypressEvent: triggerKeypressEvent,
- removeSrcAttribute: removeSrcAttribute,
+ unpatch: unpatch,
};
});
diff --git a/addons/web/static/tests/views/form_benchmarks.js b/addons/web/static/tests/views/form_benchmarks.js
new file mode 100644
index 00000000000..d664859ba2f
--- /dev/null
+++ b/addons/web/static/tests/views/form_benchmarks.js
@@ -0,0 +1,95 @@
+odoo.define('web.list_benchmarks', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('Form View', {
+ beforeEach: function () {
+ this.data = {
+ foo: {
+ fields: {
+ many2many: { string: "bar", type: "many2many", relation: 'bar'},
+ },
+ records: [
+ { id: 1, many2many: []},
+ ],
+ onchanges: {}
+ },
+ bar: {
+ fields: {
+ char: {string: "char", type: "char"},
+ many2many: { string: "pokemon", type: "many2many", relation: 'pokemon'},
+ },
+ records: [],
+ onchanges: {}
+ },
+ pokemon: {
+ fields: {
+ name: {string: "Name", type: "char"},
+ },
+ records: [],
+ onchanges: {}
+ },
+ };
+ this.arch = null;
+ this.run = function (assert, done, cb) {
+ var data = this.data;
+ var arch = this.arch;
+ var viewOptions = this.viewOptions;
+ new Benchmark.Suite({})
+ .add('form', function () {
+ var list = createView({
+ View: FormView,
+ model: 'foo',
+ data: data,
+ arch: arch,
+ res_id: 1,
+ });
+ if (cb) {
+ cb(list);
+ }
+ list.destroy();
+ })
+ .on('cycle', function(event) {
+ assert.ok(true, String(event.target));
+ })
+ .on('complete', done)
+ .run({ 'async': true });
+ };
+ }
+}, function () {
+ QUnit.test('x2many with 250 rows, 2 fields (with many2many_tags, and modifiers), onchanges, and edition', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ this.data.foo.onchanges.many2many = function (obj) {
+ obj.many2many = [5].concat(obj.many2many);
+ };
+ for (var i = 2; i < 500; i) {
+ this.data.bar.records.push({
+ id: i,
+ char: "automated data",
+ });
+ this.data.foo.records[0].many2many.push(i);
+ }
+ this.arch =
+ '<form string="Partners">'
+ '<field name="many2many">'
+ '<tree editable="top" limit="250">'
+ '<field name="char"/>'
+ '<field name="many2many" widget="many2many_tags" attrs="{\'readonly\': [(\'char\', \'==\', \'toto\')]}"/>'
+ '</tree>'
+ '</field>'
+ '</form>';
+ this.run(assert, done, function (form) {
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_cell:first').click();
+ form.$('input:first').val("tralala").trigger('input');
+ });
+ });
+});
+
+}); \ No newline at end of file
diff --git a/addons/web/static/tests/views/graph_tests.js b/addons/web/static/tests/views/graph_tests.js
index 07f7d060347..f7d22f27a64 100644
--- a/addons/web/static/tests/views/graph_tests.js
+++ b/addons/web/static/tests/views/graph_tests.js
@@ -90,7 +90,6 @@ QUnit.module('Views', {
});
});
-
QUnit.test('default type attribute', function (assert) {
assert.expect(1);
@@ -106,7 +105,6 @@ QUnit.module('Views', {
graph.destroy();
});
-
QUnit.test('switching mode', function (assert) {
assert.expect(6);
@@ -191,7 +189,6 @@ QUnit.module('Views', {
});
});
-
QUnit.test('no content helper', function (assert) {
var done = assert.async();
assert.expect(4);
@@ -442,6 +439,43 @@ QUnit.module('Views', {
graph.destroy();
});
+
+ QUnit.test('initial groupby is kept when reloading', function (assert) {
+ var done = assert.async();
+ assert.expect(4);
+
+ var graph = createView({
+ View: GraphView,
+ model: 'foo',
+ data: this.data,
+ arch: '<graph>' +
+ '<field name="product_id" type="row"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</graph>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read_group') {
+ assert.deepEqual(args.kwargs.groupby, ['product_id'],
+ "should group by the correct field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+
+ return concurrency.delay(0).then(function () {
+ assert.strictEqual(graph.$('.nv-groups rect').length, 2,
+ "should display two groups");
+
+ graph.reload({groupBy: []});
+ return concurrency.delay(0).then(function () {
+ assert.strictEqual(graph.$('.nv-groups rect').length, 2,
+ "should still display two groups");
+
+ graph.destroy();
+ done();
+ });
+ });
+ });
});
});
diff --git a/addons/web/static/tests/views/kanban_mobile_tests.js b/addons/web/static/tests/views/kanban_mobile_tests.js
index d777f5bd87b..0ee35c2c9c3 100644
--- a/addons/web/static/tests/views/kanban_mobile_tests.js
+++ b/addons/web/static/tests/views/kanban_mobile_tests.js
@@ -26,6 +26,7 @@ QUnit.module('Views', {
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6]},
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7]},
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: []},
+ {id: 5, bar: false, foo: "Hello \"World\"! #peace_n'_love", int_field: -9, qux: 10, state: "jkl", category_ids: []},
]
},
product: {
@@ -55,7 +56,7 @@ QUnit.module('Views', {
QUnit.module('KanbanView Mobile');
QUnit.test('mobile grouped rendering', function (assert) {
- assert.expect(8);
+ assert.expect(9);
var kanban = createView({
View: KanbanView,
@@ -66,6 +67,7 @@ QUnit.module('Views', {
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
+ domain: [['product_id', '!=', false]],
groupBy: ['product_id'],
});
@@ -95,6 +97,65 @@ QUnit.module('Views', {
assert.ok(kanban.$('.o_kanban_group:nth(1) > div:nth(1)').hasClass('o_kanban_quick_create'),
"clicking on create should open the quick_create in the second column");
+ // kanban column should match kanban mobile tabs
+ var column_ids = kanban.$('.o_kanban_group').map(function(){ return $(this).data('id') }).get();
+ var tab_ids = kanban.$('.o_kanban_mobile_tab').map(function(){ return $(this).data('id') }).get();
+ assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id");
+
+ kanban.destroy();
+ });
+ QUnit.test('mobile grouped with undefined column', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test o_kanban_small_column">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['product_id'],
+ });
+
+ // first column should be undefined with framework unique identifier
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 3, "should have 3 columns" );
+ assert.strictEqual(kanban.$('.o_kanban_group:first-child[data-id^="partner_"]').length, 1,
+ "Undefined column should be first and have unique framework identifier as data-id")
+
+ // kanban column should match kanban mobile tabs
+ var column_ids = kanban.$('.o_kanban_group').map(function(){ return $(this).data('id') }).get();
+ var tab_ids = kanban.$('.o_kanban_mobile_tab').map(function(){ return $(this).data('id') }).get();
+ assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id");
+
+ kanban.destroy();
+ });
+ QUnit.test('mobile grouped on many2one rendering', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test o_kanban_small_column">' +
+ '<templates><t t-name="kanban-box">' +
+ '<div><field name="foo"/></div>' +
+ '</t></templates>' +
+ '</kanban>',
+ groupBy: ['foo'],
+ });
+
+ // basic rendering tests
+ assert.strictEqual(kanban.$('.o_kanban_group').length, 4, "should have 4 columns" );
+ assert.strictEqual(kanban.$('.o_kanban_group[data-id^="partner_"]').length, 4,
+ "all column should have framework unique identifiers");
+
+ // kanban column should match kanban mobile tabs
+ var column_ids = kanban.$('.o_kanban_group').map(function(){ return $(this).data('id') }).get();
+ var tab_ids = kanban.$('.o_kanban_mobile_tab').map(function(){ return $(this).data('id') }).get();
+ assert.deepEqual(column_ids, tab_ids, "all columns data-id should match mobile tabs data-id");
+
kanban.destroy();
});
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 70e60b37bd5..9c8361e63ae 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -3,6 +3,7 @@ odoo.define('web.kanban_tests', function (require) {
var core = require('web.core');
var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
+var KanbanRenderer = require('web.KanbanRenderer');
var KanbanView = require('web.KanbanView');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
@@ -1015,7 +1016,19 @@ QUnit.module('Views', {
});
QUnit.test('delete a column in grouped on m2o', function (assert) {
- assert.expect(26);
+ assert.expect(32);
+
+ testUtils.patch(KanbanRenderer, {
+ _renderGrouped: function () {
+ this._super.apply(this, arguments);
+ // set delay and revert animation time to 0 so dummy drag and drop works
+ if (this.$el.sortable('instance')) {
+ this.$el.sortable('option', {delay: 0, revert: 0});
+ }
+ },
+ });
+
+ var resequencedIDs;
var kanban = createView({
View: KanbanView,
@@ -1029,6 +1042,12 @@ QUnit.module('Views', {
'</kanban>',
groupBy: ['product_id'],
mockRPC: function (route, args) {
+ if (route === '/web/dataset/resequence') {
+ resequencedIDs = args.ids;
+ assert.strictEqual(_.reject(args.ids, _.isNumber).length, 0,
+ "column resequenced should be existing records with IDs");
+ return $.when(true);
+ }
if (args.method) {
assert.step(args.method);
}
@@ -1071,7 +1090,7 @@ QUnit.module('Views', {
assert.strictEqual(kanban.$('.o_kanban_group:last').data('id'), 3,
'last column should now be [3, "hello"]');
assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should still have two columns");
- assert.ok(!kanban.$('.o_kanban_group:first').data('id'),
+ assert.ok(!_.isNumber(kanban.$('.o_kanban_group:first').data('id')),
'first column should have no id (Undefined column)');
// check available actions on 'Undefined' column
assert.ok(kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').length,
@@ -1087,7 +1106,32 @@ QUnit.module('Views', {
assert.verifySteps(['read_group', 'unlink', 'read_group']);
assert.strictEqual(kanban.renderer.widgets.length, 2,
"the old widgets should have been correctly deleted");
+
+ // test column drag and drop having an 'Undefined' column
+ testUtils.dragAndDrop(
+ kanban.$('.o_kanban_header_title:first'),
+ kanban.$('.o_kanban_header_title:last'), {position: 'right'}
+ );
+ assert.strictEqual(resequencedIDs, undefined,
+ "resequencing require at least 2 not Undefined columns");
+ kanban.$('.o_column_quick_create input').val('once third column');
+ kanban.$('.o_column_quick_create button.o_kanban_add').click();
+ var newColumnID = kanban.$('.o_kanban_group:last').data('id');
+ testUtils.dragAndDrop(
+ kanban.$('.o_kanban_header_title:first'),
+ kanban.$('.o_kanban_header_title:last'), {position: 'right'}
+ );
+ assert.deepEqual([3, newColumnID], resequencedIDs,
+ "moving the Undefined column should not affect order of other columns")
+ testUtils.dragAndDrop(
+ kanban.$('.o_kanban_header_title:first'),
+ kanban.$('.o_kanban_header_title:nth(1)'), {position: 'right'}
+ );
+ assert.deepEqual([newColumnID, 3], resequencedIDs,
+ "moved column should be resequenced accordingly")
+
kanban.destroy();
+ testUtils.unpatch(KanbanRenderer);
});
QUnit.test('create a column, delete it and create another one', function (assert) {
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 9180015685e..2993e393a4f 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -415,11 +415,12 @@ QUnit.module('Views', {
QUnit.test('editable list: add a line and discard', function (assert) {
assert.expect(11);
- var oldDestroy = basicFields.FieldChar.prototype.destroy;
- basicFields.FieldChar.prototype.destroy = function () {
- assert.step('destroy');
- oldDestroy.apply(this, arguments);
- };
+ testUtils.patch(basicFields.FieldChar, {
+ destroy: function () {
+ assert.step('destroy');
+ this._super.apply(this, arguments);
+ },
+ });
var list = createView({
View: ListView,
@@ -456,7 +457,7 @@ QUnit.module('Views', {
assert.verifySteps(['destroy'],
"should have destroyed the widget of the removed line");
- basicFields.FieldChar.prototype.destroy = oldDestroy;
+ testUtils.unpatch(basicFields.FieldChar);
list.destroy();
});
@@ -473,13 +474,13 @@ QUnit.module('Views', {
var n = 0;
testUtils.intercept(list, "field_changed", function () {
- n = 1;
+ n += 1;
});
$td.click();
$td.find('input').val('abc').trigger('input');
- assert.strictEqual(n, 1, "field_changed should not have been triggered");
- list.$('td:not(.o_list_record_selector)').eq(2).click();
assert.strictEqual(n, 1, "field_changed should have been triggered");
+ list.$('td:not(.o_list_record_selector)').eq(2).click();
+ assert.strictEqual(n, 1, "field_changed should not have been triggered");
list.destroy();
});
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index c3d4e4eceae..a02bbdbcf6a 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -587,6 +587,7 @@
<script type="text/javascript" src="/web/static/tests/views/list_benchmarks.js"></script>
<script type="text/javascript" src="/web/static/tests/views/kanban_benchmarks.js"></script>
+ <script type="text/javascript" src="/web/static/tests/views/form_benchmarks.js"></script>
</t>
<div id="qunit"/>