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