1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Business Applications
5 # Copyright (c) 2011-2014 OpenERP S.A. <http://openerp.com>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
26 import openerp.service.report
28 from werkzeug.exceptions import BadRequest
29 from datetime import datetime, timedelta
30 from dateutil import parser
31 from dateutil import rrule
32 from dateutil.relativedelta import relativedelta
33 from openerp import tools, SUPERUSER_ID
34 from openerp.osv import fields, osv
35 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
36 from openerp.tools.translate import _
37 from openerp.http import request
38 from operator import itemgetter
41 _logger = logging.getLogger(__name__)
44 def calendar_id2real_id(calendar_id=None, with_date=False):
46 Convert a "virtual/recurring event id" (type string) into a real event id (type int).
47 E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
48 @param calendar_id: id of calendar
49 @param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id
50 @return: real event id
52 if calendar_id and isinstance(calendar_id, (str, unicode)):
53 res = calendar_id.split('-')
57 real_date = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(res[1], "%Y%m%d%H%M%S"))
58 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
59 end = start + timedelta(hours=with_date)
60 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
62 return calendar_id and int(calendar_id) or calendar_id
65 def get_real_ids(ids):
66 if isinstance(ids, (str, int, long)):
67 return calendar_id2real_id(ids)
69 if isinstance(ids, (list, tuple)):
70 return [calendar_id2real_id(id) for id in ids]
73 class calendar_attendee(osv.Model):
75 Calendar Attendee Information
77 _name = 'calendar.attendee'
79 _description = 'Attendee information'
81 def _compute_data(self, cr, uid, ids, name, arg, context=None):
83 Compute data on function fields for attendee values.
84 @param ids: list of calendar attendee's IDs
85 @param name: name of field
86 @return: dictionary of form {id: {'field Name': value'}}
90 for attdata in self.browse(cr, uid, ids, context=context):
94 if attdata.partner_id:
95 result[id][name] = attdata.partner_id.name or False
97 result[id][name] = attdata.email or ''
98 if name == 'event_date':
99 result[id][name] = attdata.event_id.date
100 if name == 'event_end_date':
101 result[id][name] = attdata.event_id.date_deadline
105 ('needsAction', 'Needs Action'),
106 ('tentative', 'Uncertain'),
107 ('declined', 'Declined'),
108 ('accepted', 'Accepted'),
112 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="Status of the attendee's participation"),
113 'cn': fields.function(_compute_data, string='Common name', type="char", multi='cn', store=True),
114 'partner_id': fields.many2one('res.partner', 'Contact', readonly="True"),
115 'email': fields.char('Email', help="Email of Invited Person"),
116 'event_date': fields.function(_compute_data, string='Event Date', type="datetime", multi='event_date'),
117 'event_end_date': fields.function(_compute_data, string='Event End Date', type="datetime", multi='event_end_date'),
118 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
119 'access_token': fields.char('Invitation Token'),
120 'event_id': fields.many2one('calendar.event', 'Meeting linked'),
123 'state': 'needsAction',
126 def copy(self, cr, uid, id, default=None, context=None):
127 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
129 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
131 Make entry on email and availability on change of partner_id field.
132 @param partner_id: changed value of partner id
135 return {'value': {'email': ''}}
136 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
137 return {'value': {'email': partner.email}}
139 def get_ics_file(self, cr, uid, event_obj, context=None):
141 Returns iCalendar file for the event invitation.
142 @param event_obj: event object (browse record)
143 @return: .ics file content
147 def ics_datetime(idate, short=False):
149 return datetime.strptime(idate.split('.')[0], '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.timezone('UTC'))
153 # FIXME: why isn't this in CalDAV?
158 cal = vobject.iCalendar()
159 event = cal.add('vevent')
160 if not event_obj.date_deadline or not event_obj.date:
161 raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation."))
162 event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S'))
163 event.add('dtstart').value = ics_datetime(event_obj.date)
164 event.add('dtend').value = ics_datetime(event_obj.date_deadline)
165 event.add('summary').value = event_obj.name
166 if event_obj.description:
167 event.add('description').value = event_obj.description
168 if event_obj.location:
169 event.add('location').value = event_obj.location
171 event.add('rrule').value = event_obj.rrule
173 if event_obj.alarm_ids:
174 for alarm in event_obj.alarm_ids:
175 valarm = event.add('valarm')
176 interval = alarm.interval
177 duration = alarm.duration
178 trigger = valarm.add('TRIGGER')
179 trigger.params['related'] = ["START"]
180 if interval == 'days':
181 delta = timedelta(days=duration)
182 elif interval == 'hours':
183 delta = timedelta(hours=duration)
184 elif interval == 'minutes':
185 delta = timedelta(minutes=duration)
186 trigger.value = delta
187 valarm.add('DESCRIPTION').value = alarm.name or 'OpenERP'
188 for attendee in event_obj.attendee_ids:
189 attendee_add = event.add('attendee')
190 attendee_add.value = 'MAILTO:' + (attendee.email or '')
191 res = cal.serialize()
194 def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False), template_xmlid='calendar_template_meeting_invitation', context=None):
196 Send mail for event invitation to event attendees.
197 @param email_from: email address for user sending the mail
201 data_pool = self.pool['ir.model.data']
202 mailmess_pool = self.pool['mail.message']
203 mail_pool = self.pool['mail.mail']
204 template_pool = self.pool['email.template']
205 local_context = context.copy()
207 'needsAction': 'grey',
209 'tentative': '#FFFF00',
213 if not isinstance(ids, (tuple, list)):
216 dummy, template_id = data_pool.get_object_reference(cr, uid, 'calendar', template_xmlid)
217 dummy, act_id = data_pool.get_object_reference(cr, uid, 'calendar', "view_calendar_event_calendar")
218 local_context.update({
220 'action_id': self.pool['ir.actions.act_window'].search(cr, uid, [('view_id', '=', act_id)], context=context)[0],
222 'base_url': self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
225 for attendee in self.browse(cr, uid, ids, context=context):
226 if attendee.email and email_from and attendee.email != email_from:
227 ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
228 mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
232 vals['attachment_ids'] = [(0, 0, {'name': 'invitation.ics',
233 'datas_fname': 'invitation.ics',
234 'datas': str(ics_file).encode('base64')})]
235 vals['model'] = None # We don't want to have the mail in the tchatter while in queue!
236 the_mailmess = mail_pool.browse(cr, uid, mail_id, context=context).mail_message_id
237 mailmess_pool.write(cr, uid, [the_mailmess.id], vals, context=context)
238 mail_ids.append(mail_id)
241 res = mail_pool.send(cr, uid, mail_ids, context=context)
245 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
247 Make entry on email and availability on change of user_id field.
248 @param ids: list of attendee's IDs
249 @param user_id: changed value of User id
250 @return: dictionary of values which put value in email and availability fields
253 return {'value': {'email': ''}}
255 user = self.pool['res.users'].browse(cr, uid, user_id, *args)
256 return {'value': {'email': user.email, 'availability': user.availability}}
258 def do_tentative(self, cr, uid, ids, context=None, *args):
260 Makes event invitation as Tentative.
261 @param ids: list of attendee's IDs
263 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
265 def do_accept(self, cr, uid, ids, context=None, *args):
267 Marks event invitation as Accepted.
268 @param ids: list of attendee's IDs
272 meeting_obj = self.pool['calendar.event']
273 res = self.write(cr, uid, ids, {'state': 'accepted'}, 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 accepted invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
279 def do_decline(self, cr, uid, ids, context=None, *args):
281 Marks event invitation as Declined.
282 @param ids: list of calendar attendee's IDs
286 meeting_obj = self.pool['calendar.event']
287 res = self.write(cr, uid, ids, {'state': 'declined'}, context)
288 for attendee in self.browse(cr, uid, ids, context=context):
289 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has declined invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
292 def create(self, cr, uid, vals, context=None):
295 if not vals.get("email") and vals.get("cn"):
296 cnval = vals.get("cn").split(':')
297 email = filter(lambda x: x.__contains__('@'), cnval)
298 vals['email'] = email and email[0] or ''
299 vals['cn'] = vals.get("cn")
300 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
304 class res_partner(osv.Model):
305 _inherit = 'res.partner'
307 'calendar_last_notif_ack': fields.datetime('Last notification marked as read from base Calendar'),
310 def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
314 meeting = self.pool['calendar.event'].browse(cr, uid, get_real_ids(meeting_id), context=context)
315 for partner in self.browse(cr, uid, ids, context=context):
316 data = self.name_get(cr, uid, [partner.id], context)[0]
318 for attendee in meeting.attendee_ids:
319 if attendee.partner_id.id == partner.id:
320 data = (data[0], data[1], attendee.state)
324 def calendar_last_notif_ack(self, cr, uid, context=None):
325 partner = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id
326 self.write(cr, uid, partner.id, {'calendar_last_notif_ack': datetime.now()}, context=context)
330 class calendar_alarm_manager(osv.AbstractModel):
331 _name = 'calendar.alarm_manager'
333 def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None):
338 cal.date - interval '1' minute * calcul_delta.max_delta AS first_alarm,
340 WHEN cal.recurrency THEN cal.end_date - interval '1' minute * calcul_delta.min_delta
341 ELSE cal.date_deadline - interval '1' minute * calcul_delta.min_delta
343 cal.date as first_event_date,
345 WHEN cal.recurrency THEN cal.end_date
346 ELSE cal.date_deadline
347 END as last_event_date,
348 calcul_delta.min_delta,
349 calcul_delta.max_delta,
352 calendar_event AS cal
356 rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
358 calendar_alarm_calendar_event_rel AS rel
359 LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
360 WHERE alarm.type in %s
361 GROUP BY rel.calendar_event_id
362 ) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id
366 RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
367 AND part_rel.res_partner_id = %s
373 type_to_read += ('notification',)
375 type_to_read += ('email',)
377 tuple_params = (type_to_read,)
379 #ADD FILTER ON PARTNER_ID
381 base_request += filter_user
382 tuple_params += (partner_id, )
385 tuple_params += (seconds, seconds,)
387 cr.execute("""SELECT *
388 FROM ( %s ) AS ALL_EVENTS
389 WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second )
390 AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%%s' second )
391 """ % base_request, tuple_params)
393 for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
395 'event_id': event_id,
396 'first_alarm': first_alarm,
397 'last_alarm': last_alarm,
398 'first_meeting': first_meeting,
399 'last_meeting': last_meeting,
400 'min_duration': min_duration,
401 'max_duration': max_duration,
407 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):
412 alarm_type.append('notification')
414 alarm_type.append('email')
416 if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
417 for alarm in event.alarm_ids:
418 if alarm.type in alarm_type and \
419 one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
420 (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], "%Y-%m-%d %H:%M:%S")):
422 'alarm_id': alarm.id,
423 'event_id': event.id,
424 'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
429 def get_next_mail(self, cr, uid, context=None):
430 cron = self.pool.get('ir.cron').search(cr, uid, [('model', 'ilike', self._name)], context=context)
431 if cron and len(cron) == 1:
432 cron = self.pool.get('ir.cron').browse(cr, uid, cron[0], context=context)
434 raise ("Cron for " + self._name + " not identified :( !")
436 if cron.interval_type == "weeks":
437 cron_interval = cron.interval_number * 7 * 24 * 60 * 60
438 elif cron.interval_type == "days":
439 cron_interval = cron.interval_number * 24 * 60 * 60
440 elif cron.interval_type == "hours":
441 cron_interval = cron.interval_number * 60 * 60
442 elif cron.interval_type == "minutes":
443 cron_interval = cron.interval_number * 60
444 elif cron.interval_type == "seconds":
445 cron_interval = cron.interval_number
447 if not cron_interval:
448 raise ("Cron delay for " + self._name + " can not be calculated :( !")
450 all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
452 for event in all_events: # .values()
453 max_delta = all_events[event]['max_duration']
454 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
455 if curEvent.recurrency:
458 for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
459 in_date_format = one_date.replace(tzinfo=None)
460 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
462 for alert in LastFound:
463 self.do_mail_reminder(cr, uid, alert, context=context)
465 if not bFound: # if it's the first alarm for this recurrent event
467 if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event
470 in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
471 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
473 for alert in LastFound:
474 self.do_mail_reminder(cr, uid, alert, context=context)
476 def get_next_notif(self, cr, uid, context=None):
477 ajax_check_every_seconds = 300
478 partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
484 all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
486 for event in all_events: # .values()
487 max_delta = all_events[event]['max_duration']
488 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
489 if curEvent.recurrency:
492 for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
493 in_date_format = one_date.replace(tzinfo=None)
494 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)
496 for alert in LastFound:
497 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
498 if not bFound: # if it's the first alarm for this recurrent event
500 if bFound and not LastFound: # if the precedent event had alarm but not this one, we can stop the search fot this event
503 in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
504 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)
506 for alert in LastFound:
507 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
510 def do_mail_reminder(self, cr, uid, alert, context=None):
515 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
516 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
518 if alarm.type == 'email':
519 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)
523 def do_notif_reminder(self, cr, uid, alert, context=None):
524 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
525 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
527 if alarm.type == 'notification':
528 message = event.display_time
530 delta = alert['notify_at'] - datetime.now()
531 delta = delta.seconds + delta.days * 3600 * 24
534 'event_id': event.id,
538 'notify_at': alert['notify_at'].strftime("%Y-%m-%d %H:%M:%S"),
542 class calendar_alarm(osv.Model):
543 _name = 'calendar.alarm'
544 _description = 'Event alarm'
546 def _get_duration(self, cr, uid, ids, field_name, arg, context=None):
548 for alarm in self.browse(cr, uid, ids, context=context):
549 if alarm.interval == "minutes":
550 res[alarm.id] = alarm.duration
551 elif alarm.interval == "hours":
552 res[alarm.id] = alarm.duration * 60
553 elif alarm.interval == "days":
554 res[alarm.id] = alarm.duration * 60 * 24
560 'name': fields.char('Name', required=True), # fields function
561 'type': fields.selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True),
562 'duration': fields.integer('Amount', required=True),
563 'interval': fields.selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], 'Unit', required=True),
564 'duration_minutes': fields.function(_get_duration, type='integer', string='duration_minutes', store=True),
568 'type': 'notification',
574 class ir_values(osv.Model):
575 _inherit = 'ir.values'
577 def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
581 if type(data) in (list, tuple):
582 new_model.append((data[0], calendar_id2real_id(data[1])))
584 new_model.append(data)
585 return super(ir_values, self).set(cr, uid, key, key2, name, new_model,
586 value, replace, isobject, meta, preserve_user, company)
588 def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
593 if type(data) in (list, tuple):
594 new_model.append((data[0], calendar_id2real_id(data[1])))
596 new_model.append(data)
597 return super(ir_values, self).get(cr, uid, key, key2, new_model,
598 meta, context, res_id_req, without_user, key2_req)
601 class ir_model(osv.Model):
603 _inherit = 'ir.model'
605 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
606 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
609 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, context=context, load=load)
612 val['id'] = calendar_id2real_id(val['id'])
613 return isinstance(ids, (str, int, long)) and data[0] or data
616 original_exp_report = openerp.service.report.exp_report
619 def exp_report(db, uid, object, ids, data=None, context=None):
623 if object == 'printscreen.list':
624 original_exp_report(db, uid, object, ids, data, context)
627 new_ids.append(calendar_id2real_id(id))
628 if data.get('id', False):
629 data['id'] = calendar_id2real_id(data['id'])
630 return original_exp_report(db, uid, object, new_ids, data, context)
633 openerp.service.report.exp_report = exp_report
636 class calendar_event_type(osv.Model):
637 _name = 'calendar.event.type'
638 _description = 'Meeting Type'
640 'name': fields.char('Name', required=True, translate=True),
644 class calendar_event(osv.Model):
645 """ Model for Calendar Event """
646 _name = 'calendar.event'
647 _description = "Meeting"
649 _inherit = ["mail.thread", "ir.needaction_mixin"]
651 def do_run_scheduler(self, cr, uid, id, context=None):
652 self.pool['calendar.alarm_manager'].do_run_scheduler(cr, uid, context=context)
654 def get_recurrent_date_by_event(self, cr, uid, event, context=None):
655 """Get recurrent dates based on Rule string and all event where recurrent_id is child
659 val = parser.parse(''.join((re.compile('\d')).findall(date)))
660 ## Dates are localized to saved timezone if any, else current timezone.
662 val = pytz.UTC.localize(val)
663 return val.astimezone(timezone)
665 timezone = pytz.timezone(event.vtimezone or context.get('tz') or 'UTC')
666 startdate = pytz.UTC.localize(datetime.strptime(event.date, "%Y-%m-%d %H:%M:%S")) # Add "+hh:mm" timezone
668 startdate = datetime.now()
670 ## Convert the start date to saved timezone (or context tz) as it'll
671 ## define the correct hour/day asked by the user to repeat for recurrence.
672 startdate = startdate.astimezone(timezone) # transform "+hh:mm" timezone
673 rset1 = rrule.rrulestr(str(event.rrule), dtstart=startdate, forceset=True)
674 ids_depending = self.search(cr, uid, [('recurrent_id', '=', event.id), '|', ('active', '=', False), ('active', '=', True)], context=context)
675 all_events = self.browse(cr, uid, ids_depending, context=context)
677 for ev in all_events:
678 rset1._exdate.append(todate(ev.recurrent_id_date))
680 return [d.astimezone(pytz.UTC) for d in rset1]
682 def _get_recurrency_end_date(self, data, context=None):
683 if not data.get('recurrency'):
686 end_type = data.get('end_type')
687 end_date = data.get('end_date')
689 if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'date_deadline']):
690 count = data['count'] + 1
692 'daily': ('days', 1),
693 'weekly': ('days', 7),
694 'monthly': ('months', 1),
695 'yearly': ('years', 1),
696 }[data['rrule_type']]
698 deadline = datetime.strptime(data['date_deadline'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
699 return deadline + relativedelta(**{delay: count * mult})
702 def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
704 Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
706 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
707 for meeting_id in meeting_ids:
708 for attendee in self.browse(cr, uid, meeting_id, context).attendee_ids:
709 if user.partner_id.id == attendee.partner_id.id:
713 def _get_display_time(self, cr, uid, meeting_id, context=None):
715 Return date and time (from to from) based on duration with timezone in string :
717 1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
718 2) if event all day ,return : AllDay, July-31-2013
723 tz = context.get('tz', False)
724 if not tz: # tz can have a value False, so dont do it in the default value of get !
725 tz = pytz.timezone('UTC')
727 meeting = self.browse(cr, uid, meeting_id, context=context)
728 date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
729 date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
730 event_date = date.strftime('%B-%d-%Y')
731 display_time = date.strftime('%H-%M')
733 time = _("AllDay , %s") % (event_date)
734 elif meeting.duration < 24:
735 duration = date + timedelta(hours=meeting.duration)
736 time = ("%s at (%s To %s) (%s)") % (event_date, display_time, duration.strftime('%H-%M'), tz)
738 time = ("%s at %s To\n %s at %s (%s)") % (event_date, display_time, date_deadline.strftime('%B-%d-%Y'), date_deadline.strftime('%H-%M'), tz)
741 def _compute(self, cr, uid, ids, fields, arg, context=None):
743 for meeting_id in ids:
745 attendee = self._find_my_attendee(cr, uid, [meeting_id], context)
747 if field == 'is_attendee':
748 res[meeting_id][field] = True if attendee else False
749 elif field == 'attendee_status':
750 res[meeting_id][field] = attendee.state if attendee else 'needsAction'
751 elif field == 'display_time':
752 res[meeting_id][field] = self._get_display_time(cr, uid, meeting_id, context=context)
755 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
757 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
758 @return: dictionary of rrule value.
761 if not isinstance(ids, list):
765 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
766 data = self.browse(cr, SUPERUSER_ID, id, context=context)
768 if data.interval and data.interval < 0:
769 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
770 if data.count and data.count <= 0:
771 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
773 data = self.read(cr, uid, id, ['id', 'byday', 'recurrency', 'month_list', 'end_date', 'rrule_type', 'month_by', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'day', 'week_list'], context=context)
775 if data['recurrency']:
776 result[event] = self.compute_rule_string(data)
781 # retro compatibility function
782 def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
783 return self._set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=context)
785 def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
786 if not isinstance(ids, list):
788 data = self._get_empty_rrule_data()
790 data['recurrency'] = True
791 for event in self.browse(cr, uid, ids, context=context):
793 update_data = self._parse_rrule(field_value, dict(data), rdate)
794 data.update(update_data)
795 self.write(cr, uid, ids, data, context=context)
798 def _tz_get(self, cr, uid, context=None):
799 return [(x.lower(), x) for x in pytz.all_timezones]
803 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
806 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
810 'id': fields.integer('ID', readonly=True),
811 'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
812 'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
813 'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
814 'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
815 'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
816 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True, track_visibility='onchange'),
817 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
818 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
819 'description': fields.text('Description', states={'done': [('readonly', True)]}),
820 'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
821 'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
822 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
823 'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
824 '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"),
825 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
826 'recurrent_id': fields.integer('Recurrent ID'),
827 'recurrent_id_date': fields.datetime('Recurrent ID date'),
828 'vtimezone': fields.selection(_tz_get, string='Timezone'),
829 'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
830 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
831 'count': fields.integer('Repeat', help="Repeat x times"),
832 'mo': fields.boolean('Mon'),
833 'tu': fields.boolean('Tue'),
834 'we': fields.boolean('Wed'),
835 'th': fields.boolean('Thu'),
836 'fr': fields.boolean('Fri'),
837 'sa': fields.boolean('Sat'),
838 'su': fields.boolean('Sun'),
839 'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
840 'day': fields.integer('Date of month'),
841 'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
842 'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
843 'end_date': fields.date('Repeat Until'),
844 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
845 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
846 'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False), # Color of creator
847 '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."),
848 'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
849 'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
850 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
851 'alarm_ids': fields.many2many('calendar.alarm', string='Reminders', ondelete="restrict"),
864 'user_id': lambda self, cr, uid, ctx: uid,
865 'partner_ids': lambda self, cr, uid, ctx: [self.pool['res.users'].browse(cr, uid, [uid], context=ctx)[0].partner_id.id]
868 def _check_closing_date(self, cr, uid, ids, context=None):
869 for event in self.browse(cr, uid, ids, context=context):
870 if event.date_deadline < event.date:
875 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
878 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
880 """Returns duration and/or end date based on values passed
881 @param ids: List of calendar event's IDs.
890 if not end_date and not duration:
892 value['duration'] = duration
894 if allday: # For all day event
895 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
896 user = self.pool['res.users'].browse(cr, uid, uid)
897 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
898 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
899 start = start.astimezone(pytz.utc) # convert start back to utc
901 value['duration'] = 24.0
902 value['date'] = datetime.strftime(start, "%Y-%m-%d %H:%M:%S")
904 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
906 if end_date and not duration:
907 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
909 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
910 value['duration'] = round(duration, 2)
912 end = start + timedelta(hours=duration)
913 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
914 elif end_date and duration and not allday:
915 # we have both, keep them synchronized:
916 # set duration based on end_date (arbitrary decision: this avoid
917 # getting dates like 06:31:48 instead of 06:32:00)
918 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
920 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
921 value['duration'] = round(duration, 2)
923 return {'value': value}
925 def new_invitation_token(self, cr, uid, record, partner_id):
926 return uuid.uuid4().hex
928 def create_attendees(self, cr, uid, ids, context):
929 user_obj = self.pool['res.users']
930 current_user = user_obj.browse(cr, uid, uid, context=context)
932 for event in self.browse(cr, uid, ids, context):
934 for att in event.attendee_ids:
935 attendees[att.partner_id.id] = True
937 new_att_partner_ids = []
938 for partner in event.partner_ids:
939 if partner.id in attendees:
941 access_token = self.new_invitation_token(cr, uid, event, partner.id)
943 'partner_id': partner.id,
944 'event_id': event.id,
945 'access_token': access_token,
946 'email': partner.email,
949 if partner.id == current_user.partner_id.id:
950 values['state'] = 'accepted'
952 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
953 new_attendees.append(att_id)
954 new_att_partner_ids.append(partner.id)
956 if not current_user.email or current_user.email != partner.email:
957 mail_from = current_user.email or tools.config.get('email_from', False)
958 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
959 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)
962 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
963 if new_att_partner_ids:
964 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
966 # We remove old attendees who are not in partner_ids now.
967 all_partner_ids = [part.id for part in event.partner_ids]
968 all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
969 all_attendee_ids = [att.id for att in event.attendee_ids]
970 partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
972 attendee_ids_to_remove = []
974 if partner_ids_to_remove:
975 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)
976 if attendee_ids_to_remove:
977 self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
980 'new_attendee_ids': new_attendees,
981 'old_attendee_ids': all_attendee_ids,
982 'removed_attendee_ids': attendee_ids_to_remove
986 def get_search_fields(self, browse_event, order_fields, r_date=None):
988 for ord in order_fields:
989 if ord == 'id' and r_date:
990 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
992 sort_fields[ord] = browse_event[ord]
993 'If we sort on FK, we obtain a browse_record, so we need to sort on name_get'
994 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
995 name_get = browse_event[ord].name_get()
996 if len(name_get) and len(name_get[0]) >= 2:
997 sort_fields[ord] = name_get[0][1]
1001 def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
1003 """Gives virtual event ids for recurring events
1004 This method gives ids of dates that comes between start date and end date of calendar views
1006 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1012 if isinstance(event_id, (str, int, long)):
1013 ids_to_browse = [event_id] # keep select for return
1015 ids_to_browse = event_id
1018 order_fields = [field.split()[0] for field in order.split(',')]
1020 # fallback on self._order defined on the model
1021 order_fields = [field.split()[0] for field in self._order.split(',')]
1023 if 'id' not in order_fields:
1024 order_fields.append('id')
1028 for ev in self.browse(cr, uid, ids_to_browse, context=context):
1029 if not ev.recurrency or not ev.rrule:
1030 result.append(ev.id)
1031 result_data.append(self.get_search_fields(ev, order_fields))
1034 rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1036 for r_date in rdates:
1037 # fix domain evaluation
1038 # step 1: check date and replace expression by True or False, replace other expressions by True
1039 # step 2: evaluation of & and |
1040 # check if there are one False
1044 if str(arg[0]) in (str('date'), str('date_deadline'), str('end_date')):
1046 ok = r_date.strftime('%Y-%m-%d') == arg[2]
1048 ok = r_date.strftime('%Y-%m-%d') > arg[2]
1050 ok = r_date.strftime('%Y-%m-%d') < arg[2]
1051 if (arg[1] == '>='):
1052 ok = r_date.strftime('%Y-%m-%d') >= arg[2]
1053 if (arg[1] == '<='):
1054 ok = r_date.strftime('%Y-%m-%d') <= arg[2]
1056 elif str(arg) == str('&') or str(arg) == str('|'):
1063 if not isinstance(item, basestring):
1065 elif str(item) == str('&'):
1066 first = new_pile.pop()
1067 second = new_pile.pop()
1068 res = first and second
1069 elif str(item) == str('|'):
1070 first = new_pile.pop()
1071 second = new_pile.pop()
1072 res = first or second
1073 new_pile.append(res)
1075 if [True for item in new_pile if not item]:
1077 result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
1080 def comparer(left, right):
1081 for fn, mult in comparers:
1082 result = cmp(fn(left), fn(right))
1084 return mult * result
1087 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1088 comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1089 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1091 if isinstance(event_id, (str, int, long)):
1092 return ids and ids[0] or False
1096 def compute_rule_string(self, data):
1098 Compute rule string according to value type RECUR of iCalendar from the values given.
1099 @param self: the object pointer
1100 @param data: dictionary of freq and interval value
1101 @return: string containing recurring rule (empty if no rule)
1103 def get_week_string(freq, data):
1104 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1105 if freq == 'weekly':
1106 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1107 # byday = map(lambda x: x.upper(),[data[day] for day in weekdays if data[day]])
1110 return ';BYDAY=' + ','.join(byday)
1113 def get_month_string(freq, data):
1114 if freq == 'monthly':
1115 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1116 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1118 if data.get('month_by') == 'day': # Eg : Second Monday of the month
1119 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1120 elif data.get('month_by') == 'date': # Eg : 16th of the month
1121 return ';BYMONTHDAY=' + str(data.get('day'))
1124 def get_end_date(data):
1125 if data.get('end_date'):
1126 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1128 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1129 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1131 freq = data.get('rrule_type', False) # day/week/month/year
1134 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1135 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1139 def _get_empty_rrule_data(self):
1142 'recurrency': False,
1144 'rrule_type': False,
1160 def _parse_rrule(self, rule, data, date_start):
1161 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1162 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1163 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1165 if r._freq > 0 and r._freq < 4:
1166 data['rrule_type'] = rrule_type[r._freq]
1168 data['count'] = r._count
1169 data['interval'] = r._interval
1170 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1173 for i in xrange(0, 7):
1174 if i in r._byweekday:
1175 data[day_list[i]] = True
1176 data['rrule_type'] = 'weekly'
1177 #repeat monthly by nweekday ((weekday, weeknumber), )
1179 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1180 data['byday'] = str(r._bynweekday[0][1])
1181 data['month_by'] = 'day'
1182 data['rrule_type'] = 'monthly'
1185 data['day'] = r._bymonthday[0]
1186 data['month_by'] = 'date'
1187 data['rrule_type'] = 'monthly'
1189 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1191 data['interval'] = data['interval'] * 12
1193 #FIXEME handle forever case
1195 #in case of repeat for ever that we do not support right now
1196 if not (data.get('count') or data.get('end_date')):
1198 if data.get('count'):
1199 data['end_type'] = 'count'
1201 data['end_type'] = 'end_date'
1204 def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1206 for virtual_id in ids:
1207 real_id = calendar_id2real_id(virtual_id)
1208 result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1209 res[virtual_id] = result[real_id]
1212 def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1213 """ The basic purpose of this method is to check that destination partners
1214 effectively have email addresses. Otherwise a warning is thrown.
1215 :param value: value format: [[6, 0, [3, 4]]]
1219 if not value or not value[0] or not value[0][0] == 6:
1222 res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1225 def onchange_rec_day(self, cr, uid, id, date, mo, tu, we, th, fr, sa, su):
1226 """ set the start date according to the first occurence of rrule"""
1227 rrule_obj = self._get_empty_rrule_data()
1230 'rrule_type': 'weekly',
1240 str_rrule = self.compute_rule_string(rrule_obj)
1241 first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
1242 return {'value': {'date': first_occurence.strftime("%Y-%m-%d") + ' 00:00:00'}}
1244 def check_partners_email(self, cr, uid, partner_ids, context=None):
1245 """ Verify that selected partner_ids have an email_address defined.
1246 Otherwise throw a warning. """
1247 partner_wo_email_lst = []
1248 for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1249 if not partner.email:
1250 partner_wo_email_lst.append(partner)
1251 if not partner_wo_email_lst:
1253 warning_msg = _('The following contacts have no email address :')
1254 for partner in partner_wo_email_lst:
1255 warning_msg += '\n- %s' % (partner.name)
1256 return {'warning': {
1257 'title': _('Email addresses not found'),
1258 'message': warning_msg,
1261 # ----------------------------------------
1263 # ----------------------------------------
1265 # shows events of the day for this user
1267 def _needaction_domain_get(self, cr, uid, context=None):
1268 return [('end_date', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 23:59:59')), ('date', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 23:59:59')), ('user_id', '=', uid)]
1270 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
1271 if isinstance(thread_id, str):
1272 thread_id = get_real_ids(thread_id)
1273 if context.get('default_date'):
1274 del context['default_date']
1275 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)
1277 def do_sendmail(self, cr, uid, ids, context=None):
1278 for event in self.browse(cr, uid, ids, context):
1279 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1281 if current_user.email:
1282 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):
1283 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="calendar.subtype_invitation", context=context)
1286 def get_attendee(self, cr, uid, meeting_id, context=None):
1287 # Used for view in controller
1288 invitation = {'meeting': {}, 'attendee': []}
1290 meeting = self.browse(cr, uid, int(meeting_id), context)
1291 invitation['meeting'] = {
1292 'event': meeting.name,
1293 'where': meeting.location,
1294 'when': meeting.display_time
1297 for attendee in meeting.attendee_ids:
1298 invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1301 def get_interval(self, cr, uid, ids, date, interval, tz=None, context=None):
1302 #Function used only in calendar_event_data.xml for email template
1303 date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
1306 timezone = pytz.timezone(tz or 'UTC')
1307 date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
1309 if interval == 'day':
1311 elif interval == 'month':
1312 res = date.strftime('%B') + " " + str(date.year)
1313 elif interval == 'dayname':
1314 res = date.strftime('%A')
1315 elif interval == 'time':
1316 res = date.strftime('%I:%M %p')
1319 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1323 if context.get('mymeetings', False):
1324 partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1325 args += ['|', ('partner_ids', 'in', [partner_id]), ('user_id', '=', uid)]
1331 if arg[0] in ('date', unicode('date')) and arg[1] == ">=":
1332 if context.get('virtual_id', True):
1333 new_args += ['|', '&', ('recurrency', '=', 1), ('end_date', arg[1], arg[2])]
1334 elif arg[0] == "id":
1335 new_id = get_real_ids(arg[2])
1336 new_arg = (arg[0], arg[1], new_id)
1337 new_args.append(new_arg)
1339 if not context.get('virtual_id', True):
1340 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count)
1342 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1343 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1344 res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1348 return res[offset: offset + limit]
1351 def copy(self, cr, uid, id, default=None, context=None):
1355 default = default or {}
1356 default['attendee_ids'] = False
1358 res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1361 def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1362 real_event_id = calendar_id2real_id(id)
1363 data = self.read(cr, uid, id, ['date', 'date_deadline', 'rrule', 'duration'])
1365 if data.get('rrule'):
1368 recurrent_id=real_event_id,
1369 recurrent_id_date=data.get('date'),
1373 end_date=datetime.strptime(values.get('date', False) or data.get('date'), "%Y-%m-%d %H:%M:%S") + timedelta(hours=values.get('duration', False) or data.get('duration'))
1379 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1382 def open_after_detach_event(self, cr, uid, ids, context=None):
1386 new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1388 'type': 'ir.actions.act_window',
1389 'res_model': 'calendar.event',
1390 'view_mode': 'form',
1392 'target': 'current',
1393 'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
1396 def write(self, cr, uid, ids, values, context=None):
1397 def _only_changes_to_apply_on_real_ids(field_names):
1398 ''' return True if changes are only to be made on the real ids'''
1399 for field in field_names:
1400 if field in ['date', 'active']:
1404 context = context or {}
1406 if isinstance(ids, (str, int, long)):
1407 if len(str(ids).split('-')) == 1:
1415 # Special write of complex IDS
1416 for event_id in ids:
1417 if len(str(event_id).split('-')) == 1:
1420 ids.remove(event_id)
1421 real_event_id = calendar_id2real_id(event_id)
1423 # if we are setting the recurrency flag to False or if we are only changing fields that
1424 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1425 # then set real ids to be updated.
1426 if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()):
1427 ids.append(real_event_id)
1430 data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration'])
1431 if data.get('rrule'):
1432 new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
1434 res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
1436 # set end_date for calendar searching
1437 if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
1438 (values.get('rrule_type') or values.get('count') or values.get('date') or values.get('date_deadline')):
1439 for data in self.read(cr, uid, ids, ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
1440 end_date = self._get_recurrency_end_date(data, context=context)
1441 super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
1443 attendees_create = False
1444 if values.get('partner_ids', False):
1445 attendees_create = self.create_attendees(cr, uid, ids, context)
1447 if values.get('date', False) and values.get('active', True):
1448 the_id = new_id or (ids and int(ids[0]))
1450 if attendees_create:
1451 attendees_create = attendees_create[the_id]
1452 mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
1454 mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1457 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1458 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):
1459 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)
1461 return res or True and False
1463 def create(self, cr, uid, vals, context=None):
1467 if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user
1468 vals['user_id'] = uid
1470 res = super(calendar_event, self).create(cr, uid, vals, context=context)
1472 data = self.read(cr, uid, [res], ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context)[0]
1473 end_date = self._get_recurrency_end_date(data, context=context)
1474 self.write(cr, uid, [res], {'end_date': end_date}, context=context)
1476 self.create_attendees(cr, uid, [res], context=context)
1479 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1483 if 'date' in groupby:
1484 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1485 virtual_id = context.get('virtual_id', True)
1486 context.update({'virtual_id': False})
1487 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1489 #remove the count, since the value is not consistent with the result of the search when expand the group
1490 for groupname in groupby:
1491 if result.get(groupname + "_count"):
1492 del result[groupname + "_count"]
1493 result.get('__context', {}).update({'virtual_id': virtual_id})
1496 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1499 fields2 = fields and fields[:] or None
1500 EXTRAFIELDS = ('class', 'user_id', 'duration', 'date', 'rrule', 'vtimezone')
1501 for f in EXTRAFIELDS:
1502 if fields and (f not in fields):
1505 if isinstance(ids, (str, int, long)):
1510 select = map(lambda x: (x, calendar_id2real_id(x)), select)
1513 real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1514 real_data = dict(zip([x['id'] for x in real_data], real_data))
1516 for calendar_id, real_id in select:
1517 res = real_data[real_id].copy()
1518 ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) or 0)
1519 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1521 res['date_deadline'] = ls[2]
1522 res['id'] = calendar_id
1527 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1530 if r['class'] == 'private':
1532 if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date'):
1533 if isinstance(r[f], list):
1541 for k in EXTRAFIELDS:
1542 if (k in r) and (fields and (k not in fields)):
1545 if isinstance(ids, (str, int, long)):
1546 return result and result[0] or False
1549 def unlink(self, cr, uid, ids, unlink_level=0, context=None):
1550 if not isinstance(ids, list):
1557 # One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it
1558 for event_id in ids:
1559 if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL
1560 if self.browse(cr, uid, event_id).recurrent_id:
1561 ids_to_exclure.append(event_id)
1563 ids_to_unlink.append(event_id)
1565 ids_to_exclure.append(event_id)
1568 res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1571 for id_to_exclure in ids_to_exclure:
1572 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1577 class mail_message(osv.Model):
1578 _inherit = "mail.message"
1580 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1582 convert the search on real ids in the case it was asked on virtual ids, then call super()
1584 for index in range(len(args)):
1585 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1586 args[index][2] = get_real_ids(args[index][2])
1587 return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1589 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1592 if doc_model == 'calendar.event':
1593 order = context.get('order', self._order)
1594 for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1595 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1596 return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1599 class ir_attachment(osv.Model):
1600 _inherit = "ir.attachment"
1602 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1604 convert the search on real ids in the case it was asked on virtual ids, then call super()
1606 for index in range(len(args)):
1607 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1608 args[index][2] = get_real_ids(args[index][2])
1609 return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1611 def write(self, cr, uid, ids, vals, context=None):
1613 when posting an attachment (new or not), convert the virtual ids in real ids.
1615 if isinstance(vals.get('res_id'), str):
1616 vals['res_id'] = get_real_ids(vals.get('res_id'))
1617 return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1620 class ir_http(osv.AbstractModel):
1621 _inherit = 'ir.http'
1623 def _auth_method_calendar(self):
1624 token = request.params['token']
1625 db = request.params['db']
1627 registry = openerp.modules.registry.RegistryManager.get(db)
1628 attendee_pool = registry.get('calendar.attendee')
1629 error_message = False
1630 with registry.cursor() as cr:
1631 attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
1633 error_message = """Invalid Invitation Token."""
1634 elif request.session.uid and request.session.login != 'anonymous':
1635 # if valid session but user is not match
1636 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1637 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1638 if attendee.partner_id.id != user.partner_id.id:
1639 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)
1642 raise BadRequest(error_message)
1646 class invite_wizard(osv.osv_memory):
1647 _inherit = 'mail.wizard.invite'
1649 def default_get(self, cr, uid, fields, context=None):
1651 in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1653 result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1654 if 'res_id' in result:
1655 result['res_id'] = get_real_ids(result['res_id'])