[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / addons / calendar / calendar.py
1 # -*- coding: utf-8 -*-
2
3 import pytz
4 import re
5 import time
6 import openerp
7 import openerp.service.report
8 import uuid
9 import collections
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
22
23 import logging
24 _logger = logging.getLogger(__name__)
25
26
27 def calendar_id2real_id(calendar_id=None, with_date=False):
28     """
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
34     """
35     if calendar_id and isinstance(calendar_id, (str, unicode)):
36         res = calendar_id.split('-')
37         if len(res) >= 2:
38             real_id = res[0]
39             if with_date:
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))
44             return int(real_id)
45     return calendar_id and int(calendar_id) or calendar_id
46
47
48 def get_real_ids(ids):
49     if isinstance(ids, (str, int, long)):
50         return calendar_id2real_id(ids)
51
52     if isinstance(ids, (list, tuple)):
53         return [calendar_id2real_id(id) for id in ids]
54
55
56 class calendar_attendee(osv.Model):
57     """
58     Calendar Attendee Information
59     """
60     _name = 'calendar.attendee'
61     _rec_name = 'cn'
62     _description = 'Attendee information'
63
64     def _compute_data(self, cr, uid, ids, name, arg, context=None):
65         """
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'}}
70         """
71         name = name[0]
72         result = {}
73         for attdata in self.browse(cr, uid, ids, context=context):
74             id = attdata.id
75             result[id] = {}
76             if name == 'cn':
77                 if attdata.partner_id:
78                     result[id][name] = attdata.partner_id.name or False
79                 else:
80                     result[id][name] = attdata.email or ''
81         return result
82
83     STATE_SELECTION = [
84         ('needsAction', 'Needs Action'),
85         ('tentative', 'Uncertain'),
86         ('declined', 'Declined'),
87         ('accepted', 'Accepted'),
88     ]
89
90     _columns = {
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'),
98     }
99     _defaults = {
100         'state': 'needsAction',
101     }
102
103     def copy(self, cr, uid, id, default=None, context=None):
104         raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
105
106     def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
107         """
108         Make entry on email and availability on change of partner_id field.
109         @param partner_id: changed value of partner id
110         """
111         if not 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}}
115
116     def get_ics_file(self, cr, uid, event_obj, context=None):
117         """
118         Returns iCalendar file for the event invitation.
119         @param event_obj: event object (browse record)
120         @return: .ics file content
121         """
122         res = None
123
124         def ics_datetime(idate, allday=False):
125             if idate:
126                 if allday:
127                     return datetime.strptime(idate.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
128                 else:
129                     return datetime.strptime(idate.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
130             return False
131
132         try:
133             # FIXME: why isn't this in CalDAV?
134             import vobject
135         except ImportError:
136             return res
137
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
150         if event_obj.rrule:
151             event.add('rrule').value = event_obj.rrule
152
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()
172         return res
173
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):
176         """
177         Send mail for event invitation to event attendees.
178         @param email_from: email address for user sending the mail
179         """
180         res = False
181
182         if self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_mail', default=False) or context.get("no_mail_to_attendees"):
183             return res
184
185         mail_ids = []
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()
191         color = {
192             'needsAction': 'grey',
193             'accepted': 'green',
194             'tentative': '#FFFF00',
195             'declined': 'red'
196         }
197
198         if not isinstance(ids, (tuple, list)):
199             ids = [ids]
200
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({
204             'color': color,
205             'action_id': self.pool['ir.actions.act_window'].search(cr, uid, [('view_id', '=', act_id)], context=context)[0],
206             'dbname': cr.dbname,
207             'base_url': self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
208         })
209
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)
214
215                 vals = {}
216                 if ics_file:
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)
224
225         if mail_ids:
226             res = mail_pool.send(cr, uid, mail_ids, context=context)
227
228         return res
229
230     def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
231         """
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
236         """
237         if not user_id:
238             return {'value': {'email': ''}}
239
240         user = self.pool['res.users'].browse(cr, uid, user_id, *args)
241         return {'value': {'email': user.email, 'availability': user.availability}}
242
243     def do_tentative(self, cr, uid, ids, context=None, *args):
244         """
245         Makes event invitation as Tentative.
246         @param ids: list of attendee's IDs
247         """
248         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
249
250     def do_accept(self, cr, uid, ids, context=None, *args):
251         """
252         Marks event invitation as Accepted.
253         @param ids: list of attendee's IDs
254         """
255         if context is None:
256             context = {}
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)
262
263         return res
264
265     def do_decline(self, cr, uid, ids, context=None, *args):
266         """
267         Marks event invitation as Declined.
268         @param ids: list of calendar attendee's IDs
269         """
270         if context is None:
271             context = {}
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)
276         return res
277
278     def create(self, cr, uid, vals, context=None):
279         if context is None:
280             context = {}
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)
287         return res
288
289
290 class res_partner(osv.Model):
291     _inherit = 'res.partner'
292     _columns = {
293         'calendar_last_notif_ack': fields.datetime('Last notification marked as read from base Calendar'),
294     }
295
296     def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
297         """
298         Return a list of tuple (id, name, status)
299         Used by web_calendar.js : Many2ManyAttendee
300         """
301         datas = []
302         meeting = None
303         if meeting_id:
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]
307             if meeting:
308                 for attendee in meeting.attendee_ids:
309                     if attendee.partner_id.id == partner.id:
310                         data = (data[0], data[1], attendee.state)
311             datas.append(data)
312         return datas
313
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)
317         return
318
319
320 class calendar_alarm_manager(osv.AbstractModel):
321     _name = 'calendar.alarm_manager'
322
323     def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None):
324         res = {}
325         base_request = """
326                     SELECT
327                         cal.id,
328                         cal.start - interval '1' minute  * calcul_delta.max_delta AS first_alarm,
329                         CASE
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
332                         END as last_alarm,
333                         cal.start as first_event_date,
334                         CASE
335                             WHEN cal.recurrency THEN cal.final_date
336                             ELSE cal.stop
337                         END as last_event_date,
338                         calcul_delta.min_delta,
339                         calcul_delta.max_delta,
340                         cal.rrule AS rule
341                     FROM
342                         calendar_event AS cal
343                         RIGHT JOIN
344                             (
345                                 SELECT
346                                     rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
347                                 FROM
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
353              """
354
355         filter_user = """
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
358         """
359
360         #Add filter on type
361         type_to_read = ()
362         if notif:
363             type_to_read += ('notification',)
364         if mail:
365             type_to_read += ('email',)
366
367         tuple_params = (type_to_read,)
368
369         # ADD FILTER ON PARTNER_ID
370         if partner_id:
371             base_request += filter_user
372             tuple_params += (partner_id, )
373
374         #Add filter on hours
375         tuple_params += (seconds, seconds,)
376
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)
382
383         for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
384             res[event_id] = {
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,
392                 'rrule': rule
393             }
394
395         return res
396
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):
398         res = []
399         alarm_type = []
400
401         if notif:
402             alarm_type.append('notification')
403         if mail:
404             alarm_type.append('email')
405
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)):
411                         alert = {
412                             'alarm_id': alarm.id,
413                             'event_id': event.id,
414                             'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
415                         }
416                         res.append(alert)
417         return res
418
419     def get_next_mail(self, cr, uid, context=None):
420         try:
421             cron = self.pool['ir.model.data'].get_object(
422                 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
423         except ValueError:
424             _logger.error("Cron for " + self._name + " can not be identified !")
425             return False
426
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
437         else:
438             cron_interval = False
439
440         if not cron_interval:
441             _logger.error("Cron delay can not be computed !")
442             return False
443
444         all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
445
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:
450                 bFound = False
451                 LastFound = False
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)
455                     if LastFound:
456                         for alert in LastFound:
457                             self.do_mail_reminder(cr, uid, alert, context=context)
458
459                         if not bFound:  # if it's the first alarm for this recurrent event
460                             bFound = True
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
462                         break
463             else:
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)
466                 if LastFound:
467                     for alert in LastFound:
468                         self.do_mail_reminder(cr, uid, alert, context=context)
469
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
473         all_notif = []
474
475         if not partner:
476             return []
477
478         all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
479
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:
484                 bFound = False
485                 LastFound = False
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)
489                     if LastFound:
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
493                             bFound = True
494                     if bFound and not LastFound:  # if the precedent event had alarm but not this one, we can stop the search fot this event
495                         break
496             else:
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)
499                 if LastFound:
500                     for alert in LastFound:
501                         all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
502         return all_notif
503
504     def do_mail_reminder(self, cr, uid, alert, context=None):
505         if context is None:
506             context = {}
507         res = False
508
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)
511
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)
514
515         return res
516
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)
520
521         if alarm.type == 'notification':
522             message = event.display_time
523
524             delta = alert['notify_at'] - datetime.now()
525             delta = delta.seconds + delta.days * 3600 * 24
526
527             return {
528                 'event_id': event.id,
529                 'title': event.name,
530                 'message': message,
531                 'timer': delta,
532                 'notify_at': alert['notify_at'].strftime(DEFAULT_SERVER_DATETIME_FORMAT),
533             }
534
535
536 class calendar_alarm(osv.Model):
537     _name = 'calendar.alarm'
538     _description = 'Event alarm'
539
540     def _get_duration(self, cr, uid, ids, field_name, arg, context=None):
541         res = {}
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
549             else:
550                 res[alarm.id] = 0
551         return res
552
553     _columns = {
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),
559     }
560
561     _defaults = {
562         'type': 'notification',
563         'duration': 1,
564         'interval': 'hours',
565     }
566
567     def _update_cron(self, cr, uid, context=None):
568         try:
569             cron = self.pool['ir.model.data'].get_object(
570                 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
571         except ValueError:
572             return False
573         return cron.toggle(model=self._name, domain=[('type', '=', 'email')])
574
575     def create(self, cr, uid, values, context=None):
576         res = super(calendar_alarm, self).create(cr, uid, values, context=context)
577
578         self._update_cron(cr, uid, context=context)
579
580         return res
581
582     def write(self, cr, uid, ids, values, context=None):
583         res = super(calendar_alarm, self).write(cr, uid, ids, values, context=context)
584
585         self._update_cron(cr, uid, context=context)
586
587         return res
588
589     def unlink(self, cr, uid, ids, context=None):
590         res = super(calendar_alarm, self).unlink(cr, uid, ids, context=context)
591
592         self._update_cron(cr, uid, context=context)
593
594         return res
595
596
597 class ir_values(osv.Model):
598     _inherit = 'ir.values'
599
600     def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
601         new_model = []
602         for data in models:
603             if type(data) in (list, tuple):
604                 new_model.append((data[0], calendar_id2real_id(data[1])))
605             else:
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)
609
610     def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
611         if context is None:
612             context = {}
613         new_model = []
614         for data in models:
615             if type(data) in (list, tuple):
616                 new_model.append((data[0], calendar_id2real_id(data[1])))
617             else:
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)
621
622
623 class ir_model(osv.Model):
624
625     _inherit = 'ir.model'
626
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
629         if context is None:
630             context = {}
631         data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, context=context, load=load)
632         if data:
633             for val in data:
634                 val['id'] = calendar_id2real_id(val['id'])
635         return isinstance(ids, (str, int, long)) and data[0] or data
636
637
638 original_exp_report = openerp.service.report.exp_report
639
640
641 def exp_report(db, uid, object, ids, data=None, context=None):
642     """
643     Export Report
644     """
645     if object == 'printscreen.list':
646         original_exp_report(db, uid, object, ids, data, context)
647     new_ids = []
648     for id in ids:
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)
653
654
655 openerp.service.report.exp_report = exp_report
656
657
658 class calendar_event_type(osv.Model):
659     _name = 'calendar.event.type'
660     _description = 'Meeting Type'
661     _columns = {
662         'name': fields.char('Name', required=True, translate=True),
663     }
664
665
666 class calendar_event(osv.Model):
667     """ Model for Calendar Event """
668     _name = 'calendar.event'
669     _description = "Event"
670     _order = "id desc"
671     _inherit = ["mail.thread", "ir.needaction_mixin"]
672
673     def do_run_scheduler(self, cr, uid, id, context=None):
674         self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context)
675
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
678         """
679         def todate(date):
680             val = parser.parse(''.join((re.compile('\d')).findall(date)))
681             ## Dates are localized to saved timezone if any, else current timezone.
682             if not val.tzinfo:
683                 val = pytz.UTC.localize(val)
684             return val.astimezone(timezone)
685
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
688         if not startdate:
689             startdate = datetime.now()
690
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]
700
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)
703
704         if not data.get('recurrency'):
705             return False
706
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
711             delay, mult = {
712                 'daily': ('days', 1),
713                 'weekly': ('days', 7),
714                 'monthly': ('months', 1),
715                 'yearly': ('years', 1),
716             }[data['rrule_type']]
717
718             deadline = datetime.strptime(data['stop'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
719             return deadline + relativedelta(**{delay: count * mult})
720         return final_date
721
722     def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
723         """
724             Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
725         """
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:
730                     return attendee
731         return False
732
733     def get_date_formats(self, cr, uid, context):
734         lang = context.get("lang")
735         res_lang = self.pool.get('res.lang')
736         lang_params = {}
737         if lang:
738             ids = res_lang.search(request.cr, uid, [("code", "=", lang)])
739             if ids:
740                 lang_params = res_lang.read(request.cr, uid, ids[0], ["date_format", "time_format"])
741
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)
746
747     def get_display_time_tz(self, cr, uid, ids, tz=False, context=None):
748         context = dict(context or {})
749         if tz:
750             context["tz"] = tz
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)
753
754     def _get_display_time(self, cr, uid, start, stop, zduration, zallday, context=None):
755         """
756             Return date and time (from to from) based on duration with timezone in string :
757             eg.
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
760         """
761         context = dict(context or {})
762
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']
766             tz = context['tz']
767         tz = tools.ustr(tz).encode('utf-8') # make safe for str{p,f}time()
768
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)
774
775         if zallday:
776             time = _("AllDay , %s") % (event_date)
777         elif zduration < 24:
778             duration = date + timedelta(hours=zduration)
779             time = ("%s at (%s To %s) (%s)") % (event_date, display_time, duration.strftime(format_time), tz)
780         else:
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)
782         return time
783
784     def _compute(self, cr, uid, ids, fields, arg, context=None):
785         res = {}
786         if not isinstance(fields, list):
787             fields = [fields]
788         for meeting in self.browse(cr, uid, ids, context=context):
789             meeting_data = {}
790             res[meeting.id] = meeting_data
791             attendee = self._find_my_attendee(cr, uid, [meeting.id], context)
792             for field in fields:
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
805         return res
806
807     def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
808         """
809         Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
810         @return: dictionary of rrule value.
811         """
812         result = {}
813         if not isinstance(ids, list):
814             ids = [ids]
815
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)
821         for event in events:
822             if event['recurrency']:
823                 result[event['id']] = self.compute_rule_string(event)
824             else:
825                 result[event['id']] = ''
826
827         return result
828
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)
832
833     def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
834         if not isinstance(ids, list):
835             ids = [ids]
836         data = self._get_empty_rrule_data()
837         if field_value:
838             data['recurrency'] = True
839             for event in self.browse(cr, uid, ids, context=context):
840                 rdate = event.start
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)
844         return True
845
846     def _set_date(self, cr, uid, values, id=False, context=None):
847
848         if context is None:
849             context = {}
850
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)
854
855             if allday is None:
856                 if id:
857                     allday = self.read(cr, uid, [id], ['allday'], context=context)[0].get('allday')
858                 else:
859                     allday = False
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..."))
862
863             key = "date" if allday else "datetime"
864             notkey = "datetime" if allday else "date"
865
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)]
872
873             diff = False
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)
878             if diff:
879                 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
880                 values['duration'] = round(duration, 2)
881
882     _track = {
883         'location': {
884             'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
885         },
886         'start': {
887             'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
888         },
889     }
890     _columns = {
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)]}),
910
911         # RECURRENCE FIELD
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
932
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),
940     }
941
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)
948         return ret
949
950     _defaults = {
951         'end_type': 'count',
952         'count': 1,
953         'rrule_type': False,
954         'allday': False,
955         'state': 'draft',
956         'class': 'public',
957         'show_as': 'busy',
958         'month_by': 'date',
959         'interval': 1,
960         'active': 1,
961         'user_id': lambda self, cr, uid, ctx: uid,
962         'partner_ids': _get_default_partners,
963     }
964
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:
968                 return False
969         return True
970
971     _constraints = [
972         (_check_closing_date, 'Error ! End date cannot be set before start date.', ['start', 'stop'])
973     ]
974
975     def onchange_allday(self, cr, uid, ids, start=False, end=False, starttime=False, endtime=False, startdatetime=False, enddatetime=False, checkallday=False, context=None):
976
977         value = {}
978
979         if not ((starttime and endtime) or (start and end)):  # At first intialize, we have not datetime
980             return value
981
982         if checkallday:  # from datetime to date
983             startdatetime = startdatetime or start
984             if startdatetime:
985                 start = datetime.strptime(startdatetime, DEFAULT_SERVER_DATETIME_FORMAT)
986                 value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
987
988             enddatetime = enddatetime or end
989             if enddatetime:
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
995
996             if starttime:
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)
1002             elif start:
1003                 value['start_datetime'] = start
1004
1005             if endtime:
1006                 end = datetime.strptime(endtime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
1007                 enddate = tz.localize(end).replace(hour=18).astimezone(pytz.utc)
1008
1009                 value['stop_datetime'] = datetime.strftime(enddate, DEFAULT_SERVER_DATETIME_FORMAT)
1010             elif end:
1011                 value['stop_datetime'] = end
1012
1013         return {'value': value}
1014
1015     def onchange_dates(self, cr, uid, ids, fromtype, start=False, end=False, checkallday=False, allday=False, context=None):
1016
1017         """Returns duration and end date based on values passed
1018         @param ids: List of calendar event's IDs.
1019         """
1020         value = {}
1021
1022         if checkallday != allday:
1023             return value
1024
1025         value['allday'] = checkallday  # Force to be rewrited
1026
1027         if allday:
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)
1032
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)
1037
1038         else:
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)
1047
1048         return {'value': value}
1049
1050     def new_invitation_token(self, cr, uid, record, partner_id):
1051         return uuid.uuid4().hex
1052
1053     def create_attendees(self, cr, uid, ids, context=None):
1054         if context is None:
1055             context = {}
1056         user_obj = self.pool['res.users']
1057         current_user = user_obj.browse(cr, uid, uid, context=context)
1058         res = {}
1059         for event in self.browse(cr, uid, ids, context):
1060             attendees = {}
1061             for att in event.attendee_ids:
1062                 attendees[att.partner_id.id] = True
1063             new_attendees = []
1064             new_att_partner_ids = []
1065             for partner in event.partner_ids:
1066                 if partner.id in attendees:
1067                     continue
1068                 access_token = self.new_invitation_token(cr, uid, event, partner.id)
1069                 values = {
1070                     'partner_id': partner.id,
1071                     'event_id': event.id,
1072                     'access_token': access_token,
1073                     'email': partner.email,
1074                 }
1075
1076                 if partner.id == current_user.partner_id.id:
1077                     values['state'] = 'accepted'
1078
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)
1082
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)
1088
1089             if new_attendees:
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)
1093
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))
1099
1100             attendee_ids_to_remove = []
1101
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)
1106
1107             res[event.id] = {
1108                 'new_attendee_ids': new_attendees,
1109                 'old_attendee_ids': all_attendee_ids,
1110                 'removed_attendee_ids': attendee_ids_to_remove
1111             }
1112         return res
1113
1114     def get_search_fields(self, browse_event, order_fields, r_date=None):
1115         sort_fields = {}
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"))
1119             else:
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]
1125         if r_date:
1126             sort_fields['sort_start'] = r_date.strftime("%Y%m%d%H%M%S")
1127         else:
1128             sort_fields['sort_start'] = browse_event['display_start'].replace(' ', '').replace('-', '')
1129         return sort_fields
1130
1131     def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
1132
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
1135
1136         @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1137         """
1138         if not context:
1139             context = {}
1140
1141         if isinstance(event_id, (str, int, long)):
1142             ids_to_browse = [event_id]  # keep select for return
1143         else:
1144             ids_to_browse = event_id
1145
1146         if order:
1147             order_fields = [field.split()[0] for field in order.split(',')]
1148         else:
1149             # fallback on self._order defined on the model
1150             order_fields = [field.split()[0] for field in self._order.split(',')]
1151
1152         if 'id' not in order_fields:
1153             order_fields.append('id')
1154
1155         result_data = []
1156         result = []
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))
1161                 continue
1162             rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1163
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
1169                 pile = []
1170                 ok = True
1171                 for arg in domain:
1172                     if str(arg[0]) in ('start', 'stop', 'final_date'):
1173                         if (arg[1] == '='):
1174                             ok = r_date.strftime('%Y-%m-%d') == arg[2]
1175                         if (arg[1] == '>'):
1176                             ok = r_date.strftime('%Y-%m-%d') > arg[2]
1177                         if (arg[1] == '<'):
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]
1183                         pile.append(ok)
1184                     elif str(arg) == str('&') or str(arg) == str('|'):
1185                         pile.append(arg)
1186                     else:
1187                         pile.append(True)
1188                 pile.reverse()
1189                 new_pile = []
1190                 for item in pile:
1191                     if not isinstance(item, basestring):
1192                         res = item
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)
1202
1203                 if [True for item in new_pile if not item]:
1204                     continue
1205                 result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
1206
1207         if order_fields:
1208             uniq = lambda it: collections.OrderedDict((id(x), x) for x in it).values()
1209
1210             def comparer(left, right):
1211                 for fn, mult in comparers:
1212                     result = cmp(fn(left), fn(right))
1213                     if result:
1214                         return mult * result
1215                 return 0
1216
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)]
1222
1223         if isinstance(event_id, (str, int, long)):
1224             return ids and ids[0] or False
1225         else:
1226             return ids
1227
1228     def compute_rule_string(self, data):
1229         """
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)
1234         """
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.'))
1239
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))
1244                 if byday:
1245                     return ';BYDAY=' + ','.join(byday)
1246             return ''
1247
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."))
1252
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'))
1257             return ''
1258
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'
1262
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 '')
1265
1266         freq = data.get('rrule_type', False)  # day/week/month/year
1267         res = ''
1268         if freq:
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)
1271
1272         return res
1273
1274     def _get_empty_rrule_data(self):
1275         return {
1276             'byday': False,
1277             'recurrency': False,
1278             'final_date': False,
1279             'rrule_type': False,
1280             'month_by': False,
1281             'interval': 0,
1282             'count': False,
1283             'end_type': False,
1284             'mo': False,
1285             'tu': False,
1286             'we': False,
1287             'th': False,
1288             'fr': False,
1289             'sa': False,
1290             'su': False,
1291             'day': False,
1292             'week_list': False
1293         }
1294
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))
1299
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)
1305         #repeat weekly
1306         if r._byweekday:
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), )
1312         if r._bynweekday:
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'
1317
1318         if r._bymonthday:
1319             data['day'] = r._bymonthday[0]
1320             data['month_by'] = 'date'
1321             data['rrule_type'] = 'monthly'
1322
1323         #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1324         if r._bymonth:
1325             data['interval'] = data['interval'] * 12
1326
1327         #FIXEME handle forever case
1328         #end of recurrence
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')):
1331             data['count'] = 100
1332         if data.get('count'):
1333             data['end_type'] = 'count'
1334         else:
1335             data['end_type'] = 'end_date'
1336         return data
1337
1338     def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1339         res = {}
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]
1344         return res
1345
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]]]
1350         """
1351         res = {'value': {}}
1352
1353         if not value or not value[0] or not value[0][0] == 6:
1354             return
1355
1356         res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1357         return res
1358
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:
1367             return {}
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,
1374                 }}
1375
1376     # shows events of the day for this user
1377     def _needaction_domain_get(self, cr, uid, context=None):
1378         return [
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),
1382         ]
1383
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)
1391
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)
1395
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)
1399         return
1400
1401     def get_attendee(self, cr, uid, meeting_id, context=None):
1402         # Used for view in controller
1403         invitation = {'meeting': {}, 'attendee': []}
1404
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
1410         }
1411
1412         for attendee in meeting.attendee_ids:
1413             invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1414         return invitation
1415
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)
1419
1420         if tz:
1421             timezone = pytz.timezone(tz or 'UTC')
1422             date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
1423
1424         if interval == 'day':
1425             res = str(date.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")
1433         return res
1434
1435     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1436         if context is None:
1437             context = {}
1438
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])]
1442
1443         new_args = []
1444         for arg in args:
1445             new_arg = arg
1446
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)
1454
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)
1457
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)
1461         if count:
1462             return len(res)
1463         elif limit:
1464             return res[offset: offset + limit]
1465         return res
1466
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)
1471
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'):
1478             data.update(
1479                 values,
1480                 recurrent_id=real_event_id,
1481                 recurrent_id_date=data.get('start'),
1482                 rrule_type=False,
1483                 rrule='',
1484                 recurrency=False,
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'))
1486             )
1487
1488             #do not copy the id
1489             if data.get('id'):
1490                 del(data['id'])
1491             new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1492             return new_id
1493
1494     def open_after_detach_event(self, cr, uid, ids, context=None):
1495         if context is None:
1496             context = {}
1497
1498         new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1499         return {
1500             'type': 'ir.actions.act_window',
1501             'res_model': 'calendar.event',
1502             'view_mode': 'form',
1503             'res_id': new_id,
1504             'target': 'current',
1505             'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
1506         }
1507
1508     def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
1509         for arg in args:
1510             if arg[0] == 'id':
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)
1515
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']:
1521                     return True
1522             return False
1523
1524         if not isinstance(ids, (tuple, list)):
1525             ids = [ids]
1526
1527         context = context or {}
1528         self._set_date(cr, uid, values, id=ids[0], context=context)
1529
1530         for one_ids in ids:
1531             if isinstance(one_ids, (str, int, long)):
1532                 if len(str(one_ids).split('-')) == 1:
1533                     ids = [int(one_ids)]
1534                 else:
1535                     ids = [one_ids]
1536
1537         res = False
1538         new_id = False
1539
1540         # Special write of complex IDS
1541         for event_id in list(ids):
1542             if len(str(event_id).split('-')) == 1:
1543                 continue
1544
1545             ids.remove(event_id)
1546             real_event_id = calendar_id2real_id(event_id)
1547
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)
1553                 continue
1554             else:
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)
1558
1559         res = super(calendar_event, self).write(cr, uid, [int(event_id) for event_id in ids], values, context=context)
1560
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')):
1564             for id in ids:
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)
1567
1568         attendees_create = False
1569         if values.get('partner_ids', False):
1570             attendees_create = self.create_attendees(cr, uid, ids, context)
1571
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]))
1574             if the_id:
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']))
1578                 else:
1579                     mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1580
1581                 if mail_to_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
1586
1587     def create(self, cr, uid, vals, context=None):
1588         if context is None:
1589             context = {}
1590
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
1594
1595         res = super(calendar_event, self).create(cr, uid, vals, context=context)
1596
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)
1599
1600         self.create_attendees(cr, uid, [res], context=context)
1601         return res
1602
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 {})
1605
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)
1611         for result in res:
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})
1617         return res
1618
1619     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1620         if context is None:
1621             context = {}
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):
1626                 fields2.append(f)
1627         if isinstance(ids, (str, int, long)):
1628             select = [ids]
1629         else:
1630             select = ids
1631         select = map(lambda x: (x, calendar_id2real_id(x)), select)
1632         result = []
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))
1635
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]
1641                 res['stop'] = ls[2]
1642
1643                 if res['allday']:
1644                     res['start_date'] = ls[1]
1645                     res['stop_date'] = ls[2]
1646                 else:
1647                     res['start_datetime'] = ls[1]
1648                     res['stop_datetime'] = ls[2]
1649
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)
1652
1653             res['id'] = calendar_id
1654             result.append(res)
1655
1656         for r in result:
1657             if r['user_id']:
1658                 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1659                 if user_id == uid:
1660                     continue
1661             if r['class'] == 'private':
1662                 for f in r.keys():
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):
1665                             r[f] = []
1666                         else:
1667                             r[f] = False
1668                     if f == 'name':
1669                         r[f] = _('Busy')
1670
1671         for r in result:
1672             for k in EXTRAFIELDS:
1673                 if (k in r) and (fields and (k not in fields)):
1674                     del r[k]
1675         if isinstance(ids, (str, int, long)):
1676             return result and result[0] or False
1677         return result
1678
1679     def unlink(self, cr, uid, ids, can_be_deleted=True, context=None):
1680         if not isinstance(ids, list):
1681             ids = [ids]
1682         res = False
1683
1684         ids_to_exclure = []
1685         ids_to_unlink = []
1686
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)
1691                 else:
1692                     ids_to_unlink.append(int(event_id))
1693             else:
1694                 ids_to_exclure.append(event_id)
1695
1696         if ids_to_unlink:
1697             res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1698
1699         if ids_to_exclure:
1700             for id_to_exclure in ids_to_exclure:
1701                 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1702
1703         return res
1704
1705
1706 class mail_message(osv.Model):
1707     _inherit = "mail.message"
1708
1709     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1710         '''
1711         convert the search on real ids in the case it was asked on virtual ids, then call super()
1712         '''
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)
1717
1718     def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1719         if context is None:
1720             context = {}
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)
1726
1727
1728 class ir_attachment(osv.Model):
1729     _inherit = "ir.attachment"
1730
1731     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1732         '''
1733         convert the search on real ids in the case it was asked on virtual ids, then call super()
1734         '''
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)
1739
1740     def write(self, cr, uid, ids, vals, context=None):
1741         '''
1742         when posting an attachment (new or not), convert the virtual ids in real ids.
1743         '''
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)
1747
1748
1749 class ir_http(osv.AbstractModel):
1750     _inherit = 'ir.http'
1751
1752     def _auth_method_calendar(self):
1753         token = request.params['token']
1754         db = request.params['db']
1755
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)])
1761             if not attendee_id:
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)
1769
1770         if error_message:
1771             raise BadRequest(error_message)
1772
1773         return True
1774
1775
1776 class invite_wizard(osv.osv_memory):
1777     _inherit = 'mail.wizard.invite'
1778
1779     def default_get(self, cr, uid, fields, context=None):
1780         '''
1781         in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1782         '''
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'])
1786         return result