diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-01-03 12:28:52 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-01-03 12:28:52 +0100 |
| commit | 362f0489d07d5bcd3673278e93848d0ea3d177a8 (patch) | |
| tree | ce164613ca633d736bf41c5b3d02b8ee541baa89 /addons/web | |
| parent | a39491d459f7a70af21bce1f45434bab05f1c284 (diff) | |
| parent | fe22a0f9ca92ef0e76bb2f730a2b22da500db5dd (diff) | |
[MERGE] forward port branch 11.0 up to fe22a0f9ca9
Diffstat (limited to 'addons/web')
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"/> |
