[MERGE] forward port of branch saas-3 up to 3d80dc2
[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 from werkzeug.exceptions import BadRequest
10 from datetime import datetime, timedelta
11 from dateutil import parser
12 from dateutil import rrule
13 from dateutil.relativedelta import relativedelta
14 from openerp import tools, SUPERUSER_ID
15 from openerp.osv import fields, osv
16 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
17 from openerp.tools.translate import _
18 from openerp.http import request
19 from operator import itemgetter
20
21 import logging
22 _logger = logging.getLogger(__name__)
23
24
25 def calendar_id2real_id(calendar_id=None, with_date=False):
26     """
27     Convert a "virtual/recurring event id" (type string) into a real event id (type int).
28     E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
29     @param calendar_id: id of calendar
30     @param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id
31     @return: real event id
32     """
33     if calendar_id and isinstance(calendar_id, (str, unicode)):
34         res = calendar_id.split('-')
35         if len(res) >= 2:
36             real_id = res[0]
37             if with_date:
38                 real_date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT, time.strptime(res[1], "%Y%m%d%H%M%S"))
39                 start = datetime.strptime(real_date, DEFAULT_SERVER_DATETIME_FORMAT)
40                 end = start + timedelta(hours=with_date)
41                 return (int(real_id), real_date, end.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
42             return int(real_id)
43     return calendar_id and int(calendar_id) or calendar_id
44
45
46 def get_real_ids(ids):
47     if isinstance(ids, (str, int, long)):
48         return calendar_id2real_id(ids)
49
50     if isinstance(ids, (list, tuple)):
51         return [calendar_id2real_id(id) for id in ids]
52
53
54 class calendar_attendee(osv.Model):
55     """
56     Calendar Attendee Information
57     """
58     _name = 'calendar.attendee'
59     _rec_name = 'cn'
60     _description = 'Attendee information'
61
62     def _compute_data(self, cr, uid, ids, name, arg, context=None):
63         """
64         Compute data on function fields for attendee values.
65         @param ids: list of calendar attendee's IDs
66         @param name: name of field
67         @return: dictionary of form {id: {'field Name': value'}}
68         """
69         name = name[0]
70         result = {}
71         for attdata in self.browse(cr, uid, ids, context=context):
72             id = attdata.id
73             result[id] = {}
74             if name == 'cn':
75                 if attdata.partner_id:
76                     result[id][name] = attdata.partner_id.name or False
77                 else:
78                     result[id][name] = attdata.email or ''
79         return result
80
81     STATE_SELECTION = [
82         ('needsAction', 'Needs Action'),
83         ('tentative', 'Uncertain'),
84         ('declined', 'Declined'),
85         ('accepted', 'Accepted'),
86     ]
87
88     _columns = {
89         'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="Status of the attendee's participation"),
90         'cn': fields.function(_compute_data, string='Common name', type="char", multi='cn', store=True),
91         'partner_id': fields.many2one('res.partner', 'Contact', readonly="True"),
92         'email': fields.char('Email', help="Email of Invited Person"),
93         'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
94         'access_token': fields.char('Invitation Token'),
95         'event_id': fields.many2one('calendar.event', 'Meeting linked'),
96     }
97     _defaults = {
98         'state': 'needsAction',
99     }
100
101     def copy(self, cr, uid, id, default=None, context=None):
102         raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
103
104     def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
105         """
106         Make entry on email and availability on change of partner_id field.
107         @param partner_id: changed value of partner id
108         """
109         if not partner_id:
110             return {'value': {'email': ''}}
111         partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
112         return {'value': {'email': partner.email}}
113
114     def get_ics_file(self, cr, uid, event_obj, context=None):
115         """
116         Returns iCalendar file for the event invitation.
117         @param event_obj: event object (browse record)
118         @return: .ics file content
119         """
120         res = None
121
122         def ics_datetime(idate, allday=False):
123             if idate:
124                 if allday:
125                     return datetime.strptime(idate.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
126                 else:
127                     return datetime.strptime(idate.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT).replace(tzinfo=pytz.timezone('UTC'))
128             return False
129
130         try:
131             # FIXME: why isn't this in CalDAV?
132             import vobject
133         except ImportError:
134             return res
135
136         cal = vobject.iCalendar()
137         event = cal.add('vevent')
138         if not event_obj.start or not event_obj.stop:
139             raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation."))
140         event.add('created').value = ics_datetime(time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
141         event.add('dtstart').value = ics_datetime(event_obj.start, event_obj.allday)
142         event.add('dtend').value = ics_datetime(event_obj.stop, event_obj.allday)
143         event.add('summary').value = event_obj.name
144         if event_obj.description:
145             event.add('description').value = event_obj.description
146         if event_obj.location:
147             event.add('location').value = event_obj.location
148         if event_obj.rrule:
149             event.add('rrule').value = event_obj.rrule
150
151         if event_obj.alarm_ids:
152             for alarm in event_obj.alarm_ids:
153                 valarm = event.add('valarm')
154                 interval = alarm.interval
155                 duration = alarm.duration
156                 trigger = valarm.add('TRIGGER')
157                 trigger.params['related'] = ["START"]
158                 if interval == 'days':
159                     delta = timedelta(days=duration)
160                 elif interval == 'hours':
161                     delta = timedelta(hours=duration)
162                 elif interval == 'minutes':
163                     delta = timedelta(minutes=duration)
164                 trigger.value = delta
165                 valarm.add('DESCRIPTION').value = alarm.name or 'OpenERP'
166         for attendee in event_obj.attendee_ids:
167             attendee_add = event.add('attendee')
168             attendee_add.value = 'MAILTO:' + (attendee.email or '')
169         res = cal.serialize()
170         return res
171
172     def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False),
173                                 template_xmlid='calendar_template_meeting_invitation', context=None):
174         """
175         Send mail for event invitation to event attendees.
176         @param email_from: email address for user sending the mail
177         """
178         res = False
179
180         if self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_mail', default=False) or context.get("no_mail_to_attendees"):
181             return res
182
183         mail_ids = []
184         data_pool = self.pool['ir.model.data']
185         mailmess_pool = self.pool['mail.message']
186         mail_pool = self.pool['mail.mail']
187         template_pool = self.pool['email.template']
188         local_context = context.copy()
189         color = {
190             'needsAction': 'grey',
191             'accepted': 'green',
192             'tentative': '#FFFF00',
193             'declined': 'red'
194         }
195
196         if not isinstance(ids, (tuple, list)):
197             ids = [ids]
198
199         dummy, template_id = data_pool.get_object_reference(cr, uid, 'calendar', template_xmlid)
200         dummy, act_id = data_pool.get_object_reference(cr, uid, 'calendar', "view_calendar_event_calendar")
201         local_context.update({
202             'color': color,
203             'action_id': self.pool['ir.actions.act_window'].search(cr, uid, [('view_id', '=', act_id)], context=context)[0],
204             'dbname': cr.dbname,
205             'base_url': self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
206         })
207
208         for attendee in self.browse(cr, uid, ids, context=context):
209             if attendee.email and email_from and attendee.email != email_from:
210                 ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
211                 mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
212
213                 vals = {}
214                 if ics_file:
215                     vals['attachment_ids'] = [(0, 0, {'name': 'invitation.ics',
216                                                       'datas_fname': 'invitation.ics',
217                                                       'datas': str(ics_file).encode('base64')})]
218                 vals['model'] = None  # We don't want to have the mail in the tchatter while in queue!
219                 the_mailmess = mail_pool.browse(cr, uid, mail_id, context=context).mail_message_id
220                 mailmess_pool.write(cr, uid, [the_mailmess.id], vals, context=context)
221                 mail_ids.append(mail_id)
222
223         if mail_ids:
224             res = mail_pool.send(cr, uid, mail_ids, context=context)
225
226         return res
227
228     def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
229         """
230         Make entry on email and availability on change of user_id field.
231         @param ids: list of attendee's IDs
232         @param user_id: changed value of User id
233         @return: dictionary of values which put value in email and availability fields
234         """
235         if not user_id:
236             return {'value': {'email': ''}}
237
238         user = self.pool['res.users'].browse(cr, uid, user_id, *args)
239         return {'value': {'email': user.email, 'availability': user.availability}}
240
241     def do_tentative(self, cr, uid, ids, context=None, *args):
242         """
243         Makes event invitation as Tentative.
244         @param ids: list of attendee's IDs
245         """
246         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
247
248     def do_accept(self, cr, uid, ids, context=None, *args):
249         """
250         Marks event invitation as Accepted.
251         @param ids: list of attendee's IDs
252         """
253         if context is None:
254             context = {}
255         meeting_obj = self.pool['calendar.event']
256         res = self.write(cr, uid, ids, {'state': 'accepted'}, context)
257         for attendee in self.browse(cr, uid, ids, context=context):
258             meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)),
259                                      subtype="calendar.subtype_invitation", context=context)
260
261         return res
262
263     def do_decline(self, cr, uid, ids, context=None, *args):
264         """
265         Marks event invitation as Declined.
266         @param ids: list of calendar attendee's IDs
267         """
268         if context is None:
269             context = {}
270         meeting_obj = self.pool['calendar.event']
271         res = self.write(cr, uid, ids, {'state': 'declined'}, context)
272         for attendee in self.browse(cr, uid, ids, context=context):
273             meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has declined invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
274         return res
275
276     def create(self, cr, uid, vals, context=None):
277         if context is None:
278             context = {}
279         if not vals.get("email") and vals.get("cn"):
280             cnval = vals.get("cn").split(':')
281             email = filter(lambda x: x.__contains__('@'), cnval)
282             vals['email'] = email and email[0] or ''
283             vals['cn'] = vals.get("cn")
284         res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
285         return res
286
287
288 class res_partner(osv.Model):
289     _inherit = 'res.partner'
290     _columns = {
291         'calendar_last_notif_ack': fields.datetime('Last notification marked as read from base Calendar'),
292     }
293
294     def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
295         """
296         Return a list of tuple (id, name, status)
297         Used by web_calendar.js : Many2ManyAttendee
298         """
299         datas = []
300         meeting = False
301         if meeting_id:
302             meeting = self.pool['calendar.event'].browse(cr, uid, get_real_ids(meeting_id), context=context)
303         for partner in self.browse(cr, uid, ids, context=context):
304             data = self.name_get(cr, uid, [partner.id], context)[0]
305             if meeting:
306                 for attendee in meeting.attendee_ids:
307                     if attendee.partner_id.id == partner.id:
308                         data = (data[0], data[1], attendee.state)
309             datas.append(data)
310         return datas
311
312     def calendar_last_notif_ack(self, cr, uid, context=None):
313         partner = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id
314         self.write(cr, uid, partner.id, {'calendar_last_notif_ack': datetime.now()}, context=context)
315         return
316
317
318 class calendar_alarm_manager(osv.AbstractModel):
319     _name = 'calendar.alarm_manager'
320
321     def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None):
322         res = {}
323         base_request = """
324                     SELECT
325                         cal.id,
326                         cal.start - interval '1' minute  * calcul_delta.max_delta AS first_alarm,
327                         CASE
328                             WHEN cal.recurrency THEN cal.final_date - interval '1' minute  * calcul_delta.min_delta
329                             ELSE cal.stop - interval '1' minute  * calcul_delta.min_delta
330                         END as last_alarm,
331                         cal.start as first_event_date,
332                         CASE
333                             WHEN cal.recurrency THEN cal.final_date
334                             ELSE cal.stop
335                         END as last_event_date,
336                         calcul_delta.min_delta,
337                         calcul_delta.max_delta,
338                         cal.rrule AS rule
339                     FROM
340                         calendar_event AS cal
341                         RIGHT JOIN
342                             (
343                                 SELECT
344                                     rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
345                                 FROM
346                                     calendar_alarm_calendar_event_rel AS rel
347                                         LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
348                                 WHERE alarm.type in %s
349                                 GROUP BY rel.calendar_event_id
350                             ) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id
351              """
352
353         filter_user = """
354                 RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
355                     AND part_rel.res_partner_id = %s
356         """
357
358         #Add filter on type
359         type_to_read = ()
360         if notif:
361             type_to_read += ('notification',)
362         if mail:
363             type_to_read += ('email',)
364
365         tuple_params = (type_to_read,)
366
367         # ADD FILTER ON PARTNER_ID
368         if partner_id:
369             base_request += filter_user
370             tuple_params += (partner_id, )
371
372         #Add filter on hours
373         tuple_params += (seconds, seconds,)
374
375         cr.execute("""SELECT *
376                         FROM ( %s WHERE cal.active = True ) AS ALL_EVENTS
377                        WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second )
378                          AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%%s' second )
379                    """ % base_request, tuple_params)
380
381         for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
382             res[event_id] = {
383                 'event_id': event_id,
384                 'first_alarm': first_alarm,
385                 'last_alarm': last_alarm,
386                 'first_meeting': first_meeting,
387                 'last_meeting': last_meeting,
388                 'min_duration': min_duration,
389                 'max_duration': max_duration,
390                 'rrule': rule
391             }
392
393         return res
394
395     def do_check_alarm_for_one_date(self, cr, uid, one_date, event, event_maxdelta, in_the_next_X_seconds, after=False, notif=True, mail=True, context=None):
396         res = []
397         alarm_type = []
398
399         if notif:
400             alarm_type.append('notification')
401         if mail:
402             alarm_type.append('email')
403
404         if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds):  # if an alarm is possible for this date
405             for alarm in event.alarm_ids:
406                 if alarm.type in alarm_type and \
407                     one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
408                         (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)):
409                         alert = {
410                             'alarm_id': alarm.id,
411                             'event_id': event.id,
412                             'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
413                         }
414                         res.append(alert)
415         return res
416
417     def get_next_mail(self, cr, uid, context=None):
418         try:
419             cron = self.pool['ir.model.data'].get_object(
420                 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
421         except ValueError:
422             _logger.error("Cron for " + self._name + " can not be identified !")
423             return False
424
425         if cron.interval_type == "weeks":
426             cron_interval = cron.interval_number * 7 * 24 * 60 * 60
427         elif cron.interval_type == "days":
428             cron_interval = cron.interval_number * 24 * 60 * 60
429         elif cron.interval_type == "hours":
430             cron_interval = cron.interval_number * 60 * 60
431         elif cron.interval_type == "minutes":
432             cron_interval = cron.interval_number * 60
433         elif cron.interval_type == "seconds":
434             cron_interval = cron.interval_number
435         else:
436             cron_interval = False
437
438         if not cron_interval:
439             _logger.error("Cron delay can not be computed !")
440             return False
441
442         all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
443
444         for event in all_events:  # .values()
445             max_delta = all_events[event]['max_duration']
446             curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
447             if curEvent.recurrency:
448                 bFound = False
449                 LastFound = False
450                 for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
451                     in_date_format = one_date.replace(tzinfo=None)
452                     LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
453                     if LastFound:
454                         for alert in LastFound:
455                             self.do_mail_reminder(cr, uid, alert, context=context)
456
457                         if not bFound:  # if it's the first alarm for this recurrent event
458                             bFound = True
459                     if bFound and not LastFound:  # if the precedent event had an alarm but not this one, we can stop the search for this event
460                         break
461             else:
462                 in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT)
463                 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
464                 if LastFound:
465                     for alert in LastFound:
466                         self.do_mail_reminder(cr, uid, alert, context=context)
467
468     def get_next_notif(self, cr, uid, context=None):
469         ajax_check_every_seconds = 300
470         partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
471         all_notif = []
472
473         if not partner:
474             return []
475
476         all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
477
478         for event in all_events:  # .values()
479             max_delta = all_events[event]['max_duration']
480             curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
481             if curEvent.recurrency:
482                 bFound = False
483                 LastFound = False
484                 for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
485                     in_date_format = one_date.replace(tzinfo=None)
486                     LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner.calendar_last_notif_ack, mail=False, context=context)
487                     if LastFound:
488                         for alert in LastFound:
489                             all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
490                         if not bFound:  # if it's the first alarm for this recurrent event
491                             bFound = True
492                     if bFound and not LastFound:  # if the precedent event had alarm but not this one, we can stop the search fot this event
493                         break
494             else:
495                 in_date_format = datetime.strptime(curEvent.start, DEFAULT_SERVER_DATETIME_FORMAT)
496                 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, partner.calendar_last_notif_ack, mail=False, context=context)
497                 if LastFound:
498                     for alert in LastFound:
499                         all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
500         return all_notif
501
502     def do_mail_reminder(self, cr, uid, alert, context=None):
503         if context is None:
504             context = {}
505         res = False
506
507         event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
508         alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
509
510         if alarm.type == 'email':
511             res = self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], template_xmlid='calendar_template_meeting_reminder', context=context)
512
513         return res
514
515     def do_notif_reminder(self, cr, uid, alert, context=None):
516         alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
517         event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
518
519         if alarm.type == 'notification':
520             message = event.display_time
521
522             delta = alert['notify_at'] - datetime.now()
523             delta = delta.seconds + delta.days * 3600 * 24
524
525             return {
526                 'event_id': event.id,
527                 'title': event.name,
528                 'message': message,
529                 'timer': delta,
530                 'notify_at': alert['notify_at'].strftime(DEFAULT_SERVER_DATETIME_FORMAT),
531             }
532
533
534 class calendar_alarm(osv.Model):
535     _name = 'calendar.alarm'
536     _description = 'Event alarm'
537
538     def _get_duration(self, cr, uid, ids, field_name, arg, context=None):
539         res = {}
540         for alarm in self.browse(cr, uid, ids, context=context):
541             if alarm.interval == "minutes":
542                 res[alarm.id] = alarm.duration
543             elif alarm.interval == "hours":
544                 res[alarm.id] = alarm.duration * 60
545             elif alarm.interval == "days":
546                 res[alarm.id] = alarm.duration * 60 * 24
547             else:
548                 res[alarm.id] = 0
549         return res
550
551     _columns = {
552         'name': fields.char('Name', required=True),
553         'type': fields.selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True),
554         'duration': fields.integer('Amount', required=True),
555         'interval': fields.selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], 'Unit', required=True),
556         'duration_minutes': fields.function(_get_duration, type='integer', string='duration_minutes', store=True),
557     }
558
559     _defaults = {
560         'type': 'notification',
561         'duration': 1,
562         'interval': 'hours',
563     }
564
565     def _update_cron(self, cr, uid, context=None):
566         try:
567             cron = self.pool['ir.model.data'].get_object(
568                 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
569         except ValueError:
570             return False
571         return cron.toggle(model=self._name, domain=[('type', '=', 'email')])
572
573     def create(self, cr, uid, values, context=None):
574         res = super(calendar_alarm, self).create(cr, uid, values, context=context)
575
576         self._update_cron(cr, uid, context=context)
577
578         return res
579
580     def write(self, cr, uid, ids, values, context=None):
581         res = super(calendar_alarm, self).write(cr, uid, ids, values, context=context)
582
583         self._update_cron(cr, uid, context=context)
584
585         return res
586
587     def unlink(self, cr, uid, ids, context=None):
588         res = super(calendar_alarm, self).unlink(cr, uid, ids, context=context)
589
590         self._update_cron(cr, uid, context=context)
591
592         return res
593
594
595 class ir_values(osv.Model):
596     _inherit = 'ir.values'
597
598     def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
599         new_model = []
600         for data in models:
601             if type(data) in (list, tuple):
602                 new_model.append((data[0], calendar_id2real_id(data[1])))
603             else:
604                 new_model.append(data)
605         return super(ir_values, self).set(cr, uid, key, key2, name, new_model,
606                                           value, replace, isobject, meta, preserve_user, company)
607
608     def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
609         if context is None:
610             context = {}
611         new_model = []
612         for data in models:
613             if type(data) in (list, tuple):
614                 new_model.append((data[0], calendar_id2real_id(data[1])))
615             else:
616                 new_model.append(data)
617         return super(ir_values, self).get(cr, uid, key, key2, new_model,
618                                           meta, context, res_id_req, without_user, key2_req)
619
620
621 class ir_model(osv.Model):
622
623     _inherit = 'ir.model'
624
625     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
626         new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
627         if context is None:
628             context = {}
629         data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, context=context, load=load)
630         if data:
631             for val in data:
632                 val['id'] = calendar_id2real_id(val['id'])
633         return isinstance(ids, (str, int, long)) and data[0] or data
634
635
636 original_exp_report = openerp.service.report.exp_report
637
638
639 def exp_report(db, uid, object, ids, data=None, context=None):
640     """
641     Export Report
642     """
643     if object == 'printscreen.list':
644         original_exp_report(db, uid, object, ids, data, context)
645     new_ids = []
646     for id in ids:
647         new_ids.append(calendar_id2real_id(id))
648     if data.get('id', False):
649         data['id'] = calendar_id2real_id(data['id'])
650     return original_exp_report(db, uid, object, new_ids, data, context)
651
652
653 openerp.service.report.exp_report = exp_report
654
655
656 class calendar_event_type(osv.Model):
657     _name = 'calendar.event.type'
658     _description = 'Meeting Type'
659     _columns = {
660         'name': fields.char('Name', required=True, translate=True),
661     }
662
663
664 class calendar_event(osv.Model):
665     """ Model for Calendar Event """
666     _name = 'calendar.event'
667     _description = "Event"
668     _order = "id desc"
669     _inherit = ["mail.thread", "ir.needaction_mixin"]
670
671     def do_run_scheduler(self, cr, uid, id, context=None):
672         self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context)
673
674     def get_recurrent_date_by_event(self, cr, uid, event, context=None):
675         """Get recurrent dates based on Rule string and all event where recurrent_id is child
676         """
677         def todate(date):
678             val = parser.parse(''.join((re.compile('\d')).findall(date)))
679             ## Dates are localized to saved timezone if any, else current timezone.
680             if not val.tzinfo:
681                 val = pytz.UTC.localize(val)
682             return val.astimezone(timezone)
683
684         timezone = pytz.timezone(context.get('tz') or 'UTC')
685         startdate = pytz.UTC.localize(datetime.strptime(event.start, DEFAULT_SERVER_DATETIME_FORMAT))  # Add "+hh:mm" timezone
686         if not startdate:
687             startdate = datetime.now()
688
689         ## Convert the start date to saved timezone (or context tz) as it'll
690         ## define the correct hour/day asked by the user to repeat for recurrence.
691         startdate = startdate.astimezone(timezone)  # transform "+hh:mm" timezone
692         rset1 = rrule.rrulestr(str(event.rrule), dtstart=startdate, forceset=True)
693         ids_depending = self.search(cr, uid, [('recurrent_id', '=', event.id), '|', ('active', '=', False), ('active', '=', True)], context=context)
694         all_events = self.browse(cr, uid, ids_depending, context=context)
695         for ev in all_events:
696             rset1._exdate.append(todate(ev.recurrent_id_date))
697         return [d.astimezone(pytz.UTC) for d in rset1]
698
699     def _get_recurrency_end_date(self, cr, uid, id, context=None):
700         data = self.read(cr, uid, id, ['final_date', 'recurrency', 'rrule_type', 'count', 'end_type', 'stop'], context=context)
701
702         if not data.get('recurrency'):
703             return False
704
705         end_type = data.get('end_type')
706         final_date = data.get('final_date')
707         if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'stop']):
708             count = data['count'] + 1
709             delay, mult = {
710                 'daily': ('days', 1),
711                 'weekly': ('days', 7),
712                 'monthly': ('months', 1),
713                 'yearly': ('years', 1),
714             }[data['rrule_type']]
715
716             deadline = datetime.strptime(data['stop'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
717             return deadline + relativedelta(**{delay: count * mult})
718         return final_date
719
720     def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
721         """
722             Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
723         """
724         user = self.pool['res.users'].browse(cr, uid, uid, context=context)
725         for meeting_id in meeting_ids:
726             for attendee in self.browse(cr, uid, meeting_id, context).attendee_ids:
727                 if user.partner_id.id == attendee.partner_id.id:
728                     return attendee
729         return False
730
731     def get_date_formats(self, cr, uid, context):
732         lang = context.get("lang")
733         res_lang = self.pool.get('res.lang')
734         lang_params = {}
735         if lang:
736             ids = res_lang.search(request.cr, uid, [("code", "=", lang)])
737             if ids:
738                 lang_params = res_lang.read(request.cr, uid, ids[0], ["date_format", "time_format"])
739         format_date = lang_params.get("date_format", '%B-%d-%Y')
740         format_time = lang_params.get("time_format", '%I-%M %p')
741         return (format_date, format_time)
742
743     def get_display_time_tz(self, cr, uid, ids, tz=False, context=None):
744         if tz:
745             context["tz"] = tz
746         ev = self.browse(cr, uid, ids, context=context)[0]
747         return self._get_display_time(cr, uid, ev.start, ev.stop, ev.duration, ev.allday, context=context)
748
749     def _get_display_time(self, cr, uid, start, stop, zduration, zallday, context=None):
750         """
751             Return date and time (from to from) based on duration with timezone in string :
752             eg.
753             1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
754             2) if event all day ,return : AllDay, July-31-2013
755         """
756         if context is None:
757             context = {}
758
759         tz = context.get('tz', False)
760         if not tz:  # tz can have a value False, so dont do it in the default value of get !
761             context['tz'] = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
762             tz = context['tz']
763
764         format_date, format_time = self.get_date_formats(cr, uid, context=context)
765         date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(start, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
766         date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(stop, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
767         event_date = date.strftime(format_date)
768         display_time = date.strftime(format_time)
769
770         if zallday:
771             time = _("AllDay , %s") % (event_date)
772         elif zduration < 24:
773             duration = date + timedelta(hours=zduration)
774             time = ("%s at (%s To %s) (%s)") % (event_date, display_time, duration.strftime(format_time), tz)
775         else:
776             time = ("%s at %s To\n %s at %s (%s)") % (event_date, display_time, date_deadline.strftime(format_date), date_deadline.strftime(format_time), tz)
777         return time
778
779     def _compute(self, cr, uid, ids, fields, arg, context=None):
780         res = {}
781         for meeting_id in ids:
782             res[meeting_id] = {}
783             attendee = self._find_my_attendee(cr, uid, [meeting_id], context)
784             meeting = self.browse(cr, uid, [meeting_id], context=context)[0]
785             for field in fields:
786                 if field == 'is_attendee':
787                     res[meeting_id][field] = True if attendee else False
788                 elif field == 'attendee_status':
789                     res[meeting_id][field] = attendee.state if attendee else 'needsAction'
790                 elif field == 'display_time':
791                     res[meeting_id][field] = self._get_display_time(cr, uid, meeting.start, meeting.stop, meeting.duration, meeting.allday, context=context)
792                 elif field == "display_start":
793                     res[meeting_id][field] = meeting.start_date if meeting.allday else meeting.start_datetime
794                 elif field == 'start':
795                     res[meeting_id][field] = meeting.start_date if meeting.allday else meeting.start_datetime
796                 elif field == 'stop':
797                     res[meeting_id][field] = meeting.stop_date if meeting.allday else meeting.stop_datetime
798
799         return res
800
801     def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
802         """
803         Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
804         @return: dictionary of rrule value.
805         """
806         result = {}
807         if not isinstance(ids, list):
808             ids = [ids]
809
810         for id in ids:
811             #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
812             data = self.browse(cr, SUPERUSER_ID, id, context=context)
813
814             if data.interval and data.interval < 0:
815                 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
816             if data.count and data.count <= 0:
817                 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
818
819             data = self.read(cr, uid, id, ['id', 'byday', 'recurrency', 'month_list', 'final_date', 'rrule_type', 'month_by', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'day', 'week_list'], context=context)
820             event = data['id']
821             if data['recurrency']:
822                 result[event] = self.compute_rule_string(data)
823             else:
824                 result[event] = ""
825         return result
826
827     # retro compatibility function
828     def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
829         return self._set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=context)
830
831     def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
832         if not isinstance(ids, list):
833             ids = [ids]
834         data = self._get_empty_rrule_data()
835         if field_value:
836             data['recurrency'] = True
837             for event in self.browse(cr, uid, ids, context=context):
838                 rdate = event.start
839                 update_data = self._parse_rrule(field_value, dict(data), rdate)
840                 data.update(update_data)
841                 self.write(cr, uid, ids, data, context=context)
842         return True
843
844     def _set_date(self, cr, uid, values, id=False, context=None):
845
846         if context is None:
847             context = {}
848
849         if values.get('start_datetime') or values.get('start_date') or values.get('start') \
850                 or values.get('stop_datetime') or values.get('stop_date') or values.get('stop'):
851             allday = values.get("allday", None)
852
853             if allday is None:
854                 if id:
855                     allday = self.read(cr, uid, [id], ['allday'], context=context)[0].get('allday')
856                 else:
857                     allday = False
858                     _logger.warning("Calendar - All day is not specified, arbitrarily set to False")
859                     #raise osv.except_osv(_('Error!'), ("Need to know if it's an allday or not..."))
860
861             key = "date" if allday else "datetime"
862             notkey = "datetime" if allday else "date"
863
864             for fld in ('start', 'stop'):
865                 if values.get('%s_%s' % (fld, key)) or values.get(fld):
866                     values['%s_%s' % (fld, key)] = values.get('%s_%s' % (fld, key)) or values.get(fld)
867                     values['%s_%s' % (fld, notkey)] = None
868                     if fld not in values.keys():
869                         values[fld] = values['%s_%s' % (fld, key)]
870
871             diff = False
872             if allday and values.get('stop_date') and values.get('start_date'):
873                 diff = datetime.strptime(values['stop_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT) - datetime.strptime(values['start_date'].split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
874             elif values.get('stop_datetime') and values.get('start_datetime'):
875                 diff = datetime.strptime(values['stop_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime(values['start_datetime'].split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
876             if diff:
877                 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
878                 values['duration'] = round(duration, 2)
879
880     _track = {
881         'location': {
882             'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
883         },
884         'start': {
885             'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
886         },
887     }
888     _columns = {
889         'id': fields.integer('ID', readonly=True),
890         'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
891         'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
892         'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
893         'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
894         'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
895         'display_start': fields.function(_compute, string='Date', type="char", multi='display_start', store=True),
896         'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
897         'start': fields.function(_compute, string='Calculated start', type="datetime", multi='start', store=True, required=True),
898         'stop': fields.function(_compute, string='Calculated stop', type="datetime", multi='stop', store=True, required=True),
899         'start_date': fields.date('Start Date', states={'done': [('readonly', True)]}, track_visibility='onchange'),
900         'start_datetime': fields.datetime('Start DateTime', states={'done': [('readonly', True)]}, track_visibility='onchange'),
901         'stop_date': fields.date('End Date', states={'done': [('readonly', True)]}, track_visibility='onchange'),
902         'stop_datetime': fields.datetime('End Datetime', states={'done': [('readonly', True)]}, track_visibility='onchange'),  # old date_deadline
903         'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
904         'description': fields.text('Description', states={'done': [('readonly', True)]}),
905         'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
906         'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
907         'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
908
909         # RECURRENCE FIELD
910         'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
911         'rrule_type': fields.selection([('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)')], 'Recurrency', states={'done': [('readonly', True)]}, help="Let the event automatically repeat at that interval"),
912         'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
913         'recurrent_id': fields.integer('Recurrent ID'),
914         'recurrent_id_date': fields.datetime('Recurrent ID date'),
915         'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
916         'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
917         'count': fields.integer('Repeat', help="Repeat x times"),
918         'mo': fields.boolean('Mon'),
919         'tu': fields.boolean('Tue'),
920         'we': fields.boolean('Wed'),
921         'th': fields.boolean('Thu'),
922         'fr': fields.boolean('Fri'),
923         'sa': fields.boolean('Sat'),
924         'su': fields.boolean('Sun'),
925         'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
926         'day': fields.integer('Date of month'),
927         'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
928         'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
929         'final_date': fields.date('Repeat Until'),  # The last event of a recurrence
930
931         'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
932         'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False),  # Color of creator
933         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the event alarm information without removing it."),
934         'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
935         'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
936         'partner_ids': fields.many2many('res.partner', 'calendar_event_res_partner_rel', string='Attendees', states={'done': [('readonly', True)]}),
937         'alarm_ids': fields.many2many('calendar.alarm', 'calendar_alarm_calendar_event_rel', string='Reminders', ondelete="restrict"),
938     }
939
940     def _get_default_partners(self, cr, uid, ctx=None):
941         ret = [self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id]
942         active_id = ctx.get('active_id')
943         if ctx.get('active_model') == 'res.partner' and active_id:
944             if active_id not in ret:
945                 ret.append(active_id)
946         return ret
947
948     _defaults = {
949         'end_type': 'count',
950         'count': 1,
951         'rrule_type': False,
952         'allday': False,
953         'state': 'draft',
954         'class': 'public',
955         'show_as': 'busy',
956         'month_by': 'date',
957         'interval': 1,
958         'active': 1,
959         'user_id': lambda self, cr, uid, ctx: uid,
960         'partner_ids': _get_default_partners,
961     }
962
963     def _check_closing_date(self, cr, uid, ids, context=None):
964         for event in self.browse(cr, uid, ids, context=context):
965             if event.stop < event.start:
966                 return False
967         return True
968
969     _constraints = [
970         (_check_closing_date, 'Error ! End date cannot be set before start date.', ['start', 'stop'])
971     ]
972
973     def onchange_allday(self, cr, uid, ids, start=False, end=False, starttime=False, endtime=False, startdatetime=False, enddatetime=False, checkallday=False, context=None):
974
975         value = {}
976
977         if not ((starttime and endtime) or (start and end)):  # At first intialize, we have not datetime
978             return value
979
980         if checkallday:  # from datetime to date
981             startdatetime = startdatetime or start
982             if startdatetime:
983                 start = datetime.strptime(startdatetime, DEFAULT_SERVER_DATETIME_FORMAT)
984                 value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
985
986             enddatetime = enddatetime or end
987             if enddatetime:
988                 end = datetime.strptime(enddatetime, DEFAULT_SERVER_DATETIME_FORMAT)
989                 value['stop_date'] = datetime.strftime(end, DEFAULT_SERVER_DATE_FORMAT)
990         else:  # from date to datetime
991             user = self.pool['res.users'].browse(cr, uid, uid, context)
992             tz = pytz.timezone(user.tz) if user.tz else pytz.utc
993
994             if starttime:
995                 start = datetime.strptime(starttime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
996                 startdate = tz.localize(start)  # Add "+hh:mm" timezone
997                 startdate = startdate.replace(hour=8)  # Set 8 AM in localtime
998                 startdate = startdate.astimezone(pytz.utc)  # Convert to UTC
999                 value['start_datetime'] = datetime.strftime(startdate, DEFAULT_SERVER_DATETIME_FORMAT)
1000             elif start:
1001                 value['start_datetime'] = start
1002
1003             if endtime:
1004                 end = datetime.strptime(endtime.split(' ')[0], DEFAULT_SERVER_DATE_FORMAT)
1005                 enddate = tz.localize(end).replace(hour=18).astimezone(pytz.utc)
1006
1007                 value['stop_datetime'] = datetime.strftime(enddate, DEFAULT_SERVER_DATETIME_FORMAT)
1008             elif end:
1009                 value['stop_datetime'] = end
1010
1011         return {'value': value}
1012
1013     def onchange_dates(self, cr, uid, ids, fromtype, start=False, end=False, checkallday=False, allday=False, context=None):
1014
1015         """Returns duration and end date based on values passed
1016         @param ids: List of calendar event's IDs.
1017         """
1018         value = {}
1019
1020         if checkallday != allday:
1021             return value
1022
1023         value['allday'] = checkallday  # Force to be rewrited
1024
1025         if allday:
1026             if fromtype == 'start':
1027                 start = datetime.strptime(start, DEFAULT_SERVER_DATE_FORMAT)
1028                 value['start_datetime'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1029                 value['start'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1030
1031             if fromtype == 'stop':
1032                 end = datetime.strptime(end, DEFAULT_SERVER_DATE_FORMAT)
1033                 value['stop_datetime'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1034                 value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1035
1036         else:
1037             if fromtype == 'start':
1038                 start = datetime.strptime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1039                 value['start_date'] = datetime.strftime(start, DEFAULT_SERVER_DATE_FORMAT)
1040                 value['start'] = datetime.strftime(start, DEFAULT_SERVER_DATETIME_FORMAT)
1041             if fromtype == 'stop':
1042                 end = datetime.strptime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1043                 value['stop_date'] = datetime.strftime(end, DEFAULT_SERVER_DATE_FORMAT)
1044                 value['stop'] = datetime.strftime(end, DEFAULT_SERVER_DATETIME_FORMAT)
1045
1046         return {'value': value}
1047
1048     def new_invitation_token(self, cr, uid, record, partner_id):
1049         return uuid.uuid4().hex
1050
1051     def create_attendees(self, cr, uid, ids, context):
1052         user_obj = self.pool['res.users']
1053         current_user = user_obj.browse(cr, uid, uid, context=context)
1054         res = {}
1055         for event in self.browse(cr, uid, ids, context):
1056             attendees = {}
1057             for att in event.attendee_ids:
1058                 attendees[att.partner_id.id] = True
1059             new_attendees = []
1060             new_att_partner_ids = []
1061             for partner in event.partner_ids:
1062                 if partner.id in attendees:
1063                     continue
1064                 access_token = self.new_invitation_token(cr, uid, event, partner.id)
1065                 values = {
1066                     'partner_id': partner.id,
1067                     'event_id': event.id,
1068                     'access_token': access_token,
1069                     'email': partner.email,
1070                 }
1071
1072                 if partner.id == current_user.partner_id.id:
1073                     values['state'] = 'accepted'
1074
1075                 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
1076                 new_attendees.append(att_id)
1077                 new_att_partner_ids.append(partner.id)
1078
1079                 if not current_user.email or current_user.email != partner.email:
1080                     mail_from = current_user.email or tools.config.get('email_from', False)
1081                     if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
1082                         self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee %s") % (partner.name,), subtype="calendar.subtype_invitation", context=context)
1083
1084             if new_attendees:
1085                 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
1086             if new_att_partner_ids:
1087                 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
1088
1089             # We remove old attendees who are not in partner_ids now.
1090             all_partner_ids = [part.id for part in event.partner_ids]
1091             all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
1092             all_attendee_ids = [att.id for att in event.attendee_ids]
1093             partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
1094
1095             attendee_ids_to_remove = []
1096
1097             if partner_ids_to_remove:
1098                 attendee_ids_to_remove = self.pool["calendar.attendee"].search(cr, uid, [('partner_id.id', 'in', partner_ids_to_remove), ('event_id.id', '=', event.id)], context=context)
1099                 if attendee_ids_to_remove:
1100                     self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
1101
1102             res[event.id] = {
1103                 'new_attendee_ids': new_attendees,
1104                 'old_attendee_ids': all_attendee_ids,
1105                 'removed_attendee_ids': attendee_ids_to_remove
1106             }
1107         return res
1108
1109     def get_search_fields(self, browse_event, order_fields, r_date=None):
1110         sort_fields = {}
1111         for ord in order_fields:
1112             if ord == 'id' and r_date:
1113                 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
1114             else:
1115                 sort_fields[ord] = browse_event[ord]
1116                 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
1117                     name_get = browse_event[ord].name_get()
1118                     if len(name_get) and len(name_get[0]) >= 2:
1119                         sort_fields[ord] = name_get[0][1]
1120
1121         return sort_fields
1122
1123     def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
1124
1125         """Gives virtual event ids for recurring events
1126         This method gives ids of dates that comes between start date and end date of calendar views
1127
1128         @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1129         """
1130         if not context:
1131             context = {}
1132
1133         if isinstance(event_id, (str, int, long)):
1134             ids_to_browse = [event_id]  # keep select for return
1135         else:
1136             ids_to_browse = event_id
1137
1138         if order:
1139             order_fields = [field.split()[0] for field in order.split(',')]
1140         else:
1141             # fallback on self._order defined on the model
1142             order_fields = [field.split()[0] for field in self._order.split(',')]
1143
1144         if 'id' not in order_fields:
1145             order_fields.append('id')
1146
1147         result_data = []
1148         result = []
1149         for ev in self.browse(cr, uid, ids_to_browse, context=context):
1150             if not ev.recurrency or not ev.rrule:
1151                 result.append(ev.id)
1152                 result_data.append(self.get_search_fields(ev, order_fields))
1153                 continue
1154             rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1155
1156             for r_date in rdates:
1157                 # fix domain evaluation
1158                 # step 1: check date and replace expression by True or False, replace other expressions by True
1159                 # step 2: evaluation of & and |
1160                 # check if there are one False
1161                 pile = []
1162                 ok = True
1163                 for arg in domain:
1164                     if str(arg[0]) in ('start', 'stop', 'final_date'):
1165                         if (arg[1] == '='):
1166                             ok = r_date.strftime('%Y-%m-%d') == arg[2]
1167                         if (arg[1] == '>'):
1168                             ok = r_date.strftime('%Y-%m-%d') > arg[2]
1169                         if (arg[1] == '<'):
1170                             ok = r_date.strftime('%Y-%m-%d') < arg[2]
1171                         if (arg[1] == '>='):
1172                             ok = r_date.strftime('%Y-%m-%d') >= arg[2]
1173                         if (arg[1] == '<='):
1174                             ok = r_date.strftime('%Y-%m-%d') <= arg[2]
1175                         pile.append(ok)
1176                     elif str(arg) == str('&') or str(arg) == str('|'):
1177                         pile.append(arg)
1178                     else:
1179                         pile.append(True)
1180                 pile.reverse()
1181                 new_pile = []
1182                 for item in pile:
1183                     if not isinstance(item, basestring):
1184                         res = item
1185                     elif str(item) == str('&'):
1186                         first = new_pile.pop()
1187                         second = new_pile.pop()
1188                         res = first and second
1189                     elif str(item) == str('|'):
1190                         first = new_pile.pop()
1191                         second = new_pile.pop()
1192                         res = first or second
1193                     new_pile.append(res)
1194
1195                 if [True for item in new_pile if not item]:
1196                     continue
1197                 result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
1198
1199         if order_fields:
1200             def comparer(left, right):
1201                 for fn, mult in comparers:
1202                     result = cmp(fn(left), fn(right))
1203                     if result:
1204                         return mult * result
1205                 return 0
1206
1207             sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1208             comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1209             ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1210
1211         if isinstance(event_id, (str, int, long)):
1212             return ids and ids[0] or False
1213         else:
1214             return ids
1215
1216     def compute_rule_string(self, data):
1217         """
1218         Compute rule string according to value type RECUR of iCalendar from the values given.
1219         @param self: the object pointer
1220         @param data: dictionary of freq and interval value
1221         @return: string containing recurring rule (empty if no rule)
1222         """
1223         def get_week_string(freq, data):
1224             weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1225             if freq == 'weekly':
1226                 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1227                 if byday:
1228                     return ';BYDAY=' + ','.join(byday)
1229             return ''
1230
1231         def get_month_string(freq, data):
1232             if freq == 'monthly':
1233                 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1234                     raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1235
1236                 if data.get('month_by') == 'day':  # Eg : Second Monday of the month
1237                     return ';BYDAY=' + data.get('byday') + data.get('week_list')
1238                 elif data.get('month_by') == 'date':  # Eg : 16th of the month
1239                     return ';BYMONTHDAY=' + str(data.get('day'))
1240             return ''
1241
1242         def get_end_date(data):
1243             if data.get('final_date'):
1244                 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('final_date'))) + 'T235959Z'
1245
1246             return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1247                 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1248
1249         freq = data.get('rrule_type', False)  # day/week/month/year
1250         res = ''
1251         if freq:
1252             interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1253             res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1254
1255         return res
1256
1257     def _get_empty_rrule_data(self):
1258         return {
1259             'byday': False,
1260             'recurrency': False,
1261             'final_date': False,
1262             'rrule_type': False,
1263             'month_by': False,
1264             'interval': 0,
1265             'count': False,
1266             'end_type': False,
1267             'mo': False,
1268             'tu': False,
1269             'we': False,
1270             'th': False,
1271             'fr': False,
1272             'sa': False,
1273             'su': False,
1274             'day': False,
1275             'week_list': False
1276         }
1277
1278     def _parse_rrule(self, rule, data, date_start):
1279         day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1280         rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1281         r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, DEFAULT_SERVER_DATETIME_FORMAT))
1282
1283         if r._freq > 0 and r._freq < 4:
1284             data['rrule_type'] = rrule_type[r._freq]
1285         data['count'] = r._count
1286         data['interval'] = r._interval
1287         data['final_date'] = r._until and r._until.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1288         #repeat weekly
1289         if r._byweekday:
1290             for i in xrange(0, 7):
1291                 if i in r._byweekday:
1292                     data[day_list[i]] = True
1293             data['rrule_type'] = 'weekly'
1294         #repeat monthly by nweekday ((weekday, weeknumber), )
1295         if r._bynweekday:
1296             data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1297             data['byday'] = str(r._bynweekday[0][1])
1298             data['month_by'] = 'day'
1299             data['rrule_type'] = 'monthly'
1300
1301         if r._bymonthday:
1302             data['day'] = r._bymonthday[0]
1303             data['month_by'] = 'date'
1304             data['rrule_type'] = 'monthly'
1305
1306         #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1307         if r._bymonth:
1308             data['interval'] = data['interval'] * 12
1309
1310         #FIXEME handle forever case
1311         #end of recurrence
1312         #in case of repeat for ever that we do not support right now
1313         if not (data.get('count') or data.get('final_date')):
1314             data['count'] = 100
1315         if data.get('count'):
1316             data['end_type'] = 'count'
1317         else:
1318             data['end_type'] = 'end_date'
1319         return data
1320
1321     def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1322         res = {}
1323         for virtual_id in ids:
1324             real_id = calendar_id2real_id(virtual_id)
1325             result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1326             res[virtual_id] = result[real_id]
1327         return res
1328
1329     def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1330         """ The basic purpose of this method is to check that destination partners
1331             effectively have email addresses. Otherwise a warning is thrown.
1332             :param value: value format: [[6, 0, [3, 4]]]
1333         """
1334         res = {'value': {}}
1335
1336         if not value or not value[0] or not value[0][0] == 6:
1337             return
1338
1339         res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1340         return res
1341
1342     def check_partners_email(self, cr, uid, partner_ids, context=None):
1343         """ Verify that selected partner_ids have an email_address defined.
1344             Otherwise throw a warning. """
1345         partner_wo_email_lst = []
1346         for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1347             if not partner.email:
1348                 partner_wo_email_lst.append(partner)
1349         if not partner_wo_email_lst:
1350             return {}
1351         warning_msg = _('The following contacts have no email address :')
1352         for partner in partner_wo_email_lst:
1353             warning_msg += '\n- %s' % (partner.name)
1354         return {'warning': {
1355                 'title': _('Email addresses not found'),
1356                 'message': warning_msg,
1357                 }}
1358
1359     # shows events of the day for this user
1360     def _needaction_domain_get(self, cr, uid, context=None):
1361         return [
1362             ('stop', '<=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 23:59:59')),
1363             ('start', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 00:00:00')),
1364             ('user_id', '=', uid),
1365         ]
1366
1367     def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
1368         if isinstance(thread_id, str):
1369             thread_id = get_real_ids(thread_id)
1370         if context.get('default_date'):
1371             del context['default_date']
1372         return super(calendar_event, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs)
1373
1374     def do_sendmail(self, cr, uid, ids, context=None):
1375         for event in self.browse(cr, uid, ids, context):
1376             current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1377
1378             if current_user.email:
1379                 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], email_from=current_user.email, context=context):
1380                     self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="calendar.subtype_invitation", context=context)
1381         return
1382
1383     def get_attendee(self, cr, uid, meeting_id, context=None):
1384         # Used for view in controller
1385         invitation = {'meeting': {}, 'attendee': []}
1386
1387         meeting = self.browse(cr, uid, int(meeting_id), context=context)
1388         invitation['meeting'] = {
1389             'event': meeting.name,
1390             'where': meeting.location,
1391             'when': meeting.display_time
1392         }
1393
1394         for attendee in meeting.attendee_ids:
1395             invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1396         return invitation
1397
1398     def get_interval(self, cr, uid, ids, date, interval, tz=None, context=None):
1399         #Function used only in calendar_event_data.xml for email template
1400         date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
1401
1402         if tz:
1403             timezone = pytz.timezone(tz or 'UTC')
1404             date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
1405
1406         if interval == 'day':
1407             res = str(date.day)
1408         elif interval == 'month':
1409             res = date.strftime('%B') + " " + str(date.year)
1410         elif interval == 'dayname':
1411             res = date.strftime('%A')
1412         elif interval == 'time':
1413             dummy, format_time = self.get_date_formats(cr, uid, context=context)
1414             res = date.strftime(format_time + " %Z")
1415         return res
1416
1417     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1418         if context is None:
1419             context = {}
1420
1421         if context.get('mymeetings', False):
1422             partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1423             args += [('partner_ids', 'in', [partner_id])]
1424
1425         new_args = []
1426         for arg in args:
1427             new_arg = arg
1428
1429             if arg[0] in ('start_date', 'start_datetime', 'start',) and arg[1] == ">=":
1430                 if context.get('virtual_id', True):
1431                     new_args += ['|', '&', ('recurrency', '=', 1), ('final_date', arg[1], arg[2])]
1432             elif arg[0] == "id":
1433                 new_id = get_real_ids(arg[2])
1434                 new_arg = (arg[0], arg[1], new_id)
1435             new_args.append(new_arg)
1436
1437         if not context.get('virtual_id', True):
1438             return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, count=count, context=context)
1439
1440         # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1441         res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1442         res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1443         if count:
1444             return len(res)
1445         elif limit:
1446             return res[offset: offset + limit]
1447         return res
1448
1449     def copy(self, cr, uid, id, default=None, context=None):
1450         if context is None:
1451             context = {}
1452
1453         default = default or {}
1454
1455         self._set_date(cr, uid, default, id=default.get('id'), context=context)
1456         default['attendee_ids'] = False
1457
1458         res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1459         return res
1460
1461     def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1462         real_event_id = calendar_id2real_id(id)
1463         data = self.read(cr, uid, id, ['allday', 'start', 'stop', 'rrule', 'duration'])
1464         data['start_date' if data['allday'] else 'start_datetime'] = data['start']
1465         data['stop_date' if data['allday'] else 'stop_datetime'] = data['stop']
1466         if data.get('rrule'):
1467             data.update(
1468                 values,
1469                 recurrent_id=real_event_id,
1470                 recurrent_id_date=data.get('start'),
1471                 rrule_type=False,
1472                 rrule='',
1473                 recurrency=False,
1474                 final_date=datetime.strptime(data.get('start'), DEFAULT_SERVER_DATETIME_FORMAT if data['allday'] else DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=values.get('duration', False) or data.get('duration'))
1475             )
1476
1477             #do not copy the id
1478             if data.get('id'):
1479                 del(data['id'])
1480             new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1481             return new_id
1482
1483     def open_after_detach_event(self, cr, uid, ids, context=None):
1484         if context is None:
1485             context = {}
1486
1487         new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1488         return {
1489             'type': 'ir.actions.act_window',
1490             'res_model': 'calendar.event',
1491             'view_mode': 'form',
1492             'res_id': new_id,
1493             'target': 'current',
1494             'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
1495         }
1496
1497     def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
1498         for arg in args:
1499             if arg[0] == 'id':
1500                 for n, calendar_id in enumerate(arg[2]):
1501                     if isinstance(calendar_id, str):
1502                         arg[2][n] = calendar_id.split('-')[0]
1503         return super(calendar_event, self)._name_search(cr, user, name=name, args=args, operator=operator, context=context, limit=limit, name_get_uid=name_get_uid)
1504
1505     def write(self, cr, uid, ids, values, context=None):
1506         def _only_changes_to_apply_on_real_ids(field_names):
1507             ''' return True if changes are only to be made on the real ids'''
1508             for field in field_names:
1509                 if field in ['start', 'start_date', 'start_datetime', 'stop', 'stop_date', 'stop_datetime', 'active']:
1510                     return True
1511             return False
1512
1513         if not isinstance(ids, (tuple, list)):
1514             ids = [ids]
1515
1516         context = context or {}
1517         self._set_date(cr, uid, values, id=ids[0], context=context)
1518
1519         for one_ids in ids:
1520             if isinstance(one_ids, (str, int, long)):
1521                 if len(str(one_ids).split('-')) == 1:
1522                     ids = [int(one_ids)]
1523                 else:
1524                     ids = [one_ids]
1525
1526         res = False
1527         new_id = False
1528
1529          # Special write of complex IDS
1530         for event_id in ids:
1531             if len(str(event_id).split('-')) == 1:
1532                 continue
1533
1534             ids.remove(event_id)
1535             real_event_id = calendar_id2real_id(event_id)
1536
1537             # if we are setting the recurrency flag to False or if we are only changing fields that
1538             # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1539             # then set real ids to be updated.
1540             if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()):
1541                 ids.append(real_event_id)
1542                 continue
1543             else:
1544                 data = self.read(cr, uid, event_id, ['start', 'stop', 'rrule', 'duration'])
1545                 if data.get('rrule'):
1546                     new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
1547
1548         res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
1549
1550         # set end_date for calendar searching
1551         if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
1552                 (values.get('rrule_type') or values.get('count') or values.get('start') or values.get('stop')):
1553             for id in ids:
1554                 final_date = self._get_recurrency_end_date(cr, uid, id, context=context)
1555                 super(calendar_event, self).write(cr, uid, [id], {'final_date': final_date}, context=context)
1556
1557         attendees_create = False
1558         if values.get('partner_ids', False):
1559             attendees_create = self.create_attendees(cr, uid, ids, context)
1560
1561         if (values.get('start_date') or values.get('start_datetime', False)) and values.get('active', True):
1562             the_id = new_id or (ids and int(ids[0]))
1563             if the_id:
1564                 if attendees_create:
1565                     attendees_create = attendees_create[the_id]
1566                     mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
1567                 else:
1568                     mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1569
1570                 if mail_to_ids:
1571                     current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1572                     if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context):
1573                         self.message_post(cr, uid, the_id, body=_("A email has been send to specify that the date has been changed !"), subtype="calendar.subtype_invitation", context=context)
1574         return res or True and False
1575
1576     def create(self, cr, uid, vals, context=None):
1577         if context is None:
1578             context = {}
1579
1580         self._set_date(cr, uid, vals, id=False, context=context)
1581         if not 'user_id' in vals:  # Else bug with quick_create when we are filter on an other user
1582             vals['user_id'] = uid
1583
1584         res = super(calendar_event, self).create(cr, uid, vals, context=context)
1585
1586         final_date = self._get_recurrency_end_date(cr, uid, res, context=context)
1587         self.write(cr, uid, [res], {'final_date': final_date}, context=context)
1588
1589         self.create_attendees(cr, uid, [res], context=context)
1590         return res
1591
1592     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
1593         if not context:
1594             context = {}
1595
1596         if 'date' in groupby:
1597             raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1598         virtual_id = context.get('virtual_id', True)
1599         context.update({'virtual_id': False})
1600         res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
1601         for result in res:
1602             #remove the count, since the value is not consistent with the result of the search when expand the group
1603             for groupname in groupby:
1604                 if result.get(groupname + "_count"):
1605                     del result[groupname + "_count"]
1606             result.get('__context', {}).update({'virtual_id': virtual_id})
1607         return res
1608
1609     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1610         if context is None:
1611             context = {}
1612         fields2 = fields and fields[:] or None
1613         EXTRAFIELDS = ('class', 'user_id', 'duration', 'allday', 'start', 'start_date', 'start_datetime', 'rrule')
1614         for f in EXTRAFIELDS:
1615             if fields and (f not in fields):
1616                 fields2.append(f)
1617         if isinstance(ids, (str, int, long)):
1618             select = [ids]
1619         else:
1620             select = ids
1621         select = map(lambda x: (x, calendar_id2real_id(x)), select)
1622         result = []
1623         real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1624         real_data = dict(zip([x['id'] for x in real_data], real_data))
1625
1626         for calendar_id, real_id in select:
1627             res = real_data[real_id].copy()
1628             ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) > 0 and res.get('duration') or 1)
1629             if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1630                 res['start'] = ls[1]
1631                 res['stop'] = ls[2]
1632
1633                 if res['allday']:
1634                     res['start_date'] = ls[1]
1635                     res['stop_date'] = ls[2]
1636                 else:
1637                     res['start_datetime'] = ls[1]
1638                     res['stop_datetime'] = ls[2]
1639
1640                 res['display_time'] = self._get_display_time(cr, uid, ls[1], ls[2], res['duration'], res['allday'], context=context)
1641
1642             res['id'] = calendar_id
1643             result.append(res)
1644
1645         for r in result:
1646             if r['user_id']:
1647                 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1648                 if user_id == uid:
1649                     continue
1650             if r['class'] == 'private':
1651                 for f in r.keys():
1652                     if f not in ('id', 'allday', 'start', 'stop', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date', 'rrule'):
1653                         if isinstance(r[f], list):
1654                             r[f] = []
1655                         else:
1656                             r[f] = False
1657                     if f == 'name':
1658                         r[f] = _('Busy')
1659
1660         for r in result:
1661             for k in EXTRAFIELDS:
1662                 if (k in r) and (fields and (k not in fields)):
1663                     del r[k]
1664         if isinstance(ids, (str, int, long)):
1665             return result and result[0] or False
1666         return result
1667
1668     def unlink(self, cr, uid, ids, can_be_deleted=True, context=None):
1669         if not isinstance(ids, list):
1670             ids = [ids]
1671         res = False
1672
1673         ids_to_exclure = []
1674         ids_to_unlink = []
1675
1676         for event_id in ids:
1677             if can_be_deleted and len(str(event_id).split('-')) == 1:  # if  ID REAL
1678                 if self.browse(cr, uid, int(event_id), context).recurrent_id:
1679                     ids_to_exclure.append(event_id)
1680                 else:
1681                     ids_to_unlink.append(int(event_id))
1682             else:
1683                 ids_to_exclure.append(event_id)
1684
1685         if ids_to_unlink:
1686             res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1687
1688         if ids_to_exclure:
1689             for id_to_exclure in ids_to_exclure:
1690                 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1691
1692         return res
1693
1694
1695 class mail_message(osv.Model):
1696     _inherit = "mail.message"
1697
1698     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1699         '''
1700         convert the search on real ids in the case it was asked on virtual ids, then call super()
1701         '''
1702         for index in range(len(args)):
1703             if args[index][0] == "res_id" and isinstance(args[index][2], str):
1704                 args[index][2] = get_real_ids(args[index][2])
1705         return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1706
1707     def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1708         if context is None:
1709             context = {}
1710         if doc_model == 'calendar.event':
1711             order = context.get('order', self._order)
1712             for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1713                 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1714         return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1715
1716
1717 class ir_attachment(osv.Model):
1718     _inherit = "ir.attachment"
1719
1720     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1721         '''
1722         convert the search on real ids in the case it was asked on virtual ids, then call super()
1723         '''
1724         for index in range(len(args)):
1725             if args[index][0] == "res_id" and isinstance(args[index][2], str):
1726                 args[index][2] = get_real_ids(args[index][2])
1727         return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1728
1729     def write(self, cr, uid, ids, vals, context=None):
1730         '''
1731         when posting an attachment (new or not), convert the virtual ids in real ids.
1732         '''
1733         if isinstance(vals.get('res_id'), str):
1734             vals['res_id'] = get_real_ids(vals.get('res_id'))
1735         return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1736
1737
1738 class ir_http(osv.AbstractModel):
1739     _inherit = 'ir.http'
1740
1741     def _auth_method_calendar(self):
1742         token = request.params['token']
1743         db = request.params['db']
1744
1745         registry = openerp.modules.registry.RegistryManager.get(db)
1746         attendee_pool = registry.get('calendar.attendee')
1747         error_message = False
1748         with registry.cursor() as cr:
1749             attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
1750             if not attendee_id:
1751                 error_message = """Invalid Invitation Token."""
1752             elif request.session.uid and request.session.login != 'anonymous':
1753                  # if valid session but user is not match
1754                 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1755                 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1756                 if attendee.partner_id.id != user.partner_id.id:
1757                     error_message = """Invitation cannot be forwarded via email. This event/meeting belongs to %s and you are logged in as %s. Please ask organizer to add you.""" % (attendee.email, user.email)
1758
1759         if error_message:
1760             raise BadRequest(error_message)
1761
1762         return True
1763
1764
1765 class invite_wizard(osv.osv_memory):
1766     _inherit = 'mail.wizard.invite'
1767
1768     def default_get(self, cr, uid, fields, context=None):
1769         '''
1770         in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1771         '''
1772         result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1773         if 'res_id' in result:
1774             result['res_id'] = get_real_ids(result['res_id'])
1775         return result