diff options
Diffstat (limited to 'addons/web/static/src/js/views')
9 files changed, 260 insertions, 85 deletions
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 6ba9dbea477..cfdee6793dd 100644 --- a/addons/web/static/src/js/views/basic/basic_controller.js +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -329,9 +329,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, { if (options && options.readonlyIfRealDiscard && !needDiscard) { return; } - if (needDiscard) { // Just some optimization - self.model.discardChanges(recordID); - } + self.model.discardChanges(recordID); if (self.model.isNew(recordID)) { self._abandonRecord(recordID); return; 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 8755805f993..0d95c292070 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -186,6 +186,29 @@ var BasicModel = AbstractModel.extend({ }); }, /** + * Onchange RPCs may return values for fields that are not in the current + * view. Those fields might even be unknown when the onchange returns (e.g. + * in x2manys, we only know the fields that are used in the inner view, but + * not those used in the potential form view opened in a dialog when a sub- + * record is clicked). When this happens, we can't infer their type, so the + * given value can't be processed. It is instead stored in the '_rawChanges' + * key of the record, without any processing. Later on, if this record is + * displayed in another view (e.g. the user clicked on it in the x2many + * list, and the record opens in a dialog), those changes that were left + * behind must be applied. This function applies changes stored in + * '_rawChanges' for a given viewType. + * + * @param {string} id local resource id of a record + * @param {string} viewType the current viewType + * @returns {Deferred -> string} resolves to the id of the record + */ + applyRawChanges: function (recordID, viewType) { + var record = this.localData[recordID]; + return this._applyOnChange(record._rawChanges, record, viewType).then(function () { + return record.id; + }); + }, + /** * Delete a list of records, then, if the records have a parent, reload it. * * @todo we should remove the deleted records from the localData @@ -754,14 +777,15 @@ var BasicModel = AbstractModel.extend({ if (newValue instanceof Array) { rec._savePoint = newValue.slice(0); } else { - // save the viewType of edition, so that the correct readonly modifiers - // can be evaluated when the record will be saved - for (var fieldName in (rec._changes || {})) { - rec._editionViewType[fieldName] = options.viewType; - } rec._savePoint = _.extend({}, newValue); } }); + + // save the viewType of edition, so that the correct readonly modifiers + // can be evaluated when the record will be saved + _.each((record._changes || {}), function (value, fieldName) { + record._editionViewType[fieldName] = options.viewType; + }); } var shouldReload = 'reload' in options ? options.reload : true; var method = self.isNew(record_id) ? 'create' : 'write'; @@ -896,18 +920,23 @@ var BasicModel = AbstractModel.extend({ * @returns {Deferred -> string} resolves to the group id */ toggleGroup: function (groupId) { + var self = this; var group = this.localData[groupId]; if (group.isOpen) { group.isOpen = false; group.data = []; + group.res_ids = []; group.offset = 0; + this._updateParentResIDs(group); return $.when(groupId); } if (!group.isOpen) { group.isOpen = true; var def; if (group.count > 0) { - def = this._load(group); + def = this._load(group).then(function () { + self._updateParentResIDs(group); + }); } return $.when(def).then(function () { return groupId; @@ -1093,21 +1122,33 @@ var BasicModel = AbstractModel.extend({ * @param {Object} values the result of the onchange RPC (a mapping of * fieldnames to their value) * @param {Object} record + * @param {string} [viewType] current viewType. If not set, we will assume + * main viewType from the record * @returns {Deferred} */ - _applyOnChange: function (values, record) { + _applyOnChange: function (values, record, viewType) { var self = this; var defs = []; var rec; + viewType = viewType || record.viewType; record._changes = record._changes || {}; _.each(values, function (val, name) { var field = record.fields[name]; if (!field) { - return; // ignore changes of unknown fields + // this field is unknown so we can't process it for now (it is not + // in the current view anyway, otherwise it wouldn't be unknown. + // we store its value without processing it, so that if we later + // on switch to another view in which this field is displayed, + // we could process it as we would know its type then. + // use case: an onchange sends a create command for a one2many, + // in the dict of values, there is a value for a field that is + // not in the one2many list, but that is in the one2many form. + record._rawChanges[name] = val; + return; } - - if (field.type === 'many2one' ) { - var id = false; + var id; + if (field.type === 'many2one') { + id = false; // in some case, the value returned by the onchange can // be false (no value), so we need to avoid creating a // local record for that. @@ -1126,13 +1167,27 @@ var BasicModel = AbstractModel.extend({ id = rec.id; } record._changes[name] = id; + } else if (field.type === 'reference') { + id = false; + if (val) { + var ref = val.split(','); + rec = self._makeDataPoint({ + context: record.context, + data: {id: parseInt(ref[1])}, + modelName: ref[0], + parentID: record.id, + }); + defs.push(self._fetchNameGet(rec)); + id = rec.id; + } + record._changes[name] = id; } else if (field.type === 'one2many' || field.type === 'many2many') { var listId = record._changes[name] || record.data[name]; var list; if (listId) { list = self.localData[listId]; } else { - var fieldInfo = record.fieldsInfo[record.viewType][name]; + var fieldInfo = record.fieldsInfo[viewType][name]; if (!fieldInfo) { return; //ignore changes of x2many not in view } @@ -1142,7 +1197,7 @@ var BasicModel = AbstractModel.extend({ parentID: record.id, static: true, type: 'list', - viewtype: fieldInfo.viewType, + viewType: fieldInfo.viewType, }); } record._changes[name] = list.id; @@ -1535,15 +1590,17 @@ var BasicModel = AbstractModel.extend({ var self = this; var model; var records = []; + var ids = []; list = this._applyX2ManyOperations(list); - var ids = _.map(list.data, function (localId) { + _.each(list.data, function (localId) { var record = self.localData[localId]; var data = record._changes || record.data; var many2oneId = data[fieldName]; + if (!many2oneId) { return; } var many2oneRecord = self.localData[many2oneId]; records.push(many2oneRecord); + ids.push(many2oneRecord.res_id); model = many2oneRecord.model; - return many2oneRecord.res_id; }); return this._rpc({ model: model, @@ -1600,6 +1657,39 @@ var BasicModel = AbstractModel.extend({ }); }, /** + * Fetch the `name_get` for a reference field. + * + * @private + * @param {Object} record + * @param {string} fieldName + * @returns {Deferred} + */ + _fetchReference: function (record, fieldName) { + var self = this; + var def; + var value = record._changes && record._changes[fieldName] || record.data[fieldName]; + var model = value && value.split(',')[0]; + var resID = value && parseInt(value.split(',')[1]); + if (model && model !== 'False' && resID) { + def = self._rpc({ + model: model, + method: 'name_get', + args: [resID], + context: record.getContext({fieldName: fieldName}), + }).then(function (result) { + return self._makeDataPoint({ + data: { + id: result[0][0], + display_name: result[0][1], + }, + modelName: model, + parentID: record.id, + }); + }); + } + return $.when(def); + }, + /** * Fetch the extra data (`name_get`) for the reference fields of the record * model. * @@ -1614,28 +1704,12 @@ var BasicModel = AbstractModel.extend({ _.each(fieldNames, function (fieldName) { var field = record.fields[fieldName]; if (field.type === 'reference') { - var value = record.data[fieldName]; - if (value) { - var model = value.split(',')[0]; - var resID = parseInt(value.split(',')[1]); - var def = self._rpc({ - model: model, - method: 'name_get', - args: [resID], - context: record.getContext({fieldName: fieldName}), - }).then(function (result) { - var referenceDp = self._makeDataPoint({ - data: { - id: result[0][0], - display_name: result[0][1], - }, - modelName: model, - parentID: record.id, - }); - record.data[fieldName] = referenceDp.id; - }); - defs.push(def); - } + var def = self._fetchReference(record, fieldName).then(function (dataPoint) { + if (dataPoint) { + record.data[fieldName] = dataPoint.id; + } + }); + defs.push(def); } }); return $.when.apply($, defs); @@ -1906,6 +1980,25 @@ var BasicModel = AbstractModel.extend({ }); }, /** + * Fetches the `name_get` associated to the reference widget if the field is + * a `char` (which is a supported case). + * + * @private + * @param {Object} record - an element from the localData + * @param {Object} fieldName - the name of the field + * @returns {Deferred} + */ + _fetchSpecialReference: function (record, fieldName) { + var def; + var field = record.fields[fieldName]; + if (field.type === 'char') { + // if the widget reference is set on a char field, the name_get + // needs to be fetched a posteriori + def = this._fetchReference(record, fieldName); + } + return $.when(def); + }, + /** * Fetches all the m2o records associated to the given fieldName. If the * given fieldName is not a m2o field, nothing is done. The difference with * _fetchSpecialMany2ones is that the field given by options.fold_field is @@ -2712,6 +2805,7 @@ var BasicModel = AbstractModel.extend({ _cache: type === 'list' ? {} : undefined, _changes: null, _domains: {}, + _rawChanges: {}, aggregateValues: params.aggregateValues || {}, context: params.context || {}, count: params.count || res_ids.length, @@ -2826,14 +2920,25 @@ var BasicModel = AbstractModel.extend({ var field = params.fields[name]; data[name] = null; record._changes = record._changes || {}; + var dp; if (field.type === 'many2one' && result[name]) { - var rec = self._makeDataPoint({ + dp = self._makeDataPoint({ context: record.context, data: {id: result[name]}, modelName: field.relation, parentID: record.id, }); - record._changes[name] = rec.id; + record._changes[name] = dp.id; + } else if (field.type === 'reference' && result[name]) { + var ref = result[name].split(','); + dp = self._makeDataPoint({ + context: record.context, + data: {id: parseInt(ref[1])}, + modelName: ref[0], + parentID: record.id, + }); + defs.push(self._fetchNameGet(dp)); + record._changes[name] = dp.id; } else if (field.type === 'one2many' || field.type === 'many2many') { var fieldInfo = record.fieldsInfo[record.viewType][name]; var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode]; @@ -2859,6 +2964,7 @@ var BasicModel = AbstractModel.extend({ x2manyList._changes = []; var many2ones = {}; var r; + result[name] = result[name] || []; // handle false value var isCommandList = result[name].length && _.isArray(result[name][0]); if (!isCommandList) { result[name] = [[6, false, result[name]]]; @@ -2885,7 +2991,7 @@ var BasicModel = AbstractModel.extend({ r._changes = _.defaults(value[2], r.data); for (var name in r._changes) { - if (r._changes[name] === null) { + if (!r._changes[name]) { continue; } var isFieldInView = name in r.fields; @@ -3318,6 +3424,7 @@ var BasicModel = AbstractModel.extend({ list.data = data; list.res_ids = ids; } + self._updateParentResIDs(list); return list; }); }, @@ -3364,6 +3471,26 @@ var BasicModel = AbstractModel.extend({ } }, /** + * Updates the res_ids of the parent of a given element of type list. + * + * After some operations (e.g. loading more records, folding/unfolding a + * group), the res_ids list of an element may be updated. When this happens, + * the res_ids of its ancestors need to be updated as well. This is the + * purpose of this function. + * + * @param {Object} element + */ + _updateParentResIDs: function (element) { + var self = this; + if (element.parentID) { + var parent = this.localData[element.parentID]; + parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) { + return self.localData[dataPointID].res_ids; + })); + this._updateParentResIDs(parent); + } + }, + /** * Helper method. Recursively traverses the data, starting from the element * record (or list), then following all relations. This is useful when one * want to determine a property for the current record. diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 736416dea27..bdc250011f0 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -63,6 +63,7 @@ var BasicView = AbstractView.extend({ */ _loadData: function () { if (this.recordID) { + var self = this; var record = this.model.get(this.recordID); var viewType = this.viewType; var viewFields = Object.keys(record.fieldsInfo[viewType]); @@ -107,15 +108,27 @@ var BasicView = AbstractView.extend({ } } }); - if (fieldNames.length && !this.model.isNew(record.id)) { - return this.model.reload(this.recordID, { - fieldNames: fieldNames, - keepChanges: true, - viewType: viewType, + + var def; + if (fieldNames.length) { + // Some fields in the new view weren't in the previous one, so + // we might have stored changes for them (e.g. coming from + // onchange RPCs), that we haven't been able to process earlier + // (because those fields were unknow at that time). So we ask + // the model to process them. + def = this.model.applyRawChanges(record.id, viewType).then(function () { + if (!self.model.isNew(record.id)) { + return self.model.reload(record.id, { + fieldNames: fieldNames, + keepChanges: true, + viewType: viewType, + }); + } }); - } else { - return $.when(this.recordID); } + return $.when(def).then(function () { + return record.id; + }); } return this._super.apply(this, arguments); }, 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 d2b0c02c524..c8b9fd574b2 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -38,39 +38,47 @@ return AbstractModel.extend({ var start = event.start.clone(); var end = event.end && event.end.clone(); - if (!end || end.diff(start) < 0) { - end = start.clone().add(1, 'h'); + // Detects allDay events (86400000 = 1 day in ms) + if (event.allDay || (end && end.diff(start) % 86400000 === 0)) { + event.allDay = true; } - if (event.allDay || end.diff(start) % 86400000 === 0) { - event.allDay = true; + // Set end date if not existing + if (!end || end.diff(start) < 0) { // undefined or invalid end date + if (event.allDay) { + end = start.clone(); + } else { + // in week mode or day mode, convert allday event to event + end = start.clone().add(2, 'h'); + } + } else if (event.allDay) { + // For an "allDay", FullCalendar gives the end day as the + // next day at midnight (instead of 23h59). + end.add(-1, 'days'); + } - if (this.scale === 'month') { + // An "allDay" event without the "all_day" option is not considered + // as a 24h day. It's just a part of the day (by default: 7h-19h). + if (event.allDay) { + if (!this.mapping.all_day) { if (event.r_start) { start.hours(event.r_start.hours()) .minutes(event.r_start.minutes()) .seconds(event.r_start.seconds()) .utc(); - end.add(-1, 'days') - .hours(event.r_end.hours()) + end.hours(event.r_end.hours()) .minutes(event.r_end.minutes()) .seconds(event.r_end.seconds()) .utc(); } else { - start.utc(); - end.utc(); + // default hours in the user's timezone + start.hours(7).add(-this.getSession().getTZOffset(start), 'minutes'); + end.hours(19).add(-this.getSession().getTZOffset(end), 'minutes'); } - } else if (this.mapping.all_day) { - start.startOf('day'); - end.startOf('day').add(-1, 'days'); - } else { - // default hours in the user's timezone - start.hours(7).add(-this.getSession().tzOffset, 'minutes'); - end.hours(19).add(-this.getSession().tzOffset, 'minutes'); } } else { - start.add(-this.getSession().tzOffset, 'minutes'); - end.add(-this.getSession().tzOffset, 'minutes'); + start.add(-this.getSession().getTZOffset(start), 'minutes'); + end.add(-this.getSession().getTZOffset(end), 'minutes'); } if (this.mapping.all_day) { @@ -365,6 +373,7 @@ return AbstractModel.extend({ monthNamesShort: moment.monthsShort(), dayNames: moment.weekdays(), dayNamesShort: moment.weekdaysShort(), + firstDay: moment().startOf('week').isoWeekday(), }; }, _getRangeDomain: function () { @@ -599,8 +608,8 @@ return AbstractModel.extend({ date_stop = date_start.clone().add(date_delay,'hours'); } - date_start.add(this.getSession().tzOffset, 'minutes'); - date_stop.add(this.getSession().tzOffset, 'minutes'); + date_start.add(this.getSession().getTZOffset(date_start), 'minutes'); + date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes'); if (this.mapping.all_day && evt[this.mapping.all_day]) { date_stop.add(1, 'days'); @@ -622,6 +631,10 @@ return AbstractModel.extend({ // r.start = date_start.format('YYYY-MM-DD'); // r.end = date_stop.format('YYYY-MM-DD'); } else if (this.data.scale === 'month' && this.fields[this.mapping.date_start].type !== 'date') { + // In month, FullCalendar gives the end day as the + // next day at midnight (instead of 23h59). + date_stop.add(1, 'days'); + // allow to resize in month mode r.reset_allday = r.allDay; r.allDay = true; diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js index 5fb79cfa3a0..422258b4035 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -331,12 +331,15 @@ return AbstractRenderer.extend({ } element.find('.fc-content .fc-time').text(display_hour); }, + // Dirty hack to ensure a correct first render + eventAfterAllRender: function (view) { + window.dispatchEvent(new Event('resize')); + }, height: 'parent', unselectAuto: false, }); this.$calendar.fullCalendar(fc_options); - window.dispatchEvent(new Event('resize')); }, /** * Initialize the mini calendar in the sidebar diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js index 6870dbbbe24..91ada3a2629 100644 --- a/addons/web/static/src/js/views/kanban/kanban_controller.js +++ b/addons/web/static/src/js/views/kanban/kanban_controller.js @@ -107,7 +107,10 @@ var KanbanController = BasicController.extend({ * @returns {Deferred} */ _resequenceRecords: function (column_id, ids) { - return this.model.resequence(this.modelName, ids, column_id); + var self = this; + return this.model.resequence(this.modelName, ids, column_id).then(function () { + self._updateEnv(); + }); }, /** * In grouped mode, set 'Create' button as btn-default if there is no column @@ -181,6 +184,7 @@ var KanbanController = BasicController.extend({ .then(function (db_id) { var data = self.model.get(db_id); self.renderer.updateColumn(db_id, data); + self._updateEnv(); }); } }, @@ -218,6 +222,7 @@ var KanbanController = BasicController.extend({ this.model.loadMore(column.db_id).then(function (db_id) { var data = self.model.get(db_id); self.renderer.updateColumn(db_id, data); + self._updateEnv(); }); }, /** @@ -325,9 +330,12 @@ var KanbanController = BasicController.extend({ * @param {OdooEvent} event */ _onResequenceColumn: function (event) { + var self = this; var state = this.model.get(this.handle, {raw: true}); var model = state.fields[state.groupedBy[0]].relation; - this.model.resequence(model, event.data.ids, this.handle); + this.model.resequence(model, event.data.ids, this.handle).then(function () { + self._updateEnv(); + }); }, /** * @param {OdooEvent} event @@ -338,6 +346,7 @@ var KanbanController = BasicController.extend({ this.model.toggleGroup(column.db_id).then(function (db_id) { var data = self.model.get(db_id); self.renderer.updateColumn(db_id, data); + self._updateEnv(); }); }, /** 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 71402fc92ec..e21a1376e51 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -35,12 +35,8 @@ var KanbanModel = BasicModel.extend({ group.count++; // update the res_ids and count of the parent - var self = this; - var parent = this.localData[group.parentID]; - parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) { - return self.localData[dataPointID].res_ids; - })); - parent.count++; + this.localData[group.parentID].count++; + this._updateParentResIDs(group); return this._fetchRecord(new_record).then(function (result) { return result.id; @@ -151,6 +147,7 @@ var KanbanModel = BasicModel.extend({ return this.notifyChanges(recordID, changes).then(function () { return self.save(recordID); }).then(function () { + var resID = self.localData[recordID].res_id; // Remove record from its current group var old_group; for (var i = 0; i < parent.data.length; i++) { @@ -159,11 +156,14 @@ var KanbanModel = BasicModel.extend({ if (index >= 0) { old_group.data.splice(index, 1); old_group.count--; + old_group.res_ids = _.without(old_group.res_ids, resID); + self._updateParentResIDs(old_group); break; } } // Add record to its new group new_group.data.push(recordID); + new_group.res_ids.push(resID); new_group.count++; return [old_group.id, new_group.id]; }); @@ -201,6 +201,16 @@ var KanbanModel = BasicModel.extend({ data.data = _.sortBy(data.data, function (d) { return _.indexOf(resIDs, self.localData[d].res_id); }); + data.res_ids = []; + _.each(data.data, function (d) { + var dataPoint = self.localData[d]; + if (dataPoint.type === 'record') { + data.res_ids.push(dataPoint.res_id); + } else { + data.res_ids = data.res_ids.concat(dataPoint.res_ids); + } + }); + self._updateParentResIDs(data); return parentID; }); }, 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 6e69c0217b2..2976ab7926a 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -387,15 +387,15 @@ var ListRenderer = BasicRenderer.extend({ var $th = $('<th>') .addClass('o_group_name') .text(name + ' (' + group.count + ')'); - if (group.count > 0) { - var $arrow = $('<span>') + var $arrow = $('<span>') .css('padding-left', (groupLevel * 20) + 'px') .css('padding-right', '5px') - .addClass('fa') - .toggleClass('fa-caret-right', !group.isOpen) - .toggleClass('fa-caret-down', group.isOpen); - $th.prepend($arrow); + .addClass('fa'); + if (group.count > 0) { + $arrow.toggleClass('fa-caret-right', !group.isOpen) + .toggleClass('fa-caret-down', group.isOpen); } + $th.prepend($arrow); if (group.isOpen && !group.groupedBy.length && (group.count > group.data.length)) { var $pager = this._renderGroupPager(group); var $lastCell = $cells[$cells.length-1]; diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index d38400c2e84..876c37fbd59 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -343,6 +343,7 @@ var SelectCreateDialog = ViewDialog.extend({ groupBy: searchResult.groupBy, modelName: self.dataset.model, hasSelectors: !self.options.disable_multiple_selection, + readonly: true, }, self.options.list_view_options)); listView.setController(SelectCreateListController); return listView.getController(self); @@ -402,6 +403,7 @@ var SelectCreateDialog = ViewDialog.extend({ }, })).open(); dialog.on('closed', this, this.close.bind(this)); + return dialog; }, }); |
