1 # -*- coding: utf-8 -*-
7 import openerp.service.report
9 from werkzeug.exceptions import BadRequest
10 from datetime import datetime, timedelta
11 from dateutil import parser
12 from dateutil import rrule
13 from dateutil.relativedelta import relativedelta
14 from openerp import tools, SUPERUSER_ID
15 from openerp.osv import fields, osv
16 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
17 from openerp.tools.translate import _
18 from openerp.http import request
19 from operator import itemgetter
22 _logger = logging.getLogger(__name__)
25 def calendar_id2real_id(calendar_id=None, with_date=False):
27 Convert a "virtual/recurring event id" (type string) into a real event id (type int).
28 E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
29 @param calendar_id: id of calendar
30 @param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id
31 @return: real event id
33 if calendar_id and isinstance(calendar_id, (str, unicode)):
34 res = calendar_id.split('-')
38 real_date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT, time.strptime(res[1], "%Y%m%d%H%M%S"))
39 start = datetime.strptime(real_date, DEFAULT_SERVER_DATETIME_FORMAT)
40 end = start + timedelta(hours=with_date)
41 return (int(real_id), real_date, end.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
43 return calendar_id and int(calendar_id) or calendar_id
46 def get_real_ids(ids):
47 if isinstance(ids, (str, int, long)):
48 return calendar_id2real_id(ids)
50 if isinstance(ids, (list, tuple)):
51 return [calendar_id2real_id(id) for id in ids]
54 class calendar_attendee(osv.Model):
56 Calendar Attendee Information
58 _name = 'calendar.attendee'
60 _description = 'Attendee information'
62 def _compute_data(self, cr, uid, ids, name, arg, context=None):
64 Compute data on function fields for attendee values.
65 @param ids: list of calendar attendee's IDs
66 @param name: name of field
67 @return: dictionary of form {id: {'field Name': value'}}
71 for attdata in self.browse(cr, uid, ids, context=context):
75 if attdata.partner_id:
76 result[id][name] = attdata.partner_id.name or False
78 result[id][name] = attdata.email or ''
82 ('needsAction', 'Needs Action'),
83 ('tentative', 'Uncertain'),
84 ('declined', 'Declined'),
85 ('accepted', 'Accepted'),
89 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="Status of the attendee's participation"),
90 'cn': fields.function(_compute_data, string='Common name', type="char", multi='cn', store=True),
91 'partner_id': fields.many2one('res.partner', 'Contact', readonly="True"),
92 'email': fields.char('Email', help="Email of Invited Person"),
93 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
94 'access_token': fields.char('Invitation Token'),
95 'event_id': fields.many2one('calendar.event', 'Meeting linked'),
98 'state': 'needsAction',
101 def copy(self, cr, uid, id, default=None, context=None):
102 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
104 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
106 Make entry on email and availability on change of partner_id field.
107 @param partner_id: changed value of partner id
110 return {'value': {'email': ''}}
111 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
112 return {'value': {'email': partner.email}}
114 def get_ics_file(self, cr, uid, event_obj, context=None):
116 Returns iCalendar file for the event invitation.
117 @param event_obj: event object (browse record)
118 @return: .ics file content
122 def ics_datetime(idate, allday=False):
125 return datetime.strptime(idate.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
127 return datetime.strptime(idate.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
131 # FIXME: why isn't this in CalDAV?
136 cal = vobject.iCalendar()
137 event = cal.add('vevent')
138 if not event_obj.start or not event_obj.stop:
139 raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation."))
140 event.add('created').value = ics_datetime(time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
141 event.add('dtstart').value = ics_datetime(event_obj.start, event_obj.allday)
142 event.add('dtend').value = ics_datetime(event_obj.stop, event_obj.allday)
143 event.add('summary').value = event_obj.name
144 if event_obj.description:
145 event.add('description').value = event_obj.description
146 if event_obj.location:
147 event.add('location').value = event_obj.location
149 event.add('rrule').value = event_obj.rrule
151 if event_obj.alarm_ids:
152 for alarm in event_obj.alarm_ids:
153 valarm = event.add('valarm')
154 interval = alarm.interval
155 duration = alarm.duration
156 trigger = valarm.add('TRIGGER')
157 trigger.params['related'] = ["START"]
158 if interval == 'days':
159 delta = timedelta(days=duration)
160 elif interval == 'hours':
161 delta = timedelta(hours=duration)
162 elif interval == 'minutes':
163 delta = timedelta(minutes=duration)
164 trigger.value = delta
165 valarm.add('DESCRIPTION').value = alarm.name or 'OpenERP'
166 for attendee in event_obj.attendee_ids:
167 attendee_add = event.add('attendee')
168 attendee_add.value = 'MAILTO:' + (attendee.email or '')
169 res = cal.serialize()
172 def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False),
173 template_xmlid='calendar_template_meeting_invitation', context=None):
175 Send mail for event invitation to event attendees.
176 @param email_from: email address for user sending the mail
180 if self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_mail', default=False) or context.get("no_mail_to_attendees"):
184 data_pool = self.pool['ir.model.data']
185 mailmess_pool = self.pool['mail.message']
186 mail_pool = self.pool['mail.mail']
187 template_pool = self.pool['email.template']
188 local_context = context.copy()
190 'needsAction': 'grey',
192 'tentative': '#FFFF00',
196 if not isinstance(ids, (tuple, list)):
199 dummy, template_id = data_pool.get_object_reference(cr, uid, 'calendar', template_xmlid)
200 dummy, act_id = data_pool.get_object_reference(cr, uid, 'calendar', "view_calendar_event_calendar")
201 local_context.update({
203 'action_id': self.pool['ir.actions.act_window'].search(cr, uid, [('view_id', '=', act_id)], context=context)[0],
205 'base_url': self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
208 for attendee in self.browse(cr, uid, ids, context=context):
209 if attendee.email and email_from and attendee.email != email_from:
210 ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
211 mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
215 vals['attachment_ids'] = [(0, 0, {'name': 'invitation.ics',
216 'datas_fname': 'invitation.ics',
217 'datas': str(ics_file).encode('base64')})]
218 vals['model'] = None # We don't want to have the mail in the tchatter while in queue!
219 the_mailmess = mail_pool.browse(cr, uid, mail_id, context=context).mail_message_id
220 mailmess_pool.write(cr, uid, [the_mailmess.id], vals, context=context)
221 mail_ids.append(mail_id)
224 res = mail_pool.send(cr, uid, mail_ids, context=context)
228 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
230 Make entry on email and availability on change of user_id field.
231 @param ids: list of attendee's IDs
232 @param user_id: changed value of User id
233 @return: dictionary of values which put value in email and availability fields
236 return {'value': {'email': ''}}
238 user = self.pool['res.users'].browse(cr, uid, user_id, *args)
239 return {'value': {'email': user.email, 'availability': user.availability}}
241 def do_tentative(self, cr, uid, ids, context=None, *args):
243 Makes event invitation as Tentative.
244 @param ids: list of attendee's IDs
246 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
248 def do_accept(self, cr, uid, ids, context=None, *args):
250 Marks event invitation as Accepted.
251 @param ids: list of attendee's IDs
255 meeting_obj = self.pool['calendar.event']
256 res = self.write(cr, uid, ids, {'state': 'accepted'}, context)
257 for attendee in self.browse(cr, uid, ids, context=context):
258 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)),
259 subtype="calendar.subtype_invitation", context=context)
263 def do_decline(self, cr, uid, ids, context=None, *args):
265 Marks event invitation as Declined.
266 @param ids: list of calendar attendee's IDs
270 meeting_obj = self.pool['calendar.event']
271 res = self.write(cr, uid, ids, {'state': 'declined'}, context)
272 for attendee in self.browse(cr, uid, ids, context=context):
273 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has declined invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
276 def create(self, cr, uid, vals, context=None):
279 if not vals.get("email") and vals.get("cn"):
280 cnval = vals.get("cn").split(':')
281 email = filter(lambda x: x.__contains__('@'), cnval)
282 vals['email'] = email and email[0] or ''
283 vals['cn'] = vals.get("cn")
284 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
288 class res_partner(osv.Model):
289 _inherit = 'res.partner'
291 'calendar_last_notif_ack': fields.datetime('Last notification marked as read from base Calendar'),
294 def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
296 Return a list of tuple (id, name, status)
297 Used by web_calendar.js : Many2ManyAttendee
302 meeting = self.pool['calendar.event'].browse(cr, uid, get_real_ids(meeting_id), context=context)
303 for partner in self.browse(cr, uid, ids, context=context):
304 data = self.name_get(cr, uid, [partner.id], context)[0]
306 for attendee in meeting.attendee_ids:
307 if attendee.partner_id.id == partner.id:
308 data = (data[0], data[1], attendee.state)
312 def calendar_last_notif_ack(self, cr, uid, context=None):
313 partner = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id
314 self.write(cr, uid, partner.id, {'calendar_last_notif_ack': datetime.now()}, context=context)
318 class calendar_alarm_manager(osv.AbstractModel):
319 _name = 'calendar.alarm_manager'
321 def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None):
326 cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm,
328 WHEN cal.recurrency THEN cal.final_date - interval '1' minute * calcul_delta.min_delta
329 ELSE cal.stop - interval '1' minute * calcul_delta.min_delta
331 cal.start as first_event_date,
333 WHEN cal.recurrency THEN cal.final_date
335 END as last_event_date,
336 calcul_delta.min_delta,
337 calcul_delta.max_delta,
340 calendar_event AS cal
344 rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
346 calendar_alarm_calendar_event_rel AS rel
347 LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
348 WHERE alarm.type in %s
349 GROUP BY rel.calendar_event_id
350 ) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id
354 RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
355 AND part_rel.res_partner_id = %s
361 type_to_read += ('notification',)
363 type_to_read += ('email',)
365 tuple_params = (type_to_read,)
367 # ADD FILTER ON PARTNER_ID
369 base_request += filter_user
370 tuple_params += (partner_id, )
373 tuple_params += (seconds, seconds,)
375 cr.execute("""SELECT *
376 FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS
377 WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second )
378 AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%%s' second )
379 """ % base_request, tuple_params)
381 for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
383 'event_id': event_id,
384 'first_alarm': first_alarm,
385 'last_alarm': last_alarm,
386 'first_meeting': first_meeting,
387 'last_meeting': last_meeting,
388 'min_duration': min_duration,
389 'max_duration': max_duration,
395 def do_check_alarm_for_one_date(self, cr, uid, one_date, event, event_maxdelta, in_the_next_X_seconds, after=False, notif=True, mail=True, context=None):
400 alarm_type.append('notification')
402 alarm_type.append('email')
404 if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
405 for alarm in event.alarm_ids:
406 if alarm.type in alarm_type and \
407 one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
408 (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)):
410 'alarm_id': alarm.id,
411 'event_id': event.id,
412 'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
417 def get_next_mail(self, cr, uid, context=None):
419 cron = self.pool['ir.model.data'].get_object(
420 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
422 _logger.error("Cron for " + self._name + " can not be identified !")
425 if cron.interval_type == "weeks":
426 cron_interval = cron.interval_number * 7 * 24 * 60 * 60
427 elif cron.interval_type == "days":
428 cron_interval = cron.interval_number * 24 * 60 * 60
429 elif cron.interval_type == "hours":
430 cron_interval = cron.interval_number * 60 * 60
431 elif cron.interval_type == "minutes":
432 cron_interval = cron.interval_number * 60
433 elif cron.interval_type == "seconds":
434 cron_interval = cron.interval_number
436 cron_interval = False
438 if not cron_interval:
439 _logger.error("Cron delay can not be computed !")
442 all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
444 for event in all_events: # .values()
445 max_delta = all_events[event]['max_duration']
446 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
447 if curEvent.recurrency:
450 for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
451 in_date_format = one_date.replace(tzinfo=None)
452 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
454 for alert in LastFound:
455 self.do_mail_reminder(cr, uid, alert, context=context)
457 if not bFound: # if it's the first alarm for this recurrent event
459 if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event
462 in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT)
463 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
465 for alert in LastFound:
466 self.do_mail_reminder(cr, uid, alert, context=context)
468 def get_next_notif(self, cr, uid, context=None):
469 ajax_check_every_seconds = 300
470 partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
476 all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
478 for event in all_events: # .values()
479 max_delta = all_events[event]['max_duration']
480 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
481 if curEvent.recurrency:
484 for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
485 in_date_format = one_date.replace(tzinfo=None)
486 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner.calendar_last_notif_ack, mail=False, context=context)
488 for alert in LastFound:
489 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
490 if not bFound: # if it's the first alarm for this recurrent event
492 if bFound and not LastFound: # if the precedent event had alarm but not this one, we can stop the search fot this event
495 in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT)
496 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, partner.calendar_last_notif_ack, mail=False, context=context)
498 for alert in LastFound:
499 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
502 def do_mail_reminder(self, cr, uid, alert, context=None):
507 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
508 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
510 if alarm.type == 'email':
511 res = self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], template_xmlid='calendar_template_meeting_reminder', context=context)
515 def do_notif_reminder(self, cr, uid, alert, context=None):
516 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
517 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
519 if alarm.type == 'notification':
520 message = event.display_time
522 delta = alert['notify_at'] - datetime.now()
523 delta = delta.seconds + delta.days * 3600 * 24
526 'event_id': event.id,
530 'notify_at': alert['notify_at'].strftime(DEFAULT_SERVER_DATETIME_FORMAT),
534 class calendar_alarm(osv.Model):
535 _name = 'calendar.alarm'
536 _description = 'Event alarm'
538 def _get_duration(self, cr, uid, ids, field_name, arg, context=None):
540 for alarm in self.browse(cr, uid, ids, context=context):
541 if alarm.interval == "minutes":
542 res[alarm.id] = alarm.duration
543 elif alarm.interval == "hours":
544 res[alarm.id] = alarm.duration * 60
545 elif alarm.interval == "days":
546 res[alarm.id] = alarm.duration * 60 * 24
552 'name': fields.char('Name', required=True),
553 'type': fields.selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True),
554 'duration': fields.integer('Amount', required=True),
555 'interval': fields.selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], 'Unit', required=True),
556 'duration_minutes': fields.function(_get_duration, type='integer', string='duration_minutes', store=True),
560 'type': 'notification',
565 def _update_cron(self, cr, uid, context=None):
567 cron = self.pool['ir.model.data'].get_object(
568 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
571 return cron.toggle(model=self._name, domain=[('type', '=', 'email')])
573 def create(self, cr, uid, values, context=None):
574 res = super(calendar_alarm, self).create(cr, uid, values, context=context)
576 self._update_cron(cr, uid, context=context)
580 def write(self, cr, uid, ids, values, context=None):
581 res = super(calendar_alarm, self).write(cr, uid, ids, values, context=context)
583 self._update_cron(cr, uid, context=context)
587 def unlink(self, cr, uid, ids, context=None):
588 res = super(calendar_alarm, self).unlink(cr, uid, ids, context=context)
590 self._update_cron(cr, uid, context=context)
595 class ir_values(osv.Model):
596 _inherit = 'ir.values'
598 def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
601 if type(data) in (list, tuple):
602 new_model.append((data[0], calendar_id2real_id(data[1])))
604 new_model.append(data)
605 return super(ir_values, self).set(cr, uid, key, key2, name, new_model,
606 value, replace, isobject, meta, preserve_user, company)
608 def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
613 if type(data) in (list, tuple):
614 new_model.append((data[0], calendar_id2real_id(data[1])))
616 new_model.append(data)
617 return super(ir_values, self).get(cr, uid, key, key2, new_model,
618 meta, context, res_id_req, without_user, key2_req)
621 class ir_model(osv.Model):
623 _inherit = 'ir.model'
625 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
626 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
629 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, context=context, load=load)
632 val['id'] = calendar_id2real_id(val['id'])
633 return isinstance(ids, (str, int, long)) and data[0] or data
636 original_exp_report = openerp.service.report.exp_report
639 def exp_report(db, uid, object, ids, data=None, context=None):
643 if object == 'printscreen.list':
644 original_exp_report(db, uid, object, ids, data, context)
647 new_ids.append(calendar_id2real_id(id))
648 if data.get('id', False):
649 data['id'] = calendar_id2real_id(data['id'])
650 return original_exp_report(db, uid, object, new_ids, data, context)
653 openerp.service.report.exp_report = exp_report
656 class calendar_event_type(osv.Model):
657 _name = 'calendar.event.type'
658 _description = 'Meeting Type'
660 'name': fields.char('Name', required=True, translate=True),
664 class calendar_event(osv.Model):
665 """ Model for Calendar Event """
666 _name = 'calendar.event'
667 _description = "Event"
669 _inherit = ["mail.thread", "ir.needaction_mixin"]
671 def do_run_scheduler(self, cr, uid, id, context=None):
672 self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context)
674 def get_recurrent_date_by_event(self, cr, uid, event, context=None):
675 """Get recurrent dates based on Rule string and all event where recurrent_id is child
678 val = parser.parse(''.join((re.compile('\d')).findall(date)))
679 ## Dates are localized to saved timezone if any, else current timezone.
681 val = pytz.UTC.localize(val)
682 return val.astimezone(timezone)
684 timezone = pytz.timezone(context.get('tz') or 'UTC')
685 startdate = pytz.UTC.localize(datetime.strptime(event.start, DEFAULT_SERVER_DATETIME_FORMAT)) # Add "+hh:mm" timezone
687 startdate = datetime.now()
689 ## Convert the start date to saved timezone (or context tz) as it'll
690 ## define the correct hour/day asked by the user to repeat for recurrence.
691 startdate = startdate.astimezone(timezone) # transform "+hh:mm" timezone
692 rset1 = rrule.rrulestr(str(event.rrule), dtstart=startdate, forceset=True)
693 ids_depending = self.search(cr, uid, [('recurrent_id', '=', event.id), '|', ('active', '=', False), ('active', '=', True)], context=context)
694 all_events = self.browse(cr, uid, ids_depending, context=context)
695 for ev in all_events:
696 rset1._exdate.append(todate(ev.recurrent_id_date))
697 return [d.astimezone(pytz.UTC) for d in rset1]
699 def _get_recurrency_end_date(self, cr, uid, id, context=None):
700 data = self.read(cr, uid, id, ['final_date', 'recurrency', 'rrule_type', 'count', 'end_type', 'stop'], context=context)
702 if not data.get('recurrency'):
705 end_type = data.get('end_type')
706 final_date = data.get('final_date')
707 if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'stop']):
708 count = data['count'] + 1
710 'daily': ('days', 1),
711 'weekly': ('days', 7),
712 'monthly': ('months', 1),
713 'yearly': ('years', 1),
714 }[data['rrule_type']]
716 deadline = datetime.strptime(data['stop'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
717 return deadline + relativedelta(**{delay: count * mult})
720 def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
722 Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
724 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
725 for meeting_id in meeting_ids:
726 for attendee in self.browse(cr, uid, meeting_id, context).attendee_ids:
727 if user.partner_id.id == attendee.partner_id.id:
731 def get_date_formats(self, cr, uid, context):
732 lang = context.get("lang")
733 res_lang = self.pool.get('res.lang')
736 ids = res_lang.search(request.cr, uid, [("code", "=", lang)])
738 lang_params = res_lang.read(request.cr, uid, ids[0], ["date_format", "time_format"])
739 format_date = lang_params.get("date_format", '%B-%d-%Y')
740 format_time = lang_params.get("time_format", '%I-%M %p')
741 return (format_date, format_time)
743 def get_display_time_tz(self, cr, uid, ids, tz=False, context=None):
746 ev = self.browse(cr, uid, ids, context=context)[0]
747 return self._get_display_time(cr, uid, ev.start, ev.stop, ev.duration, ev.allday, context=context)
749 def _get_display_time(self, cr, uid, start, stop, zduration, zallday, context=None):
751 Return date and time (from to from) based on duration with timezone in string :
753 1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
754 2) if event all day ,return : AllDay, July-31-2013
759 tz = context.get('tz', False)
760 if not tz: # tz can have a value False, so dont do it in the default value of get !
761 context['tz'] = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
764 format_date, format_time = self.get_date_formats(cr, uid, context=context)
765 date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
766 date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(stop, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
767 event_date = date.strftime(format_date)
768 display_time = date.strftime(format_time)
771 time = _("AllDay , %s") % (event_date)
773 duration = date + timedelta(hours=zduration)
774 time = ("%s at (%s To %s) (%s)") % (event_date, display_time, duration.strftime(format_time), tz)
776 time = ("%s at %s To\n %s at %s (%s)") % (event_date, display_time, date_deadline.strftime(format_date), date_deadline.strftime(format_time), tz)
779 def _compute(self, cr, uid, ids, fields, arg, context=None):
781 for meeting_id in ids:
783 attendee = self._find_my_attendee(cr, uid, [meeting_id], context)
784 meeting = self.browse(cr, uid, [meeting_id], context=context)[0]
786 if field == 'is_attendee':
787 res[meeting_id][field] = True if attendee else False
788 elif field == 'attendee_status':
789 res[meeting_id][field] = attendee.state if attendee else 'needsAction'
790 elif field == 'display_time':
791 res[meeting_id][field] = self._get_display_time(cr, uid, meeting.start, meeting.stop, meeting.duration, meeting.allday, context=context)
792 elif field == "display_start":
793 res[meeting_id][field] = meeting.start_date if meeting.allday else meeting.start_datetime
794 elif field == 'start':
795 res[meeting_id][field] = meeting.start_date if meeting.allday else meeting.start_datetime
796 elif field == 'stop':
797 res[meeting_id][field] = meeting.stop_date if meeting.allday else meeting.stop_datetime
801 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
803 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
804 @return: dictionary of rrule value.
807 if not isinstance(ids, list):
811 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
812 data = self.browse(cr, SUPERUSER_ID, id, context=context)
814 if data.interval and data.interval < 0:
815 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
816 if data.count and data.count <= 0:
817 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
819 data = self.read(cr, uid, id, ['id', 'byday', 'recurrency', 'month_list', 'final_date', 'rrule_type', 'month_by', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'day', 'week_list'], context=context)
821 if data['recurrency']:
822 result[event] = self.compute_rule_string(data)
827 # retro compatibility function
828 def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
829 return self._set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=context)
831 def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
832 if not isinstance(ids, list):
834 data = self._get_empty_rrule_data()
836 data['recurrency'] = True
837 for event in self.browse(cr, uid, ids, context=context):
839 update_data = self._parse_rrule(field_value, dict(data), rdate)
840 data.update(update_data)
841 self.write(cr, uid, ids, data, context=context)
844 def _set_date(self, cr, uid, values, id=False, context=None):
849 if values.get('start_datetime') or values.get('start_date') or values.get('start') \
850 or values.get('stop_datetime') or values.get('stop_date') or values.get('stop'):
851 allday = values.get("allday", None)
855 allday = self.read(cr, uid, [id], ['allday'], context=context)[0].get('allday')
858 _logger.warning("Calendar - All day is not specified, arbitrarily set to False")
859 #raise osv.except_osv(_('Error!'), ("Need to know if it's an allday or not..."))
861 key = "date" if allday else "datetime"
862 notkey = "datetime" if allday else "date"
864 for fld in ('start', 'stop'):
865 if values.get('%s_%s' % (fld, key)) or values.get(fld):
866 values['%s_%s' % (fld, key)] = values.get('%s_%s' % (fld, key)) or values.get(fld)
867 values['%s_%s' % (fld, notkey)] = None
868 if fld not in values.keys():
869 values[fld] = values['%s_%s' % (fld, key)]
872 if allday and values.get('stop_date') and values.get('start_date'):
873 diff = datetime.strptime(values['stop_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) - datetime.strptime(values['start_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
874 elif values.get('stop_datetime') and values.get('start_datetime'):
875 diff = datetime.strptime(values['stop_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime(values['start_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
877 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
878 values['duration'] = round(duration, 2)
882 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
885 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
889 'id': fields.integer('ID', readonly=True),
890 'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
891 'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
892 'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
893 'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
894 'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
895 'display_start': fields.function(_compute, string='Date', type="char", multi='display_start', store=True),
896 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
897 'start': fields.function(_compute, string='Calculated start', type="datetime", multi='start', store=True, required=True),
898 'stop': fields.function(_compute, string='Calculated stop', type="datetime", multi='stop', store=True, required=True),
899 'start_date': fields.date('Start Date', states={'done': [('readonly', True)]}, track_visibility='onchange'),
900 'start_datetime': fields.datetime('Start DateTime', states={'done': [('readonly', True)]}, track_visibility='onchange'),
901 'stop_date': fields.date('End Date', states={'done': [('readonly', True)]}, track_visibility='onchange'),
902 'stop_datetime': fields.datetime('End Datetime', states={'done': [('readonly', True)]}, track_visibility='onchange'), # old date_deadline
903 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
904 'description': fields.text('Description', states={'done': [('readonly', True)]}),
905 'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
906 'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
907 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
910 'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
911 'rrule_type': fields.selection([('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)')], 'Recurrency', states={'done': [('readonly', True)]}, help="Let the event automatically repeat at that interval"),
912 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
913 'recurrent_id': fields.integer('Recurrent ID'),
914 'recurrent_id_date': fields.datetime('Recurrent ID date'),
915 'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
916 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
917 'count': fields.integer('Repeat', help="Repeat x times"),
918 'mo': fields.boolean('Mon'),
919 'tu': fields.boolean('Tue'),
920 'we': fields.boolean('Wed'),
921 'th': fields.boolean('Thu'),
922 'fr': fields.boolean('Fri'),
923 'sa': fields.boolean('Sat'),
924 'su': fields.boolean('Sun'),
925 'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
926 'day': fields.integer('Date of month'),
927 'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
928 'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
929 'final_date': fields.date('Repeat Until'), # The last event of a recurrence
931 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
932 'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False), # Color of creator
933 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the event alarm information without removing it."),
934 'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
935 'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
936 'partner_ids': fields.many2many('res.partner', 'calendar_event_res_partner_rel', string='Attendees', states={'done': [('readonly', True)]}),
937 'alarm_ids': fields.many2many('calendar.alarm', 'calendar_alarm_calendar_event_rel', string='Reminders', ondelete="restrict"),
940 def _get_default_partners(self, cr, uid, ctx=None):
941 ret = [self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id]
942 active_id = ctx.get('active_id')
943 if ctx.get('active_model') == 'res.partner' and active_id:
944 if active_id not in ret:
945 ret.append(active_id)
959 'user_id': lambda self, cr, uid, ctx: uid,
960 'partner_ids': _get_default_partners,
963 def _check_closing_date(self, cr, uid, ids, context=None):
964 for event in self.browse(cr, uid, ids, context=context):
965 if event.stop < event.start:
970 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['start', 'stop'])
973 def onchange_allday(self, cr, uid, ids, start=False, end=False, starttime=False, endtime=False, startdatetime=False, enddatetime=False, checkallday=False, context=None):
977 if not ((starttime and endtime) or (start and end)): # At first intialize, we have not datetime
980 if checkallday: # from datetime to date
981 startdatetime = startdatetime or start
983 start = datetime.strptime(startdatetime, DEFAULT_SERVER_DATETIME_FORMAT)
984 value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
986 enddatetime = enddatetime or end
988 end = datetime.strptime(enddatetime, DEFAULT_SERVER_DATETIME_FORMAT)
989 value['stop_date'] = datetime.strftime(end, DEFAULT_SERVER_DATE_FORMAT)
990 else: # from date to datetime
991 user = self.pool['res.users'].browse(cr, uid, uid, context)
992 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
995 start = datetime.strptime(starttime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
996 startdate = tz.localize(start) # Add "+hh:mm" timezone
997 startdate = startdate.replace(hour=8) # Set 8 AM in localtime
998 startdate = startdate.astimezone(pytz.utc) # Convert to UTC
999 value['start_datetime'] = datetime.strftime(startdate, DEFAULT_SERVER_DATETIME_FORMAT)
1001 value['start_datetime'] = start
1004 end = datetime.strptime(endtime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
1005 enddate = tz.localize(end).replace(hour=18).astimezone(pytz.utc)
1007 value['stop_datetime'] = datetime.strftime(enddate, DEFAULT_SERVER_DATETIME_FORMAT)
1009 value['stop_datetime'] = end
1011 return {'value': value}
1013 def onchange_dates(self, cr, uid, ids, fromtype, start=False, end=False, checkallday=False, allday=False, context=None):
1015 """Returns duration and end date based on values passed
1016 @param ids: List of calendar event's IDs.
1020 if checkallday != allday:
1023 value['allday'] = checkallday # Force to be rewrited
1026 if fromtype == 'start':
1027 start = datetime.strptime(start, DEFAULT_SERVER_DATE_FORMAT)
1028 value['start_datetime'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1029 value['start'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1031 if fromtype == 'stop':
1032 end = datetime.strptime(end, DEFAULT_SERVER_DATE_FORMAT)
1033 value['stop_datetime'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1034 value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1037 if fromtype == 'start':
1038 start = datetime.strptime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1039 value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
1040 value['start'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1041 if fromtype == 'stop':
1042 end = datetime.strptime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1043 value['stop_date'] = datetime.strftime(end, DEFAULT_SERVER_DATE_FORMAT)
1044 value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1046 return {'value': value}
1048 def new_invitation_token(self, cr, uid, record, partner_id):
1049 return uuid.uuid4().hex
1051 def create_attendees(self, cr, uid, ids, context):
1052 user_obj = self.pool['res.users']
1053 current_user = user_obj.browse(cr, uid, uid, context=context)
1055 for event in self.browse(cr, uid, ids, context):
1057 for att in event.attendee_ids:
1058 attendees[att.partner_id.id] = True
1060 new_att_partner_ids = []
1061 for partner in event.partner_ids:
1062 if partner.id in attendees:
1064 access_token = self.new_invitation_token(cr, uid, event, partner.id)
1066 'partner_id': partner.id,
1067 'event_id': event.id,
1068 'access_token': access_token,
1069 'email': partner.email,
1072 if partner.id == current_user.partner_id.id:
1073 values['state'] = 'accepted'
1075 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
1076 new_attendees.append(att_id)
1077 new_att_partner_ids.append(partner.id)
1079 if not current_user.email or current_user.email != partner.email:
1080 mail_from = current_user.email or tools.config.get('email_from', False)
1081 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
1082 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee %s") % (partner.name,), subtype="calendar.subtype_invitation", context=context)
1085 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
1086 if new_att_partner_ids:
1087 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
1089 # We remove old attendees who are not in partner_ids now.
1090 all_partner_ids = [part.id for part in event.partner_ids]
1091 all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
1092 all_attendee_ids = [att.id for att in event.attendee_ids]
1093 partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
1095 attendee_ids_to_remove = []
1097 if partner_ids_to_remove:
1098 attendee_ids_to_remove = self.pool["calendar.attendee"].search(cr, uid, [('partner_id.id', 'in', partner_ids_to_remove), ('event_id.id', '=', event.id)], context=context)
1099 if attendee_ids_to_remove:
1100 self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
1103 'new_attendee_ids': new_attendees,
1104 'old_attendee_ids': all_attendee_ids,
1105 'removed_attendee_ids': attendee_ids_to_remove
1109 def get_search_fields(self, browse_event, order_fields, r_date=None):
1111 for ord in order_fields:
1112 if ord == 'id' and r_date:
1113 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
1115 sort_fields[ord] = browse_event[ord]
1116 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
1117 name_get = browse_event[ord].name_get()
1118 if len(name_get) and len(name_get[0]) >= 2:
1119 sort_fields[ord] = name_get[0][1]
1123 def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
1125 """Gives virtual event ids for recurring events
1126 This method gives ids of dates that comes between start date and end date of calendar views
1128 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1133 if isinstance(event_id, (str, int, long)):
1134 ids_to_browse = [event_id] # keep select for return
1136 ids_to_browse = event_id
1139 order_fields = [field.split()[0] for field in order.split(',')]
1141 # fallback on self._order defined on the model
1142 order_fields = [field.split()[0] for field in self._order.split(',')]
1144 if 'id' not in order_fields:
1145 order_fields.append('id')
1149 for ev in self.browse(cr, uid, ids_to_browse, context=context):
1150 if not ev.recurrency or not ev.rrule:
1151 result.append(ev.id)
1152 result_data.append(self.get_search_fields(ev, order_fields))
1154 rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1156 for r_date in rdates:
1157 # fix domain evaluation
1158 # step 1: check date and replace expression by True or False, replace other expressions by True
1159 # step 2: evaluation of & and |
1160 # check if there are one False
1164 if str(arg[0]) in ('start', 'stop', 'final_date'):
1166 ok = r_date.strftime('%Y-%m-%d') == arg[2]
1168 ok = r_date.strftime('%Y-%m-%d') > arg[2]
1170 ok = r_date.strftime('%Y-%m-%d') < arg[2]
1171 if (arg[1] == '>='):
1172 ok = r_date.strftime('%Y-%m-%d') >= arg[2]
1173 if (arg[1] == '<='):
1174 ok = r_date.strftime('%Y-%m-%d') <= arg[2]
1176 elif str(arg) == str('&') or str(arg) == str('|'):
1183 if not isinstance(item, basestring):
1185 elif str(item) == str('&'):
1186 first = new_pile.pop()
1187 second = new_pile.pop()
1188 res = first and second
1189 elif str(item) == str('|'):
1190 first = new_pile.pop()
1191 second = new_pile.pop()
1192 res = first or second
1193 new_pile.append(res)
1195 if [True for item in new_pile if not item]:
1197 result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
1200 def comparer(left, right):
1201 for fn, mult in comparers:
1202 result = cmp(fn(left), fn(right))
1204 return mult * result
1207 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1208 comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1209 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1211 if isinstance(event_id, (str, int, long)):
1212 return ids and ids[0] or False
1216 def compute_rule_string(self, data):
1218 Compute rule string according to value type RECUR of iCalendar from the values given.
1219 @param self: the object pointer
1220 @param data: dictionary of freq and interval value
1221 @return: string containing recurring rule (empty if no rule)
1223 def get_week_string(freq, data):
1224 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1225 if freq == 'weekly':
1226 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1228 return ';BYDAY=' + ','.join(byday)
1231 def get_month_string(freq, data):
1232 if freq == 'monthly':
1233 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1234 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1236 if data.get('month_by') == 'day': # Eg : Second Monday of the month
1237 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1238 elif data.get('month_by') == 'date': # Eg : 16th of the month
1239 return ';BYMONTHDAY=' + str(data.get('day'))
1242 def get_end_date(data):
1243 if data.get('final_date'):
1244 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('final_date'))) + 'T235959Z'
1246 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1247 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1249 freq = data.get('rrule_type', False) # day/week/month/year
1252 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1253 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1257 def _get_empty_rrule_data(self):
1260 'recurrency': False,
1261 'final_date': False,
1262 'rrule_type': False,
1278 def _parse_rrule(self, rule, data, date_start):
1279 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1280 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1281 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, DEFAULT_SERVER_DATETIME_FORMAT))
1283 if r._freq > 0 and r._freq < 4:
1284 data['rrule_type'] = rrule_type[r._freq]
1285 data['count'] = r._count
1286 data['interval'] = r._interval
1287 data['final_date'] = r._until and r._until.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1290 for i in xrange(0, 7):
1291 if i in r._byweekday:
1292 data[day_list[i]] = True
1293 data['rrule_type'] = 'weekly'
1294 #repeat monthly by nweekday ((weekday, weeknumber), )
1296 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1297 data['byday'] = str(r._bynweekday[0][1])
1298 data['month_by'] = 'day'
1299 data['rrule_type'] = 'monthly'
1302 data['day'] = r._bymonthday[0]
1303 data['month_by'] = 'date'
1304 data['rrule_type'] = 'monthly'
1306 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1308 data['interval'] = data['interval'] * 12
1310 #FIXEME handle forever case
1312 #in case of repeat for ever that we do not support right now
1313 if not (data.get('count') or data.get('final_date')):
1315 if data.get('count'):
1316 data['end_type'] = 'count'
1318 data['end_type'] = 'end_date'
1321 def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1323 for virtual_id in ids:
1324 real_id = calendar_id2real_id(virtual_id)
1325 result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1326 res[virtual_id] = result[real_id]
1329 def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1330 """ The basic purpose of this method is to check that destination partners
1331 effectively have email addresses. Otherwise a warning is thrown.
1332 :param value: value format: [[6, 0, [3, 4]]]
1336 if not value or not value[0] or not value[0][0] == 6:
1339 res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1342 def check_partners_email(self, cr, uid, partner_ids, context=None):
1343 """ Verify that selected partner_ids have an email_address defined.
1344 Otherwise throw a warning. """
1345 partner_wo_email_lst = []
1346 for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1347 if not partner.email:
1348 partner_wo_email_lst.append(partner)
1349 if not partner_wo_email_lst:
1351 warning_msg = _('The following contacts have no email address :')
1352 for partner in partner_wo_email_lst:
1353 warning_msg += '\n- %s' % (partner.name)
1354 return {'warning': {
1355 'title': _('Email addresses not found'),
1356 'message': warning_msg,
1359 # shows events of the day for this user
1360 def _needaction_domain_get(self, cr, uid, context=None):
1362 ('stop', '<=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 23:59:59')),
1363 ('start', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 00:00:00')),
1364 ('user_id', '=', uid),
1367 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
1368 if isinstance(thread_id, str):
1369 thread_id = get_real_ids(thread_id)
1370 if context.get('default_date'):
1371 del context['default_date']
1372 return super(calendar_event, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs)
1374 def do_sendmail(self, cr, uid, ids, context=None):
1375 for event in self.browse(cr, uid, ids, context):
1376 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1378 if current_user.email:
1379 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], email_from=current_user.email, context=context):
1380 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="calendar.subtype_invitation", context=context)
1383 def get_attendee(self, cr, uid, meeting_id, context=None):
1384 # Used for view in controller
1385 invitation = {'meeting': {}, 'attendee': []}
1387 meeting = self.browse(cr, uid, int(meeting_id), context=context)
1388 invitation['meeting'] = {
1389 'event': meeting.name,
1390 'where': meeting.location,
1391 'when': meeting.display_time
1394 for attendee in meeting.attendee_ids:
1395 invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1398 def get_interval(self, cr, uid, ids, date, interval, tz=None, context=None):
1399 #Function used only in calendar_event_data.xml for email template
1400 date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
1403 timezone = pytz.timezone(tz or 'UTC')
1404 date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
1406 if interval == 'day':
1408 elif interval == 'month':
1409 res = date.strftime('%B') + " " + str(date.year)
1410 elif interval == 'dayname':
1411 res = date.strftime('%A')
1412 elif interval == 'time':
1413 dummy, format_time = self.get_date_formats(cr, uid, context=context)
1414 res = date.strftime(format_time + " %Z")
1417 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1421 if context.get('mymeetings', False):
1422 partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1423 args += [('partner_ids', 'in', [partner_id])]
1429 if arg[0] in ('start_date', 'start_datetime', 'start',) and arg[1] == ">=":
1430 if context.get('virtual_id', True):
1431 new_args += ['|', '&', ('recurrency', '=', 1), ('final_date', arg[1], arg[2])]
1432 elif arg[0] == "id":
1433 new_id = get_real_ids(arg[2])
1434 new_arg = (arg[0], arg[1], new_id)
1435 new_args.append(new_arg)
1437 if not context.get('virtual_id', True):
1438 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, count=count, context=context)
1440 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1441 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1442 res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1446 return res[offset: offset + limit]
1449 def copy(self, cr, uid, id, default=None, context=None):
1453 default = default or {}
1455 self._set_date(cr, uid, default, id=default.get('id'), context=context)
1456 default['attendee_ids'] = False
1458 res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1461 def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1462 real_event_id = calendar_id2real_id(id)
1463 data = self.read(cr, uid, id, ['allday', 'start', 'stop', 'rrule', 'duration'])
1464 data['start_date' if data['allday'] else 'start_datetime'] = data['start']
1465 data['stop_date' if data['allday'] else 'stop_datetime'] = data['stop']
1466 if data.get('rrule'):
1469 recurrent_id=real_event_id,
1470 recurrent_id_date=data.get('start'),
1474 final_date=datetime.strptime(data.get('start'), DEFAULT_SERVER_DATETIME_FORMAT if data['allday'] else DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=values.get('duration', False) or data.get('duration'))
1480 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1483 def open_after_detach_event(self, cr, uid, ids, context=None):
1487 new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1489 'type': 'ir.actions.act_window',
1490 'res_model': 'calendar.event',
1491 'view_mode': 'form',
1493 'target': 'current',
1494 'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
1497 def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
1500 for n, calendar_id in enumerate(arg[2]):
1501 if isinstance(calendar_id, str):
1502 arg[2][n] = calendar_id.split('-')[0]
1503 return super(calendar_event, self)._name_search(cr, user, name=name, args=args, operator=operator, context=context, limit=limit, name_get_uid=name_get_uid)
1505 def write(self, cr, uid, ids, values, context=None):
1506 def _only_changes_to_apply_on_real_ids(field_names):
1507 ''' return True if changes are only to be made on the real ids'''
1508 for field in field_names:
1509 if field in ['start', 'start_date', 'start_datetime', 'stop', 'stop_date', 'stop_datetime', 'active']:
1513 if not isinstance(ids, (tuple, list)):
1516 context = context or {}
1517 self._set_date(cr, uid, values, id=ids[0], context=context)
1520 if isinstance(one_ids, (str, int, long)):
1521 if len(str(one_ids).split('-')) == 1:
1522 ids = [int(one_ids)]
1529 # Special write of complex IDS
1530 for event_id in ids:
1531 if len(str(event_id).split('-')) == 1:
1534 ids.remove(event_id)
1535 real_event_id = calendar_id2real_id(event_id)
1537 # if we are setting the recurrency flag to False or if we are only changing fields that
1538 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1539 # then set real ids to be updated.
1540 if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()):
1541 ids.append(real_event_id)
1544 data = self.read(cr, uid, event_id, ['start', 'stop', 'rrule', 'duration'])
1545 if data.get('rrule'):
1546 new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
1548 res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
1550 # set end_date for calendar searching
1551 if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
1552 (values.get('rrule_type') or values.get('count') or values.get('start') or values.get('stop')):
1554 final_date = self._get_recurrency_end_date(cr, uid, id, context=context)
1555 super(calendar_event, self).write(cr, uid, [id], {'final_date': final_date}, context=context)
1557 attendees_create = False
1558 if values.get('partner_ids', False):
1559 attendees_create = self.create_attendees(cr, uid, ids, context)
1561 if (values.get('start_date') or values.get('start_datetime', False)) and values.get('active', True):
1562 the_id = new_id or (ids and int(ids[0]))
1564 if attendees_create:
1565 attendees_create = attendees_create[the_id]
1566 mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
1568 mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1571 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1572 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context):
1573 self.message_post(cr, uid, the_id, body=_("A email has been send to specify that the date has been changed !"), subtype="calendar.subtype_invitation", context=context)
1574 return res or True and False
1576 def create(self, cr, uid, vals, context=None):
1580 self._set_date(cr, uid, vals, id=False, context=context)
1581 if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user
1582 vals['user_id'] = uid
1584 res = super(calendar_event, self).create(cr, uid, vals, context=context)
1586 final_date = self._get_recurrency_end_date(cr, uid, res, context=context)
1587 self.write(cr, uid, [res], {'final_date': final_date}, context=context)
1589 self.create_attendees(cr, uid, [res], context=context)
1592 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
1596 if 'date' in groupby:
1597 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1598 virtual_id = context.get('virtual_id', True)
1599 context.update({'virtual_id': False})
1600 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
1602 #remove the count, since the value is not consistent with the result of the search when expand the group
1603 for groupname in groupby:
1604 if result.get(groupname + "_count"):
1605 del result[groupname + "_count"]
1606 result.get('__context', {}).update({'virtual_id': virtual_id})
1609 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1612 fields2 = fields and fields[:] or None
1613 EXTRAFIELDS = ('class', 'user_id', 'duration', 'allday', 'start', 'start_date', 'start_datetime', 'rrule')
1614 for f in EXTRAFIELDS:
1615 if fields and (f not in fields):
1617 if isinstance(ids, (str, int, long)):
1621 select = map(lambda x: (x, calendar_id2real_id(x)), select)
1623 real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1624 real_data = dict(zip([x['id'] for x in real_data], real_data))
1626 for calendar_id, real_id in select:
1627 res = real_data[real_id].copy()
1628 ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) > 0 and res.get('duration') or 1)
1629 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1630 res['start'] = ls[1]
1634 res['start_date'] = ls[1]
1635 res['stop_date'] = ls[2]
1637 res['start_datetime'] = ls[1]
1638 res['stop_datetime'] = ls[2]
1640 res['display_time'] = self._get_display_time(cr, uid, ls[1], ls[2], res['duration'], res['allday'], context=context)
1642 res['id'] = calendar_id
1647 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1650 if r['class'] == 'private':
1652 if f not in ('id', 'allday', 'start', 'stop', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date', 'rrule'):
1653 if isinstance(r[f], list):
1661 for k in EXTRAFIELDS:
1662 if (k in r) and (fields and (k not in fields)):
1664 if isinstance(ids, (str, int, long)):
1665 return result and result[0] or False
1668 def unlink(self, cr, uid, ids, can_be_deleted=True, context=None):
1669 if not isinstance(ids, list):
1676 for event_id in ids:
1677 if can_be_deleted and len(str(event_id).split('-')) == 1: # if ID REAL
1678 if self.browse(cr, uid, int(event_id), context).recurrent_id:
1679 ids_to_exclure.append(event_id)
1681 ids_to_unlink.append(int(event_id))
1683 ids_to_exclure.append(event_id)
1686 res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1689 for id_to_exclure in ids_to_exclure:
1690 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1695 class mail_message(osv.Model):
1696 _inherit = "mail.message"
1698 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1700 convert the search on real ids in the case it was asked on virtual ids, then call super()
1702 for index in range(len(args)):
1703 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1704 args[index][2] = get_real_ids(args[index][2])
1705 return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1707 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1710 if doc_model == 'calendar.event':
1711 order = context.get('order', self._order)
1712 for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1713 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1714 return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1717 class ir_attachment(osv.Model):
1718 _inherit = "ir.attachment"
1720 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1722 convert the search on real ids in the case it was asked on virtual ids, then call super()
1724 for index in range(len(args)):
1725 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1726 args[index][2] = get_real_ids(args[index][2])
1727 return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1729 def write(self, cr, uid, ids, vals, context=None):
1731 when posting an attachment (new or not), convert the virtual ids in real ids.
1733 if isinstance(vals.get('res_id'), str):
1734 vals['res_id'] = get_real_ids(vals.get('res_id'))
1735 return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1738 class ir_http(osv.AbstractModel):
1739 _inherit = 'ir.http'
1741 def _auth_method_calendar(self):
1742 token = request.params['token']
1743 db = request.params['db']
1745 registry = openerp.modules.registry.RegistryManager.get(db)
1746 attendee_pool = registry.get('calendar.attendee')
1747 error_message = False
1748 with registry.cursor() as cr:
1749 attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
1751 error_message = """Invalid Invitation Token."""
1752 elif request.session.uid and request.session.login != 'anonymous':
1753 # if valid session but user is not match
1754 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1755 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1756 if attendee.partner_id.id != user.partner_id.id:
1757 error_message = """Invitation cannot be forwarded via email. This event/meeting belongs to %s and you are logged in as %s. Please ask organizer to add you.""" % (attendee.email, user.email)
1760 raise BadRequest(error_message)
1765 class invite_wizard(osv.osv_memory):
1766 _inherit = 'mail.wizard.invite'
1768 def default_get(self, cr, uid, fields, context=None):
1770 in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1772 result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1773 if 'res_id' in result:
1774 result['res_id'] = get_real_ids(result['res_id'])