diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/calendar/calendar_model.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/views/calendar/calendar_model.js')
| -rw-r--r-- | addons/web/static/src/js/views/calendar/calendar_model.js | 777 |
1 files changed, 777 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js new file mode 100644 index 00000000..93999bee --- /dev/null +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -0,0 +1,777 @@ +odoo.define('web.CalendarModel', function (require) { +"use strict"; + +var AbstractModel = require('web.AbstractModel'); +var Context = require('web.Context'); +var core = require('web.core'); +var fieldUtils = require('web.field_utils'); +var session = require('web.session'); + +var _t = core._t; + +function dateToServer (date) { + return date.clone().utc().locale('en').format('YYYY-MM-DD HH:mm:ss'); +} + +return AbstractModel.extend({ + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.end_date = null; + var week_start = _t.database.parameters.week_start; + // calendar uses index 0 for Sunday but Odoo stores it as 7 + this.week_start = week_start !== undefined && week_start !== false ? week_start % 7 : moment().startOf('week').day(); + this.week_stop = this.week_start + 6; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Transform fullcalendar event object to OpenERP Data object + */ + calendarEventToRecord: function (event) { + // Normalize event_end without changing fullcalendars event. + var data = {'name': event.title}; + var start = event.start.clone(); + var end = event.end && event.end.clone(); + + // 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'); + } + + var isDateEvent = this.fields[this.mapping.date_start].type === 'date'; + // 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 && !isDateEvent) { + if (event.r_start) { + start.hours(event.r_start.hours()) + .minutes(event.r_start.minutes()) + .seconds(event.r_start.seconds()) + .utc(); + end.hours(event.r_end.hours()) + .minutes(event.r_end.minutes()) + .seconds(event.r_end.seconds()) + .utc(); + } else { + // default hours in the user's timezone + start.hours(7); + end.hours(19); + } + start.add(-this.getSession().getTZOffset(start), 'minutes'); + end.add(-this.getSession().getTZOffset(end), 'minutes'); + } + } else { + start.add(-this.getSession().getTZOffset(start), 'minutes'); + end.add(-this.getSession().getTZOffset(end), 'minutes'); + } + + if (this.mapping.all_day) { + if (event.record) { + data[this.mapping.all_day] = + (this.data.scale !== 'month' && event.allDay) || + event.record[this.mapping.all_day] && + end.diff(start) < 10 || + false; + } else { + data[this.mapping.all_day] = event.allDay; + } + } + + data[this.mapping.date_start] = start; + if (this.mapping.date_stop) { + data[this.mapping.date_stop] = end; + } + + if (this.mapping.date_delay) { + if (this.data.scale !== 'month' || (this.data.scale === 'month' && !event.drop)) { + data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600; + } + } + + return data; + }, + /** + * @param {Object} filter + * @returns {boolean} + */ + changeFilter: function (filter) { + var Filter = this.data.filters[filter.fieldName]; + if (filter.value === 'all') { + Filter.all = filter.active; + } + var f = _.find(Filter.filters, function (f) { + return f.value === filter.value; + }); + if (f) { + if (f.active !== filter.active) { + f.active = filter.active; + } else { + return false; + } + } else if (filter.active) { + Filter.filters.push({ + value: filter.value, + active: true, + }); + } + return true; + }, + /** + * @param {OdooEvent} event + */ + createRecord: function (event) { + var data = this.calendarEventToRecord(event.data.data); + for (var k in data) { + if (data[k] && data[k]._isAMomentObject) { + data[k] = dateToServer(data[k]); + } + } + return this._rpc({ + model: this.modelName, + method: 'create', + args: [data], + context: event.data.options.context, + }); + }, + /** + * @todo I think this is dead code + * + * @param {any} ids + * @param {any} model + * @returns + */ + deleteRecords: function (ids, model) { + return this._rpc({ + model: model, + method: 'unlink', + args: [ids], + context: session.user_context, // todo: combine with view context + }); + }, + /** + * @override + * @returns {Object} + */ + __get: function () { + return _.extend({}, this.data, { + fields: this.fields + }); + }, + /** + * @override + * @param {any} params + * @returns {Promise} + */ + __load: function (params) { + var self = this; + this.modelName = params.modelName; + this.fields = params.fields; + this.fieldNames = params.fieldNames; + this.fieldsInfo = params.fieldsInfo; + this.mapping = params.mapping; + this.mode = params.mode; // one of month, week or day + this.scales = params.scales; // one of month, week or day + this.scalesInfo = params.scalesInfo; + + // Check whether the date field is editable (i.e. if the events can be + // dragged and dropped) + this.editable = params.editable; + this.creatable = params.creatable; + + // display more button when there are too much event on one day + this.eventLimit = params.eventLimit; + + // fields to display color, e.g.: user_id.partner_id + this.fieldColor = params.fieldColor; + if (!this.preloadPromise) { + this.preloadPromise = new Promise(function (resolve, reject) { + Promise.all([ + self._rpc({model: self.modelName, method: 'check_access_rights', args: ["write", false]}), + self._rpc({model: self.modelName, method: 'check_access_rights', args: ["create", false]}) + ]).then(function (result) { + var write = result[0]; + var create = result[1]; + self.write_right = write; + self.create_right = create; + resolve(); + }).guardedCatch(reject); + }); + } + + this.data = { + domain: params.domain, + context: params.context, + // get in arch the filter to display in the sidebar and the field to read + filters: params.filters, + }; + + this.setDate(params.initialDate); + // Use mode attribute in xml file to specify zoom timeline (day,week,month) + // by default month. + this.setScale(params.mode); + + _.each(this.data.filters, function (filter) { + if (filter.avatar_field && !filter.avatar_model) { + filter.avatar_model = self.modelName; + } + }); + + return this.preloadPromise.then(this._loadCalendar.bind(this)); + }, + /** + * Move the current date range to the next period + */ + next: function () { + this.setDate(this.data.target_date.clone().add(1, this.data.scale)); + }, + /** + * Move the current date range to the previous period + */ + prev: function () { + this.setDate(this.data.target_date.clone().add(-1, this.data.scale)); + }, + /** + * @override + * @param {Object} [params.context] + * @param {Array} [params.domain] + * @returns {Promise} + */ + __reload: function (handle, params) { + if (params.domain) { + this.data.domain = params.domain; + } + if (params.context) { + this.data.context = params.context; + } + return this._loadCalendar(); + }, + /** + * @param {Moment} start. in local TZ + */ + setDate: function (start) { + // keep highlight/target_date in localtime + this.data.highlight_date = this.data.target_date = start.clone(); + this.data.start_date = this.data.end_date = start; + switch (this.data.scale) { + case 'year': { + const yearStart = this.data.start_date.clone().startOf('year'); + let yearStartDay = this.week_start; + if (yearStart.day() < yearStartDay) { + // the 1st of January is before our week start (e.g. week start is Monday, and + // 01/01 is Sunday), so we go one week back + yearStartDay -= 7; + } + this.data.start_date = yearStart.day(yearStartDay).startOf('day'); + this.data.end_date = this.data.end_date.clone() + .endOf('year').day(this.week_stop).endOf('day'); + break; + } + case 'month': + var monthStart = this.data.start_date.clone().startOf('month'); + + var monthStartDay; + if (monthStart.day() >= this.week_start) { + // the month's first day is after our week start + // Then we are in the right week + monthStartDay = this.week_start; + } else { + // The month's first day is before our week start + // Then we should go back to the the previous week + monthStartDay = this.week_start - 7; + } + + this.data.start_date = monthStart.day(monthStartDay).startOf('day'); + this.data.end_date = this.data.start_date.clone().add(5, 'week').day(this.week_stop).endOf('day'); + break; + case 'week': + var weekStart = this.data.start_date.clone().startOf('week'); + var weekStartDay = this.week_start; + if (this.data.start_date.day() < this.week_start) { + // The week's first day is after our current day + // Then we should go back to the previous week + weekStartDay -= 7; + } + this.data.start_date = this.data.start_date.clone().day(weekStartDay).startOf('day'); + this.data.end_date = this.data.end_date.clone().day(weekStartDay + 6).endOf('day'); + break; + default: + this.data.start_date = this.data.start_date.clone().startOf('day'); + this.data.end_date = this.data.end_date.clone().endOf('day'); + } + // We have set start/stop datetime as definite begin/end boundaries of a period (month, week, day) + // in local TZ (what is the begining of the week *I am* in ?) + // The following code: + // - converts those to UTC using our homemade method (testable) + // - sets the moment UTC flag to true, to ensure compatibility with third party libs + var manualUtcDateStart = this.data.start_date.clone().add(-this.getSession().getTZOffset(this.data.start_date), 'minutes'); + var formattedUtcDateStart = manualUtcDateStart.format('YYYY-MM-DDTHH:mm:ss') + 'Z'; + this.data.start_date = moment.utc(formattedUtcDateStart); + + var manualUtcDateEnd = this.data.end_date.clone().add(-this.getSession().getTZOffset(this.data.start_date), 'minutes'); + var formattedUtcDateEnd = manualUtcDateEnd.format('YYYY-MM-DDTHH:mm:ss') + 'Z'; + this.data.end_date = moment.utc(formattedUtcDateEnd); + }, + /** + * @param {string} scale the scale to set + */ + setScale: function (scale) { + if (!_.contains(this.scales, scale)) { + scale = "week"; + } + this.data.scale = scale; + this.setDate(this.data.target_date); + }, + /** + * Move the current date range to the period containing today + */ + today: function () { + this.setDate(moment(new Date())); + }, + /** + * @param {Object} record + * @param {integer} record.id + * @returns {Promise} + */ + updateRecord: function (record) { + // Cannot modify actual name yet + var data = _.omit(this.calendarEventToRecord(record), 'name'); + for (var k in data) { + if (data[k] && data[k]._isAMomentObject) { + data[k] = dateToServer(data[k]); + } + } + var context = new Context(this.data.context, {from_ui: true}); + return this._rpc({ + model: this.modelName, + method: 'write', + args: [[parseInt(record.id, 10)], data], + context: context + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Converts this.data.filters into a domain + * + * @private + * @returns {Array} + */ + _getFilterDomain: function () { + // List authorized values for every field + // fields with an active 'all' filter are skipped + var authorizedValues = {}; + var avoidValues = {}; + + _.each(this.data.filters, function (filter) { + // Skip 'all' filters because they do not affect the domain + if (filter.all) return; + + // Loop over subfilters to complete authorizedValues + _.each(filter.filters, function (f) { + if (filter.write_model) { + if (!authorizedValues[filter.fieldName]) + authorizedValues[filter.fieldName] = []; + + if (f.active) { + authorizedValues[filter.fieldName].push(f.value); + } + } else { + if (!f.active) { + if (!avoidValues[filter.fieldName]) + avoidValues[filter.fieldName] = []; + + avoidValues[filter.fieldName].push(f.value); + } + } + }); + }); + + // Compute the domain + var domain = []; + for (var field in authorizedValues) { + domain.push([field, 'in', authorizedValues[field]]); + } + for (var field in avoidValues) { + if (avoidValues[field].length > 0) { + domain.push([field, 'not in', avoidValues[field]]); + } + } + + return domain; + }, + /** + * @private + * @returns {Object} + */ + _getFullCalendarOptions: function () { + var format12Hour = { + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'short' + }; + var format24Hour = { + hour: 'numeric', + minute: '2-digit', + hour12: false, + }; + return { + defaultView: this.scalesInfo[this.mode || 'week'], + header: false, + selectable: this.creatable && this.create_right, + selectMirror: true, + editable: this.editable, + droppable: true, + navLinks: false, + eventLimit: this.eventLimit, // allow "more" link when too many events + snapMinutes: 15, + longPressDelay: 500, + eventResizableFromStart: true, + nowIndicator: true, + weekNumbers: true, + weekNumbersWithinDays: true, + weekNumberCalculation: function (date) { + // Since FullCalendar v4 ISO 8601 week date is preferred so we force the old system + return moment(date).week(); + }, + weekLabel: _t("Week"), + allDayText: _t("All day"), + monthNames: moment.months(), + monthNamesShort: moment.monthsShort(), + dayNames: moment.weekdays(), + dayNamesShort: moment.weekdaysShort(), + firstDay: this.week_start, + slotLabelFormat: _t.database.parameters.time_format.search("%H") !== -1 ? format24Hour : format12Hour, + allDaySlot: this.mapping.all_day || this.fields[this.mapping.date_start].type === 'date', + }; + }, + /** + * Return a domain from the date range + * + * @private + * @returns {Array} A domain containing datetimes start and stop in UTC + * those datetimes are formatted according to server's standards + */ + _getRangeDomain: function () { + // Build OpenERP Domain to filter object by this.mapping.date_start field + // between given start, end dates. + var domain = [[this.mapping.date_start, '<=', dateToServer(this.data.end_date)]]; + if (this.mapping.date_stop) { + domain.push([this.mapping.date_stop, '>=', dateToServer(this.data.start_date)]); + } else if (!this.mapping.date_delay) { + domain.push([this.mapping.date_start, '>=', dateToServer(this.data.start_date)]); + } + return domain; + }, + /** + * @private + * @returns {Promise} + */ + _loadCalendar: function () { + var self = this; + this.data.fc_options = this._getFullCalendarOptions(); + + var defs = _.map(this.data.filters, this._loadFilter.bind(this)); + + return Promise.all(defs).then(function () { + return self._rpc({ + model: self.modelName, + method: 'search_read', + context: self.data.context, + fields: self.fieldNames, + domain: self.data.domain.concat(self._getRangeDomain()).concat(self._getFilterDomain()) + }) + .then(function (events) { + self._parseServerData(events); + self.data.data = _.map(events, self._recordToCalendarEvent.bind(self)); + return Promise.all([ + self._loadColors(self.data, self.data.data), + self._loadRecordsToFilters(self.data, self.data.data) + ]); + }); + }); + }, + /** + * @private + * @param {any} element + * @param {any} events + * @returns {Promise} + */ + _loadColors: function (element, events) { + if (this.fieldColor) { + var fieldName = this.fieldColor; + _.each(events, function (event) { + var value = event.record[fieldName]; + event.color_index = _.isArray(value) ? value[0] % 30 : value % 30; + }); + this.model_color = this.fields[fieldName].relation || element.model; + } + return Promise.resolve(); + }, + /** + * @private + * @param {any} filter + * @returns {Promise} + */ + _loadFilter: function (filter) { + if (!filter.write_model) { + return Promise.resolve(); + } + + var field = this.fields[filter.fieldName]; + return this._rpc({ + model: filter.write_model, + method: 'search_read', + domain: [["user_id", "=", session.uid]], + fields: [filter.write_field], + }) + .then(function (res) { + var records = _.map(res, function (record) { + var _value = record[filter.write_field]; + var value = _.isArray(_value) ? _value[0] : _value; + var f = _.find(filter.filters, function (f) {return f.value === value;}); + var formater = fieldUtils.format[_.contains(['many2many', 'one2many'], field.type) ? 'many2one' : field.type]; + return { + 'id': record.id, + 'value': value, + 'label': formater(_value, field), + 'active': !f || f.active, + }; + }); + records.sort(function (f1,f2) { + return _.string.naturalCmp(f2.label, f1.label); + }); + + // add my profile + if (field.relation === 'res.partner' || field.relation === 'res.users') { + var value = field.relation === 'res.partner' ? session.partner_id : session.uid; + var me = _.find(records, function (record) { + return record.value === value; + }); + if (me) { + records.splice(records.indexOf(me), 1); + } else { + var f = _.find(filter.filters, function (f) {return f.value === value;}); + me = { + 'value': value, + 'label': session.name + _t(" [Me]"), + 'active': !f || f.active, + }; + } + records.unshift(me); + } + // add all selection + records.push({ + 'value': 'all', + 'label': field.relation === 'res.partner' || field.relation === 'res.users' ? _t("Everybody's calendars") : _t("Everything"), + 'active': filter.all, + }); + + filter.filters = records; + }); + }, + /** + * @private + * @param {any} element + * @param {any} events + * @returns {Promise} + */ + _loadRecordsToFilters: function (element, events) { + var self = this; + var new_filters = {}; + var to_read = {}; + var defs = []; + var color_filter = {}; + + _.each(this.data.filters, function (filter, fieldName) { + var field = self.fields[fieldName]; + + new_filters[fieldName] = filter; + if (filter.write_model) { + if (field.relation === self.model_color) { + _.each(filter.filters, function (f) { + f.color_index = f.value; + }); + } + return; + } + + _.each(filter.filters, function (filter) { + filter.display = !filter.active; + }); + + var fs = []; + var undefined_fs = []; + _.each(events, function (event) { + var data = event.record[fieldName]; + if (!_.contains(['many2many', 'one2many'], field.type)) { + data = [data]; + } else { + to_read[field.relation] = (to_read[field.relation] || []).concat(data); + } + _.each(data, function (_value) { + var value = _.isArray(_value) ? _value[0] : _value; + var f = { + 'color_index': self.model_color === (field.relation || element.model) ? value % 30 : false, + 'value': value, + 'label': fieldUtils.format[field.type](_value, field) || _t("Undefined"), + 'avatar_model': field.relation || element.model, + }; + // if field used as color does not have value then push filter in undefined_fs, + // such filters should come last in filter list with Undefined string, later merge it with fs + value ? fs.push(f) : undefined_fs.push(f); + }); + }); + _.each(_.union(fs, undefined_fs), function (f) { + var f1 = _.findWhere(filter.filters, _.omit(f, 'color_index')); + if (f1) { + f1.display = true; + } else { + f.display = f.active = true; + filter.filters.push(f); + } + }); + + if (filter.color_model && filter.field_color) { + var ids = filter.filters.reduce((acc, f) => { + if (!f.color_index && f.value) { + acc.push(f.value); + } + return acc; + }, []); + if (!color_filter[filter.color_model]) { + color_filter[filter.color_model] = {}; + } + if (ids.length) { + defs.push(self._rpc({ + model: filter.color_model, + method: 'read', + args: [_.uniq(ids), [filter.field_color]], + }) + .then(function (res) { + _.each(res, function (c) { + color_filter[filter.color_model][c.id] = c[filter.field_color]; + }); + })); + } + } + }); + + _.each(to_read, function (ids, model) { + defs.push(self._rpc({ + model: model, + method: 'name_get', + args: [_.uniq(ids)], + }) + .then(function (res) { + to_read[model] = _.object(res); + })); + }); + return Promise.all(defs).then(function () { + _.each(self.data.filters, function (filter) { + if (filter.write_model) { + return; + } + if (filter.filters.length && (filter.filters[0].avatar_model in to_read)) { + _.each(filter.filters, function (f) { + f.label = to_read[f.avatar_model][f.value]; + }); + } + if (filter.color_model && filter.field_color) { + _.each(filter.filters, function (f) { + if (!f.color_index) { + f.color_index = color_filter[filter.color_model] && color_filter[filter.color_model][f.value]; + } + }); + } + }); + }); + }, + /** + * parse the server values to javascript framwork + * + * @private + * @param {Object} data the server data to parse + */ + _parseServerData: function (data) { + var self = this; + _.each(data, function(event) { + _.each(self.fieldNames, function (fieldName) { + event[fieldName] = self._parseServerValue(self.fields[fieldName], event[fieldName]); + }); + }); + }, + /** + * Transform OpenERP event object to fullcalendar event object + * + * @private + * @param {Object} evt + */ + _recordToCalendarEvent: function (evt) { + var date_start; + var date_stop; + var date_delay = evt[this.mapping.date_delay] || 1.0, + all_day = this.fields[this.mapping.date_start].type === 'date' || + this.mapping.all_day && evt[this.mapping.all_day] || false, + the_title = '', + attendees = []; + + if (!all_day) { + date_start = evt[this.mapping.date_start].clone(); + date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone() : null; + } else { + date_start = evt[this.mapping.date_start].clone().startOf('day'); + date_stop = this.mapping.date_stop ? evt[this.mapping.date_stop].clone().startOf('day') : null; + } + + if (!date_stop && date_delay) { + date_stop = date_start.clone().add(date_delay,'hours'); + } + + if (!all_day) { + 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'); + } + var r = { + 'record': evt, + 'start': date_start.local(true).toDate(), + 'end': date_stop.local(true).toDate(), + 'r_start': date_start.clone().local(true).toDate(), + 'r_end': date_stop.clone().local(true).toDate(), + 'title': the_title, + 'allDay': all_day, + 'id': evt.id, + 'attendees':attendees, + }; + + if (!(this.mapping.all_day && evt[this.mapping.all_day]) && this.data.scale === 'month' && this.fields[this.mapping.date_start].type !== 'date') { + r.showTime = true; + } + + return r; + }, +}); + +}); |
