diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-04-16 19:50:50 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-04-16 19:50:50 +0200 |
| commit | 74033eb007749c88b84ea883b006a5877355306e (patch) | |
| tree | 8f8b6cb22cc2bf6046ba04e388ea29855a33c597 /addons/web/static | |
| parent | 71cdefd9f41881d9720433d5fc7732759098f9a4 (diff) | |
| parent | 90f8f17fb4fc795c2c12d68d21900e81f3d7a223 (diff) | |
[MERGE] forward port branch saas-11.2 up to 90f8f17fb4f
Diffstat (limited to 'addons/web/static')
27 files changed, 775 insertions, 71 deletions
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js index b97262079ef..b36e1b8eec9 100644 --- a/addons/web/static/src/js/chrome/action_manager.js +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -839,12 +839,27 @@ var ActionManager = Widget.extend({ * is ready when it will be appended to the DOM. This allows to prevent * flickering for widgets doing async stuff in willStart() or start(). * + * Also updates the control panel on any change of the title on controller's + * widget. + * * @private * @param {Object} controller * @returns {Deferred<Object>} resolved with the controller when it is ready */ _startController: function (controller) { + var self = this; var fragment = document.createDocumentFragment(); + // AAB: change this logic to stop using the properties mixin + controller.widget.on("change:title", this, function () { + if (self.getCurrentController() !== controller) { + return; + } + var action = self.actions[controller.actionID]; + if (!action.flags || !action.flags.headless) { + var breadcrumbs = self._getBreadcrumbs(); + self.controlPanel.update({breadcrumbs: breadcrumbs}, {clear: false}); + } + }); return controller.widget.appendTo(fragment).then(function () { return controller; }); diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index 9dc0ff4c1c8..0ed909e2512 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -229,15 +229,7 @@ ActionManager.include({ // the action has been removed, so simply destroy the widget widget.destroy(); } else { - // AAB: change this logic to stop using the properties mixin - widget.on("change:title", this, function () { - if (!action.flags.headless) { - var breadcrumbs = self._getBreadcrumbs(); - self.controlPanel.update({breadcrumbs: breadcrumbs}, {clear: false}); - } - }); controller.widget = widget; - def.resolve(controller); } }).fail(def.reject.bind(def)); diff --git a/addons/web/static/src/js/core/rpc.js b/addons/web/static/src/js/core/rpc.js index 53d298c9019..cc56cb74ebb 100644 --- a/addons/web/static/src/js/core/rpc.js +++ b/addons/web/static/src/js/core/rpc.js @@ -52,19 +52,19 @@ return { if (options.method === 'read_group') { if (!(params.args && params.args[0] !== undefined)) { - params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || []; + params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || []; } if (!(params.args && params.args[1] !== undefined)) { - params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || []; + params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || []; } if (!(params.args && params.args[2] !== undefined)) { - params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || []; + params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || []; } - params.kwargs.offset = options.offset || params.offset || params.kwargs.offset; - params.kwargs.limit = options.limit || params.limit || params.kwargs.limit; + params.kwargs.offset = options.offset || params.offset || params.kwargs.offset; + params.kwargs.limit = options.limit || params.limit || params.kwargs.limit; // In kwargs, we look for "orderby" rather than "orderBy" (note the absence of capital B), // since the Python argument to the actual function is "orderby". - var orderBy = options.orderBy || params.orderBy || params.kwargs.orderby; + var orderBy = options.orderBy || params.orderBy || params.kwargs.orderby; params.kwargs.orderby = orderBy ? this._serializeSort(orderBy) : orderBy; params.kwargs.lazy = 'lazy' in options ? options.lazy : params.lazy; } @@ -72,7 +72,7 @@ return { if (options.method === 'search_read') { // call the model method params.kwargs.domain = options.domain || params.domain || params.kwargs.domain; - params.kwargs.fields = options.fields || params.fields || params.kwargs.fields; + params.kwargs.fields = options.fields || params.fields || params.kwargs.fields; params.kwargs.offset = options.offset || params.offset || params.kwargs.offset; params.kwargs.limit = options.limit || params.limit || params.kwargs.limit; // In kwargs, we look for "order" rather than "orderBy" since the Python diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 0f426dedd3b..c703694a9d4 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -998,6 +998,21 @@ var FieldPhone = FieldEmail.extend({ // important right now. this.$el.removeClass('o_text_overflow'); }, + * Remove possibly present ­ characters when saving number + * + * @override + * @private + */ + _setValue: function (value, options) { + // NOT NEEDED AS OF SAAS-11.3 + if (value) { + // remove possibly pasted ­ characters + value = value.replace(/\u00AD/g, ''); + } + return this._super(value, options); + }, + + /** }); var UrlWidget = InputField.extend({ diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 93c924fad4c..c3df03fcff9 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -1059,7 +1059,7 @@ var FieldX2Many = AbstractField.extend({ */ _onEditLine: function (ev) { ev.stopPropagation(); - this.trigger_up('freeze_order', {id: this.value.id}); + this.trigger_up('edited_list', { id: this.value.id }); var editedRecord = this.value.data[ev.data.index]; this.renderer.setRowMode(editedRecord.id, 'edit') .done(ev.data.onSuccess); @@ -1147,7 +1147,7 @@ var FieldX2Many = AbstractField.extend({ _onResequence: function (event) { event.stopPropagation(); var self = this; - this.trigger_up('freeze_order', {id: this.value.id}); + this.trigger_up('edited_list', { id: this.value.id }); var rowIDs = event.data.rowIDs.slice(); var rowID = rowIDs.pop(); var defs = _.map(rowIDs, function (rowID, index) { @@ -1300,7 +1300,7 @@ var FieldOne2Many = FieldX2Many.extend({ } } else if (!this.creatingRecord) { this.creatingRecord = true; - this.trigger_up('freeze_order', {id: this.value.id}); + this.trigger_up('edited_list', { id: this.value.id }); this._setValue({ operation: 'CREATE', position: this.editable, @@ -1330,12 +1330,27 @@ var FieldOne2Many = FieldX2Many.extend({ // we don't want interference with the components upstream. ev.stopPropagation(); + var self = this; var id = ev.data.id; - // trigger an empty 'UPDATE' operation when the user clicks on 'Save' in - // the dialog, to notify the main record that a subrecord of this - // relational field has changed (those changes will be already stored on - // that subrecord, thanks to the 'Save'). - var onSaved = this._setValue.bind(this, { operation: 'UPDATE', id: id }, {}); + var onSaved = function (record, hasChanged) { + if (!hasChanged) { + return; + } + if (_.some(self.value.data, {id: record.id})) { + // the record already exists in the relation, so trigger an + // empty 'UPDATE' operation when the user clicks on 'Save' in + // the dialog, to notify the main record that a subrecord of + // this relational field has changed (those changes will be + // already stored on that subrecord, thanks to the 'Save'). + self._setValue({ operation: 'UPDATE', id: record.id }); + } else { + // the record isn't in the relation yet, so add it ; this can + // happen if the user clicks on 'Save & New' in the dialog (the + // opened record will be updated, and other records will be + // created) + self._setValue({ operation: 'ADD', id: record.id }); + } + }; this._openFormDialog({ id: id, on_saved: onSaved, diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js index 8a9b3713f15..b4d00b6bb76 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -314,7 +314,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * * @private */ - _disableButtons: function () { + _disableButtons: function () { if (this.$buttons) { this.$buttons.find('button').attr('disabled', true); } @@ -346,10 +346,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { return; } self.model.discardChanges(recordID); - if (self.model.isNew(recordID)) { - if (self.model.canBeAbandoned(recordID)) { - self._abandonRecord(recordID); - } + if (self.model.canBeAbandoned(recordID)) { + self._abandonRecord(recordID); return; } return self._confirmSave(recordID); @@ -360,7 +358,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { * * @private */ - _enableButtons: function () { + _enableButtons: function () { if (this.$buttons) { this.$buttons.find('button').removeAttr('disabled'); } diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 4cf6f9fa27b..962bb2899e1 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -386,11 +386,12 @@ var BasicModel = AbstractModel.extend({ duplicateRecord: function (recordID) { var self = this; var record = this.localData[recordID]; + var context = this._getContext(record); return this._rpc({ model: record.model, method: 'copy', args: [record.data.id], - context: this._getContext(record), + context: context, }) .then(function (res_id) { var index = record.res_ids.indexOf(record.res_id); @@ -402,6 +403,7 @@ var BasicModel = AbstractModel.extend({ res_id: res_id, res_ids: record.res_ids.slice(0), viewType: record.viewType, + context: context, }); }); }, @@ -578,10 +580,15 @@ var BasicModel = AbstractModel.extend({ return _t("New"); }, /** - * Returns true if a record can be abandoned from a list datapoint. + * Returns true if a record can be abandoned. + * + * A record can be abandonned if it is a new record, except if + * this datapoint has been specifically tagged as "do not abandon". + * + * Example: * - * A record cannot be abandonned if it has been registered as "added" - * in the parent's savepoint, otherwise it can be abandonned. + * - Discard record from "Add an item" => "New" record => abandon + * - Discard record from `default_get`/`onchange` => do not abandon * * This is useful when discarding changes on this record, as it means that * we must keep the record even if some fields are invalids (e.g. required @@ -591,15 +598,7 @@ var BasicModel = AbstractModel.extend({ * @returns {boolean} */ canBeAbandoned: function (id) { - var data = this.localData[id]; - var parent = this.localData[data.parentID]; - var abandonable = true; - if (parent) { - abandonable = !_.some(parent._savePoint, function (entry) { - return entry.operation === 'ADD' && entry.id === id; - }); - } - return abandonable; + return !this.localData[id]._noAbandon && this.isNew(id); }, /** * Returns true if a record is dirty. A record is considered dirty if it has @@ -960,11 +959,11 @@ var BasicModel = AbstractModel.extend({ * @returns {Deferred} * Resolved with the list of field names (whose value has been modified) */ - save: function (record_id, options) { + save: function (recordID, options) { var self = this; return this.mutex.exec(function () { options = options || {}; - var record = self.localData[record_id]; + var record = self.localData[recordID]; if (options.savePoint) { self._visitChildren(record, function (rec) { var newValue = rec._changes || rec.data; @@ -982,7 +981,7 @@ var BasicModel = AbstractModel.extend({ }); } var shouldReload = 'reload' in options ? options.reload : true; - var method = self.isNew(record_id) ? 'create' : 'write'; + var method = self.isNew(recordID) ? 'create' : 'write'; if (record._changes) { // id never changes, and should not be written delete record._changes.id; @@ -1553,6 +1552,7 @@ var BasicModel = AbstractModel.extend({ list._cache[rec.res_id] = rec.id; } + rec._noAbandon = true; list._changes.push({operation: 'ADD', id: rec.id}); if (command[0] === 1) { list._changes.push({operation: 'UPDATE', id: rec.id}); @@ -2029,7 +2029,7 @@ var BasicModel = AbstractModel.extend({ .then(function () { return $.when( self._fetchX2Manys(record, options), - self._fetchReferences(record) + self._fetchReferences(record, options) ).then(function () { return self._postprocess(record, options); }); @@ -2076,10 +2076,10 @@ var BasicModel = AbstractModel.extend({ * @param {Object} record * @returns {Deferred} */ - _fetchReferences: function (record) { + _fetchReferences: function (record, options) { var self = this; var defs = []; - var fieldNames = record.getFieldNames(); + var fieldNames = options && options.fieldNames || record.getFieldNames(); _.each(fieldNames, function (fieldName) { var field = record.fields[fieldName]; if (field.type === 'reference') { @@ -3199,6 +3199,9 @@ var BasicModel = AbstractModel.extend({ * @param {Object} [options] * @param {string[]} [options.fieldNames] the fields to fetch for a record * @param {boolean} [options.onlyGroups=false] + * @param {boolean} [options.keepEmptyGroups=false] if set, the groups not + * present in the read_group anymore (empty groups) will stay in the + * datapoint (used to mimic the kanban renderer behaviour for example) * @returns {Deferred} */ _load: function (dataPoint, options) { @@ -3351,9 +3354,29 @@ var BasicModel = AbstractModel.extend({ */ _makeDefaultRecord: function (modelName, params) { var self = this; + + var determineExtraFields = function() { + // Fields that are present in the originating view, that need to be initialized + // Hence preventing their value to crash when getting back to the originating view + var parentRecord = self.localData[params.parentID]; + + var originView = parentRecord && parentRecord.fieldsInfo; + if (!originView || !originView[parentRecord.viewType]) + return []; + + var fieldsFromOrigin = _.filter(Object.keys(originView[parentRecord.viewType]), + function(fieldname) { + return params.fields[fieldname] !== undefined; + }); + + return fieldsFromOrigin; + } + var fieldNames = Object.keys(params.fieldsInfo[params.viewType]); var fields_key = _.without(fieldNames, '__last_update'); + var extraFields = determineExtraFields(); + return this._rpc({ model: modelName, method: 'default_get', @@ -3371,7 +3394,7 @@ var BasicModel = AbstractModel.extend({ viewType: params.viewType, }); - return self.applyDefaultValues(record.id, result) + return self.applyDefaultValues(record.id, result, {fieldNames: _.union(fieldNames, extraFields)}) .then(function () { var def = $.Deferred(); self._performOnChange(record, fields_key).always(function () { @@ -3605,6 +3628,7 @@ var BasicModel = AbstractModel.extend({ parentID: x2manyList.id, viewType: viewType, }); + r._noAbandon = true; x2manyList._changes.push({operation: 'ADD', id: r.id}); x2manyList._cache[r.res_id] = r.id; @@ -3634,6 +3658,8 @@ var BasicModel = AbstractModel.extend({ } else if (_.contains(['one2many', 'many2many'], fieldType)) { var x2mCommands = commands[0][2][fieldName]; defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands)); + } else { + r._changes[fieldName] = self._parseServerValue(field, r._changes[fieldName]); } } } @@ -3832,6 +3858,16 @@ var BasicModel = AbstractModel.extend({ defs.push(self._load(newGroup, options)); } }); + if (options && options.keepEmptyGroups) { + // Find the groups that were available in a previous + // readGroup but are not there anymore. + // Note that these groups are put after existing groups so + // the order is not conserved. A sort *might* be useful. + var emptyGroupsIDs = _.difference(_.pluck(previousGroups, 'id'), list.data); + _.each(emptyGroupsIDs, function (groupID) { + list.data.push(groupID); + }); + } return $.when.apply($, defs).then(function () { // generate the res_ids of the main list, being the concatenation // of the fetched res_ids in each group diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 6a2010708fc..b847d7ca391 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -262,7 +262,10 @@ return AbstractModel.extend({ * @param {Moment} start */ setDate: function (start) { - this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start; + // keep highlight/target_date in localtime + this.data.highlight_date = this.data.target_date = start.clone(); + // set dates in UTC with timezone applied manually + this.data.start_date = this.data.end_date = start; this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes'); switch (this.data.scale) { diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js index 084c9ae4579..e66194dcf99 100644 --- a/addons/web/static/src/js/views/form/form_controller.js +++ b/addons/web/static/src/js/views/form/form_controller.js @@ -15,7 +15,7 @@ var FormController = BasicController.extend({ bounce_edit: '_onBounceEdit', button_clicked: '_onButtonClicked', do_action: '_onDoAction', - freeze_order: '_onFreezeOrder', + edited_list: '_onEditedList', open_one2many_record: '_onOpenOne2ManyRecord', open_record: '_onOpenRecord', toggle_column_order: '_onToggleColumnOrder', @@ -109,6 +109,7 @@ var FormController = BasicController.extend({ * @todo convert to new style */ on_attach_callback: function () { + this._super.apply(this, arguments); this.autofocus(); }, /** @@ -529,11 +530,15 @@ var FormController = BasicController.extend({ * in a x2many list view * * @private - * @param {OdooEvent} event + * @param {OdooEvent} ev + * @param {integer} ev.id of the list to freeze while editing a line */ - _onFreezeOrder: function (event) { - event.stopPropagation(); - this.model.freezeOrder(event.data.id); + _onEditedList: function (ev) { + ev.stopPropagation(); + if (ev.data.id) { + this.model.save(ev.data.id, {savePoint: true}); + } + this.model.freezeOrder(ev.data.id); }, /** * Set the focus on the first primary button of the controller (likely Edit) diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index bc11bb675df..1fbd6572a9a 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -74,10 +74,9 @@ return AbstractRenderer.extend({ * Render the chart. * * Note that This method is synchronous, but the actual rendering is done - * asynchronously (in a setTimeout). The reason for that is that nvd3/d3 - * needs to be in the DOM to correctly render itself. So, we trick Odoo by - * returning immediately, then wait a tiny interval before actually - * displaying the data. + * asynchronously. The reason for that is that nvd3/d3 needs to be in the + * DOM to correctly render itself. So, we trick Odoo by returning + * immediately, then we render the chart when the widget is in the DOM. * * @override * @private diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index 5f758953658..9be13fb0ae6 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -10,6 +10,7 @@ odoo.define('web.KanbanController', function (require) { var BasicController = require('web.BasicController'); var Context = require('web.Context'); var core = require('web.core'); +var Domain = require('web.Domain'); var view_dialogs = require('web.view_dialogs'); var _t = core._t; @@ -190,10 +191,57 @@ var KanbanController = BasicController.extend({ resIDs: record.res_ids, }, on_closed: function () { + var recordModel = self.model.localData[record.id]; + var group = self.model.localData[recordModel.parentID]; + var parent = self.model.localData[group.parentID]; + self.model.reload(record.id).then(function (db_id) { var data = self.model.get(db_id); var kanban_record = event.target; kanban_record.update(data); + + // Check if we still need to display the record. Some fields of the domain are + // not guaranteed to be in data. This is for example the case if the action + // contains a domain on a field which is not in the Kanban view. Therefore, + // we need to handle multiple cases based on 3 variables: + // domInData: all domain fields are in the data + // activeInDomain: 'active' is already in the domain + // activeInData: 'active' is available in the data + + var domain = (parent ? parent.domain : group.domain) || []; + var domInData = _.every(domain, function (d) { + return d[0] in data.data; + }); + var activeInDomain = _.pluck(domain, 0).indexOf('active') !== -1; + var activeInData = 'active' in data.data; + + // Case # | domInData | activeInDomain | activeInData + // 1 | true | true | true => no domain change + // 2 | true | true | false => not possible + // 3 | true | false | true => add active in domain + // 4 | true | false | false => no domain change + // 5 | false | true | true => no evaluation + // 6 | false | true | false => no evaluation + // 7 | false | false | true => replace domain + // 8 | false | false | false => no evaluation + + // There are 3 cases which cannot be evaluated since we don't have all the + // necessary information. The complete solution would be to perform a RPC in + // these cases, but this is out of scope. A simpler one is to do a try / catch. + + if (domInData && !activeInDomain && activeInData) { + domain = domain.concat([['active', '=', true]]); + } else if (!domInData && !activeInDomain && activeInData) { + domain = [['active', '=', true]]; + } + try { + var visible = new Domain(domain).compute(data.evalContext); + } catch (e) { + return; + } + if (!visible) { + kanban_record.destroy(); + } }); }, }); diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index 15d6284cfad..411d4cc7b14 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -419,7 +419,10 @@ var KanbanModel = BasicModel.extend({ while (element) { if (element.progressBar) { return def.then(function (data) { - return self._load(element, {onlyGroups: true}).then(function () { + return self._load(element, { + keepEmptyGroups: true, + onlyGroups: true, + }).then(function () { return data; }); }); diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 8d0b04332ad..8f87b36c874 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -79,8 +79,15 @@ var DateWidget = Widget.extend({ var oldValue = this.getValue(); this._setValueFromUi(); var newValue = this.getValue(); - - if (!oldValue !== !newValue || oldValue && newValue && !oldValue.isSame(newValue)) { + var hasChanged = !oldValue !== !newValue; + if (oldValue && newValue) { + var formattedOldValue = oldValue.format(time.getLangDatetimeFormat()); + var formattedNewValue = newValue.format(time.getLangDatetimeFormat()) + if (formattedNewValue !== formattedOldValue) { + hasChanged = true; + } + } + if (hasChanged) { // The condition is strangely written; this is because the // values can be false/undefined this.trigger("datetime_changed"); diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js index ffe65a7efcf..4b86b76c36d 100644 --- a/addons/web/static/src/js/widgets/debug_manager.js +++ b/addons/web/static/src/js/widgets/debug_manager.js @@ -178,6 +178,7 @@ var DebugManager = Widget.extend({ }) .then(function (views) { var view = views[0]; + view.type = view.type === 'tree' ? 'list' : view.type; // ignore tree view self.do_action({ type: 'ir.actions.act_window', name: view.name, diff --git a/addons/web/static/src/less/bootstrap_overridden.less b/addons/web/static/src/less/bootstrap_overridden.less index 012efa60cda..5bbe61ecdf6 100644 --- a/addons/web/static/src/less/bootstrap_overridden.less +++ b/addons/web/static/src/less/bootstrap_overridden.less @@ -164,3 +164,5 @@ overflow-x: initial; } } + +@icon-font-path: "/web/static/lib/bootstrap/fonts/"; diff --git a/addons/web/static/src/less/datepicker.less b/addons/web/static/src/less/datepicker.less index 99c909efacc..fdf3a585b57 100644 --- a/addons/web/static/src/less/datepicker.less +++ b/addons/web/static/src/less/datepicker.less @@ -17,11 +17,12 @@ .o_datepicker_input { width: 100%; + cursor: pointer; } .o_datepicker_button { .o-position-absolute(2px, 4px); - cursor: pointer; + pointer-events: none; // let click events go through the underlying input &:after { .o-caret-down; } diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less index ee9f1d9556a..ff6fe9eb20b 100644 --- a/addons/web/static/src/less/fields.less +++ b/addons/web/static/src/less/fields.less @@ -185,7 +185,7 @@ display: inline-block; padding: 0; margin: 0; - vertical-align: middle; + vertical-align: baseline; > .o_priority_star { display: inline-block; font-size: 1.35em; diff --git a/addons/web/static/src/less/form_view.less b/addons/web/static/src/less/form_view.less index 96e8bd2ac97..c36fe2d0d9a 100644 --- a/addons/web/static/src/less/form_view.less +++ b/addons/web/static/src/less/form_view.less @@ -249,6 +249,10 @@ border-left-color: @gray-lighter; } } + + .caret { + margin-top: -2px; + } } .o-status-more { diff --git a/addons/web/static/src/less/kanban_column_progressbar.less b/addons/web/static/src/less/kanban_column_progressbar.less index 86fe0853de2..f4cffd6c097 100644 --- a/addons/web/static/src/less/kanban_column_progressbar.less +++ b/addons/web/static/src/less/kanban_column_progressbar.less @@ -91,6 +91,7 @@ margin-left: 3%; color: @headings-color; text-align: right; + white-space: nowrap; .o-transform-origin(right, center); &.o_kanban_grow { diff --git a/addons/web/static/src/less/kanban_dashboard.less b/addons/web/static/src/less/kanban_dashboard.less index da8f0adac72..83db9319166 100644 --- a/addons/web/static/src/less/kanban_dashboard.less +++ b/addons/web/static/src/less/kanban_dashboard.less @@ -231,6 +231,7 @@ } .o_favorite { + top: 3px; left: 0; right: auto; } diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less index ddc530e540a..b5237d305cb 100644 --- a/addons/web/static/src/less/list_view.less +++ b/addons/web/static/src/less/list_view.less @@ -37,12 +37,14 @@ .user-select(none); // Prevent unwanted selection while sorting &::after { - margin-left: 6px; - position: absolute; font-family: FontAwesome; content: "\f0d7"; opacity: 0; } + &:not(:empty)::after { + margin-left: 6px; + position: absolute; + } &.o-sort-up { cursor: n-resize; &::after { diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index c7117e7a4b7..350bf20a06a 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -1643,6 +1643,36 @@ QUnit.module('ActionManager', { delete core.action_registry.map.HelloWorldTest; }); + QUnit.test('breadcrumb is updated on title change', function (assert) { + assert.expect(2); + + var ClientAction = Widget.extend(ControlPanelMixin, { + className: 'o_client_action_test', + events: { + click: function () { + this.set("title", 'new title'); + }, + }, + start: function () { + this.set("title", 'initial title'); + this.$el.text('Hello World'); + }, + }); + var actionManager = createActionManager(); + core.action_registry.add('HelloWorldTest', ClientAction); + actionManager.doAction('HelloWorldTest'); + + assert.strictEqual($('ol.breadcrumb').text(), "initial title", + "should have initial title as breadcrumb content"); + + actionManager.$('.o_client_action_test').click(); + assert.strictEqual($('ol.breadcrumb').text(), "new title", + "should have updated title as breadcrumb content"); + + actionManager.destroy(); + delete core.action_registry.map.HelloWorldTest; + }); + QUnit.module('Server actions'); QUnit.test('can execute server actions from db ID', function (assert) { diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 7a2999e18b3..ff60e639421 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2365,6 +2365,38 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('datetime field in form view', function (assert) { + assert.expect(1); + + this.data.partner.fields.datetime.default = "2017-08-02 12:00:05"; + this.data.partner.fields.datetime.required = true; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners"><field name="datetime"/></form>', + res_id: 1, + translateParameters: { // Avoid issues due to localization formats + date_format: '%m/%d/%Y', + time_format: '%H:%M', + }, + }); + testUtils.patch(basicFields.FieldDate, { + _setValue: function () { + throw "The time format of the language must be taken into account." + return this._super.apply(this, arguments); + }, + }); + form.$buttons.find('.o_form_button_create').click(); + var expectedDateString = "08/02/2017 12:00"; // 10:00:00 without timezone + assert.strictEqual(form.$('.o_field_date input').val(), expectedDateString, + 'the datetime should be correctly displayed in readonly'); + + testUtils.unpatch(basicFields.FieldDate); + form.destroy(); + }); + QUnit.test('datetime field in editable list view', function (assert) { assert.expect(8); @@ -3113,7 +3145,7 @@ QUnit.module('basic_fields', { QUnit.module('PhoneWidget'); QUnit.test('phone field in form view on extra small screens', function (assert) { - assert.expect(7); + assert.expect(8); var form = createView({ View: FormView, @@ -3160,6 +3192,15 @@ QUnit.module('basic_fields', { assert.strictEqual($phoneLink.attr('href'), 'tel:new', "should still have proper tel prefix"); + // NOT NEEDED AS OF SAAS-11.3 + // save phone with ­ and verify it is removed + form.$buttons.find('.o_form_button_edit').click(); + form.$('input[type="text"].o_field_widget').val('h\u00ADi').trigger('input'); + form.$buttons.find('.o_form_button_save').click(); + $phoneLink = form.$('a.o_form_uri.o_field_widget'); + assert.strictEqual($phoneLink.attr('href'), 'tel:hi', + "U+00AD should have been removed"); + form.destroy(); }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 1a9576edd75..a046e79de6c 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -1150,12 +1150,12 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.test('list in form: discard newly added element with empty required field', function (assert) { + QUnit.test('list in form: discard newly added element with empty required field (default_get)', function (assert) { // This test simulates discarding a record that has been created with // one of its required field that is empty. When we discard the changes // on this empty field, it should not assume that this record should be // abandonned, since it has been added (even though it is a new record). - assert.expect(6); + assert.expect(8); var form = createView({ View: FormView, @@ -1173,7 +1173,7 @@ QUnit.module('relational_fields', { '</form>', mockRPC: function (route, args) { if (args.method === 'default_get') { - return $.when({p: [[0, 0, {display_name: 'new record', trululu: false}]]}); + return $.when({ p: [[0, 0, { display_name: 'new record', trululu: false }]] }); } return this._super.apply(this, arguments); }, @@ -1181,6 +1181,8 @@ QUnit.module('relational_fields', { assert.strictEqual($('tr.o_data_row').length, 1, "should have created the new record in the o2m"); + assert.strictEqual($('td.o_data_cell').first().text(), "new record", + "should have the correct displayed name"); var requiredElement = $('td.o_data_cell.o_required_modifier'); assert.strictEqual(requiredElement.length, 1, @@ -1194,6 +1196,8 @@ QUnit.module('relational_fields', { assert.strictEqual($('tr.o_data_row').length, 1, "should still have the record in the o2m"); + assert.strictEqual($('td.o_data_cell').first().text(), "new record", + "should still have the correct displayed name"); // update selector of required field element requiredElement = $('td.o_data_cell.o_required_modifier'); @@ -1204,6 +1208,72 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('list in form: discard newly added element with empty required field (onchange)', function (assert) { + assert.expect(8); + + var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + + this.data.partner.onchanges = { + product_id: function (obj) { + if (obj.product_id === 37) { + obj.p = [[0, 0, { display_name: "entry", trululu: false }]]; + } + }, + trululu: function () {}, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="product_id"/>' + + '<field name="p">' + + '<tree editable="bottom">' + + '<field name="display_name"/>' + + '<field name="trululu" required="1"/>' + + '</tree>' + + '</field>' + + '</form>', + }); + + // check no record in list + assert.strictEqual(form.$('.o_data_row').length, 0, + "should have no row in the editable list"); + + // select product_id to force on_change in editable list + form.$('.o_field_widget[name="product_id"] .o_input').click(); + $('.ui-menu-item').first().click(); + + // check that there is a record in the editable list with empty string as required field + assert.strictEqual(form.$('.o_data_row').length, 1, + "should have a row in the editable list"); + assert.strictEqual($('td.o_data_cell').first().text(), "entry", + "should have the correct displayed name"); + var requiredField = $('td.o_data_cell.o_required_modifier'); + assert.strictEqual(requiredField.length, 1, + "should have a required field on this record"); + assert.strictEqual(requiredField.text(), "", + "should have empty string in the required field on this record"); + + // click on empty required field in editable list record + requiredField.click(); + // click off so that the required field still stay empty + $('body').click(); + + // record should not be dropped + assert.strictEqual(form.$('.o_data_row').length, 1, + "should not have droppped record in the editable list"); + assert.strictEqual($('td.o_data_cell').first().text(), "entry", + "should still have the correct displayed name"); + assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "", + "should still have empty string in the required field"); + + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; + form.destroy(); + }); + QUnit.test('list in form: default_get with x2many create', function (assert) { assert.expect(5); @@ -4208,7 +4278,6 @@ QUnit.module('relational_fields', { form.destroy(); }); - QUnit.test('one2many list (non editable): edition', function (assert) { assert.expect(12); @@ -5077,6 +5146,32 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many and default_get (with date)', function (assert) { + assert.expect(1); + + this.data.partner.fields.p.default = [ + [0, false, {date: '2017-10-08'}], + ]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="p">' + + '<tree>' + + '<field name="date"/>' + + '</tree>' + + '</field>' + + '</form>', + }); + + assert.strictEqual(form.$('.o_data_cell').text(), '10/08/2017', + "should correctly display the date"); + + form.destroy(); + }); + QUnit.test('one2many and onchange (with integer)', function (assert) { assert.expect(4); @@ -8742,6 +8837,53 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('add a line, edit it and "Save & New"', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="p">' + + '<tree><field name="display_name"/></tree>' + + '<form><field name="display_name"/></form>' + + '</field>' + + '</form>', + }); + + assert.strictEqual(form.$('.o_data_row').length, 0, + "there should be no record in the relation"); + + // add a new record + form.$('.o_field_x2many_list_row_add a').click(); + $('.modal .o_field_widget').val('new record').trigger('input'); + $('.modal .modal-footer .btn-primary:first').click(); // Save & Close + + assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new record', + "should display the new record"); + + // reopen freshly added record and edit it + form.$('.o_data_row .o_data_cell').click(); + $('.modal .o_field_widget').val('new record edited').trigger('input'); + + // save it, and choose to directly create another record + $('.modal .modal-footer .btn-primary:nth(1)').click(); // Save & New + + assert.strictEqual($('.modal').length, 1, + "the model should still be open"); + assert.strictEqual($('.modal .o_field_widget').text(), '', + "should have cleared the input"); + + $('.modal .o_field_widget').val('another new record').trigger('input'); + $('.modal .modal-footer .btn-primary:first').click(); // Save & Close + + assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), + 'new record editedanother new record', "should display the two records"); + + form.destroy(); + }); + QUnit.module('FieldMany2Many'); QUnit.test('many2many kanban: edition', function (assert) { @@ -11011,6 +11153,99 @@ QUnit.module('relational_fields', { QUnit.module('FieldReference'); + QUnit.test('Reference field in modal readonly mode', function (assert) { + assert.expect(4); + + this.data.partner.records[0].p = [2]; + this.data.partner.records[1].trululu = 1; + this.data.partner.records[1].reference = 'product,41'; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="reference"/>' + + '<field name="p"/>' + + '</form>', + archs: { + 'partner,false,form': '<form><field name="reference"/></form>', + 'partner,false,list': '<tree><field name="display_name"/></tree>', + }, + res_id: 1, + }); + + // Cuurent Form + assert.equal(form.$('.o_form_uri.o_field_widget[name=reference]').text(), 'xphone', + 'the field reference of the form should have the right value'); + + var $cell_o2m = form.$('.o_data_cell'); + assert.equal($cell_o2m.text(), 'second record', + 'the list should have one record'); + + $cell_o2m.click(); + + // In modal + var $modal = $('.modal-dialog.modal-lg'); + assert.equal($modal.length, 1, + 'there should be one modal opened'); + + assert.equal($modal.find('.o_form_uri.o_field_widget[name=reference]').text(), 'xpad', + 'The field reference in the modal should have the right value'); + + $modal.find('.o_form_button_cancel').click(); + + form.destroy(); + }); + + QUnit.test('Reference field in modal write mode', function (assert) { + assert.expect(5); + + this.data.partner.records[0].p = [2]; + this.data.partner.records[1].trululu = 1; + this.data.partner.records[1].reference = 'product,41'; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="reference"/>' + + '<field name="p"/>' + + '</form>', + archs: { + 'partner,false,form': '<form><field name="reference"/></form>', + 'partner,false,list': '<tree><field name="display_name"/></tree>', + }, + res_id: 1, + }); + + // current form + form.$buttons.find('.o_form_button_edit').click(); + + var $fieldRef = form.$('.o_field_widget.o_field_many2one[name=reference]'); + assert.equal($fieldRef.find('option:selected').text(), 'Product', + 'The reference field\'s model should be Product'); + assert.equal($fieldRef.find('.o_input.ui-autocomplete-input').val(), 'xphone', + 'The reference field\'s record should be xphone'); + + form.$('.o_data_cell').click(); + + // In modal + var $modal = $('.modal-dialog.modal-lg'); + assert.equal($modal.length, 1, + 'there should be one modal opened'); + + var $fieldRefModal = $modal.find('.o_field_widget.o_field_many2one[name=reference]'); + + assert.equal($fieldRefModal.find('option:selected').text(), 'Product', + 'The reference field\'s model should be Product'); + assert.equal($fieldRefModal.find('.o_input.ui-autocomplete-input').val(), 'xpad', + 'The reference field\'s record should be xpad'); + + form.destroy(); + }); + QUnit.test('reference in form view', function (assert) { assert.expect(15); @@ -11207,6 +11442,78 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many with extra field from server not in form', function (assert) { + assert.expect(6); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="p" >' + + '<tree>' + + '<field name="datetime"/>' + + '<field name="display_name"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + archs: { + 'partner,false,form': '<form>' + + '<field name="display_name"/>' + + '</form>'}, + mockRPC: function(route, args) { + if (route === '/web/dataset/call_kw/partner/write') { + args.args[1].p[0][2].datetime = '2018-04-05 12:00:00'; + } + return this._super.apply(this, arguments); + } + }); + + form.$buttons.find('.o_form_button_edit').click(); + + var x2mList = form.$('.o_field_x2many_list[name=p]'); + + // Add a record in the list + x2mList.find('.o_field_x2many_list_row_add a').click(); + + var modal = $('.modal-dialog.modal-lg'); + + var nameInput = modal.find('input.o_input[name=display_name]'); + nameInput.val('michelangelo').trigger('input'); + + // Save the record in the modal (though it is still virtual) + modal.find('.btn-primary').first().click(); + + assert.equal(x2mList.find('.o_data_row').length, 1, + 'There should be 1 records in the x2m list'); + + var newlyAdded = x2mList.find('.o_data_row').eq(0); + + assert.equal(newlyAdded.find('.o_data_cell').first().text(), '', + 'The create_date field should be empty'); + assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo', + 'The display name field should have the right value'); + + // Save the whole thing + form.$buttons.find('.o_form_button_save').click(); + + x2mList = form.$('.o_field_x2many_list[name=p]'); + + // Redo asserts in RO mode after saving + assert.equal(x2mList.find('.o_data_row').length, 1, + 'There should be 1 records in the x2m list'); + + newlyAdded = x2mList.find('.o_data_row').eq(0); + + assert.equal(newlyAdded.find('.o_data_cell').first().text(), '04/05/2018 12:00:00', + 'The create_date field should have the right value'); + assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo', + 'The display name field should have the right value'); + + form.destroy(); + }); + QUnit.test('one2many invisible depends on parent field', function (assert) { assert.expect(4); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index ada068202c7..bc92254d2ad 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -1824,6 +1824,41 @@ QUnit.module('Views', { assert.ok(!$groupBy.is(':visible'), 'groupby menu should not be visible'); actionManager.destroy(); }); + + QUnit.test('timezone does not affect current day', function (assert) { + assert.expect(2); + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar date_start="start_date">'+ + '<field name="name"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + session: { + getTZOffset: function () { + return -2400; // 40 hours timezone + }, + }, + + }); + + var $sidebar = calendar.$('.o_calendar_sidebar'); + + assert.strictEqual($sidebar.find('.ui-datepicker-current-day').text(), "12", "should highlight the target day"); + + // go to previous day + $sidebar.find('.ui-datepicker-current-day').prev().click(); + + assert.strictEqual($sidebar.find('.ui-datepicker-current-day').text(), "11", "should highlight the selected day"); + + calendar.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 0fd60afb402..ce206777ac5 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1629,6 +1629,37 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('duplicating a record preserve the context', function (assert) { + assert.expect(2); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="foo"/>' + + '</form>', + res_id: 1, + viewOptions: {hasSidebar: true, context: {hey: 'hoy'}}, + mockRPC: function (route, args) { + if (args.method === 'read') { + // should have 2 read, one for initial load, second for + // read after duplicating + assert.strictEqual(args.kwargs.context.hey, 'hoy', + "should have send the correct context"); + } + if (args.method === 'search_read' && args.model === 'ir.attachment') { + return $.when([]); + } + return this._super.apply(this, arguments); + }, + }); + + form.sidebar.$('a:contains(Duplicate)').click(); + + form.destroy(); + }); + QUnit.test('cannot duplicate a record', function (assert) { assert.expect(2); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index 0337dce52c6..fbe345887f3 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -2511,6 +2511,73 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('button executes action and check domain', function (assert) { + assert.expect(2); + + var data = this.data; + data.partner.fields.active = {string: "Active", type: "boolean", default: true}; + for (var k in this.data.partner.records) { + data.partner.records[k].active = true; + } + + var kanban = createView({ + View: KanbanView, + model: "partner", + data: data, + arch: + '<kanban>' + + '<templates><div t-name="kanban-box">' + + '<field name="foo"/>' + + '<field name="active"/>' + + '<button type="object" name="a1" />' + + '<button type="object" name="toggle_active" />' + + '</div></templates>' + + '</kanban>', + }); + + testUtils.intercept(kanban, 'execute_action', function (event) { + data.partner.records[0].active = false; + event.data.on_closed(); + }); + + assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 1, "should display 'yop' record"); + kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_active"]').click(); + assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 0, "should remove 'yop' record from the view"); + + kanban.destroy(); + }); + + QUnit.test('button executes action with domain field not in view', function (assert) { + assert.expect(1); + + var kanban = createView({ + View: KanbanView, + model: "partner", + data: this.data, + domain: [['bar', '=', true]], + arch: + '<kanban>' + + '<templates><div t-name="kanban-box">' + + '<field name="foo"/>' + + '<button type="object" name="a1" />' + + '<button type="object" name="toggle_action" />' + + '</div></templates>' + + '</kanban>', + }); + + testUtils.intercept(kanban, 'execute_action', function (event) { + event.data.on_closed(); + }); + + try { + kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_action"]').click(); + assert.strictEqual(true, true, 'Everything went fine'); + } catch (e) { + assert.strictEqual(true, false, 'Error triggered at action execution'); + } + kanban.destroy(); + }); + QUnit.test('rendering date and datetime', function (assert) { assert.expect(2); @@ -3150,6 +3217,51 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('drag & drop records grouped by m2o with progressbar', function (assert) { + assert.expect(4); + + this.data.partner.records[0].product_id = false; + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: + '<kanban>' + + '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\'/>' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="int_field"/>' + + '</div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['product_id'], + mockRPC: function (route, args) { + if (route === '/web/dataset/resequence') { + return $.when(true); + } + return this._super(route, args); + }, + }); + + assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1", + "counter should contain the correct value"); + + testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)')); + assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0", + "counter should contain the correct value"); + + testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(2)'), kanban.$('.o_kanban_group:eq(0)')); + assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1", + "counter should contain the correct value"); + + testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)')); + assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0", + "counter should contain the correct value"); + + kanban.destroy(); + }); + QUnit.test('progress bar subgroup count recompute', function(assert) { assert.expect(2); |
