1 # -*- coding: utf-8 -*-
7 import openerp.service.report
10 from werkzeug.exceptions import BadRequest
11 from datetime import datetime, timedelta
12 from dateutil import parser
13 from dateutil import rrule
14 from dateutil.relativedelta import relativedelta
15 from openerp import api
16 from openerp import tools, SUPERUSER_ID
17 from openerp.osv import fields, osv
18 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
19 from openerp.tools.translate import _
20 from openerp.http import request
21 from operator import itemgetter
24 _logger = logging.getLogger(__name__)
27 def calendar_id2real_id(calendar_id=None, with_date=False):
29 Convert a "virtual/recurring event id" (type string) into a real event id (type int).
30 E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
31 @param calendar_id: id of calendar
32 @param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id
33 @return: real event id
35 if calendar_id and isinstance(calendar_id, (str, unicode)):
36 res = calendar_id.split('-')
40 real_date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT, time.strptime(res[1], "%Y%m%d%H%M%S"))
41 start = datetime.strptime(real_date, DEFAULT_SERVER_DATETIME_FORMAT)
42 end = start + timedelta(hours=with_date)
43 return (int(real_id), real_date, end.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
45 return calendar_id and int(calendar_id) or calendar_id
48 def get_real_ids(ids):
49 if isinstance(ids, (str, int, long)):
50 return calendar_id2real_id(ids)
52 if isinstance(ids, (list, tuple)):
53 return [calendar_id2real_id(id) for id in ids]
56 class calendar_attendee(osv.Model):
58 Calendar Attendee Information
60 _name = 'calendar.attendee'
62 _description = 'Attendee information'
64 def _compute_data(self, cr, uid, ids, name, arg, context=None):
66 Compute data on function fields for attendee values.
67 @param ids: list of calendar attendee's IDs
68 @param name: name of field
69 @return: dictionary of form {id: {'field Name': value'}}
73 for attdata in self.browse(cr, uid, ids, context=context):
77 if attdata.partner_id:
78 result[id][name] = attdata.partner_id.name or False
80 result[id][name] = attdata.email or ''
84 ('needsAction', 'Needs Action'),
85 ('tentative', 'Uncertain'),
86 ('declined', 'Declined'),
87 ('accepted', 'Accepted'),
91 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="Status of the attendee's participation"),
92 'cn': fields.function(_compute_data, string='Common name', type="char", multi='cn', store=True),
93 'partner_id': fields.many2one('res.partner', 'Contact', readonly="True"),
94 'email': fields.char('Email', help="Email of Invited Person"),
95 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
96 'access_token': fields.char('Invitation Token'),
97 'event_id': fields.many2one('calendar.event', 'Meeting linked'),
100 'state': 'needsAction',
103 def copy(self, cr, uid, id, default=None, context=None):
104 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
106 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
108 Make entry on email and availability on change of partner_id field.
109 @param partner_id: changed value of partner id
112 return {'value': {'email': ''}}
113 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
114 return {'value': {'email': partner.email}}
116 def get_ics_file(self, cr, uid, event_obj, context=None):
118 Returns iCalendar file for the event invitation.
119 @param event_obj: event object (browse record)
120 @return: .ics file content
124 def ics_datetime(idate, allday=False):
127 return datetime.strptime(idate.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
129 return datetime.strptime(idate.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
133 # FIXME: why isn't this in CalDAV?
138 cal = vobject.iCalendar()
139 event = cal.add('vevent')
140 if not event_obj.start or not event_obj.stop:
141 raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation."))
142 event.add('created').value = ics_datetime(time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
143 event.add('dtstart').value = ics_datetime(event_obj.start, event_obj.allday)
144 event.add('dtend').value = ics_datetime(event_obj.stop, event_obj.allday)
145 event.add('summary').value = event_obj.name
146 if event_obj.description:
147 event.add('description').value = event_obj.description
148 if event_obj.location:
149 event.add('location').value = event_obj.location
151 event.add('rrule').value = event_obj.rrule
153 if event_obj.alarm_ids:
154 for alarm in event_obj.alarm_ids:
155 valarm = event.add('valarm')
156 interval = alarm.interval
157 duration = alarm.duration
158 trigger = valarm.add('TRIGGER')
159 trigger.params['related'] = ["START"]
160 if interval == 'days':
161 delta = timedelta(days=duration)
162 elif interval == 'hours':
163 delta = timedelta(hours=duration)
164 elif interval == 'minutes':
165 delta = timedelta(minutes=duration)
166 trigger.value = delta
167 valarm.add('DESCRIPTION').value = alarm.name or 'Odoo'
168 for attendee in event_obj.attendee_ids:
169 attendee_add = event.add('attendee')
170 attendee_add.value = 'MAILTO:' + (attendee.email or '')
171 res = cal.serialize()
174 def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False),
175 template_xmlid='calendar_template_meeting_invitation', context=None):
177 Send mail for event invitation to event attendees.
178 @param email_from: email address for user sending the mail
182 if self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_mail', default=False) or context.get("no_mail_to_attendees"):
186 data_pool = self.pool['ir.model.data']
187 mailmess_pool = self.pool['mail.message']
188 mail_pool = self.pool['mail.mail']
189 template_pool = self.pool['email.template']
190 local_context = context.copy()
192 'needsAction': 'grey',
194 'tentative': '#FFFF00',
198 if not isinstance(ids, (tuple, list)):
201 dummy, template_id = data_pool.get_object_reference(cr, uid, 'calendar', template_xmlid)
202 dummy, act_id = data_pool.get_object_reference(cr, uid, 'calendar', "view_calendar_event_calendar")
203 local_context.update({
205 'action_id': self.pool['ir.actions.act_window'].search(cr, uid, [('view_id', '=', act_id)], context=context)[0],
207 'base_url': self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
210 for attendee in self.browse(cr, uid, ids, context=context):
211 if attendee.email and email_from and attendee.email != email_from:
212 ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
213 mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
217 vals['attachment_ids'] = [(0, 0, {'name': 'invitation.ics',
218 'datas_fname': 'invitation.ics',
219 'datas': str(ics_file).encode('base64')})]
220 vals['model'] = None # We don't want to have the mail in the tchatter while in queue!
221 the_mailmess = mail_pool.browse(cr, uid, mail_id, context=context).mail_message_id
222 mailmess_pool.write(cr, uid, [the_mailmess.id], vals, context=context)
223 mail_ids.append(mail_id)
226 res = mail_pool.send(cr, uid, mail_ids, context=context)
230 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
232 Make entry on email and availability on change of user_id field.
233 @param ids: list of attendee's IDs
234 @param user_id: changed value of User id
235 @return: dictionary of values which put value in email and availability fields
238 return {'value': {'email': ''}}
240 user = self.pool['res.users'].browse(cr, uid, user_id, *args)
241 return {'value': {'email': user.email, 'availability': user.availability}}
243 def do_tentative(self, cr, uid, ids, context=None, *args):
245 Makes event invitation as Tentative.
246 @param ids: list of attendee's IDs
248 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
250 def do_accept(self, cr, uid, ids, context=None, *args):
252 Marks event invitation as Accepted.
253 @param ids: list of attendee's IDs
257 meeting_obj = self.pool['calendar.event']
258 res = self.write(cr, uid, ids, {'state': 'accepted'}, context)
259 for attendee in self.browse(cr, uid, ids, context=context):
260 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)),
261 subtype="calendar.subtype_invitation", context=context)
265 def do_decline(self, cr, uid, ids, context=None, *args):
267 Marks event invitation as Declined.
268 @param ids: list of calendar attendee's IDs
272 meeting_obj = self.pool['calendar.event']
273 res = self.write(cr, uid, ids, {'state': 'declined'}, context)
274 for attendee in self.browse(cr, uid, ids, context=context):
275 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has declined invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
278 def create(self, cr, uid, vals, context=None):
281 if not vals.get("email") and vals.get("cn"):
282 cnval = vals.get("cn").split(':')
283 email = filter(lambda x: x.__contains__('@'), cnval)
284 vals['email'] = email and email[0] or ''
285 vals['cn'] = vals.get("cn")
286 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
290 class res_partner(osv.Model):
291 _inherit = 'res.partner'
293 'calendar_last_notif_ack': fields.datetime('Last notification marked as read from base Calendar'),
296 def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
298 Return a list of tuple (id, name, status)
299 Used by web_calendar.js : Many2ManyAttendee
304 meeting = self.pool['calendar.event'].browse(cr, uid, get_real_ids(meeting_id), context=context)
305 for partner in self.browse(cr, uid, ids, context=context):
306 data = self.name_get(cr, uid, [partner.id], context)[0]
308 for attendee in meeting.attendee_ids:
309 if attendee.partner_id.id == partner.id:
310 data = (data[0], data[1], attendee.state)
314 def _set_calendar_last_notif_ack(self, cr, uid, context=None):
315 partner = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id
316 self.write(cr, uid, partner.id, {'calendar_last_notif_ack': datetime.now()}, context=context)
320 class calendar_alarm_manager(osv.AbstractModel):
321 _name = 'calendar.alarm_manager'
323 def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None):
328 cal.start - interval '1' minute * calcul_delta.max_delta AS first_alarm,
330 WHEN cal.recurrency THEN cal.final_date - interval '1' minute * calcul_delta.min_delta
331 ELSE cal.stop - interval '1' minute * calcul_delta.min_delta
333 cal.start as first_event_date,
335 WHEN cal.recurrency THEN cal.final_date
337 END as last_event_date,
338 calcul_delta.min_delta,
339 calcul_delta.max_delta,
342 calendar_event AS cal
346 rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
348 calendar_alarm_calendar_event_rel AS rel
349 LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
350 WHERE alarm.type in %s
351 GROUP BY rel.calendar_event_id
352 ) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id
356 RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
357 AND part_rel.res_partner_id = %s
363 type_to_read += ('notification',)
365 type_to_read += ('email',)
367 tuple_params = (type_to_read,)
369 # ADD FILTER ON PARTNER_ID
371 base_request += filter_user
372 tuple_params += (partner_id, )
375 tuple_params += (seconds, seconds,)
377 cr.execute("""SELECT *
378 FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS
379 WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second )
380 AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%%s' second )
381 """ % base_request, tuple_params)
383 for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
385 'event_id': event_id,
386 'first_alarm': first_alarm,
387 'last_alarm': last_alarm,
388 'first_meeting': first_meeting,
389 'last_meeting': last_meeting,
390 'min_duration': min_duration,
391 'max_duration': max_duration,
397 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):
402 alarm_type.append('notification')
404 alarm_type.append('email')
406 if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
407 for alarm in event.alarm_ids:
408 if alarm.type in alarm_type and \
409 one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
410 (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)):
412 'alarm_id': alarm.id,
413 'event_id': event.id,
414 'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
419 def get_next_mail(self, cr, uid, context=None):
421 cron = self.pool['ir.model.data'].get_object(
422 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
424 _logger.error("Cron for " + self._name + " can not be identified !")
427 if cron.interval_type == "weeks":
428 cron_interval = cron.interval_number * 7 * 24 * 60 * 60
429 elif cron.interval_type == "days":
430 cron_interval = cron.interval_number * 24 * 60 * 60
431 elif cron.interval_type == "hours":
432 cron_interval = cron.interval_number * 60 * 60
433 elif cron.interval_type == "minutes":
434 cron_interval = cron.interval_number * 60
435 elif cron.interval_type == "seconds":
436 cron_interval = cron.interval_number
438 cron_interval = False
440 if not cron_interval:
441 _logger.error("Cron delay can not be computed !")
444 all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
446 for event in all_events: # .values()
447 max_delta = all_events[event]['max_duration']
448 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
449 if curEvent.recurrency:
452 for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
453 in_date_format = one_date.replace(tzinfo=None)
454 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
456 for alert in LastFound:
457 self.do_mail_reminder(cr, uid, alert, context=context)
459 if not bFound: # if it's the first alarm for this recurrent event
461 if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event
464 in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT)
465 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
467 for alert in LastFound:
468 self.do_mail_reminder(cr, uid, alert, context=context)
470 def get_next_notif(self, cr, uid, context=None):
471 ajax_check_every_seconds = 300
472 partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
478 all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
480 for event in all_events: # .values()
481 max_delta = all_events[event]['max_duration']
482 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
483 if curEvent.recurrency:
486 for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
487 in_date_format = one_date.replace(tzinfo=None)
488 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)
490 for alert in LastFound:
491 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
492 if not bFound: # if it's the first alarm for this recurrent event
494 if bFound and not LastFound: # if the precedent event had alarm but not this one, we can stop the search fot this event
497 in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT)
498 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)
500 for alert in LastFound:
501 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
504 def do_mail_reminder(self, cr, uid, alert, context=None):
509 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
510 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
512 if alarm.type == 'email':
513 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)
517 def do_notif_reminder(self, cr, uid, alert, context=None):
518 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
519 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
521 if alarm.type == 'notification':
522 message = event.display_time
524 delta = alert['notify_at'] - datetime.now()
525 delta = delta.seconds + delta.days * 3600 * 24
528 'event_id': event.id,
532 'notify_at': alert['notify_at'].strftime(DEFAULT_SERVER_DATETIME_FORMAT),
536 class calendar_alarm(osv.Model):
537 _name = 'calendar.alarm'
538 _description = 'Event alarm'
540 def _get_duration(self, cr, uid, ids, field_name, arg, context=None):
542 for alarm in self.browse(cr, uid, ids, context=context):
543 if alarm.interval == "minutes":
544 res[alarm.id] = alarm.duration
545 elif alarm.interval == "hours":
546 res[alarm.id] = alarm.duration * 60
547 elif alarm.interval == "days":
548 res[alarm.id] = alarm.duration * 60 * 24
554 'name': fields.char('Name', required=True),
555 'type': fields.selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True),
556 'duration': fields.integer('Amount', required=True),
557 'interval': fields.selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], 'Unit', required=True),
558 'duration_minutes': fields.function(_get_duration, type='integer', string='duration_minutes', store=True),
562 'type': 'notification',
567 def _update_cron(self, cr, uid, context=None):
569 cron = self.pool['ir.model.data'].get_object(
570 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
573 return cron.toggle(model=self._name, domain=[('type', '=', 'email')])
575 def create(self, cr, uid, values, context=None):
576 res = super(calendar_alarm, self).create(cr, uid, values, context=context)
578 self._update_cron(cr, uid, context=context)
582 def write(self, cr, uid, ids, values, context=None):
583 res = super(calendar_alarm, self).write(cr, uid, ids, values, context=context)
585 self._update_cron(cr, uid, context=context)
589 def unlink(self, cr, uid, ids, context=None):
590 res = super(calendar_alarm, self).unlink(cr, uid, ids, context=context)
592 self._update_cron(cr, uid, context=context)
597 class ir_values(osv.Model):
598 _inherit = 'ir.values'
600 def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
603 if type(data) in (list, tuple):
604 new_model.append((data[0], calendar_id2real_id(data[1])))
606 new_model.append(data)
607 return super(ir_values, self).set(cr, uid, key, key2, name, new_model,
608 value, replace, isobject, meta, preserve_user, company)
610 def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
615 if type(data) in (list, tuple):
616 new_model.append((data[0], calendar_id2real_id(data[1])))
618 new_model.append(data)
619 return super(ir_values, self).get(cr, uid, key, key2, new_model,
620 meta, context, res_id_req, without_user, key2_req)
623 class ir_model(osv.Model):
625 _inherit = 'ir.model'
627 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
628 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
631 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, context=context, load=load)
634 val['id'] = calendar_id2real_id(val['id'])
635 return isinstance(ids, (str, int, long)) and data[0] or data
638 original_exp_report = openerp.service.report.exp_report
641 def exp_report(db, uid, object, ids, data=None, context=None):
645 if object == 'printscreen.list':
646 original_exp_report(db, uid, object, ids, data, context)
649 new_ids.append(calendar_id2real_id(id))
650 if data.get('id', False):
651 data['id'] = calendar_id2real_id(data['id'])
652 return original_exp_report(db, uid, object, new_ids, data, context)
655 openerp.service.report.exp_report = exp_report
658 class calendar_event_type(osv.Model):
659 _name = 'calendar.event.type'
660 _description = 'Meeting Type'
662 'name': fields.char('Name', required=True, translate=True),
666 class calendar_event(osv.Model):
667 """ Model for Calendar Event """
668 _name = 'calendar.event'
669 _description = "Event"
671 _inherit = ["mail.thread", "ir.needaction_mixin"]
673 def do_run_scheduler(self, cr, uid, id, context=None):
674 self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context)
676 def get_recurrent_date_by_event(self, cr, uid, event, context=None):
677 """Get recurrent dates based on Rule string and all event where recurrent_id is child
680 val = parser.parse(''.join((re.compile('\d')).findall(date)))
681 ## Dates are localized to saved timezone if any, else current timezone.
683 val = pytz.UTC.localize(val)
684 return val.astimezone(timezone)
686 timezone = pytz.timezone(context.get('tz') or 'UTC')
687 startdate = pytz.UTC.localize(datetime.strptime(event.start, DEFAULT_SERVER_DATETIME_FORMAT)) # Add "+hh:mm" timezone
689 startdate = datetime.now()
691 ## Convert the start date to saved timezone (or context tz) as it'll
692 ## define the correct hour/day asked by the user to repeat for recurrence.
693 startdate = startdate.astimezone(timezone) # transform "+hh:mm" timezone
694 rset1 = rrule.rrulestr(str(event.rrule), dtstart=startdate, forceset=True)
695 ids_depending = self.search(cr, uid, [('recurrent_id', '=', event.id), '|', ('active', '=', False), ('active', '=', True)], context=context)
696 all_events = self.browse(cr, uid, ids_depending, context=context)
697 for ev in all_events:
698 rset1._exdate.append(todate(ev.recurrent_id_date))
699 return [d.astimezone(pytz.UTC) for d in rset1]
701 def _get_recurrency_end_date(self, cr, uid, id, context=None):
702 data = self.read(cr, uid, id, ['final_date', 'recurrency', 'rrule_type', 'count', 'end_type', 'stop'], context=context)
704 if not data.get('recurrency'):
707 end_type = data.get('end_type')
708 final_date = data.get('final_date')
709 if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'stop']):
710 count = data['count'] + 1
712 'daily': ('days', 1),
713 'weekly': ('days', 7),
714 'monthly': ('months', 1),
715 'yearly': ('years', 1),
716 }[data['rrule_type']]
718 deadline = datetime.strptime(data['stop'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
719 return deadline + relativedelta(**{delay: count * mult})
722 def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
724 Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
726 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
727 for meeting_id in meeting_ids:
728 for attendee in self.browse(cr, uid, meeting_id, context).attendee_ids:
729 if user.partner_id.id == attendee.partner_id.id:
733 def get_date_formats(self, cr, uid, context):
734 lang = context.get("lang")
735 res_lang = self.pool.get('res.lang')
738 ids = res_lang.search(request.cr, uid, [("code", "=", lang)])
740 lang_params = res_lang.read(request.cr, uid, ids[0], ["date_format", "time_format"])
742 # formats will be used for str{f,p}time() which do not support unicode in Python 2, coerce to str
743 format_date = lang_params.get("date_format", '%B-%d-%Y').encode('utf-8')
744 format_time = lang_params.get("time_format", '%I-%M %p').encode('utf-8')
745 return (format_date, format_time)
747 def get_display_time_tz(self, cr, uid, ids, tz=False, context=None):
748 context = dict(context or {})
751 ev = self.browse(cr, uid, ids, context=context)[0]
752 return self._get_display_time(cr, uid, ev.start, ev.stop, ev.duration, ev.allday, context=context)
754 def _get_display_time(self, cr, uid, start, stop, zduration, zallday, context=None):
756 Return date and time (from to from) based on duration with timezone in string :
758 1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
759 2) if event all day ,return : AllDay, July-31-2013
761 context = dict(context or {})
763 tz = context.get('tz', False)
764 if not tz: # tz can have a value False, so dont do it in the default value of get !
765 context['tz'] = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
767 tz = tools.ustr(tz).encode('utf-8') # make safe for str{p,f}time()
769 format_date, format_time = self.get_date_formats(cr, uid, context=context)
770 date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
771 date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(stop, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
772 event_date = date.strftime(format_date)
773 display_time = date.strftime(format_time)
776 time = _("AllDay , %s") % (event_date)
778 duration = date + timedelta(hours=zduration)
779 time = ("%s at (%s To %s) (%s)") % (event_date, display_time, duration.strftime(format_time), tz)
781 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)
784 def _compute(self, cr, uid, ids, fields, arg, context=None):
786 if not isinstance(fields, list):
788 for meeting in self.browse(cr, uid, ids, context=context):
790 res[meeting.id] = meeting_data
791 attendee = self._find_my_attendee(cr, uid, [meeting.id], context)
793 if field == 'is_attendee':
794 meeting_data[field] = bool(attendee)
795 elif field == 'attendee_status':
796 meeting_data[field] = attendee.state if attendee else 'needsAction'
797 elif field == 'display_time':
798 meeting_data[field] = self._get_display_time(cr, uid, meeting.start, meeting.stop, meeting.duration, meeting.allday, context=context)
799 elif field == "display_start":
800 meeting_data[field] = meeting.start_date if meeting.allday else meeting.start_datetime
801 elif field == 'start':
802 meeting_data[field] = meeting.start_date if meeting.allday else meeting.start_datetime
803 elif field == 'stop':
804 meeting_data[field] = meeting.stop_date if meeting.allday else meeting.stop_datetime
807 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
809 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
810 @return: dictionary of rrule value.
813 if not isinstance(ids, list):
816 #read these fields as SUPERUSER because if the record is private a normal search could raise an error
817 events = self.read(cr, SUPERUSER_ID, ids,
818 ['id', 'byday', 'recurrency', 'final_date', 'rrule_type', 'month_by',
819 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa',
820 'su', 'day', 'week_list'], context=context)
822 if event['recurrency']:
823 result[event['id']] = self.compute_rule_string(event)
825 result[event['id']] = ''
829 # retro compatibility function
830 def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
831 return self._set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=context)
833 def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
834 if not isinstance(ids, list):
836 data = self._get_empty_rrule_data()
838 data['recurrency'] = True
839 for event in self.browse(cr, uid, ids, context=context):
841 update_data = self._parse_rrule(field_value, dict(data), rdate)
842 data.update(update_data)
843 self.write(cr, uid, ids, data, context=context)
846 def _set_date(self, cr, uid, values, id=False, context=None):
851 if values.get('start_datetime') or values.get('start_date') or values.get('start') \
852 or values.get('stop_datetime') or values.get('stop_date') or values.get('stop'):
853 allday = values.get("allday", None)
857 allday = self.read(cr, uid, [id], ['allday'], context=context)[0].get('allday')
860 _logger.warning("Calendar - All day is not specified, arbitrarily set to False")
861 #raise osv.except_osv(_('Error!'), ("Need to know if it's an allday or not..."))
863 key = "date" if allday else "datetime"
864 notkey = "datetime" if allday else "date"
866 for fld in ('start', 'stop'):
867 if values.get('%s_%s' % (fld, key)) or values.get(fld):
868 values['%s_%s' % (fld, key)] = values.get('%s_%s' % (fld, key)) or values.get(fld)
869 values['%s_%s' % (fld, notkey)] = None
870 if fld not in values.keys():
871 values[fld] = values['%s_%s' % (fld, key)]
874 if allday and values.get('stop_date') and values.get('start_date'):
875 diff = datetime.strptime(values['stop_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) - datetime.strptime(values['start_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
876 elif values.get('stop_datetime') and values.get('start_datetime'):
877 diff = datetime.strptime(values['stop_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime(values['start_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
879 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
880 values['duration'] = round(duration, 2)
884 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
887 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
891 'id': fields.integer('ID', readonly=True),
892 'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
893 'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
894 'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
895 'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
896 'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
897 'display_start': fields.function(_compute, string='Date', type="char", multi='attendee', store=True),
898 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
899 'start': fields.function(_compute, string='Calculated start', type="datetime", multi='attendee', store=True, required=True),
900 'stop': fields.function(_compute, string='Calculated stop', type="datetime", multi='attendee', store=True, required=True),
901 'start_date': fields.date('Start Date', states={'done': [('readonly', True)]}, track_visibility='onchange'),
902 'start_datetime': fields.datetime('Start DateTime', states={'done': [('readonly', True)]}, track_visibility='onchange'),
903 'stop_date': fields.date('End Date', states={'done': [('readonly', True)]}, track_visibility='onchange'),
904 'stop_datetime': fields.datetime('End Datetime', states={'done': [('readonly', True)]}, track_visibility='onchange'), # old date_deadline
905 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
906 'description': fields.text('Description', states={'done': [('readonly', True)]}),
907 'class': fields.selection([('public', 'Everyone'), ('private', 'Only me'), ('confidential', 'Only internal users')], 'Privacy', states={'done': [('readonly', True)]}),
908 'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
909 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
912 'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
913 '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"),
914 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
915 'recurrent_id': fields.integer('Recurrent ID'),
916 'recurrent_id_date': fields.datetime('Recurrent ID date'),
917 'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
918 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
919 'count': fields.integer('Repeat', help="Repeat x times"),
920 'mo': fields.boolean('Mon'),
921 'tu': fields.boolean('Tue'),
922 'we': fields.boolean('Wed'),
923 'th': fields.boolean('Thu'),
924 'fr': fields.boolean('Fri'),
925 'sa': fields.boolean('Sat'),
926 'su': fields.boolean('Sun'),
927 'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
928 'day': fields.integer('Date of month'),
929 'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
930 'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
931 'final_date': fields.date('Repeat Until'), # The last event of a recurrence
933 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
934 'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False), # Color of creator
935 '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."),
936 'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
937 'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
938 'partner_ids': fields.many2many('res.partner', 'calendar_event_res_partner_rel', string='Attendees', states={'done': [('readonly', True)]}),
939 'alarm_ids': fields.many2many('calendar.alarm', 'calendar_alarm_calendar_event_rel', string='Reminders', ondelete="restrict", copy=False),
942 def _get_default_partners(self, cr, uid, ctx=None):
943 ret = [self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id]
944 active_id = ctx.get('active_id')
945 if ctx.get('active_model') == 'res.partner' and active_id:
946 if active_id not in ret:
947 ret.append(active_id)
961 'user_id': lambda self, cr, uid, ctx: uid,
962 'partner_ids': _get_default_partners,
965 def _check_closing_date(self, cr, uid, ids, context=None):
966 for event in self.browse(cr, uid, ids, context=context):
967 if event.stop < event.start:
972 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['start', 'stop'])
975 def onchange_allday(self, cr, uid, ids, start=False, end=False, starttime=False, endtime=False, startdatetime=False, enddatetime=False, checkallday=False, context=None):
979 if not ((starttime and endtime) or (start and end)): # At first intialize, we have not datetime
982 if checkallday: # from datetime to date
983 startdatetime = startdatetime or start
985 start = datetime.strptime(startdatetime, DEFAULT_SERVER_DATETIME_FORMAT)
986 value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
988 enddatetime = enddatetime or end
990 end = datetime.strptime(enddatetime, DEFAULT_SERVER_DATETIME_FORMAT)
991 value['stop_date'] = datetime.strftime(end, DEFAULT_SERVER_DATE_FORMAT)
992 else: # from date to datetime
993 user = self.pool['res.users'].browse(cr, uid, uid, context)
994 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
997 start = datetime.strptime(starttime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
998 startdate = tz.localize(start) # Add "+hh:mm" timezone
999 startdate = startdate.replace(hour=8) # Set 8 AM in localtime
1000 startdate = startdate.astimezone(pytz.utc) # Convert to UTC
1001 value['start_datetime'] = datetime.strftime(startdate, DEFAULT_SERVER_DATETIME_FORMAT)
1003 value['start_datetime'] = start
1006 end = datetime.strptime(endtime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
1007 enddate = tz.localize(end).replace(hour=18).astimezone(pytz.utc)
1009 value['stop_datetime'] = datetime.strftime(enddate, DEFAULT_SERVER_DATETIME_FORMAT)
1011 value['stop_datetime'] = end
1013 return {'value': value}
1015 def onchange_dates(self, cr, uid, ids, fromtype, start=False, end=False, checkallday=False, allday=False, context=None):
1017 """Returns duration and end date based on values passed
1018 @param ids: List of calendar event's IDs.
1022 if checkallday != allday:
1025 value['allday'] = checkallday # Force to be rewrited
1028 if fromtype == 'start' and start:
1029 start = datetime.strptime(start, DEFAULT_SERVER_DATE_FORMAT)
1030 value['start_datetime'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1031 value['start'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1033 if fromtype == 'stop' and end:
1034 end = datetime.strptime(end, DEFAULT_SERVER_DATE_FORMAT)
1035 value['stop_datetime'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1036 value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1039 if fromtype == 'start' and start:
1040 start = datetime.strptime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1041 value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
1042 value['start'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1043 if fromtype == 'stop' and end:
1044 end = datetime.strptime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1045 value['stop_date'] = datetime.strftime(end, DEFAULT_SERVER_DATE_FORMAT)
1046 value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1048 return {'value': value}
1050 def new_invitation_token(self, cr, uid, record, partner_id):
1051 return uuid.uuid4().hex
1053 def create_attendees(self, cr, uid, ids, context=None):
1056 user_obj = self.pool['res.users']
1057 current_user = user_obj.browse(cr, uid, uid, context=context)
1059 for event in self.browse(cr, uid, ids, context):
1061 for att in event.attendee_ids:
1062 attendees[att.partner_id.id] = True
1064 new_att_partner_ids = []
1065 for partner in event.partner_ids:
1066 if partner.id in attendees:
1068 access_token = self.new_invitation_token(cr, uid, event, partner.id)
1070 'partner_id': partner.id,
1071 'event_id': event.id,
1072 'access_token': access_token,
1073 'email': partner.email,
1076 if partner.id == current_user.partner_id.id:
1077 values['state'] = 'accepted'
1079 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
1080 new_attendees.append(att_id)
1081 new_att_partner_ids.append(partner.id)
1083 if not current_user.email or current_user.email != partner.email:
1084 mail_from = current_user.email or tools.config.get('email_from', False)
1085 if not context.get('no_email'):
1086 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
1087 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)
1090 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
1091 if new_att_partner_ids:
1092 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
1094 # We remove old attendees who are not in partner_ids now.
1095 all_partner_ids = [part.id for part in event.partner_ids]
1096 all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
1097 all_attendee_ids = [att.id for att in event.attendee_ids]
1098 partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
1100 attendee_ids_to_remove = []
1102 if partner_ids_to_remove:
1103 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)
1104 if attendee_ids_to_remove:
1105 self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
1108 'new_attendee_ids': new_attendees,
1109 'old_attendee_ids': all_attendee_ids,
1110 'removed_attendee_ids': attendee_ids_to_remove
1114 def get_search_fields(self, browse_event, order_fields, r_date=None):
1116 for ord in order_fields:
1117 if ord == 'id' and r_date:
1118 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
1120 sort_fields[ord] = browse_event[ord]
1121 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
1122 name_get = browse_event[ord].name_get()
1123 if len(name_get) and len(name_get[0]) >= 2:
1124 sort_fields[ord] = name_get[0][1]
1126 sort_fields['sort_start'] = r_date.strftime("%Y%m%d%H%M%S")
1128 sort_fields['sort_start'] = browse_event['display_start'].replace(' ', '').replace('-', '')
1131 def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
1133 """Gives virtual event ids for recurring events
1134 This method gives ids of dates that comes between start date and end date of calendar views
1136 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1141 if isinstance(event_id, (str, int, long)):
1142 ids_to_browse = [event_id] # keep select for return
1144 ids_to_browse = event_id
1147 order_fields = [field.split()[0] for field in order.split(',')]
1149 # fallback on self._order defined on the model
1150 order_fields = [field.split()[0] for field in self._order.split(',')]
1152 if 'id' not in order_fields:
1153 order_fields.append('id')
1157 for ev in self.browse(cr, uid, ids_to_browse, context=context):
1158 if not ev.recurrency or not ev.rrule:
1159 result.append(ev.id)
1160 result_data.append(self.get_search_fields(ev, order_fields))
1162 rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1164 for r_date in rdates:
1165 # fix domain evaluation
1166 # step 1: check date and replace expression by True or False, replace other expressions by True
1167 # step 2: evaluation of & and |
1168 # check if there are one False
1172 if str(arg[0]) in ('start', 'stop', 'final_date'):
1174 ok = r_date.strftime('%Y-%m-%d') == arg[2]
1176 ok = r_date.strftime('%Y-%m-%d') > arg[2]
1178 ok = r_date.strftime('%Y-%m-%d') < arg[2]
1179 if (arg[1] == '>='):
1180 ok = r_date.strftime('%Y-%m-%d') >= arg[2]
1181 if (arg[1] == '<='):
1182 ok = r_date.strftime('%Y-%m-%d') <= arg[2]
1184 elif str(arg) == str('&') or str(arg) == str('|'):
1191 if not isinstance(item, basestring):
1193 elif str(item) == str('&'):
1194 first = new_pile.pop()
1195 second = new_pile.pop()
1196 res = first and second
1197 elif str(item) == str('|'):
1198 first = new_pile.pop()
1199 second = new_pile.pop()
1200 res = first or second
1201 new_pile.append(res)
1203 if [True for item in new_pile if not item]:
1205 result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
1208 uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
1210 def comparer(left, right):
1211 for fn, mult in comparers:
1212 result = cmp(fn(left), fn(right))
1214 return mult * result
1217 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1218 sort_params = uniq([comp if comp not in ['start', 'start_date', 'start_datetime'] else 'sort_start' for comp in sort_params])
1219 sort_params = uniq([comp if comp not in ['-start', '-start_date', '-start_datetime'] else '-sort_start' for comp in sort_params])
1220 comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1221 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1223 if isinstance(event_id, (str, int, long)):
1224 return ids and ids[0] or False
1228 def compute_rule_string(self, data):
1230 Compute rule string according to value type RECUR of iCalendar from the values given.
1231 @param self: the object pointer
1232 @param data: dictionary of freq and interval value
1233 @return: string containing recurring rule (empty if no rule)
1235 if data['interval'] and data['interval'] < 0:
1236 raise osv.except_osv(_('warning!'), _('interval cannot be negative.'))
1237 if data['count'] and data['count'] <= 0:
1238 raise osv.except_osv(_('warning!'), _('count cannot be negative or 0.'))
1240 def get_week_string(freq, data):
1241 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1242 if freq == 'weekly':
1243 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1245 return ';BYDAY=' + ','.join(byday)
1248 def get_month_string(freq, data):
1249 if freq == 'monthly':
1250 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1251 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1253 if data.get('month_by') == 'day': # Eg : Second Monday of the month
1254 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1255 elif data.get('month_by') == 'date': # Eg : 16th of the month
1256 return ';BYMONTHDAY=' + str(data.get('day'))
1259 def get_end_date(data):
1260 if data.get('final_date'):
1261 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('final_date'))) + 'T235959Z'
1263 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1264 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1266 freq = data.get('rrule_type', False) # day/week/month/year
1269 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1270 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1274 def _get_empty_rrule_data(self):
1277 'recurrency': False,
1278 'final_date': False,
1279 'rrule_type': False,
1295 def _parse_rrule(self, rule, data, date_start):
1296 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1297 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1298 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, DEFAULT_SERVER_DATETIME_FORMAT))
1300 if r._freq > 0 and r._freq < 4:
1301 data['rrule_type'] = rrule_type[r._freq]
1302 data['count'] = r._count
1303 data['interval'] = r._interval
1304 data['final_date'] = r._until and r._until.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1307 for i in xrange(0, 7):
1308 if i in r._byweekday:
1309 data[day_list[i]] = True
1310 data['rrule_type'] = 'weekly'
1311 #repeat monthly by nweekday ((weekday, weeknumber), )
1313 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1314 data['byday'] = str(r._bynweekday[0][1])
1315 data['month_by'] = 'day'
1316 data['rrule_type'] = 'monthly'
1319 data['day'] = r._bymonthday[0]
1320 data['month_by'] = 'date'
1321 data['rrule_type'] = 'monthly'
1323 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1325 data['interval'] = data['interval'] * 12
1327 #FIXEME handle forever case
1329 #in case of repeat for ever that we do not support right now
1330 if not (data.get('count') or data.get('final_date')):
1332 if data.get('count'):
1333 data['end_type'] = 'count'
1335 data['end_type'] = 'end_date'
1338 def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1340 for virtual_id in ids:
1341 real_id = calendar_id2real_id(virtual_id)
1342 result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1343 res[virtual_id] = result[real_id]
1346 def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1347 """ The basic purpose of this method is to check that destination partners
1348 effectively have email addresses. Otherwise a warning is thrown.
1349 :param value: value format: [[6, 0, [3, 4]]]
1353 if not value or not value[0] or not value[0][0] == 6:
1356 res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1359 def check_partners_email(self, cr, uid, partner_ids, context=None):
1360 """ Verify that selected partner_ids have an email_address defined.
1361 Otherwise throw a warning. """
1362 partner_wo_email_lst = []
1363 for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1364 if not partner.email:
1365 partner_wo_email_lst.append(partner)
1366 if not partner_wo_email_lst:
1368 warning_msg = _('The following contacts have no email address :')
1369 for partner in partner_wo_email_lst:
1370 warning_msg += '\n- %s' % (partner.name)
1371 return {'warning': {
1372 'title': _('Email addresses not found'),
1373 'message': warning_msg,
1376 # shows events of the day for this user
1377 def _needaction_domain_get(self, cr, uid, context=None):
1379 ('stop', '<=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 23:59:59')),
1380 ('start', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 00:00:00')),
1381 ('user_id', '=', uid),
1384 @api.cr_uid_ids_context
1385 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
1386 if isinstance(thread_id, str):
1387 thread_id = get_real_ids(thread_id)
1388 if context.get('default_date'):
1389 del context['default_date']
1390 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)
1392 def do_sendmail(self, cr, uid, ids, context=None):
1393 for event in self.browse(cr, uid, ids, context):
1394 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1396 if current_user.email:
1397 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):
1398 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="calendar.subtype_invitation", context=context)
1401 def get_attendee(self, cr, uid, meeting_id, context=None):
1402 # Used for view in controller
1403 invitation = {'meeting': {}, 'attendee': []}
1405 meeting = self.browse(cr, uid, int(meeting_id), context=context)
1406 invitation['meeting'] = {
1407 'event': meeting.name,
1408 'where': meeting.location,
1409 'when': meeting.display_time
1412 for attendee in meeting.attendee_ids:
1413 invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1416 def get_interval(self, cr, uid, ids, date, interval, tz=None, context=None):
1417 #Function used only in calendar_event_data.xml for email template
1418 date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
1421 timezone = pytz.timezone(tz or 'UTC')
1422 date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
1424 if interval == 'day':
1426 elif interval == 'month':
1427 res = date.strftime('%B') + " " + str(date.year)
1428 elif interval == 'dayname':
1429 res = date.strftime('%A')
1430 elif interval == 'time':
1431 dummy, format_time = self.get_date_formats(cr, uid, context=context)
1432 res = date.strftime(format_time + " %Z")
1435 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1439 if context.get('mymeetings', False):
1440 partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1441 args += [('partner_ids', 'in', [partner_id])]
1447 if arg[0] in ('start_date', 'start_datetime', 'start',) and arg[1] == ">=":
1448 if context.get('virtual_id', True):
1449 new_args += ['|', '&', ('recurrency', '=', 1), ('final_date', arg[1], arg[2])]
1450 elif arg[0] == "id":
1451 new_id = get_real_ids(arg[2])
1452 new_arg = (arg[0], arg[1], new_id)
1453 new_args.append(new_arg)
1455 if not context.get('virtual_id', True):
1456 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, count=count, context=context)
1458 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1459 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1460 res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1464 return res[offset: offset + limit]
1467 def copy(self, cr, uid, id, default=None, context=None):
1468 default = default or {}
1469 self._set_date(cr, uid, default, id=default.get('id'), context=context)
1470 return super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1472 def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1473 real_event_id = calendar_id2real_id(id)
1474 data = self.read(cr, uid, id, ['allday', 'start', 'stop', 'rrule', 'duration'])
1475 data['start_date' if data['allday'] else 'start_datetime'] = data['start']
1476 data['stop_date' if data['allday'] else 'stop_datetime'] = data['stop']
1477 if data.get('rrule'):
1480 recurrent_id=real_event_id,
1481 recurrent_id_date=data.get('start'),
1485 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'))
1491 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1494 def open_after_detach_event(self, cr, uid, ids, context=None):
1498 new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1500 'type': 'ir.actions.act_window',
1501 'res_model': 'calendar.event',
1502 'view_mode': 'form',
1504 'target': 'current',
1505 'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
1508 def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
1511 for n, calendar_id in enumerate(arg[2]):
1512 if isinstance(calendar_id, str):
1513 arg[2][n] = calendar_id.split('-')[0]
1514 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)
1516 def write(self, cr, uid, ids, values, context=None):
1517 def _only_changes_to_apply_on_real_ids(field_names):
1518 ''' return True if changes are only to be made on the real ids'''
1519 for field in field_names:
1520 if field in ['start', 'start_date', 'start_datetime', 'stop', 'stop_date', 'stop_datetime', 'active']:
1524 if not isinstance(ids, (tuple, list)):
1527 context = context or {}
1528 self._set_date(cr, uid, values, id=ids[0], context=context)
1531 if isinstance(one_ids, (str, int, long)):
1532 if len(str(one_ids).split('-')) == 1:
1533 ids = [int(one_ids)]
1540 # Special write of complex IDS
1541 for event_id in list(ids):
1542 if len(str(event_id).split('-')) == 1:
1545 ids.remove(event_id)
1546 real_event_id = calendar_id2real_id(event_id)
1548 # if we are setting the recurrency flag to False or if we are only changing fields that
1549 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1550 # then set real ids to be updated.
1551 if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()):
1552 ids.append(real_event_id)
1555 data = self.read(cr, uid, event_id, ['start', 'stop', 'rrule', 'duration'])
1556 if data.get('rrule'):
1557 new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
1559 res = super(calendar_event, self).write(cr, uid, [int(event_id) for event_id in ids], values, context=context)
1561 # set end_date for calendar searching
1562 if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
1563 (values.get('rrule_type') or values.get('count') or values.get('start') or values.get('stop')):
1565 final_date = self._get_recurrency_end_date(cr, uid, id, context=context)
1566 super(calendar_event, self).write(cr, uid, [id], {'final_date': final_date}, context=context)
1568 attendees_create = False
1569 if values.get('partner_ids', False):
1570 attendees_create = self.create_attendees(cr, uid, ids, context)
1572 if (values.get('start_date') or values.get('start_datetime', False)) and values.get('active', True):
1573 the_id = new_id or (ids and int(ids[0]))
1575 if attendees_create:
1576 attendees_create = attendees_create[the_id]
1577 mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
1579 mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1582 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1583 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):
1584 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)
1585 return res or True and False
1587 def create(self, cr, uid, vals, context=None):
1591 self._set_date(cr, uid, vals, id=False, context=context)
1592 if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user
1593 vals['user_id'] = uid
1595 res = super(calendar_event, self).create(cr, uid, vals, context=context)
1597 final_date = self._get_recurrency_end_date(cr, uid, res, context=context)
1598 self.write(cr, uid, [res], {'final_date': final_date}, context=context)
1600 self.create_attendees(cr, uid, [res], context=context)
1603 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
1604 context = dict(context or {})
1606 if 'date' in groupby:
1607 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1608 virtual_id = context.get('virtual_id', True)
1609 context.update({'virtual_id': False})
1610 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
1612 #remove the count, since the value is not consistent with the result of the search when expand the group
1613 for groupname in groupby:
1614 if result.get(groupname + "_count"):
1615 del result[groupname + "_count"]
1616 result.get('__context', {}).update({'virtual_id': virtual_id})
1619 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1622 fields2 = fields and fields[:] or None
1623 EXTRAFIELDS = ('class', 'user_id', 'duration', 'allday', 'start', 'start_date', 'start_datetime', 'rrule')
1624 for f in EXTRAFIELDS:
1625 if fields and (f not in fields):
1627 if isinstance(ids, (str, int, long)):
1631 select = map(lambda x: (x, calendar_id2real_id(x)), select)
1633 real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1634 real_data = dict(zip([x['id'] for x in real_data], real_data))
1636 for calendar_id, real_id in select:
1637 res = real_data[real_id].copy()
1638 ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) > 0 and res.get('duration') or 1)
1639 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1640 res['start'] = ls[1]
1644 res['start_date'] = ls[1]
1645 res['stop_date'] = ls[2]
1647 res['start_datetime'] = ls[1]
1648 res['stop_datetime'] = ls[2]
1650 if 'display_time' in fields:
1651 res['display_time'] = self._get_display_time(cr, uid, ls[1], ls[2], res['duration'], res['allday'], context=context)
1653 res['id'] = calendar_id
1658 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1661 if r['class'] == 'private':
1663 if f not in ('id', 'allday', 'start', 'stop', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date', 'rrule'):
1664 if isinstance(r[f], list):
1672 for k in EXTRAFIELDS:
1673 if (k in r) and (fields and (k not in fields)):
1675 if isinstance(ids, (str, int, long)):
1676 return result and result[0] or False
1679 def unlink(self, cr, uid, ids, can_be_deleted=True, context=None):
1680 if not isinstance(ids, list):
1687 for event_id in ids:
1688 if can_be_deleted and len(str(event_id).split('-')) == 1: # if ID REAL
1689 if self.browse(cr, uid, int(event_id), context).recurrent_id:
1690 ids_to_exclure.append(event_id)
1692 ids_to_unlink.append(int(event_id))
1694 ids_to_exclure.append(event_id)
1697 res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1700 for id_to_exclure in ids_to_exclure:
1701 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1706 class mail_message(osv.Model):
1707 _inherit = "mail.message"
1709 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1711 convert the search on real ids in the case it was asked on virtual ids, then call super()
1713 for index in range(len(args)):
1714 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1715 args[index][2] = get_real_ids(args[index][2])
1716 return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1718 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1721 if doc_model == 'calendar.event':
1722 order = context.get('order', self._order)
1723 for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1724 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1725 return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1728 class ir_attachment(osv.Model):
1729 _inherit = "ir.attachment"
1731 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1733 convert the search on real ids in the case it was asked on virtual ids, then call super()
1735 for index in range(len(args)):
1736 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1737 args[index][2] = get_real_ids(args[index][2])
1738 return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1740 def write(self, cr, uid, ids, vals, context=None):
1742 when posting an attachment (new or not), convert the virtual ids in real ids.
1744 if isinstance(vals.get('res_id'), str):
1745 vals['res_id'] = get_real_ids(vals.get('res_id'))
1746 return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1749 class ir_http(osv.AbstractModel):
1750 _inherit = 'ir.http'
1752 def _auth_method_calendar(self):
1753 token = request.params['token']
1754 db = request.params['db']
1756 registry = openerp.modules.registry.RegistryManager.get(db)
1757 attendee_pool = registry.get('calendar.attendee')
1758 error_message = False
1759 with registry.cursor() as cr:
1760 attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
1762 error_message = """Invalid Invitation Token."""
1763 elif request.session.uid and request.session.login != 'anonymous':
1764 # if valid session but user is not match
1765 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1766 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1767 if attendee.partner_id.id != user.partner_id.id:
1768 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)
1771 raise BadRequest(error_message)
1776 class invite_wizard(osv.osv_memory):
1777 _inherit = 'mail.wizard.invite'
1779 def default_get(self, cr, uid, fields, context=None):
1781 in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1783 result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1784 if 'res_id' in result:
1785 result['res_id'] = get_real_ids(result['res_id'])