[MERGE] Merged with main addons.
[odoo/odoo.git] / addons / web_calendar / static / src / js / calendar.js
1 /*---------------------------------------------------------
2  * OpenERP web_calendar
3  *---------------------------------------------------------*/
4
5 openerp.web_calendar = function(instance) {
6 var _t = instance.web._t,
7    _lt = instance.web._lt;
8 var QWeb = instance.web.qweb;
9 instance.web.views.add('calendar', 'instance.web_calendar.CalendarView');
10 instance.web_calendar.CalendarView = instance.web.View.extend({
11     template: "CalendarView",
12     display_name: _lt('Calendar'),
13 // Dhtmlx scheduler ?
14     init: function(parent, dataset, view_id, options) {
15         this._super(parent);
16         this.ready = $.Deferred();
17         this.set_default_options(options);
18         this.dataset = dataset;
19         this.model = dataset.model;
20         this.fields_view = {};
21         this.view_id = view_id;
22         this.view_type = 'calendar';
23         this.has_been_loaded = $.Deferred();
24         this.creating_event_id = null;
25         this.dataset_events = [];
26         this.form_dialog = new instance.web_calendar.CalendarFormDialog(this, {
27                 destroy_on_close: false,
28                 width: '80%',
29                 min_width: 850
30             }, this.options.action_views_ids.form, dataset);
31         this.form_dialog.start();
32         this.COLOR_PALETTE = ['#f57900', '#cc0000', '#d400a8', '#75507b', '#3465a4', '#73d216', '#c17d11', '#edd400',
33              '#fcaf3e', '#ef2929', '#ff00c9', '#ad7fa8', '#729fcf', '#8ae234', '#e9b96e', '#fce94f',
34              '#ff8e00', '#ff0000', '#b0008c', '#9000ff', '#0078ff', '#00ff00', '#e6ff00', '#ffff00',
35              '#905000', '#9b0000', '#840067', '#510090', '#0000c9', '#009b00', '#9abe00', '#ffc900' ];
36         this.color_map = {};
37         this.last_search = [];
38         this.range_start = null;
39         this.range_stop = null;
40         this.update_range_dates(Date.today());
41         this.selected_filters = [];
42     },
43     start: function() {
44         this._super();
45         return this.rpc("/web/view/load", {"model": this.model, "view_id": this.view_id, "view_type":"calendar", 'toolbar': false}, this.on_loaded);
46     },
47     destroy: function() {
48         scheduler.clearAll();
49         this._super();
50     },
51     on_loaded: function(data) {
52         this.fields_view = data;
53         this.calendar_fields = {};
54         this.ids = this.dataset.ids;
55         this.color_values = [];
56         this.info_fields = [];
57
58         this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
59         this.view_id = this.fields_view.view_id;
60
61         // mode, one of month, week or day
62         this.mode = this.fields_view.arch.attrs.mode;
63
64         // date_start is mandatory, date_delay and date_stop are optional
65         this.date_start = this.fields_view.arch.attrs.date_start;
66         this.date_delay = this.fields_view.arch.attrs.date_delay;
67         this.date_stop = this.fields_view.arch.attrs.date_stop;
68
69         this.day_length = this.fields_view.arch.attrs.day_length || 8;
70         this.color_field = this.fields_view.arch.attrs.color;
71         this.color_string = this.fields_view.fields[this.color_field] ?
72             this.fields_view.fields[this.color_field].string : _t("Filter");
73
74         if (this.color_field && this.selected_filters.length === 0) {
75             var default_filter;
76             if (default_filter = this.dataset.context['calendar_default_' + this.color_field]) {
77                 this.selected_filters.push(default_filter + '');
78             }
79         }
80         this.fields =  this.fields_view.fields;
81
82         if (!this.date_start) {
83             throw new Error("Calendar view has not defined 'date_start' attribute.");
84         }
85
86         //* Calendar Fields *
87         this.calendar_fields.date_start = {'name': this.date_start, 'kind': this.fields[this.date_start].type};
88
89         if (this.date_delay) {
90             if (this.fields[this.date_delay].type != 'float') {
91                 throw new Error("Calendar view has a 'date_delay' type != float");
92             }
93             this.calendar_fields.date_delay = {'name': this.date_delay, 'kind': this.fields[this.date_delay].type};
94         }
95         if (this.date_stop) {
96             this.calendar_fields.date_stop = {'name': this.date_stop, 'kind': this.fields[this.date_stop].type};
97         }
98
99         for (var fld = 0; fld < this.fields_view.arch.children.length; fld++) {
100             this.info_fields.push(this.fields_view.arch.children[fld].attrs.name);
101         }
102
103         this.init_scheduler();
104
105         if (!this.sidebar && this.options.$sidebar) {
106             this.sidebar = new instance.web_calendar.Sidebar(this);
107             this.has_been_loaded.pipe(this.sidebar.appendTo(this.$element.find('.oe_calendar_sidebar_container')));
108         }
109
110         return this.has_been_loaded.resolve();
111     },
112     init_scheduler: function() {
113         var self = this;
114         scheduler.clearAll();
115         if (this.fields[this.date_start]['type'] == 'time') {
116             scheduler.config.xml_date = "%H:%M:%S";
117         } else {
118             scheduler.config.xml_date = "%Y-%m-%d %H:%i";
119         }
120         scheduler.config.api_date = "%Y-%m-%d %H:%i";
121         scheduler.config.multi_day = true; //Multi day events are not rendered in daily and weekly views
122         scheduler.config.start_on_monday = Date.CultureInfo.firstDayOfWeek !== 0; //Sunday = Sunday, Others = Monday
123         scheduler.config.time_step = 30;
124         scheduler.config.scroll_hour = 8;
125         scheduler.config.drag_resize = true;
126         scheduler.config.drag_create = true;
127         scheduler.config.mark_now = true;
128         scheduler.config.day_date = '%l %j';
129
130         scheduler.locale = {
131             date:{
132                 month_full: Date.CultureInfo.monthNames,
133                 month_short: Date.CultureInfo.abbreviatedMonthNames,
134                 day_full: Date.CultureInfo.dayNames,
135                 day_short: Date.CultureInfo.abbreviatedDayNames
136             },
137             labels:{
138                 dhx_cal_today_button: _t("Today"),
139                 day_tab: _t("Day"),
140                 week_tab: _t("Week"),
141                 month_tab: _t("Month"),
142                 new_event: _t("New event"),
143                 icon_save: _t("Save"),
144                 icon_cancel: _t("Cancel"),
145                 icon_details: _t("Details"),
146                 icon_edit: _t("Edit"),
147                 icon_delete: _t("Delete"),
148                 confirm_closing: "",//Your changes will be lost, are your sure ?
149                 confirm_deleting: _t("Event will be deleted permanently, are you sure?"),
150                 section_description: _t("Description"),
151                 section_time: _t("Time period"),
152                 full_day: _t("Full day"),
153
154                 /*recurring events*/
155                 confirm_recurring: _t("Do you want to edit the whole set of repeated events?"),
156                 section_recurring: _t("Repeat event"),
157                 button_recurring: _t("Disabled"),
158                 button_recurring_open: _t("Enabled"),
159
160                 /*agenda view extension*/
161                 agenda_tab: _t("Agenda"),
162                 date: _t("Date"),
163                 description: _t("Description"),
164
165                 /*year view extension*/
166                 year_tab: _t("Year"),
167
168                 /* week agenda extension */
169                 week_agenda_tab: _t("Agenda")
170             }
171         };
172
173         scheduler.init(this.$element.find('.oe_calendar')[0], null, this.mode || 'month');
174
175         // Remove hard coded style attributes from dhtmlx scheduler
176         this.$element.find(".dhx_cal_navline div").removeAttr('style');
177
178         scheduler.detachAllEvents();
179         scheduler.attachEvent('onEventAdded', this.do_create_event);
180         scheduler.attachEvent('onEventDeleted', this.do_delete_event);
181         scheduler.attachEvent('onEventChanged', this.do_save_event);
182         scheduler.attachEvent('onClick', this.do_edit_event);
183         scheduler.attachEvent('onLightbox', this.do_edit_event);
184
185         scheduler.attachEvent('onViewChange', this.on_view_changed);
186         this.refresh_scheduler();
187     },
188     on_view_changed: function(mode, date) {
189         this.$element.find('.oe_calendar').removeClass('oe_cal_day oe_cal_week oe_cal_month').addClass('oe_cal_' + mode);
190         if (!date.between(this.range_start, this.range_stop)) {
191             this.update_range_dates(date);
192             this.do_ranged_search();
193         }
194         this.ready.resolve();
195     },
196     update_range_dates: function(date) {
197         this.range_start = date.clone().moveToFirstDayOfMonth();
198         this.range_stop = this.range_start.clone().addMonths(1).addSeconds(-1);
199     },
200     refresh_scheduler: function() {
201         scheduler.setCurrentView(scheduler._date);
202     },
203     refresh_minical: function() {
204         if (this.sidebar) {
205             scheduler.updateCalendar(this.sidebar.mini_calendar);
206         }
207     },
208     reload_event: function(id) {
209         this.dataset.read_ids([id], _.keys(this.fields)).then(this.on_events_loaded);
210     },
211     get_color: function(key) {
212         if (this.color_map[key]) {
213             return this.color_map[key];
214         }
215         var index = _.keys(this.color_map).length % this.COLOR_PALETTE.length;
216         var color = this.COLOR_PALETTE[index];
217         this.color_map[key] = color;
218         return color;
219     },
220     on_events_loaded: function(events, fn_filter, no_filter_reload) {
221         var self = this;
222
223         //To parse Events we have to convert date Format
224         var res_events = [],
225             sidebar_items = {};
226         for (var e = 0; e < events.length; e++) {
227             var evt = events[e];
228             if (!evt[this.date_start]) {
229                 break;
230             }
231
232             if (this.color_field) {
233                 var filter = evt[this.color_field];
234                 if (filter) {
235                     var filter_value = (typeof filter === 'object') ? filter[0] : filter;
236                     if (typeof(fn_filter) === 'function' && !fn_filter(filter_value)) {
237                         continue;
238                     }
239                     var filter_item = {
240                         value: filter_value,
241                         label: (typeof filter === 'object') ? filter[1] : filter,
242                         color: this.get_color(filter_value)
243                     };
244                     if (!sidebar_items[filter_value]) {
245                         sidebar_items[filter_value] = filter_item;
246                     }
247                     evt.color = filter_item.color;
248                     evt.textColor = '#ffffff';
249                 }
250             }
251
252             if (this.fields[this.date_start]['type'] == 'date') {
253                 evt[this.date_start] = instance.web.auto_str_to_date(evt[this.date_start]).set({hour: 9}).toString('yyyy-MM-dd HH:mm:ss');
254             }
255             if (this.date_stop && evt[this.date_stop] && this.fields[this.date_stop]['type'] == 'date') {
256                 evt[this.date_stop] = instance.web.auto_str_to_date(evt[this.date_stop]).set({hour: 17}).toString('yyyy-MM-dd HH:mm:ss');
257             }
258             res_events.push(this.convert_event(evt));
259         }
260         scheduler.parse(res_events, 'json');
261         this.refresh_scheduler();
262         this.refresh_minical();
263         if (!no_filter_reload && this.sidebar) {
264             this.sidebar.filter.on_events_loaded(sidebar_items);
265         }
266     },
267     convert_event: function(evt) {
268         var date_start = instance.web.str_to_datetime(evt[this.date_start]),
269             date_stop = this.date_stop ? instance.web.str_to_datetime(evt[this.date_stop]) : null,
270             date_delay = evt[this.date_delay] || 1.0,
271             res_text = '';
272
273         if (this.info_fields) {
274             res_text = _.map(this.info_fields, function(fld) {
275                 if(evt[fld] instanceof Array)
276                     return evt[fld][1];
277                 return evt[fld];
278             });
279         }
280         if (!date_stop && date_delay) {
281             date_stop = date_start.clone().addHours(date_delay);
282         }
283         var r = {
284             'start_date': date_start.toString('yyyy-MM-dd HH:mm:ss'),
285             'end_date': date_stop.toString('yyyy-MM-dd HH:mm:ss'),
286             'text': res_text.join(', '),
287             'id': evt.id
288         };
289         if (evt.color) {
290             r.color = evt.color;
291         }
292         if (evt.textColor) {
293             r.textColor = evt.textColor;
294         }
295         return r;
296     },
297     do_create_event: function(event_id, event_obj) {
298         var self = this,
299             data = this.get_event_data(event_obj);
300         this.dataset.create(data, function(r) {
301             var id = r.result;
302             self.dataset.ids.push(id);
303             scheduler.changeEventId(event_id, id);
304             self.refresh_minical();
305             self.reload_event(id);
306         }, function(r, event) {
307             event.preventDefault();
308             self.do_create_event_with_formdialog(event_id, event_obj);
309         });
310     },
311     do_create_event_with_formdialog: function(event_id, event_obj) {
312         if (!event_obj) {
313             event_obj = scheduler.getEvent(event_id);
314         }
315         var self = this,
316             data = this.get_event_data(event_obj),
317             form = self.form_dialog.form,
318             fields_to_fetch = _(form.fields_view.fields).keys();
319         this.dataset.index = null;
320         self.creating_event_id = event_id;
321         this.form_dialog.form.do_show().then(function() {
322             _.each(['date_start', 'date_delay', 'date_stop'], function(field) {
323                 var field_name = self[field];
324                 if (field_name && form.fields[field_name]) {
325                     var ffield = form.fields[field_name];
326                     ffield._dirty_flag = false;
327                     $.when(ffield.set_value(data[field_name])).then(function() {
328                         ffield._dirty_flag = true;
329                         form.do_onchange(ffield);
330                     });
331                 }
332             });
333             self.form_dialog.open();
334         });
335     },
336     do_save_event: function(event_id, event_obj) {
337         var self = this,
338             data = this.get_event_data(event_obj),
339             index = this.dataset.get_id_index(event_id);
340         if (index != null) {
341             event_id = this.dataset.ids[index];
342             this.dataset.write(event_id, data, {}, function() {
343                 self.refresh_minical();
344             });
345         }
346     },
347     do_delete_event: function(event_id, event_obj) {
348         // dhtmlx sends this event even when it does not exist in openerp.
349         // Eg: use cancel in dhtmlx new event dialog
350         var self = this,
351             index = this.dataset.get_id_index(event_id);
352         if (index !== null) {
353             this.dataset.unlink(event_id, function() {
354                 self.refresh_minical();
355             });
356         }
357     },
358     do_edit_event: function(event_id, evt) {
359         var self = this;
360         var index = this.dataset.get_id_index(event_id);
361         if (index !== null) {
362             this.dataset.index = index;
363             this.do_switch_view('form');
364         } else if (scheduler.getState().mode === 'month') {
365             var event_obj = scheduler.getEvent(event_id);
366             if (event_obj._length === 1) {
367                 event_obj['start_date'].addHours(8);
368                 event_obj['end_date'] = new Date(event_obj['start_date']);
369                 event_obj['end_date'].addHours(1);
370             } else {
371                 event_obj['start_date'].addHours(8);
372                 event_obj['end_date'].addHours(-4);
373             }
374             this.do_create_event_with_formdialog(event_id, event_obj);
375             // return false;
376             // Theorically, returning false should prevent the lightbox to open.
377             // It works, but then the scheduler is in a buggy state where drag'n drop
378             // related internal Event won't be fired anymore.
379             // I tried scheduler.editStop(event_id); but doesn't work either
380             // After losing one hour on this, here's a quick and very dirty fix :
381             $(".dhx_cancel_btn").click();
382         } else {
383             scheduler.editStop($(evt.target).hasClass('icon_save'));
384         }
385     },
386     get_event_data: function(event_obj) {
387         var data = {
388             name: event_obj.text
389         };
390         data[this.date_start] = instance.web.datetime_to_str(event_obj.start_date);
391         if (this.date_stop) {
392             data[this.date_stop] = instance.web.datetime_to_str(event_obj.end_date);
393         }
394         if (this.date_delay) {
395             var diff_seconds = Math.round((event_obj.end_date.getTime() - event_obj.start_date.getTime()) / 1000);
396             data[this.date_delay] = diff_seconds / 3600;
397         }
398         return data;
399     },
400     do_search: function(domain, context, group_by) {
401         this.last_search = arguments;
402         this.do_ranged_search();
403     },
404     do_ranged_search: function() {
405         var self = this;
406         scheduler.clearAll();
407         $.when(this.has_been_loaded, this.ready).then(function() {
408             self.dataset.read_slice(_.keys(self.fields), {
409                 offset: 0,
410                 domain: self.get_range_domain(),
411                 context: self.last_search[1]
412             }).then(function(events) {
413                 self.dataset_events = events;
414                 self.on_events_loaded(events);
415             });
416         });
417     },
418     get_range_domain: function() {
419         var format = instance.web.date_to_str,
420             domain = this.last_search[0].slice(0);
421         domain.unshift([this.date_start, '>=', format(this.range_start.clone().addDays(-6))]);
422         domain.unshift([this.date_start, '<=', format(this.range_stop.clone().addDays(6))]);
423         return domain;
424     },
425     do_show: function () {
426         var self = this;
427         $.when(this.has_been_loaded).then(function() {
428             self.$element.show();
429             self.do_push_state({});
430         });
431     },
432     get_selected_ids: function() {
433         // no way to select a record anyway
434         return [];
435     }
436 });
437
438 instance.web_calendar.CalendarFormDialog = instance.web.Dialog.extend({
439     init: function(view, options, view_id, dataset) {
440         this._super(view, options);
441         this.dataset = dataset;
442         this.view_id = view_id;
443         this.view = view;
444     },
445     start: function() {
446         var self = this;
447         this._super();
448         this.form = new instance.web.FormView(this, this.dataset, this.view_id, {
449             pager: false
450         });
451         this.form.appendTo(this.$element);
452         this.form.on_created.add_last(this.on_form_dialog_saved);
453         this.form.on_saved.add_last(this.on_form_dialog_saved);
454         this.form.on_button_cancel = function() {
455             self.close();
456         }
457     },
458     on_form_dialog_saved: function() {
459         var id = this.dataset.ids[this.dataset.index];
460         if (this.view.creating_event_id) {
461             scheduler.changeEventId(this.view.creating_event_id, id);
462             this.view.creating_event_id = null;
463         }
464         this.view.reload_event(id);
465         this.close();
466     },
467     on_close: function() {
468         if (this.view.creating_event_id) {
469             scheduler.deleteEvent(this.view.creating_event_id);
470             this.view.creating_event_id = null;
471         }
472     }
473 });
474
475 instance.web_calendar.Sidebar = instance.web.Widget.extend({
476     template: 'CalendarView.sidebar',
477     start: function() {
478         this._super();
479         this.mini_calendar = scheduler.renderCalendar({
480             container: this.$element.find('.oe_calendar_mini')[0],
481             navigation: true,
482             date: scheduler._date,
483             handler: function(date, calendar) {
484                 scheduler.setCurrentView(date, 'day');
485             }
486         });
487         this.filter = new instance.web_calendar.SidebarFilter(this, this.getParent());
488         this.filter.appendTo(this.$element.find('.oe_calendar_filter'));
489     }
490 });
491 instance.web_calendar.SidebarFilter = instance.web.Widget.extend({
492     init: function(parent, view) {
493         this._super(parent);
494         this.view = view;
495         this.$element.delegate('input:checkbox', 'change', this.on_filter_click);
496     },
497     on_events_loaded: function(filters) {
498         var selected_filters = this.view.selected_filters.slice(0);
499         this.$element.html(QWeb.render('CalendarView.sidebar.responsible', { filters: filters }));
500         this.$element.find('div.oe_calendar_responsible input').each(function() {
501             if (_.indexOf(selected_filters, $(this).val()) > -1) {
502                 $(this).click();
503             }
504         });
505     },
506     on_filter_click: function(e) {
507         var self = this,
508             responsibles = [],
509             $e = $(e.target);
510         this.view.selected_filters = [];
511         this.$element.find('div.oe_calendar_responsible input:checked').each(function() {
512             responsibles.push($(this).val());
513             self.view.selected_filters.push($(this).val());
514         });
515         scheduler.clearAll();
516         if (responsibles.length) {
517             this.view.on_events_loaded(this.view.dataset_events, function(filter_value) {
518                 return _.indexOf(responsibles, filter_value.toString()) > -1;
519             }, true);
520         } else {
521             this.view.on_events_loaded(this.view.dataset_events, false, true);
522         }
523     }
524 });
525
526 };
527
528 // DEBUG_RPC:rpc.request:('execute', 'addons-dsh-l10n_us', 1, '*', ('ir.filters', 'get_filters', u'res.partner'))
529 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: