1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
5 _.str.toBoolElse = function (str, elseValues, trueValues, falseValues) {
6 var ret = _.str.toBool(str, trueValues, falseValues);
7 if (_.isUndefined(ret)) {
13 openerp.web_calendar = function(instance) {
14 var _t = instance.web._t,
15 _lt = instance.web._lt,
16 QWeb = instance.web.qweb;
18 function get_class(name) {
19 return new instance.web.Registry({'tmp' : name}).get_object("tmp");
22 function get_fc_defaultOptions() {
23 shortTimeformat = Date.CultureInfo.formatPatterns.shortTime;
25 weekNumberTitle: _t("W"),
26 allDayText: _t("All day"),
33 monthNames: Date.CultureInfo.monthNames,
34 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
35 dayNames: Date.CultureInfo.dayNames,
36 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
37 firstDay: Date.CultureInfo.firstDayOfWeek,
39 axisFormat : shortTimeformat.replace(/:mm/,'(:mm)'),
41 // for agendaWeek and agendaDay
42 agenda: shortTimeformat + '{ - ' + shortTimeformat + '}', // 5:00 - 6:30
43 // for all other views
44 '': shortTimeformat.replace(/:mm/,'(:mm)') // 7pm
52 function is_virtual_id(id) {
53 return typeof id === "string" && id.indexOf('-') >= 0;
56 function isNullOrUndef(value) {
57 return _.isUndefined(value) || _.isNull(value);
60 instance.web.views.add('calendar', 'instance.web_calendar.CalendarView');
62 instance.web_calendar.CalendarView = instance.web.View.extend({
63 template: "CalendarView",
64 display_name: _lt('Calendar'),
65 quick_create_instance: 'instance.web_calendar.QuickCreate',
67 init: function (parent, dataset, view_id, options) {
69 this.ready = $.Deferred();
70 this.set_default_options(options);
71 this.dataset = dataset;
72 this.model = dataset.model;
73 this.fields_view = {};
74 this.view_id = view_id;
75 this.view_type = 'calendar';
77 this.range_start = null;
78 this.range_stop = null;
79 this.selected_filters = [];
82 set_default_options: function(options) {
84 _.defaults(this.options, {
85 confirm_on_delete: true
90 this.$calendar.fullCalendar('destroy');
91 if (this.$small_calendar) {
92 this.$small_calendar.datepicker('destroy');
94 this._super.apply(this, arguments);
97 view_loading: function (fv) {
98 /* xml view calendar options */
99 var attrs = fv.arch.attrs,
101 this.fields_view = fv;
102 this.$calendar = this.$el.find(".oe_calendar_widget");
104 this.info_fields = [];
107 this.$buttons = $(QWeb.render("CalendarView.buttons", {'widget': this}));
108 if (this.options.$buttons) {
109 this.$buttons.appendTo(this.options.$buttons);
111 this.$el.find('.oe_calendar_buttons').replaceWith(this.$buttons);
114 this.$buttons.on('click', 'button.oe_calendar_button_new', function () {
115 self.dataset.index = null;
116 self.do_switch_view('form');
119 if (!attrs.date_start) {
120 throw new Error(_t("Calendar view has not defined 'date_start' attribute."));
123 this.$el.addClass(attrs['class']);
125 this.name = fv.name || attrs.string;
126 this.view_id = fv.view_id;
128 this.mode = attrs.mode; // one of month, week or day
129 this.date_start = attrs.date_start; // Field name of starting date field
130 this.date_delay = attrs.date_delay; // duration
131 this.date_stop = attrs.date_stop;
132 this.all_day = attrs.all_day;
133 this.how_display_event = '';
134 this.attendee_people = attrs.attendee;
136 if (!isNullOrUndef(attrs.quick_create_instance)) {
137 self.quick_create_instance = 'instance.' + attrs.quick_create_instance;
140 //if quick_add = False, we don't allow quick_add
141 //if quick_add = not specified in view, we use the default quick_create_instance
142 //if quick_add = is NOT False and IS specified in view, we this one for quick_create_instance'
144 this.quick_add_pop = (isNullOrUndef(attrs.quick_add) || _.str.toBoolElse(attrs.quick_add, true));
145 if (this.quick_add_pop && !isNullOrUndef(attrs.quick_add)) {
146 self.quick_create_instance = 'instance.' + attrs.quick_add;
148 // The display format which will be used to display the event where fields are between "[" and "]"
149 if (!isNullOrUndef(attrs.display)) {
150 this.how_display_event = attrs.display; // String with [FIELD]
153 // If this field is set ot true, we don't open the event in form view, but in a popup with the view_id passed by this parameter
154 if (isNullOrUndef(attrs.event_open_popup) || !_.str.toBoolElse(attrs.event_open_popup, true)) {
155 this.open_popup_action = false;
157 this.open_popup_action = attrs.event_open_popup;
159 // If this field is set to true, we will use the calendar_friends model as filter and not the color field.
160 this.useContacts = (!isNullOrUndef(attrs.use_contacts) && _.str.toBool(attrs.use_contacts)) && (!isNullOrUndef(self.options.$sidebar));
162 // If this field is set ot true, we don't add itself as an attendee when we use attendee_people to add each attendee icon on an event
163 // The color is the color of the attendee, so don't need to show again that it will be present
164 this.colorIsAttendee = (!(isNullOrUndef(attrs.color_is_attendee) || !_.str.toBoolElse(attrs.color_is_attendee, true))) && (!isNullOrUndef(self.options.$sidebar));
166 // if we have not sidebar, (eg: Dashboard), we don't use the filter "coworkers"
167 if (isNullOrUndef(self.options.$sidebar)) {
168 this.useContacts = false;
169 this.colorIsAttendee = false;
170 this.attendee_people = undefined;
174 Will be more logic to do it in futur, but see below to stay Retro-compatible
176 if (isNull(attrs.avatar_model)) {
177 this.avatar_model = 'res.partner';
180 if (attrs.avatar_model == 'False') {
181 this.avatar_model = null;
184 this.avatar_model = attrs.avatar_model;
188 if (isNullOrUndef(attrs.avatar_model)) {
189 this.avatar_model = null;
191 this.avatar_model = attrs.avatar_model;
194 if (isNullOrUndef(attrs.avatar_title)) {
195 this.avatar_title = this.avatar_model;
197 this.avatar_title = attrs.avatar_title;
200 if (isNullOrUndef(attrs.avatar_filter)) {
201 this.avatar_filter = this.avatar_model;
203 this.avatar_filter = attrs.avatar_filter;
206 this.color_field = attrs.color;
208 if (this.color_field && this.selected_filters.length === 0) {
210 if ((default_filter = this.dataset.context['calendar_default_' + this.color_field])) {
211 this.selected_filters.push(default_filter + '');
215 this.fields = fv.fields;
217 for (var fld = 0; fld < fv.arch.children.length; fld++) {
218 this.info_fields.push(fv.arch.children[fld].attrs.name);
221 var edit_check = new instance.web.Model(this.dataset.model)
222 .call("check_access_rights", ["write", false])
223 .then(function (write_right) {
224 self.write_right = write_right;
226 var init = new instance.web.Model(this.dataset.model)
227 .call("check_access_rights", ["create", false])
228 .then(function (create_right) {
229 self.create_right = create_right;
230 self.init_calendar().then(function() {
231 $(window).trigger('resize');
232 self.trigger('calendar_view_loaded', fv);
233 self.ready.resolve();
236 return $.when(edit_check, init);
239 get_fc_init_options: function () {
240 //Documentation here : http://arshaw.com/fullcalendar/docs/
242 return $.extend({}, get_fc_defaultOptions(), {
244 defaultView: (this.mode == "month")?"month":
245 (this.mode == "week"?"agendaWeek":
246 (this.mode == "day"?"agendaDay":"month")),
248 left: 'prev,next today',
250 right: 'month,agendaWeek,agendaDay'
252 selectable: !this.options.read_only_mode && this.create_right,
254 editable: !this.options.read_only_mode,
259 eventDrop: function (event, _day_delta, _minute_delta, _all_day, _revertFunc) {
260 var data = self.get_event_data(event);
261 self.proxy('update_record')(event._id, data); // we don't revert the event, but update it.
263 eventResize: function (event, _day_delta, _minute_delta, _revertFunc) {
264 var data = self.get_event_data(event);
265 self.proxy('update_record')(event._id, data);
267 eventRender: function (event, element, view) {
268 element.find('.fc-event-title').html(event.title);
270 eventAfterRender: function (event, element, view) {
271 if ((view.name !== 'month') && (((event.end-event.start)/60000)<=30)) {
272 //if duration is too small, we see the html code of img
273 var current_title = $(element.find('.fc-event-time')).text();
274 var new_title = current_title.substr(0,current_title.indexOf("<img")>0?current_title.indexOf("<img"):current_title.length);
275 element.find('.fc-event-time').html(new_title);
278 eventClick: function (event) { self.open_event(event._id,event.title); },
279 select: function (start_date, end_date, all_day, _js_event, _view) {
280 var data_template = self.get_event_data({
285 self.open_quick_create(data_template);
295 calendarMiniChanged: function (context) {
296 return function(datum,obj) {
297 var curView = context.$calendar.fullCalendar( 'getView');
298 var curDate = new Date(obj.currentYear , obj.currentMonth, obj.currentDay);
300 if (curView.name == "agendaWeek") {
301 if (curDate <= curView.end && curDate >= curView.start) {
302 context.$calendar.fullCalendar('changeView','agendaDay');
305 else if (curView.name != "agendaDay" || (curView.name == "agendaDay" && curDate.compareTo(curView.start)===0)) {
306 context.$calendar.fullCalendar('changeView','agendaWeek');
308 context.$calendar.fullCalendar('gotoDate', obj.currentYear , obj.currentMonth, obj.currentDay);
312 init_calendar: function() {
315 if (!this.sidebar && this.options.$sidebar) {
316 translate = get_fc_defaultOptions();
317 this.sidebar = new instance.web_calendar.Sidebar(this);
318 this.sidebar.appendTo(this.$el.find('.oe_calendar_sidebar_container'));
320 this.$small_calendar = self.$el.find(".oe_calendar_mini");
321 this.$small_calendar.datepicker({
322 onSelect: self.calendarMiniChanged(self),
323 dayNamesMin : translate.dayNamesShort,
324 monthNames: translate.monthNamesShort,
325 firstDay: translate.firstDay,
329 if (this.useContacts) {
332 new instance.web.Model("res.users").query(["partner_id"]).filter([["id", "=",this.dataset.context.uid]]).first()
335 var sidebar_items = {};
336 var filter_value = result.partner_id[0];
339 label: result.partner_id[1] + _lt(" [Me]"),
340 color: self.get_color(filter_value),
341 avatar_model: self.avatar_model,
345 sidebar_items[filter_value] = filter_item ;
348 label: _lt("Everybody's calendars"),
349 color: self.get_color(-1),
350 avatar_model: self.avatar_model,
353 sidebar_items[-1] = filter_item ;
354 //Get my coworkers/contacts
355 new instance.web.Model("calendar.contacts").query(["partner_id"]).filter([["user_id", "=",self.dataset.context.uid]]).all().then(function(result) {
356 _.each(result, function(item) {
357 filter_value = item.partner_id[0];
360 label: item.partner_id[1],
361 color: self.get_color(filter_value),
362 avatar_model: self.avatar_model,
365 sidebar_items[filter_value] = filter_item ;
368 self.all_filters = sidebar_items;
369 self.now_filter_ids = $.map(self.all_filters, function(o) { return o.value; });
371 self.sidebar.filter.events_loaded(self.all_filters);
372 self.sidebar.filter.set_filters();
374 self.sidebar.filter.addUpdateButton();
375 }).done(function () {
376 self.$calendar.fullCalendar('refetchEvents');
383 self.$calendar.fullCalendar(self.get_fc_init_options());
387 extraSideBar: function() {
390 open_quick_create: function(data_template) {
391 if (!isNullOrUndef(this.quick)) {
392 return this.quick.trigger('close');
394 var QuickCreate = get_class(this.quick_create_instance);
396 this.options.disable_quick_create = this.options.disable_quick_create || !this.quick_add_pop;
398 this.quick = new QuickCreate(this, this.dataset, true, this.options, data_template);
399 this.quick.on('added', this, this.quick_created)
400 .on('slowadded', this, this.slow_created)
401 .on('close', this, function() {
402 this.quick.destroy();
404 this.$calendar.fullCalendar('unselect');
406 this.quick.replace(this.$el.find('.oe_calendar_qc_placeholder'));
412 * Refresh one fullcalendar event identified by it's 'id' by reading OpenERP record state.
413 * If event was not existent in fullcalendar, it'll be created.
415 refresh_event: function(id) {
417 if (is_virtual_id(id)) {
418 // Should avoid "refreshing" a virtual ID because it can't
419 // really be modified so it should never be refreshed. As upon
420 // edition, a NEW event with a non-virtual id will be created.
421 console.warn("Unwise use of refresh_event on a virtual ID.");
423 this.dataset.read_ids([id], _.keys(this.fields)).done(function (incomplete_records) {
424 self.perform_necessary_name_gets(incomplete_records).then(function(records) {
425 // Event boundaries were already changed by fullcalendar, but we need to reload them:
426 var new_event = self.event_data_transform(records[0]);
428 var event_objs = self.$calendar.fullCalendar('clientEvents', id);
429 if (event_objs.length == 1) { // Already existing obj to update
430 var event_obj = event_objs[0];
432 _(new_event).each(function (value, key) {
433 event_obj[key] = value;
435 self.$calendar.fullCalendar('updateEvent', event_obj);
436 } else { // New event object to create
437 self.$calendar.fullCalendar('renderEvent', new_event);
438 // By forcing attribution of this event to this source, we
439 // make sure that the event will be removed when the source
440 // will be removed (which occurs at each do_search)
441 self.$calendar.fullCalendar('clientEvents', id)[0].source = self.event_source;
447 get_color: function(key) {
448 if (this.color_map[key]) {
449 return this.color_map[key];
451 var index = (((_.keys(this.color_map).length + 1) * 5) % 24) + 1;
452 this.color_map[key] = index;
458 * In o2m case, records from dataset won't have names attached to their *2o values.
459 * We should make sure this is the case.
461 perform_necessary_name_gets: function(evts) {
462 var def = $.Deferred();
465 _(this.info_fields).each(function (fieldname) {
466 if (!_(["many2one", "one2one"]).contains(
467 self.fields[fieldname].type))
469 to_get[fieldname] = [];
470 _(evts).each(function (evt) {
471 var value = evt[fieldname];
472 if (value === false || (value instanceof Array)) {
475 to_get[fieldname].push(value);
477 if (to_get[fieldname].length === 0) {
478 delete to_get[fieldname];
481 var defs = _(to_get).map(function (ids, fieldname) {
482 return (new instance.web.Model(self.fields[fieldname].relation))
483 .call('name_get', ids).then(function (vals) {
484 return [fieldname, vals];
488 $.when.apply(this, defs).then(function() {
489 var values = arguments;
490 _(values).each(function(value) {
491 var fieldname = value[0];
492 var name_gets = value[1];
493 _(name_gets).each(function(name_get) {
495 .filter(function (e) {return e[fieldname] == name_get[0];})
496 .each(function(evt) {
497 evt[fieldname] = name_get;
507 * Transform OpenERP event object to fullcalendar event object
509 event_data_transform: function(evt) {
512 var date_delay = evt[this.date_delay] || 1.0,
513 all_day = this.all_day ? evt[this.all_day] : false,
514 res_computed_text = '',
519 date_start = instance.web.auto_str_to_date(evt[this.date_start]);
520 date_stop = this.date_stop ? instance.web.auto_str_to_date(evt[this.date_stop]) : null;
523 date_start = instance.web.auto_str_to_date(evt[this.date_start].split(' ')[0],'start');
524 date_stop = this.date_stop ? instance.web.auto_str_to_date(evt[this.date_stop].split(' ')[0],'start') : null; //.addSeconds(-1) : null;
527 if (this.info_fields) {
529 res_computed_text = this.how_display_event;
531 _.each(this.info_fields, function (fieldname) {
532 var value = evt[fieldname];
533 if (_.contains(["many2one", "one2one"], self.fields[fieldname].type)) {
534 if (value === false) {
535 temp_ret[fieldname] = null;
537 else if (value instanceof Array) {
538 temp_ret[fieldname] = value[1]; // no name_get to make
541 throw new Error("Incomplete data received from dataset for record " + evt.id);
544 else if (_.contains(["one2many","many2many"], self.fields[fieldname].type)) {
545 if (value === false) {
546 temp_ret[fieldname] = null;
548 else if (value instanceof Array) {
549 temp_ret[fieldname] = value; // if x2many, keep all id !
552 throw new Error("Incomplete data received from dataset for record " + evt.id);
556 temp_ret[fieldname] = value;
558 res_computed_text = res_computed_text.replace("["+fieldname+"]",temp_ret[fieldname]);
562 if (res_computed_text.length) {
563 the_title = res_computed_text;
567 _.each(temp_ret, function(val,key) { res_text.push(val); });
568 the_title = res_text.join(', ');
570 the_title = _.escape(the_title);
573 the_title_avatar = '';
575 if (! _.isUndefined(this.attendee_people)) {
576 var MAX_ATTENDEES = 3;
577 var attendee_showed = 0;
578 var attendee_other = '';
580 _.each(temp_ret[this.attendee_people],
581 function (the_attendee_people) {
582 attendees.push(the_attendee_people);
583 attendee_showed += 1;
584 if (attendee_showed<= MAX_ATTENDEES) {
585 if (self.avatar_model !== null) {
586 the_title_avatar += '<img title="' + self.all_attendees[the_attendee_people] + '" class="attendee_head" \
587 src="/web/binary/image?model=' + self.avatar_model + '&field=image_small&id=' + the_attendee_people + '"></img>';
590 if (!self.colorIsAttendee || the_attendee_people != temp_ret[self.color_field]) {
591 tempColor = (self.all_filters[the_attendee_people] !== undefined)
592 ? self.all_filters[the_attendee_people].color
593 : (self.all_filters[-1] ? self.all_filters[-1].color : 1);
594 the_title_avatar += '<i class="fa fa-user attendee_head color_'+tempColor+'" title="' + self.all_attendees[the_attendee_people] + '" ></i>';
595 }//else don't add myself
599 attendee_other += self.all_attendees[the_attendee_people] +", ";
603 if (attendee_other.length>2) {
604 the_title_avatar += '<span class="attendee_head" title="' + attendee_other.slice(0, -2) + '">+</span>';
606 the_title = the_title_avatar + the_title;
610 if (!date_stop && date_delay) {
611 date_stop = date_start.clone().addHours(date_delay);
614 'start': date_start.toString('yyyy-MM-dd HH:mm:ss'),
615 'end': date_stop.toString('yyyy-MM-dd HH:mm:ss'),
617 'allDay': (this.fields[this.date_start].type == 'date' || (this.all_day && evt[this.all_day]) || false),
619 'attendees':attendees
621 if (!self.useContacts || self.all_filters[evt[this.color_field]] !== undefined) {
622 if (this.color_field && evt[this.color_field]) {
623 var color_key = evt[this.color_field];
624 if (typeof color_key === "object") {
625 color_key = color_key[0];
627 r.className = 'cal_opacity calendar_color_'+ this.get_color(color_key);
630 else { // if form all, get color -1
631 r.className = 'cal_opacity calendar_color_'+ self.all_filters[-1].color;
637 * Transform fullcalendar event object to OpenERP Data object
639 get_event_data: function(event) {
641 // Normalize event_end without changing fullcalendars event.
646 var event_end = event.end;
647 //Bug when we move an all_day event from week or day view, we don't have a dateend or duration...
648 if (event_end == null) {
649 event_end = new Date(event.start).addHours(2);
653 // Sometimes fullcalendar doesn't give any event.end.
654 if (event_end == null || _.isUndefined(event_end)) {
655 event_end = new Date(event.start);
658 //event_end = (new Date(event_end.getTime())).addDays(1);
659 date_start_day = new Date(Date.UTC(event.start.getFullYear(),event.start.getMonth(),event.start.getDate()));
660 date_stop_day = new Date(Date.UTC(event_end.getFullYear(),event_end.getMonth(),event_end.getDate()));
663 date_start_day = new Date(event.start.getFullYear(),event.start.getMonth(),event.start.getDate(),7);
664 date_stop_day = new Date(event_end.getFullYear(),event_end.getMonth(),event_end.getDate(),19);
666 diff_seconds = Math.round((date_stop_day.getTime() - date_start_day.getTime()) / 1000);
670 data[this.date_start] = event.start;
671 if (this.date_stop) {
672 data[this.date_stop] = event_end;
674 diff_seconds = Math.round((event_end.getTime() - event.start.getTime()) / 1000);
678 data[this.all_day] = event.allDay;
681 if (this.date_delay) {
683 data[this.date_delay] = diff_seconds / 3600;
688 do_search: function(domain, context, _group_by) {
690 if (! self.all_filters) {
691 self.all_filters = {}
694 if (! _.isUndefined(this.event_source)) {
695 this.$calendar.fullCalendar('removeEventSource', this.event_source);
697 this.event_source = {
698 events: function(start, end, callback) {
699 var current_event_source = self.event_source;
700 self.dataset.read_slice(_.keys(self.fields), {
702 domain: self.get_range_domain(domain, start, end),
704 }).done(function(events) {
706 if (self.event_source !== current_event_source) {
707 console.log("Consecutive ``do_search`` called. Cancelling.");
711 if (!self.useContacts) { // If we use all peoples displayed in the current month as filter in sidebars
715 self.now_filter_ids = [];
717 _.each(events, function (e) {
718 filter_value = e[self.color_field][0];
719 if (!self.all_filters[e[self.color_field][0]]) {
722 label: e[self.color_field][1],
723 color: self.get_color(filter_value),
724 avatar_model: (_.str.toBoolElse(self.avatar_filter, true) ? self.avatar_filter : false ),
727 self.all_filters[e[self.color_field][0]] = filter_item;
729 if (! _.contains(self.now_filter_ids, filter_value)) {
730 self.now_filter_ids.push(filter_value);
735 self.sidebar.filter.events_loaded();
736 self.sidebar.filter.set_filters();
738 events = $.map(events, function (e) {
739 if (_.contains(self.now_filter_ids,e[self.color_field][0]) && self.all_filters[e[self.color_field][0]].is_checked) {
747 else { //WE USE CONTACT
748 if (self.attendee_people !== undefined) {
749 //if we don't filter on 'Everybody's Calendar
750 if (!self.all_filters[-1] || !self.all_filters[-1].is_checked) {
751 var checked_filter = $.map(self.all_filters, function(o) { if (o.is_checked) { return o.value; }});
752 // If we filter on contacts... we keep only events from coworkers
753 events = $.map(events, function (e) {
754 if (_.intersection(checked_filter,e[self.attendee_people]).length) {
765 var all_attendees = $.map(events, function (e) { return e[self.attendee_people]; });
766 all_attendees = _.chain(all_attendees).flatten().uniq().value();
768 self.all_attendees = {};
769 if (self.avatar_title !== null) {
770 new instance.web.Model(self.avatar_title).query(["name"]).filter([["id", "in", all_attendees]]).all().then(function(result) {
771 _.each(result, function(item) {
772 self.all_attendees[item.id] = item.name;
775 return self.perform_necessary_name_gets(events).then(callback);
779 _.each(all_attendees,function(item){
780 self.all_attendees[item] = '';
782 return self.perform_necessary_name_gets(events).then(callback);
786 eventDataTransform: function (event) {
787 return self.event_data_transform(event);
790 this.$calendar.fullCalendar('addEventSource', this.event_source);
793 * Build OpenERP Domain to filter object by this.date_start field
794 * between given start, end dates.
796 get_range_domain: function(domain, start, end) {
797 var format = instance.web.date_to_str;
799 extend_domain = [[this.date_start, '>=', format(start.clone())],
800 [this.date_start, '<=', format(end.clone())]];
802 if (this.date_stop) {
804 extend_domain.splice(0,0,'|','|','&');
808 [this.date_start, '<=', format(start.clone())],
809 [this.date_stop, '>=', format(start.clone())],
811 [this.date_start, '<=', format(end.clone())],
812 [this.date_stop, '>=', format(start.clone())]
814 //final -> (A & B) | (C & D) | (E & F) -> | | & A B & C D & E F
816 return new instance.web.CompoundDomain(domain, extend_domain);
820 * Updates record identified by ``id`` with values in object ``data``
822 update_record: function(id, data) {
824 delete(data.name); // Cannot modify actual name yet
825 var index = this.dataset.get_id_index(id);
826 if (index !== null) {
827 event_id = this.dataset.ids[index];
828 this.dataset.write(event_id, data, {}).done(function() {
829 if (is_virtual_id(event_id)) {
830 // this is a virtual ID and so this will create a new event
831 // with an unknown id for us.
832 self.$calendar.fullCalendar('refetchEvents');
834 // classical event that we can refresh
835 self.refresh_event(event_id);
841 open_event: function(id, title) {
843 if (! this.open_popup_action) {
844 var index = this.dataset.get_id_index(id);
845 this.dataset.index = index;
846 if (this.write_right) {
847 this.do_switch_view('form', null, { mode: "edit" });
849 this.do_switch_view('form', null, { mode: "view" });
853 var pop = new instance.web.form.FormOpenPopup(this);
854 pop.show_element(this.dataset.model, id, this.dataset.get_context(), {
855 title: _.str.sprintf(_t("View: %s"),title),
856 view_id: +this.open_popup_action,
862 var form_controller = pop.view_form;
863 form_controller.on("load_record", self, function(){
864 button_delete = _.str.sprintf("<button class='oe_button oe_bold delme'><span> %s </span></button>",_t("Delete"));
865 button_edit = _.str.sprintf("<button class='oe_button oe_bold editme oe_highlight'><span> %s </span></button>",_t("Edit Event"));
867 pop.$el.closest(".modal").find(".modal-footer").prepend(button_delete);
868 pop.$el.closest(".modal").find(".modal-footer").prepend(button_edit);
872 $('.oe_form_button_cancel').trigger('click');
873 self.remove_event(id);
878 $('.oe_form_button_cancel').trigger('click');
879 self.dataset.index = self.dataset.get_id_index(id);
880 self.do_switch_view('form', null, { mode: "edit" });
888 do_show: function() {
890 this.$buttons.show();
892 this.do_push_state({});
893 return this._super();
895 do_hide: function () {
897 this.$buttons.hide();
899 return this._super();
901 is_action_enabled: function(action) {
902 if (action === 'create' && !this.options.creatable) {
905 return this._super(action);
909 * Handles a newly created record
911 * @param {id} id of the newly created record
913 quick_created: function (id) {
916 * it's of the most utter importance NOT to use inplace
917 * modification on this.dataset.ids as reference to this
918 * data is spread out everywhere in the various widget.
919 * Some of these reference includes values that should
920 * trigger action upon modification.
922 this.dataset.ids = this.dataset.ids.concat([id]);
923 this.dataset.trigger("dataset_changed", id);
924 this.refresh_event(id);
926 slow_created: function () {
927 // refresh all view, because maybe some recurrents item
930 // force filter refresh
931 self.sidebar.filter.is_loaded = false;
933 self.$calendar.fullCalendar('refetchEvents');
936 remove_event: function(id) {
939 return $.when(self.dataset.unlink([id])).then(function() {
940 self.$calendar.fullCalendar('removeEvents', id);
943 if (this.options.confirm_on_delete) {
944 if (confirm(_t("Are you sure you want to delete this record ?"))) {
954 * Quick creation view.
956 * Triggers a single event "added" with a single parameter "name", which is the
957 * name entered by the user
962 instance.web_calendar.QuickCreate = instance.web.Widget.extend({
963 template: 'CalendarView.quick_create',
965 init: function(parent, dataset, buttons, options, data_template) {
967 this.dataset = dataset;
968 this._buttons = buttons || false;
969 this.options = options;
971 // Can hold data pre-set from where you clicked on agenda
972 this.data_template = data_template || {};
974 get_title: function () {
975 var parent = this.getParent();
976 if (_.isUndefined(parent)) {
979 var title = (_.isUndefined(parent.field_widget)) ?
980 (parent.string || parent.name) :
981 parent.field_widget.string || parent.field_widget.name || '';
982 return _t("Create: ") + title;
987 if (this.options.disable_quick_create) {
993 self.$input = this.$el.find('input');
994 self.$input.keyup(function enterHandler (event) {
995 if(event.keyCode == 13){
996 self.$input.off('keyup', enterHandler);
997 if (!self.quick_add()){
998 self.$input.on('keyup', enterHandler);
1003 var submit = this.$el.find(".oe_calendar_quick_create_add");
1004 submit.click(function clickHandler() {
1005 submit.off('click', clickHandler);
1006 if (!self.quick_add()){
1007 submit.on('click', clickHandler); }
1010 this.$el.find(".oe_calendar_quick_create_edit").click(function () {
1014 this.$el.find(".oe_calendar_quick_create_close").click(function (ev) {
1015 ev.preventDefault();
1016 self.trigger('close');
1018 self.$input.keyup(function enterHandler (e) {
1019 if (e.keyCode == 27 && self._buttons) {
1020 self.trigger('close');
1023 self.$el.dialog({ title: this.get_title()});
1024 self.on('added', self, function() {
1025 self.trigger('close');
1028 self.$el.on('dialogclose', self, function() {
1029 self.trigger('close');
1034 this.$el.find('input').focus();
1038 * Gathers data from the quick create dialog a launch quick_create(data) method
1040 quick_add: function() {
1041 var val = this.$input.val();
1042 if (/^\s*$/.test(val)) {
1045 return this.quick_create({'name': val}).always(function() { return true; });
1048 slow_add: function() {
1049 var val = this.$input.val();
1050 this.slow_create({'name': val});
1054 * Handles saving data coming from quick create box
1056 quick_create: function(data, options) {
1058 return this.dataset.create($.extend({}, this.data_template, data), options)
1059 .then(function(id) {
1060 self.trigger('added', id);
1061 self.$input.val("");
1062 }).fail(function(r, event) {
1063 event.preventDefault();
1064 // This will occurs if there are some more fields required
1065 self.slow_create(data);
1070 * Show full form popup
1072 get_form_popup_infos: function() {
1073 var parent = this.getParent();
1078 if (!_.isUndefined(parent) && !(_.isUndefined(parent.ViewManager))) {
1079 infos.view_id = parent.ViewManager.get_view_id('form');
1083 slow_create: function(data) {
1084 //if all day, we could reset time to display 00:00:00
1087 var def = $.Deferred();
1090 _.each($.extend({}, this.data_template, data), function(val, field_name) {
1091 defaults['default_' + field_name] = val;
1094 var pop_infos = self.get_form_popup_infos();
1095 var pop = new instance.web.form.FormOpenPopup(this);
1096 var context = new instance.web.CompoundContext(this.dataset.context, defaults);
1097 pop.show_element(this.dataset.model, null, this.dataset.get_context(defaults), {
1098 title: this.get_title(),
1099 disable_multiple_selection: true,
1100 view_id: pop_infos.view_id,
1101 // Ensuring we use ``self.dataset`` and DO NOT create a new one.
1102 create_function: function(data, options) {
1103 return self.dataset.create(data, options).done(function(r) {
1104 }).fail(function (r, event) {
1105 if (!r.data.message) { //else manage by openerp
1110 read_function: function(id, fields, options) {
1111 return self.dataset.read_ids.apply(self.dataset, arguments).done(function() {
1112 }).fail(function (r, event) {
1113 if (!r.data.message) { //else manage by openerp
1119 pop.on('closed', self, function() {
1120 // ``self.trigger('close')`` would itself destroy all child element including
1121 // the slow create popup, which would then re-trigger recursively the 'closed' signal.
1122 // Thus, here, we use a deferred and its state to cut the endless recurrence.
1123 if (def.state() === "pending") {
1127 pop.on('create_completed', self, function(id) {
1128 self.trigger('slowadded');
1130 def.then(function() {
1131 self.trigger('close');
1142 function widget_calendar_lazy_init() {
1143 if (instance.web.form.Many2ManyCalendarView) {
1147 instance.web_calendar.FieldCalendarView = instance.web_calendar.CalendarView.extend({
1149 init: function (parent) {
1150 this._super.apply(this, arguments);
1151 // Warning: this means only a field_widget should instanciate this Class
1152 this.field_widget = parent;
1155 view_loading: function (fv) {
1157 return $.when(this._super.apply(this, arguments)).then(function() {
1158 self.on('event_rendered', this, function (event, element, view) {
1164 // In forms, we could be hidden in a notebook. Thus we couldn't
1165 // render correctly fullcalendar so we try to detect when we are
1166 // not visible to wait for when we will be visible.
1167 init_calendar: function() {
1168 if (this.$calendar.width() !== 0) { // visible
1169 return this._super();
1171 // find all parents tabs.
1172 var def = $.Deferred();
1174 this.$calendar.parents(".ui-tabs").on('tabsactivate', this, function() {
1175 if (self.$calendar.width() !== 0) { // visible
1176 self.$calendar.fullCalendar(self.get_fc_init_options());
1185 instance.web_calendar.BufferedDataSet = instance.web.BufferedDataSet.extend({
1188 * Adds verification on possible missing fields for the sole purpose of
1189 * O2M dataset being compatible with the ``slow_create`` detection of
1190 * missing fields... which is as simple to try to write and upon failure
1191 * go to ``slow_create``. Current BufferedDataSet would'nt fail because
1192 * they do not send data to the server at create time.
1194 create: function (data, options) {
1195 var def = $.Deferred();
1197 var create = this._super;
1198 if (_.isUndefined(this.required_fields)) {
1199 this.required_fields = (new instance.web.Model(this.model))
1200 .call('fields_get').then(function (fields_def) {
1201 return _(fields_def).chain()
1202 // equiv to .pairs()
1203 .map(function (value, key) { return [key, value]; })
1204 // equiv to .omit(self.field_widget.field.relation_field)
1205 .filter(function (pair) { return pair[0] !== self.field_widget.field.relation_field; })
1206 .filter(function (pair) { return pair[1].required; })
1207 .map(function (pair) { return pair[0]; })
1211 $.when(this.required_fields).then(function (required_fields) {
1212 var missing_fields = _(required_fields).filter(function (v) {
1213 return _.isUndefined(data[v]);
1215 var default_get = (missing_fields.length !== 0) ?
1216 self.default_get(missing_fields) : [];
1217 $.when(default_get).then(function (defaults) {
1219 // Remove all fields that have a default from the missing fields.
1220 missing_fields = _(missing_fields).filter(function (f) {
1221 return _.isUndefined(defaults[f]);
1223 if (missing_fields.length !== 0) {
1226 _t("Missing required fields %s"), missing_fields.join(", ")),
1230 create.apply(self, [data, options]).then(function (result) {
1231 def.resolve(result);
1239 instance.web_calendar.fields_dataset = new instance.web.Registry({
1240 'many2many': 'instance.web.DataSetStatic',
1241 'one2many': 'instance.web_calendar.BufferedDataSet',
1245 function get_field_dataset_class(type) {
1246 var obj = instance.web_calendar.fields_dataset.get_any([type]);
1248 throw new Error(_.str.sprintf(_t("Dataset for type '%s' is not defined."), type));
1251 // Override definition of legacy datasets to add field_widget context
1253 init: function (parent) {
1254 this._super.apply(this, arguments);
1255 this.field_widget = parent;
1257 get_context: function() {
1258 this.context = this.field_widget.build_context();
1259 return this.context;
1265 * Common part to manage any field using calendar view
1267 instance.web_calendar.FieldCalendar = instance.web.form.AbstractField.extend({
1269 disable_utility_classes: true,
1270 calendar_view_class: 'instance.web_calendar.FieldCalendarView',
1272 init: function(field_manager, node) {
1273 this._super(field_manager, node);
1274 widget_calendar_lazy_init();
1275 this.is_loaded = $.Deferred();
1276 this.initial_is_loaded = this.is_loaded;
1280 // This dataset will use current widget to '.build_context()'.
1281 var field_type = field_manager.fields_view.fields[node.attrs.name].type;
1282 this.dataset = new (get_field_dataset_class(field_type))(
1283 this, this.field.relation);
1285 this.dataset.on('unlink', this, function(_ids) {
1286 this.dataset.trigger('dataset_changed');
1289 // quick_create widget instance will be attached when spawned
1290 this.quick_create = null;
1292 this.no_rerender = true;
1297 this._super.apply(this, arguments);
1302 self.on("change:effective_readonly", self, function() {
1303 self.is_loaded = self.is_loaded.then(function() {
1304 self.calendar_view.destroy();
1305 return $.when(self.load_view()).done(function() {
1306 self.render_value();
1312 load_view: function() {
1314 var calendar_view_class = get_class(this.calendar_view_class);
1315 this.calendar_view = new calendar_view_class(this, this.dataset, false, $.extend({
1316 'create_text': _t("Add"),
1317 'creatable': self.get("effective_readonly") ? false : true,
1318 'quick_creatable': self.get("effective_readonly") ? false : true,
1319 'read_only_mode': self.get("effective_readonly") ? true : false,
1320 'confirm_on_delete': false,
1322 var embedded = (this.field.views || {}).calendar;
1324 this.calendar_view.set_embedded_view(embedded);
1326 var loaded = $.Deferred();
1327 this.calendar_view.on("calendar_view_loaded", self, function() {
1328 self.initial_is_loaded.resolve();
1331 this.calendar_view.on('switch_mode', this, this.open_popup);
1332 $.async_when().done(function () {
1333 self.calendar_view.appendTo(self.$el);
1338 render_value: function() {
1340 this.dataset.set_ids(this.get("value"));
1341 this.is_loaded = this.is_loaded.then(function() {
1342 return self.calendar_view.do_search(self.build_domain(), self.dataset.get_context(), []);
1346 open_popup: function(type, unused) {
1347 if (type !== "form") { return; }
1348 if (this.dataset.index == null) {
1349 if (typeof this.open_popup_add === "function") {
1350 this.open_popup_add();
1353 if (typeof this.open_popup_edit === "function") {
1354 this.open_popup_edit();
1359 open_popup_add: function() {
1360 throw new Error("Not Implemented");
1363 open_popup_edit: function() {
1364 var id = this.dataset.ids[this.dataset.index];
1366 var pop = (new instance.web.form.FormOpenPopup(this));
1367 pop.show_element(this.field.relation, id, this.build_context(), {
1368 title: _t("Open: ") + this.string,
1369 write_function: function(id, data, _options) {
1370 return self.dataset.write(id, data, {}).done(function() {
1371 // Note that dataset will trigger itself the
1372 // ``dataset_changed`` signal
1373 self.calendar_view.refresh_event(id);
1376 read_function: function(id, fields, options) {
1377 return self.dataset.read_ids.apply(self.dataset, arguments).done(function() {
1378 }).fail(function (r, event) {
1383 alternative_form_view: this.field.views ? this.field.views.form : undefined,
1384 parent_view: this.view,
1385 child_name: this.name,
1386 readonly: this.get("effective_readonly")
1391 instance.web_calendar.Sidebar = instance.web.Widget.extend({
1392 template: 'CalendarView.sidebar',
1396 this.filter = new instance.web_calendar.SidebarFilter(this, this.getParent());
1397 this.filter.appendTo(this.$el.find('.oe_calendar_filter'));
1400 instance.web_calendar.SidebarFilter = instance.web.Widget.extend({
1402 'change input:checkbox': 'filter_click'
1404 init: function(parent, view) {
1405 this._super(parent);
1408 set_filters: function() {
1410 _.forEach(self.view.all_filters, function(o) {
1411 if (_.contains(self.view.now_filter_ids, o.value)) {
1412 self.$('div.oe_calendar_responsible input[value=' + o.value + ']').prop('checked',o.is_checked);
1416 events_loaded: function(filters) {
1418 if (filters == null) {
1420 _.forEach(self.view.all_filters, function(o) {
1421 if (_.contains(self.view.now_filter_ids, o.value)) {
1426 this.$el.html(QWeb.render('CalendarView.sidebar.responsible', { filters: filters }));
1428 filter_click: function(e) {
1430 self.view.all_filters[parseInt(e.target.value)].is_checked = e.target.checked;
1431 self.view.$calendar.fullCalendar('refetchEvents');
1433 addUpdateButton: function() {
1435 this.$('div.oe_calendar_all_responsibles').append(QWeb.render('CalendarView.sidebar.button_add_contact'));
1436 this.$(".add_contacts_link_btn").on('click', function() {
1437 self.rpc("/web/action/load", {
1438 action_id: "calendar.action_calendar_contacts"
1439 }).then( function(result) { return self.do_action(result); });