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