[REF] stock: refactoring of recompute stock operation links
[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 from werkzeug.exceptions import BadRequest
41
42 import logging
43 _logger = logging.getLogger(__name__)
44
45
46 def calendar_id2real_id(calendar_id=None, with_date=False):
47     """
48     Convert a "virtual/recurring event id" (type string) into a real event id (type int).
49     E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
50     @param calendar_id: id of calendar
51     @param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id
52     @return: real event id
53     """
54     if calendar_id and isinstance(calendar_id, (str, unicode)):
55         res = calendar_id.split('-')
56         if len(res) >= 2:
57             real_id = res[0]
58             if with_date:
59                 real_date = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(res[1], "%Y%m%d%H%M%S"))
60                 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
61                 end = start + timedelta(hours=with_date)
62                 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
63             return int(real_id)
64     return calendar_id and int(calendar_id) or calendar_id
65
66 def get_real_ids(ids):
67     if isinstance(ids, (str, int, long)):
68         return calendar_id2real_id(ids)
69
70     if isinstance(ids, (list, tuple)):
71         return [calendar_id2real_id(id) for id in ids]
72
73
74 class calendar_attendee(osv.Model):
75     """
76     Calendar Attendee Information
77     """
78     _name = 'calendar.attendee'
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:
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                         crm.id,
338                         crm.date - interval '1' minute  * calcul_delta.max_delta AS first_alarm,
339                         CASE
340                             WHEN crm.recurrency THEN crm.end_date - interval '1' minute  * calcul_delta.min_delta
341                             ELSE crm.date_deadline - interval '1' minute  * calcul_delta.min_delta
342                         END as last_alarm,
343                         crm.date as first_event_date,
344                         CASE
345                             WHEN crm.recurrency THEN crm.end_date
346                             ELSE crm.date_deadline
347                         END as last_event_date,
348                         calcul_delta.min_delta,
349                         calcul_delta.max_delta,
350                         crm.rrule AS rule
351                     FROM
352                         calendar_event AS crm
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 = crm.id
363              """
364
365         filter_user = """
366                 LEFT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = crm.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("""
388             SELECT
389                 *
390             FROM (
391                     """
392                 + base_request
393                 + """
394             ) AS ALL_EVENTS
395             WHERE
396                 ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%s' second )
397                 AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%s' second )
398            """, tuple_params)
399
400         for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
401             res[event_id].update({
402                 'event_id': event_id,
403                 'first_alarm': first_alarm,
404                 'last_alarm': last_alarm,
405                 'first_meeting': first_meeting,
406                 'last_meeting': last_meeting,
407                 'min_duration': min_duration,
408                 'max_duration': max_duration,
409                 'rrule': rule
410             })
411
412         return res
413
414     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):
415         res = []
416         alarm_type = []
417
418         if notif:
419             alarm_type.append('notification')
420         if mail:
421             alarm_type.append('email')
422
423         if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds):  # if an alarm is possible for this date
424             for alarm in event.alarm_ids:
425                 if alarm.type in alarm_type and \
426                     one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
427                         (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], "%Y-%m-%d %H:%M:%S")):
428                         alert = {
429                             'alarm_id': alarm.id,
430                             'event_id': event.id,
431                             'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
432                         }
433                         res.append(alert)
434         return res
435
436
437     def get_next_mail(self,cr,uid,context=None):
438         cron = self.pool.get('ir.cron').search(cr,uid,[('model','ilike',self._name)],context=context)
439         if cron and len(cron) == 1:
440             cron = self.pool.get('ir.cron').browse(cr,uid,cron[0],context=context)
441         else:
442             raise ("Cron for " + self._name + " not identified :( !")
443
444         if cron.interval_type=="weeks":
445             cron_interval = cron.interval_number * 7 * 24 * 60 * 60
446         elif cron.interval_type=="days":
447             cron_interval = cron.interval_number * 24 * 60 * 60 
448         elif cron.interval_type=="hours":
449             cron_interval = cron.interval_number * 60 * 60
450         elif cron.interval_type=="minutes":
451             cron_interval = cron.interval_number * 60
452         elif cron.interval_type=="seconds":
453             cron_interval = cron.interval_number 
454
455         if not cron_interval:
456             raise ("Cron delay for " + self._name + " can not be calculated :( !")
457
458         all_events = self.get_next_potential_limit_alarm(cr,uid,cron_interval,notif=False,context=context)
459
460         for event in all_events: #.values()
461             max_delta = all_events[event]['max_duration'];
462             curEvent = self.pool.get('calendar.event').browse(cr,uid,event,context=context) 
463             if curEvent.recurrency:
464                 bFound = False
465                 LastFound = False
466                 for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr,uid,curEvent, context=context) :
467                     in_date_format = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S');
468                     LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,cron_interval,notif=False,context=context)
469                     if LastFound:
470                         for alert in LastFound:
471                             self.do_mail_reminder(cr,uid,alert,context=context)
472
473                         if not bFound:  # if it's the first alarm for this recurrent event
474                             bFound = True
475                     if bFound and not LastFound:  # if the precedent event had an alarm but not this one, we can stop the search for this event
476                         break
477             else:
478                 in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S');
479                 LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,cron_interval,notif=False,context=context)
480                 if LastFound:
481                     for alert in LastFound:
482                         self.do_mail_reminder(cr,uid,alert,context=context)
483
484     def get_next_notif(self,cr,uid,context=None):
485         ajax_check_every_seconds = 300
486         partner = self.pool.get('res.users').browse(cr,uid,uid,context=context).partner_id;
487         all_notif = []
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 = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S');
498                     LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,ajax_check_every_seconds,after=partner.cal_last_notif,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.cal_last_notif,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, 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'].do_run_scheduler(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 data.get('recurrency') and data.get('end_type') in ('count', unicode('count')):
688             data_date_deadline = datetime.strptime(data.get('date_deadline'), '%Y-%m-%d %H:%M:%S')
689             if data.get('rrule_type') in ('daily', unicode('count')):
690                 rel_date = relativedelta(days=data.get('count') + 1)
691             elif data.get('rrule_type') in ('weekly', unicode('weekly')):
692                 rel_date = relativedelta(days=(data.get('count') + 1) * 7)
693             elif data.get('rrule_type') in ('monthly', unicode('monthly')):
694                 rel_date = relativedelta(months=data.get('count') + 1)
695             elif data.get('rrule_type') in ('yearly', unicode('yearly')):
696                 rel_date = relativedelta(years=data.get('count') + 1)
697             end_date = data_date_deadline + rel_date
698         else:
699             end_date = data.get('end_date')
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     def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
782         if not isinstance(ids, list):
783             ids = [ids]
784         data = self._get_empty_rrule_data()
785         if field_value:
786             data['recurrency'] = True
787             for event in self.browse(cr, uid, ids, context=context):
788                 rdate = event.date
789                 update_data = self._parse_rrule(field_value, dict(data), rdate)
790                 data.update(update_data)
791                 self.write(cr, uid, ids, data, context=context)
792         return True
793     
794     def _tz_get(self, cr, uid, context=None):
795         return [(x.lower(), x) for x in pytz.all_timezones]
796
797     _track = {
798         'location': {
799             'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
800         },
801        'date': {
802             'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
803         },
804     }
805     _columns = {
806         'id': fields.integer('ID', readonly=True),
807         'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
808         'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
809         'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
810         'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
811         'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
812         'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True, track_visibility='onchange'),
813         'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
814         'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
815         'description': fields.text('Description', states={'done': [('readonly', True)]}),
816         'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
817         'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
818         'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
819         'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
820         '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"),
821         'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
822         'recurrent_id': fields.integer('Recurrent ID'),
823         'recurrent_id_date': fields.datetime('Recurrent ID date'),
824         'vtimezone': fields.selection(_tz_get, string='Timezone'),
825         'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
826         'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
827         'count': fields.integer('Repeat', help="Repeat x times"),
828         'mo': fields.boolean('Mon'),
829         'tu': fields.boolean('Tue'),
830         'we': fields.boolean('Wed'),
831         'th': fields.boolean('Thu'),
832         'fr': fields.boolean('Fri'),
833         'sa': fields.boolean('Sat'),
834         'su': fields.boolean('Sun'),
835         'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
836         'day': fields.integer('Date of month'),
837         'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
838         'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
839         'end_date': fields.date('Repeat Until'),
840         'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
841         'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
842         'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False),  # Color of creator
843         '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."),
844         'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
845         'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
846         'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
847         'alarm_ids': fields.many2many('calendar.alarm', string='Reminders', ondelete="restrict"),
848
849     }
850     _defaults = {
851         'end_type': 'count',
852         'count': 1,
853         'rrule_type': False,
854         'state': 'draft',
855         'class': 'public',
856         'show_as': 'busy',
857         'month_by': 'date',
858         'interval': 1,
859         'active': 1,
860         'user_id': lambda self, cr, uid, ctx: uid,
861         'partner_ids': lambda self, cr, uid, ctx: [self.pool['res.users'].browse(cr, uid, [uid], context=ctx)[0].partner_id.id]
862     }
863
864     def _check_closing_date(self, cr, uid, ids, context=None):
865         for event in self.browse(cr, uid, ids, context=context):
866             if event.date_deadline < event.date:
867                 return False
868         return True
869
870     _constraints = [
871         (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
872     ]
873
874     def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
875
876         """Returns duration and/or end date based on values passed
877         @param ids: List of calendar event's IDs.
878         """
879         if context is None:
880             context = {}
881
882         value = {}
883         if not start_date:
884             return value
885
886         if not end_date and not duration:
887             duration = 1.00
888             value['duration'] = duration
889
890         if allday:  # For all day event
891             start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
892             user = self.pool['res.users'].browse(cr, uid, uid)
893             tz = pytz.timezone(user.tz) if user.tz else pytz.utc
894             start = pytz.utc.localize(start).astimezone(tz)     # convert start in user's timezone
895             start = start.astimezone(pytz.utc)                  # convert start back to utc
896
897             value['duration'] = 24.0
898             value['date'] = datetime.strftime(start, "%Y-%m-%d %H:%M:%S")
899         else:
900             start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
901
902         if end_date and not duration:
903             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
904             diff = end - start
905             duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
906             value['duration'] = round(duration, 2)
907         elif not end_date:
908             end = start + timedelta(hours=duration)
909             value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
910         elif end_date and duration and not allday:
911             # we have both, keep them synchronized:
912             # set duration based on end_date (arbitrary decision: this avoid
913             # getting dates like 06:31:48 instead of 06:32:00)
914             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
915             diff = end - start
916             duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
917             value['duration'] = round(duration, 2)
918
919         return {'value': value}
920
921     def new_invitation_token(self, cr, uid, record, partner_id):
922         return uuid.uuid4().hex
923
924     def create_attendees(self, cr, uid, ids, context):
925         user_obj = self.pool['res.users']
926         current_user = user_obj.browse(cr, uid, uid, context=context)
927         res = {}
928         for event in self.browse(cr, uid, ids, context):
929             attendees = {}
930             for att in event.attendee_ids:
931                 attendees[att.partner_id.id] = True
932             new_attendees = []
933             new_att_partner_ids = []
934             for partner in event.partner_ids:
935                 if partner.id in attendees:
936                     continue
937                 access_token = self.new_invitation_token(cr, uid, event, partner.id)
938                 values = {
939                     'partner_id': partner.id,
940                     'event_id': event.id,
941                     'access_token': access_token,
942                     'email': partner.email,
943                 }
944
945                 if partner.id == current_user.partner_id.id:
946                     values['state'] = 'accepted'
947
948                 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
949                 new_attendees.append(att_id)
950                 new_att_partner_ids.append(partner.id)
951
952                 if not current_user.email or current_user.email != partner.email:
953                     mail_from = current_user.email or tools.config.get('email_from', False)
954                     if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
955                         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)
956
957             if new_attendees:
958                 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
959             if new_att_partner_ids:
960                 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
961
962             # We remove old attendees who are not in partner_ids now.
963             all_partner_ids = [part.id for part in event.partner_ids]
964             all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
965             all_attendee_ids = [att.id for att in event.attendee_ids]
966             partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
967
968             attendee_ids_to_remove = []
969
970             if partner_ids_to_remove:
971                 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)
972                 if attendee_ids_to_remove:
973                     self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
974
975             res[event.id] = {
976                 'new_attendee_ids': new_attendees,
977                 'old_attendee_ids': all_attendee_ids,
978                 'removed_attendee_ids': attendee_ids_to_remove
979             }
980         return res
981
982     def get_search_fields(self,browse_event,order_fields,r_date=None): 
983         sort_fields = {}
984         for ord in order_fields:
985             if ord == 'id' and r_date:
986                 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
987             else:
988                 sort_fields[ord] = browse_event[ord]
989                 'If we sort on FK, we obtain a browse_record, so we need to sort on name_get'
990                 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
991                     name_get = browse_event[ord].name_get()
992                     if len(name_get) and len(name_get[0])>=2:
993                         sort_fields[ord] = name_get[0][1]
994
995         return sort_fields
996
997     def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
998
999         """Gives virtual event ids for recurring events 
1000         This method gives ids of dates that comes between start date and end date of calendar views
1001  
1002         @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted 
1003         """
1004
1005         if not context:
1006             context = {}
1007
1008         if isinstance(event_id, (str, int, long)):
1009             ids_to_browse = [event_id]  # keep select for return
1010         else:
1011             ids_to_browse = event_id
1012
1013         if order:
1014             order_fields = [field.split()[0] for field in order.split(',')]
1015         else:
1016             # fallback on self._order defined on the model
1017             order_fields = [field.split()[0] for field in self._order.split(',')]
1018
1019         if 'id' not in order_fields:
1020             order_fields.append('id')
1021
1022         result_data = []
1023         result = []
1024         for ev in self.browse(cr, uid, ids_to_browse, context=context):
1025             if not ev.recurrency or not ev.rrule:
1026                 result.append(ev.id)
1027                 result_data.append(self.get_search_fields(ev,order_fields))
1028                 continue
1029
1030             rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1031
1032             for r_date in rdates:
1033                 # fix domain evaluation
1034                 # step 1: check date and replace expression by True or False, replace other expressions by True
1035                 # step 2: evaluation of & and |
1036                 # check if there are one False
1037                 pile = []
1038                 ok = True
1039                 for arg in domain:
1040                     if str(arg[0]) in (str('date'), str('date_deadline'), str('end_date')):
1041                         if (arg[1] == '='):
1042                             ok = r_date.strftime('%Y-%m-%d') == arg[2]
1043                         if (arg[1] == '>'):
1044                             ok = r_date.strftime('%Y-%m-%d') > arg[2]
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                         pile.append(ok)
1052                     elif str(arg) == str('&') or str(arg) == str('|'):
1053                         pile.append(arg)
1054                     else:
1055                         pile.append(True)
1056                 pile.reverse()
1057                 new_pile = []
1058                 for item in pile:
1059                     if not isinstance(item, basestring):
1060                         res = item
1061                     elif str(item) == str('&'):
1062                         first = new_pile.pop()
1063                         second = new_pile.pop()
1064                         res = first and second
1065                     elif str(item) == str('|'):
1066                         first = new_pile.pop()
1067                         second = new_pile.pop()
1068                         res = first or second
1069                     new_pile.append(res)
1070
1071                 if [True for item in new_pile if not item]:
1072                     continue
1073                 result_data.append(self.get_search_fields(ev,order_fields,r_date=r_date))
1074
1075         if order_fields:
1076             def comparer(left, right):
1077                 for fn, mult in comparers:
1078                     result = cmp(fn(left), fn(right))
1079                     if result:
1080                         return mult * result
1081                 return 0
1082
1083             sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1084             comparers = [ ((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]    
1085             ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1086
1087         if isinstance(event_id, (str, int, long)):
1088             return ids and ids[0] or False
1089         else:
1090             return ids
1091
1092
1093     def compute_rule_string(self, data):
1094         """
1095         Compute rule string according to value type RECUR of iCalendar from the values given.
1096         @param self: the object pointer
1097         @param data: dictionary of freq and interval value
1098         @return: string containing recurring rule (empty if no rule)
1099         """
1100         def get_week_string(freq, data):
1101             weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1102             if freq == 'weekly':
1103                 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1104                 # byday = map(lambda x: x.upper(),[data[day] for day in weekdays if data[day]])
1105
1106                 if byday:
1107                     return ';BYDAY=' + ','.join(byday)
1108             return ''
1109
1110         def get_month_string(freq, data):
1111             if freq == 'monthly':
1112                 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1113                     raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1114
1115                 if data.get('month_by') == 'day':  # Eg : Second Monday of the month
1116                     return ';BYDAY=' + data.get('byday') + data.get('week_list')
1117                 elif data.get('month_by') == 'date':  # Eg : 16th of the month
1118                     return ';BYMONTHDAY=' + str(data.get('day'))
1119             return ''
1120
1121         def get_end_date(data):
1122             if data.get('end_date'):
1123                 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1124
1125             return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1126                              ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1127
1128         freq = data.get('rrule_type', False)  # day/week/month/year
1129         res = ''
1130         if freq:
1131             interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1132             res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1133
1134         return res
1135
1136     def _get_empty_rrule_data(self):
1137         return {
1138             'byday': False,
1139             'recurrency': False,
1140             'end_date': False,
1141             'rrule_type': False,
1142             'month_by': False,
1143             'interval': 0,
1144             'count': False,
1145             'end_type': False,
1146             'mo': False,
1147             'tu': False,
1148             'we': False,
1149             'th': False,
1150             'fr': False,
1151             'sa': False,
1152             'su': False,
1153             'day': False,
1154             'week_list': False
1155         }
1156
1157     def _parse_rrule(self, rule, data, date_start):
1158         day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1159         rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1160         r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1161
1162         if r._freq > 0 and r._freq < 4:
1163             data['rrule_type'] = rrule_type[r._freq]
1164
1165         data['count'] = r._count
1166         data['interval'] = r._interval
1167         data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1168         #repeat weekly
1169         if r._byweekday:
1170             for i in xrange(0, 7):
1171                 if i in r._byweekday:
1172                     data[day_list[i]] = True
1173             data['rrule_type'] = 'weekly'
1174         #repeat monthly by nweekday ((weekday, weeknumber), )
1175         if r._bynweekday:
1176             data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1177             data['byday'] = r._bynweekday[0][1]
1178             data['month_by'] = 'day'
1179             data['rrule_type'] = 'monthly'
1180
1181         if r._bymonthday:
1182             data['day'] = r._bymonthday[0]
1183             data['month_by'] = 'date'
1184             data['rrule_type'] = 'monthly'
1185
1186         #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1187         if r._bymonth:
1188             data['interval'] = data['interval'] * 12
1189
1190         #FIXEME handle forever case
1191         #end of recurrence
1192         #in case of repeat for ever that we do not support right now
1193         if not (data.get('count') or data.get('end_date')):
1194             data['count'] = 100
1195         if data.get('count'):
1196             data['end_type'] = 'count'
1197         else:
1198             data['end_type'] = 'end_date'
1199         return data
1200
1201     def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1202         res = {}
1203         for virtual_id in ids:
1204             real_id = calendar_id2real_id(virtual_id)
1205             result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1206             res[virtual_id] = result[real_id]
1207         return res
1208
1209     def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1210         """ The basic purpose of this method is to check that destination partners
1211             effectively have email addresses. Otherwise a warning is thrown.
1212             :param value: value format: [[6, 0, [3, 4]]]
1213         """
1214         res = {'value': {}}
1215
1216         if not value or not value[0] or not value[0][0] == 6:
1217             return
1218
1219         res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1220         return res
1221
1222     def onchange_rec_day(self,cr,uid,id,date,mo,tu,we,th,fr,sa,su):
1223         """ set the start date according to the first occurence of rrule"""
1224         rrule_obj = self._get_empty_rrule_data()
1225         rrule_obj.update({
1226             'byday':True,
1227             'rrule_type':'weekly',
1228             'mo': mo,
1229             'tu': tu,
1230             'we': we,
1231             'th': th,
1232             'fr': fr,
1233             'sa': sa,
1234             'su': su,
1235             'interval':1
1236         })
1237         str_rrule = self.compute_rule_string(rrule_obj)
1238         first_occurence =  list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
1239         return {'value': { 'date' : first_occurence.strftime("%Y-%m-%d") + ' 00:00:00' } }
1240
1241
1242     def check_partners_email(self, cr, uid, partner_ids, context=None):
1243         """ Verify that selected partner_ids have an email_address defined.
1244             Otherwise throw a warning. """
1245         partner_wo_email_lst = []
1246         for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1247             if not partner.email:
1248                 partner_wo_email_lst.append(partner)
1249         if not partner_wo_email_lst:
1250             return {}
1251         warning_msg = _('The following contacts have no email address :')
1252         for partner in partner_wo_email_lst:
1253             warning_msg += '\n- %s' % (partner.name)
1254         return {'warning':
1255                     {
1256                         'title': _('Email addresses not found'),
1257                         'message': warning_msg,
1258                     }
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, 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         if interval == 'day':
1305             res = str(date.day)
1306         elif interval == 'month':
1307             res = date.strftime('%B') + " " + str(date.year)
1308         elif interval == 'dayname':
1309             res = date.strftime('%A')
1310         elif interval == 'time':
1311             res = date.strftime('%I:%M %p')
1312         return res
1313
1314     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1315         if context is None:
1316             context = {}
1317
1318         if context.get('mymeetings', False):
1319             partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1320             args += ['|', ('partner_ids', 'in', [partner_id]), ('user_id', '=', uid)]
1321
1322         new_args = []
1323         for arg in args:
1324             new_arg = arg
1325
1326             if arg[0] in ('date', unicode('date')) and arg[1] == ">=":
1327                 if context.get('virtual_id', True):
1328                     new_args += ['|', '&', ('recurrency', '=', 1), ('end_date', arg[1], arg[2])]
1329             elif arg[0] == "id":
1330                 new_id = get_real_ids(arg[2])
1331                 new_arg = (arg[0], arg[1], new_id)
1332             new_args.append(new_arg)
1333
1334         if not context.get('virtual_id', True):
1335             return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count)
1336
1337         # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1338         res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1339         res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1340         if count:
1341             return len(res)
1342         elif limit:
1343             return res[offset: offset + limit]
1344         return res
1345
1346     def copy(self, cr, uid, id, default=None, context=None):
1347         if context is None:
1348             context = {}
1349
1350         default = default or {}
1351         default['attendee_ids'] = False
1352
1353         res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1354         return res
1355
1356     def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1357         real_event_id = calendar_id2real_id(id)
1358         data = self.read(cr, uid, id, ['date', 'date_deadline', 'rrule', 'duration'])
1359
1360         if data.get('rrule'):
1361             data.update(
1362                 values,
1363                 recurrent_id=real_event_id,
1364                 recurrent_id_date=data.get('date'),
1365                 rrule_type=False,
1366                 rrule='',
1367                 recurrency=False,
1368                 end_date = datetime.strptime(values.get('date', False) or data.get('date'),"%Y-%m-%d %H:%M:%S") 
1369                                 + timedelta(hours=values.get('duration', False) or data.get('duration'))
1370             )
1371
1372             #do not copy the id
1373             if data.get('id'):
1374                 del(data['id'])
1375             new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1376             return new_id
1377
1378     def open_after_detach_event(self, cr, uid, ids, context=None):
1379         if context is None:
1380             context = {}
1381
1382         new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1383         return {
1384                 'type': 'ir.actions.act_window',
1385                 'res_model': 'calendar.event',
1386                 'view_mode': 'form',
1387                 'res_id': new_id,
1388                 'target': 'current',
1389                 'flags': {'form': {'action_buttons': True, 'options' : { 'mode' : 'edit' } } }
1390         }
1391
1392
1393
1394
1395     def write(self, cr, uid, ids, values, context=None):
1396         def _only_changes_to_apply_on_real_ids(field_names):
1397             ''' return True if changes are only to be made on the real ids'''   
1398             for field in field_names:
1399                 if field in ['date','active']:
1400                     return True                
1401             return False
1402
1403         context = context or {}
1404         
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, ['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         if vals.get('recurrency', True) and vals.get('end_type', 'count') in ('count', unicode('count')) and \
1471                 (vals.get('rrule_type') or vals.get('count') or vals.get('date') or vals.get('date_deadline')):
1472             vals['end_date'] = self._get_recurrency_end_date(vals, context=context)
1473
1474         res = super(calendar_event, self).create(cr, uid, vals, context=context)
1475         self.create_attendees(cr, uid, [res], context=context)
1476         return res
1477
1478     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1479         if not context:
1480             context = {}
1481
1482         if 'date' in groupby:
1483             raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1484         virtual_id = context.get('virtual_id', True)
1485         context.update({'virtual_id': False})
1486         res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1487         for result in res:
1488             #remove the count, since the value is not consistent with the result of the search when expand the group
1489             for groupname in groupby:
1490                 if result.get(groupname + "_count"):
1491                     del result[groupname + "_count"]
1492             result.get('__context', {}).update({'virtual_id': virtual_id})
1493         return res
1494
1495     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1496         if context is None:
1497             context = {}
1498         fields2 = fields and fields[:] or None
1499         EXTRAFIELDS = ('class', 'user_id', 'duration', 'date', 'rrule', 'vtimezone')
1500         for f in EXTRAFIELDS:
1501             if fields and (f not in fields):
1502                 fields2.append(f)
1503
1504         if isinstance(ids, (str, int, long)):
1505             select = [ids]
1506         else:
1507             select = ids
1508
1509         select = map(lambda x: (x, calendar_id2real_id(x)), select)
1510         result = []
1511
1512         real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1513         real_data = dict(zip([x['id'] for x in real_data], real_data))
1514
1515         for calendar_id, real_id in select:
1516             res = real_data[real_id].copy()
1517             ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) or 0)
1518             if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1519                 res['date'] = ls[1]
1520                 res['date_deadline'] = ls[2]
1521             res['id'] = calendar_id
1522             result.append(res)
1523
1524         for r in result:
1525             if r['user_id']:
1526                 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1527                 if user_id == uid:
1528                     continue
1529             if r['class'] == 'private':
1530                 for f in r.keys():
1531                     if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count'):
1532                         if isinstance(r[f], list):
1533                             r[f] = []
1534                         else:
1535                             r[f] = False
1536                     if f == 'name':
1537                         r[f] = _('Busy')
1538
1539         for r in result:
1540             for k in EXTRAFIELDS:
1541                 if (k in r) and (fields and (k not in fields)):
1542                     del r[k]
1543
1544         if isinstance(ids, (str, int, long)):
1545             return result and result[0] or False
1546         return result
1547
1548     def unlink(self, cr, uid, ids, unlink_level=0, context=None):
1549         if not isinstance(ids, list):
1550             ids = [ids]
1551         res = False
1552
1553         ids_to_exclure = []
1554         ids_to_unlink = []
1555
1556         # One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it
1557         for event_id in ids:            
1558             if unlink_level == 1 and len(str(event_id).split('-')) == 1:  # if  ID REAL
1559                 if self.browse(cr, uid, event_id).recurrent_id:
1560                     ids_to_exclure.append(event_id)
1561                 else:
1562                     ids_to_unlink.append(event_id)
1563             else:
1564                 ids_to_exclure.append(event_id)
1565
1566         if ids_to_unlink:
1567             res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1568
1569         if ids_to_exclure:
1570             for id_to_exclure in ids_to_exclure:
1571                 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1572
1573         return res
1574
1575
1576 class mail_message(osv.Model):
1577     _inherit = "mail.message"
1578
1579     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1580         '''
1581         convert the search on real ids in the case it was asked on virtual ids, then call super()
1582         '''
1583         for index in range(len(args)):
1584             if args[index][0] == "res_id" and isinstance(args[index][2], str):
1585                 args[index][2] = get_real_ids(args[index][2])
1586         return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1587
1588     def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1589         if doc_model == 'calendar.event':
1590             order =  context.get('order', self._order)
1591             for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1592                 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1593         return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1594
1595
1596 class ir_attachment(osv.Model):
1597     _inherit = "ir.attachment"
1598
1599     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1600         '''
1601         convert the search on real ids in the case it was asked on virtual ids, then call super()
1602         '''
1603         for index in range(len(args)):
1604             if args[index][0] == "res_id" and isinstance(args[index][2], str):
1605                 args[index][2] = get_real_ids(args[index][2])
1606         return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1607
1608     def write(self, cr, uid, ids, vals, context=None):
1609         '''
1610         when posting an attachment (new or not), convert the virtual ids in real ids.
1611         '''
1612         if isinstance(vals.get('res_id'), str):
1613             vals['res_id'] = get_real_ids(vals.get('res_id'))
1614         return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1615
1616
1617 class ir_http(osv.AbstractModel):
1618     _inherit = 'ir.http'
1619
1620     def _auth_method_calendar(self):
1621         token = request.params['token']
1622         db =  request.params['db']
1623
1624         registry = openerp.modules.registry.RegistryManager.get(db)
1625         attendee_pool = registry.get('calendar.attendee')
1626         error_message = False
1627         with registry.cursor() as cr:
1628             attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token)])
1629             if not attendee_id:
1630                 error_message = """Invalid Invitation Token."""
1631             elif request.session.uid and request.session.login != 'anonymous':
1632                  # if valid session but user is not match
1633                 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1634                 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1635                 if attendee.partner_id.id  != user.partner_id.id:
1636                     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)
1637
1638         if error_message:
1639             raise BadRequest(error_message)
1640         return True
1641
1642 class invite_wizard(osv.osv_memory):
1643     _inherit = 'mail.wizard.invite'
1644
1645     def default_get(self, cr, uid, fields, context=None):
1646         '''
1647         in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1648         '''
1649         result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1650         if 'res_id' in result:
1651             result['res_id'] = get_real_ids(result['res_id'])
1652         return result