[MERGE] merge with trunk
[odoo/odoo.git] / addons / base_calendar / base_calendar.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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 from datetime import datetime, timedelta, date
23 from dateutil import parser
24 from dateutil import rrule
25 from dateutil.relativedelta import relativedelta
26 from osv import fields, osv
27 from service import web_services
28 from tools.translate import _
29 import pytz
30 import re
31 import time
32 import tools
33
34 months = {
35     1: "January", 2: "February", 3: "March", 4: "April", \
36     5: "May", 6: "June", 7: "July", 8: "August", 9: "September", \
37     10: "October", 11: "November", 12: "December"
38 }
39
40 def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None):
41     """
42     Get recurrent dates based on Rule string considering exdate and start date
43     @param rrulestring: Rulestring
44     @param exdate: List of exception dates for rrule
45     @param startdate: Startdate for computing recurrent dates
46     @return: List of Recurrent dates
47     """
48     def todate(date):
49         val = parser.parse(''.join((re.compile('\d')).findall(date)))
50         return val
51
52     if not startdate:
53         startdate = datetime.now()
54
55     if not exdate:
56         exdate = []
57
58     rset1 = rrule.rrulestr(str(rrulestring), dtstart=startdate, forceset=True)
59     for date in exdate:
60         datetime_obj = todate(date)
61         rset1._exdate.append(datetime_obj)
62
63     if exrule:
64         rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate))
65
66     return list(rset1)
67
68 def base_calendar_id2real_id(base_calendar_id=None, with_date=False):
69     """
70     This function converts virtual event id into real id of actual event
71     @param base_calendar_id: Id of calendar
72     @param with_date: If value passed to this param it will return dates based on value of withdate + base_calendar_id
73     """
74
75     if base_calendar_id and isinstance(base_calendar_id, (str, unicode)):
76         res = base_calendar_id.split('-')
77
78         if len(res) >= 2:
79             real_id = res[0]
80             if with_date:
81                 real_date = time.strftime("%Y-%m-%d %H:%M:%S", \
82                                  time.strptime(res[1], "%Y%m%d%H%M%S"))
83                 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
84                 end = start + timedelta(hours=with_date)
85                 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
86             return int(real_id)
87
88     return base_calendar_id and int(base_calendar_id) or base_calendar_id
89
90 def real_id2base_calendar_id(real_id, recurrent_date):
91     """
92     Convert  real id of record into virtual id using recurrent_date
93     e.g. real id is 1 and recurrent_date is 01-12-2009 10:00:00 then it will return
94         1-20091201100000
95     @return: real id with recurrent date.
96     """
97
98     if real_id and recurrent_date:
99         recurrent_date = time.strftime("%Y%m%d%H%M%S", \
100                             time.strptime(recurrent_date, "%Y-%m-%d %H:%M:%S"))
101         return '%d-%s' % (real_id, recurrent_date)
102     return real_id
103
104 def _links_get(self, cr, uid, context=None):
105     """
106     Get request link.
107     @param cr: the current row, from the database cursor,
108     @param uid: the current user’s ID for security checks,
109     @param context: A standard dictionary for contextual values
110     @return: list of dictionary which contain object and name and id.
111     """
112     obj = self.pool.get('res.request.link')
113     ids = obj.search(cr, uid, [])
114     res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
115     return [(r['object'], r['name']) for r in res]
116
117 html_invitation = """
118 <html>
119 <head>
120 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
121 <title>%(name)s</title>
122 </head>
123 <body>
124 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
125     style="font-family: Arial, Sans-serif; font-size: 14">
126     <tr>
127         <td width="100%%">Hello,</td>
128     </tr>
129     <tr>
130         <td width="100%%">You are invited for <i>%(company)s</i> Event.</td>
131     </tr>
132     <tr>
133         <td width="100%%">Below are the details of event:</td>
134     </tr>
135 </table>
136
137 <table cellspacing="0" cellpadding="5" border="0" summary=""
138     style="width: 90%%; font-family: Arial, Sans-serif; border: 1px Solid #ccc; background-color: #f6f6f6">
139     <tr valign="center" align="center">
140         <td bgcolor="DFDFDF">
141         <h3>%(name)s</h3>
142         </td>
143     </tr>
144     <tr>
145         <td>
146         <table cellpadding="8" cellspacing="0" border="0"
147             style="font-size: 14" summary="Eventdetails" bgcolor="f6f6f6"
148             width="90%%">
149             <tr>
150                 <td width="21%%">
151                 <div><b>Start Date</b></div>
152                 </td>
153                 <td><b>:</b></td>
154                 <td>%(start_date)s</td>
155                 <td width="15%%">
156                 <div><b>End Date</b></div>
157                 </td>
158                 <td><b>:</b></td>
159                 <td width="25%%">%(end_date)s</td>
160             </tr>
161             <tr valign="top">
162                 <td><b>Description</b></td>
163                 <td><b>:</b></td>
164                 <td colspan="3">%(description)s</td>
165             </tr>
166             <tr valign="top">
167                 <td>
168                 <div><b>Location</b></div>
169                 </td>
170                 <td><b>:</b></td>
171                 <td colspan="3">%(location)s</td>
172             </tr>
173             <tr valign="top">
174                 <td>
175                 <div><b>Event Attendees</b></div>
176                 </td>
177                 <td><b>:</b></td>
178                 <td colspan="3">
179                 <div>
180                 <div>%(attendees)s</div>
181                 </div>
182                 </td>
183             </tr>
184         </table>
185         </td>
186     </tr>
187 </table>
188 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
189     style="font-family: Arial, Sans-serif; font-size: 14">
190     <tr>
191         <td width="100%%">From:</td>
192     </tr>
193     <tr>
194         <td width="100%%">%(user)s</td>
195     </tr>
196     <tr valign="top">
197         <td width="100%%">-<font color="a7a7a7">-------------------------</font></td>
198     </tr>
199     <tr>
200         <td width="100%%"> <font color="a7a7a7">%(sign)s</font></td>
201     </tr>
202 </table>
203 </body>
204 </html>
205 """
206
207 class calendar_attendee(osv.osv):
208     """
209     Calendar Attendee Information
210     """
211     _name = 'calendar.attendee'
212     _description = 'Attendee information'
213     _rec_name = 'cutype'
214
215     __attribute__ = {}
216
217     def _get_address(self, name=None, email=None):
218         """
219         Gives email information in ical CAL-ADDRESS type format
220         @param name: Name for CAL-ADDRESS value
221         @param email: Email address for CAL-ADDRESS value
222         """
223         if name and email:
224             name += ':'
225         return (name or '') + (email and ('MAILTO:' + email) or '')
226
227     def _compute_data(self, cr, uid, ids, name, arg, context=None):
228         """
229         Compute data on function fields for attendee values .
230         @param cr: the current row, from the database cursor,
231         @param uid: the current user’s ID for security checks,
232         @param ids: List of calendar attendee’s IDs.
233         @param name: name of field.
234         @param context: A standard dictionary for contextual values
235         @return: Dictionary of form {id: {'field Name': value'}}.
236         """
237         name = name[0]
238         result = {}
239         for attdata in self.browse(cr, uid, ids, context=context):
240             id = attdata.id
241             result[id] = {}
242             if name == 'sent_by':
243                 if not attdata.sent_by_uid:
244                     result[id][name] = ''
245                     continue
246                 else:
247                     result[id][name] = self._get_address(attdata.sent_by_uid.name, \
248                                         attdata.sent_by_uid.email)
249
250             if name == 'cn':
251                 if attdata.user_id:
252                     result[id][name] = attdata.user_id.name
253                 elif attdata.partner_id:
254                     result[id][name] = attdata.partner_id.name or False
255                 else:
256                     result[id][name] = attdata.email or ''
257
258             if name == 'delegated_to':
259                 todata = []
260                 for child in attdata.child_ids:
261                     if child.email:
262                         todata.append('MAILTO:' + child.email)
263                 result[id][name] = ', '.join(todata)
264
265             if name == 'delegated_from':
266                 fromdata = []
267                 for parent in attdata.parent_ids:
268                     if parent.email:
269                         fromdata.append('MAILTO:' + parent.email)
270                 result[id][name] = ', '.join(fromdata)
271
272             if name == 'event_date':
273                 if attdata.ref:
274                     result[id][name] = attdata.ref.date
275                 else:
276                     result[id][name] = False
277
278             if name == 'event_end_date':
279                 if attdata.ref:
280                     result[id][name] = attdata.ref.date_deadline
281                 else:
282                     result[id][name] = False
283
284             if name == 'sent_by_uid':
285                 if attdata.ref:
286                     result[id][name] = (attdata.ref.user_id.id, attdata.ref.user_id.name)
287                 else:
288                     result[id][name] = uid
289
290             if name == 'language':
291                 user_obj = self.pool.get('res.users')
292                 lang = user_obj.read(cr, uid, uid, ['lang'], context=context)['lang']
293                 result[id][name] = lang.replace('_', '-')
294
295         return result
296
297     def _links_get(self, cr, uid, context=None):
298         """
299         Get request link for ref field in calendar attendee.
300         @param cr: the current row, from the database cursor,
301         @param uid: the current user’s ID for security checks,
302         @param context: A standard dictionary for contextual values
303         @return: list of dictionary which contain object and name and id.
304         """
305         obj = self.pool.get('res.request.link')
306         ids = obj.search(cr, uid, [])
307         res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
308         return [(r['object'], r['name']) for r in res]
309
310     def _lang_get(self, cr, uid, context=None):
311         """
312         Get language for language selection field.
313         @param cr: the current row, from the database cursor,
314         @param uid: the current user’s ID for security checks,
315         @param context: A standard dictionary for contextual values
316         @return: list of dictionary which contain code and name and id.
317         """
318         obj = self.pool.get('res.lang')
319         ids = obj.search(cr, uid, [])
320         res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
321         res = [((r['code']).replace('_', '-').lower(), r['name']) for r in res]
322         return res
323
324     _columns = {
325         'cutype': fields.selection([('individual', 'Individual'), \
326                     ('group', 'Group'), ('resource', 'Resource'), \
327                     ('room', 'Room'), ('unknown', 'Unknown') ], \
328                     'Invite Type', help="Specify the type of Invitation"),
329         'member': fields.char('Member', size=124,
330                     help="Indicate the groups that the attendee belongs to"),
331         'role': fields.selection([('req-participant', 'Participation required'), \
332                     ('chair', 'Chair Person'), \
333                     ('opt-participant', 'Optional Participation'), \
334                     ('non-participant', 'For information Purpose')], 'Role', \
335                     help='Participation role for the calendar user'),
336         'state': fields.selection([('needs-action', 'Needs Action'),
337                         ('tentative', 'Tentative'),
338                         ('declined', 'Declined'),
339                         ('accepted', 'Accepted'),
340                         ('delegated', 'Delegated')], 'Status', readonly=True, \
341                         help="Status of the attendee's participation"),
342         'rsvp':  fields.boolean('Required Reply?',
343                     help="Indicats whether the favor of a reply is requested"),
344         'delegated_to': fields.function(_compute_data, \
345                 string='Delegated To', type="char", size=124, store=True, \
346                 multi='delegated_to', help="The users that the original \
347 request was delegated to"),
348         'delegated_from': fields.function(_compute_data, string=\
349             'Delegated From', type="char", store=True, size=124, multi='delegated_from'),
350         'parent_ids': fields.many2many('calendar.attendee', 'calendar_attendee_parent_rel', \
351                                     'attendee_id', 'parent_id', 'Delegrated From'),
352         'child_ids': fields.many2many('calendar.attendee', 'calendar_attendee_child_rel', \
353                                       'attendee_id', 'child_id', 'Delegrated To'),
354         'sent_by': fields.function(_compute_data, string='Sent By', \
355                         type="char", multi='sent_by', store=True, size=124, \
356                         help="Specify the user that is acting on behalf of the calendar user"),
357         'sent_by_uid': fields.function(_compute_data, string='Sent By User', \
358                             type="many2one", relation="res.users", multi='sent_by_uid'),
359         'cn': fields.function(_compute_data, string='Common name', \
360                             type="char", size=124, multi='cn', store=True),
361         'dir': fields.char('URI Reference', size=124, help="Reference to the URI\
362 that points to the directory information corresponding to the attendee."),
363         'language': fields.function(_compute_data, string='Language', \
364                     type="selection", selection=_lang_get, multi='language', \
365                     store=True, help="To specify the language for text values in a\
366 property or property parameter."),
367         'user_id': fields.many2one('res.users', 'User'),
368         'partner_id': fields.many2one('res.partner', 'Contact'),
369         'email': fields.char('Email', size=124, help="Email of Invited Person"),
370         'event_date': fields.function(_compute_data, string='Event Date', \
371                             type="datetime", multi='event_date'),
372         'event_end_date': fields.function(_compute_data, \
373                             string='Event End Date', type="datetime", \
374                             multi='event_end_date'),
375         'ref': fields.reference('Event Ref', selection=_links_get, size=128),
376         'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
377     }
378
379     _defaults = {
380         'state': 'needs-action',
381         'role': 'req-participant',
382         'rsvp':  True,
383         'cutype': 'individual',
384     }
385
386     def copy(self, cr, uid, id, default=None, context=None):
387         raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
388
389     def get_ics_file(self, cr, uid, event_obj, context=None):
390         """
391         Returns iCalendar file for the event invitation
392         @param self: The object pointer
393         @param cr: the current row, from the database cursor,
394         @param uid: the current user’s ID for security checks,
395         @param event_obj: Event object (browse record)
396         @param context: A standard dictionary for contextual values
397         @return: .ics file content
398         """
399         res = None
400         def ics_datetime(idate, short=False):
401             if idate:
402                 if short or len(idate)<=10:
403                     return date.fromtimestamp(time.mktime(time.strptime(idate, '%Y-%m-%d')))
404                 else:
405                     return datetime.strptime(idate, '%Y-%m-%d %H:%M:%S')
406             else:
407                 return False
408         try:
409             # FIXME: why isn't this in CalDAV?
410             import vobject
411         except ImportError:
412             return res
413         cal = vobject.iCalendar()
414         event = cal.add('vevent')
415         if not event_obj.date_deadline or not event_obj.date:
416             raise osv.except_osv(_('Warning!'),_("First you have to specify the date of the invitation."))
417         event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S'))
418         event.add('dtstart').value = ics_datetime(event_obj.date)
419         event.add('dtend').value = ics_datetime(event_obj.date_deadline)
420         event.add('summary').value = event_obj.name
421         if  event_obj.description:
422             event.add('description').value = event_obj.description
423         if event_obj.location:
424             event.add('location').value = event_obj.location
425         if event_obj.rrule:
426             event.add('rrule').value = event_obj.rrule
427         if event_obj.organizer:
428             event_org = event.add('organizer')
429             event_org.params['CN'] = [event_obj.organizer]
430             event_org.value = 'MAILTO:' + (event_obj.organizer)
431         elif event_obj.user_id or event_obj.organizer_id:
432             event_org = event.add('organizer')
433             organizer = event_obj.organizer_id
434             if not organizer:
435                 organizer = event_obj.user_id
436             event_org.params['CN'] = [organizer.name]
437             event_org.value = 'MAILTO:' + (organizer.email or organizer.name)
438
439         if event_obj.alarm_id:
440             # computes alarm data
441             valarm = event.add('valarm')
442             alarm_object = self.pool.get('res.alarm')
443             alarm_data = alarm_object.read(cr, uid, event_obj.alarm_id.id, context=context)
444             # Compute trigger data
445             interval = alarm_data['trigger_interval']
446             occurs = alarm_data['trigger_occurs']
447             duration = (occurs == 'after' and alarm_data['trigger_duration']) \
448                                             or -(alarm_data['trigger_duration'])
449             related = alarm_data['trigger_related']
450             trigger = valarm.add('TRIGGER')
451             trigger.params['related'] = [related.upper()]
452             if interval == 'days':
453                 delta = timedelta(days=duration)
454             if interval == 'hours':
455                 delta = timedelta(hours=duration)
456             if interval == 'minutes':
457                 delta = timedelta(minutes=duration)
458             trigger.value = delta
459             # Compute other details
460             valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
461
462         for attendee in event_obj.attendee_ids:
463             attendee_add = event.add('attendee')
464             attendee_add.params['CUTYPE'] = [str(attendee.cutype)]
465             attendee_add.params['ROLE'] = [str(attendee.role)]
466             attendee_add.params['RSVP'] = [str(attendee.rsvp)]
467             attendee_add.value = 'MAILTO:' + (attendee.email or '')
468         res = cal.serialize()
469         return res
470
471     def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
472         """
473         Send mail for event invitation to event attendees.
474         @param email_from: Email address for user sending the mail
475         @return: True
476         """
477         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
478         for att in self.browse(cr, uid, ids, context=context):
479             sign = att.sent_by_uid and att.sent_by_uid.signature or ''
480             sign = '<br>'.join(sign and sign.split('\n') or [])
481             res_obj = att.ref
482             if res_obj:
483                 att_infos = []
484                 sub = res_obj.name
485                 other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
486
487                 for att2 in self.browse(cr, uid, other_invitation_ids):
488                     att_infos.append(((att2.user_id and att2.user_id.name) or \
489                                  (att2.partner_id and att2.partner_id.name) or \
490                                     att2.email) + ' - Status: ' + att2.state.title())
491                 body_vals = {'name': res_obj.name,
492                             'start_date': res_obj.date,
493                             'end_date': res_obj.date_deadline or False,
494                             'description': res_obj.description or '-',
495                             'location': res_obj.location or '-',
496                             'attendees': '<br>'.join(att_infos),
497                             'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
498                             'sign': sign,
499                             'company': company
500                 }
501                 body = html_invitation % body_vals
502                 if mail_to and email_from:
503                     ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
504                     vals = {'email_from': email_from,
505                             'email_to': mail_to,
506                             'state': 'outgoing',
507                             'subject': sub,
508                             'body_html': body,
509                             'auto_delete': True}
510                     if ics_file:
511                         vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
512                                                         'datas_fname': 'invitation.ics',
513                                                         'datas': str(ics_file).encode('base64')})]
514                     self.pool.get('mail.mail').create(cr, uid, vals, context=context)
515             return True
516
517     def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
518         """
519         Make entry on email and availbility on change of user_id field.
520         @param cr: the current row, from the database cursor,
521         @param uid: the current user’s ID for security checks,
522         @param ids: List of calendar attendee’s IDs.
523         @param user_id: Changed value of User id
524         @return: dictionary of value. which put value in email and availability fields.
525         """
526
527         if not user_id:
528             return {'value': {'email': ''}}
529         usr_obj = self.pool.get('res.users')
530         user = usr_obj.browse(cr, uid, user_id, *args)
531         return {'value': {'email': user.email, 'availability':user.availability}}
532
533     def do_tentative(self, cr, uid, ids, context=None, *args):
534         """ Makes event invitation as Tentative
535         @param self: The object pointer
536         @param cr: the current row, from the database cursor,
537         @param uid: the current user’s ID for security checks,
538         @param ids: List of calendar attendee’s IDs
539         @param *args: Get Tupple value
540         @param context: A standard dictionary for contextual values
541         """
542         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
543
544     def do_accept(self, cr, uid, ids, context=None, *args):
545         """
546         Update state of invitation as Accepted and
547         if the invited user is other then event user it will make a copy of this event for invited user
548         @param cr: the current row, from the database cursor,
549         @param uid: the current user’s ID for security checks,
550         @param ids: List of calendar attendee’s IDs.
551         @param context: A standard dictionary for contextual values
552         @return: True
553         """
554         if context is None:
555             context = {}
556
557         for vals in self.browse(cr, uid, ids, context=context):
558             if vals.ref and vals.ref.user_id:
559                 mod_obj = self.pool.get(vals.ref._name)
560                 defaults = {'user_id': vals.user_id.id, 'organizer_id': vals.ref.user_id.id}
561                 mod_obj.copy(cr, uid, vals.ref.id, default=defaults, context=context)
562             self.write(cr, uid, vals.id, {'state': 'accepted'}, context)
563
564         return True
565
566     def do_decline(self, cr, uid, ids, context=None, *args):
567         """ Marks event invitation as Declined
568         @param self: The object pointer
569         @param cr: the current row, from the database cursor,
570         @param uid: the current user’s ID for security checks,
571         @param ids: List of calendar attendee’s IDs
572         @param *args: Get Tupple value
573         @param context: A standard dictionary for contextual values """
574         if context is None:
575             context = {}
576         return self.write(cr, uid, ids, {'state': 'declined'}, context)
577
578     def create(self, cr, uid, vals, context=None):
579         """ Overrides orm create method.
580         @param self: The object pointer
581         @param cr: the current row, from the database cursor,
582         @param uid: the current user’s ID for security checks,
583         @param vals: Get Values
584         @param context: A standard dictionary for contextual values """
585
586         if context is None:
587             context = {}
588         if not vals.get("email") and vals.get("cn"):
589             cnval = vals.get("cn").split(':')
590             email = filter(lambda x:x.__contains__('@'), cnval)
591             vals['email'] = email and email[0] or ''
592             vals['cn'] = vals.get("cn")
593         res = super(calendar_attendee, self).create(cr, uid, vals, context)
594         return res
595 calendar_attendee()
596
597 class res_alarm(osv.osv):
598     """Resource Alarm """
599     _name = 'res.alarm'
600     _description = 'Basic Alarm Information'
601
602     _columns = {
603         'name':fields.char('Name', size=256, required=True),
604         'trigger_occurs': fields.selection([('before', 'Before'), \
605                                             ('after', 'After')], \
606                                         'Triggers', required=True),
607         'trigger_interval': fields.selection([('minutes', 'Minutes'), \
608                                                 ('hours', 'Hours'), \
609                                                 ('days', 'Days')], 'Interval', \
610                                                 required=True),
611         'trigger_duration': fields.integer('Duration', required=True),
612         'trigger_related': fields.selection([('start', 'The event starts'), \
613                                             ('end', 'The event ends')], \
614                                             'Related to', required=True),
615         'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \
616 are both optional, but if one occurs, so MUST the other"""),
617         'repeat': fields.integer('Repeat'),
618         'active': fields.boolean('Active', help="If the active field is set to \
619 true, it will allow you to hide the event alarm information without removing it.")
620     }
621     _defaults = {
622         'trigger_interval': 'minutes',
623         'trigger_duration': 5,
624         'trigger_occurs': 'before',
625         'trigger_related': 'start',
626         'active': 1,
627     }
628
629     def do_alarm_create(self, cr, uid, ids, model, date, context=None):
630         """
631         Create Alarm for event.
632         @param cr: the current row, from the database cursor,
633         @param uid: the current user’s ID for security checks,
634         @param ids: List of res alarm’s IDs.
635         @param model: Model name.
636         @param date: Event date
637         @param context: A standard dictionary for contextual values
638         @return: True
639         """
640         if context is None:
641             context = {}
642         alarm_obj = self.pool.get('calendar.alarm')
643         res_alarm_obj = self.pool.get('res.alarm')
644         ir_obj = self.pool.get('ir.model')
645         model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
646
647         model_obj = self.pool.get(model)
648         for data in model_obj.browse(cr, uid, ids, context=context):
649
650             basic_alarm = data.alarm_id
651             cal_alarm = data.base_calendar_alarm_id
652             if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm):
653                 new_res_alarm = None
654                 # Find for existing res.alarm
655                 duration = cal_alarm.trigger_duration
656                 interval = cal_alarm.trigger_interval
657                 occurs = cal_alarm.trigger_occurs
658                 related = cal_alarm.trigger_related
659                 domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)]
660                 alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context)
661                 if not alarm_ids:
662                     val = {
663                             'trigger_duration': duration,
664                             'trigger_interval': interval,
665                             'trigger_occurs': occurs,
666                             'trigger_related': related,
667                             'name': str(duration) + ' ' + str(interval) + ' '  + str(occurs)
668                            }
669                     new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context)
670                 else:
671                     new_res_alarm = alarm_ids[0]
672                 cr.execute('UPDATE %s ' % model_obj._table + \
673                             ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \
674                             ' WHERE id=%s',
675                             (cal_alarm.id, new_res_alarm, data.id))
676
677             self.do_alarm_unlink(cr, uid, [data.id], model)
678             if basic_alarm:
679                 vals = {
680                     'action': 'display',
681                     'description': data.description,
682                     'name': data.name,
683                     'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))],
684                     'trigger_related': basic_alarm.trigger_related,
685                     'trigger_duration': basic_alarm.trigger_duration,
686                     'trigger_occurs': basic_alarm.trigger_occurs,
687                     'trigger_interval': basic_alarm.trigger_interval,
688                     'duration': basic_alarm.duration,
689                     'repeat': basic_alarm.repeat,
690                     'state': 'run',
691                     'event_date': data[date],
692                     'res_id': data.id,
693                     'model_id': model_id,
694                     'user_id': uid
695                  }
696                 alarm_id = alarm_obj.create(cr, uid, vals)
697                 cr.execute('UPDATE %s ' % model_obj._table + \
698                             ' SET base_calendar_alarm_id=%s, alarm_id=%s '
699                             ' WHERE id=%s', \
700                             ( alarm_id, basic_alarm.id, data.id) )
701         return True
702
703     def do_alarm_unlink(self, cr, uid, ids, model, context=None):
704         """
705         Delete alarm specified in ids
706         @param cr: the current row, from the database cursor,
707         @param uid: the current user’s ID for security checks,
708         @param ids: List of res alarm’s IDs.
709         @param model: Model name for which alarm is to be cleared.
710         @return: True
711         """
712         if context is None:
713             context = {}
714         alarm_obj = self.pool.get('calendar.alarm')
715         ir_obj = self.pool.get('ir.model')
716         model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
717         model_obj = self.pool.get(model)
718         for datas in model_obj.browse(cr, uid, ids, context=context):
719             alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', datas.id)])
720             if alarm_ids:
721                 alarm_obj.unlink(cr, uid, alarm_ids)
722                 cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\
723                             where id=%%s' % model_obj._table,(datas.id,))
724         return True
725
726 res_alarm()
727
728 class calendar_alarm(osv.osv):
729     _name = 'calendar.alarm'
730     _description = 'Event alarm information'
731     _inherit = 'res.alarm'
732     __attribute__ = {}
733
734     _columns = {
735         'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'),
736         'name': fields.char('Summary', size=124, help="""Contains the text to be \
737                      used as the message subject for email \
738                      or contains the text to be used for display"""),
739         'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \
740                 ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \
741                 required=True, help="Defines the action to be invoked when an alarm is triggered"),
742         'description': fields.text('Description', help='Provides a more complete \
743                             description of the calendar component, than that \
744                             provided by the "SUMMARY" property'),
745         'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \
746                                       'alarm_id', 'attendee_id', 'Attendees', readonly=True),
747         'attach': fields.binary('Attachment', help="""* Points to a sound resource,\
748                      which is rendered when the alarm is triggered for audio,
749                     * File which is intended to be sent as message attachments for email,
750                     * Points to a procedure resource, which is invoked when\
751                       the alarm is triggered for procedure."""),
752         'res_id': fields.integer('Resource ID'),
753         'model_id': fields.many2one('ir.model', 'Model'),
754         'user_id': fields.many2one('res.users', 'Owner'),
755         'event_date': fields.datetime('Event Date'),
756         'event_end_date': fields.datetime('Event End Date'),
757         'trigger_date': fields.datetime('Trigger Date', readonly="True"),
758         'state':fields.selection([
759                     ('draft', 'Draft'),
760                     ('run', 'Run'),
761                     ('stop', 'Stop'),
762                     ('done', 'Done'),
763                 ], 'Status', select=True, readonly=True),
764      }
765
766     _defaults = {
767         'action': 'email',
768         'state': 'run',
769      }
770
771     def create(self, cr, uid, vals, context=None):
772         """
773         Overrides orm create method.
774         @param self: The object pointer
775         @param cr: the current row, from the database cursor,
776         @param vals: dictionary of fields value.{‘name_of_the_field’: value, ...}
777         @param context: A standard dictionary for contextual values
778         @return: new record id for calendar_alarm.
779         """
780         if context is None:
781             context = {}
782         event_date = vals.get('event_date', False)
783         if event_date:
784             dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S")
785             if vals['trigger_interval'] == 'days':
786                 delta = timedelta(days=vals['trigger_duration'])
787             if vals['trigger_interval'] == 'hours':
788                 delta = timedelta(hours=vals['trigger_duration'])
789             if vals['trigger_interval'] == 'minutes':
790                 delta = timedelta(minutes=vals['trigger_duration'])
791             trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta)
792             vals['trigger_date'] = trigger_date
793         res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
794         return res
795
796     def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
797                        context=None):
798         """Scheduler for event reminder
799         @param self: The object pointer
800         @param cr: the current row, from the database cursor,
801         @param uid: the current user’s ID for security checks,
802         @param ids: List of calendar alarm’s IDs.
803         @param use_new_cursor: False or the dbname
804         @param context: A standard dictionary for contextual values
805         """
806         if context is None:
807             context = {}
808         current_datetime = datetime.now()
809         alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
810
811         mail_to = []
812
813         for alarm in self.browse(cr, uid, alarm_ids, context=context):
814             next_trigger_date = None
815             update_vals = {}
816             model_obj = self.pool.get(alarm.model_id.model)
817             res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
818             re_dates = []
819
820             if res_obj.rrule:
821                 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
822                 recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.exdate, event_date, res_obj.exrule)
823
824                 trigger_interval = alarm.trigger_interval
825                 if trigger_interval == 'days':
826                     delta = timedelta(days=alarm.trigger_duration)
827                 if trigger_interval == 'hours':
828                     delta = timedelta(hours=alarm.trigger_duration)
829                 if trigger_interval == 'minutes':
830                     delta = timedelta(minutes=alarm.trigger_duration)
831                 delta = alarm.trigger_occurs == 'after' and delta or -delta
832
833                 for rdate in recurrent_dates:
834                     if rdate + delta > current_datetime:
835                         break
836                     if rdate + delta <= current_datetime:
837                         re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
838                 rest_dates = recurrent_dates[len(re_dates):]
839                 next_trigger_date = rest_dates and rest_dates[0] or None
840
841             else:
842                 re_dates = [alarm.trigger_date]
843
844             if re_dates:
845                 if alarm.action == 'email':
846                     sub = '[OpenERP Reminder] %s' % (alarm.name)
847                     body = """<pre>
848 Event: %s
849 Event Date: %s
850 Description: %s
851
852 From:
853       %s
854
855 ----
856 %s
857 </pre>
858 """  % (alarm.name, alarm.trigger_date, alarm.description, \
859                         alarm.user_id.name, alarm.user_id.signature)
860                     mail_to = [alarm.user_id.email]
861                     for att in alarm.attendee_ids:
862                         mail_to.append(att.user_id.email)
863                     if mail_to:
864                         vals = {
865                             'state': 'outgoing',
866                             'subject': sub,
867                             'body_html': body,
868                             'email_to': mail_to,
869                             'email_from': tools.config.get('email_from', mail_to),
870                         }
871                         self.pool.get('mail.mail').create(cr, uid, vals, context=context)
872             if next_trigger_date:
873                 update_vals.update({'trigger_date': next_trigger_date})
874             else:
875                 update_vals.update({'state': 'done'})
876             self.write(cr, uid, [alarm.id], update_vals)
877         return True
878
879 calendar_alarm()
880
881
882 class calendar_event(osv.osv):
883     _name = "calendar.event"
884     _description = "Calendar Event"
885     __attribute__ = {}
886
887     def _tz_get(self, cr, uid, context=None):
888         return [(x.lower(), x) for x in pytz.all_timezones]
889
890     def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
891         """Returns duration and/or end date based on values passed
892         @param self: The object pointer
893         @param cr: the current row, from the database cursor,
894         @param uid: the current user’s ID for security checks,
895         @param ids: List of calendar event’s IDs.
896         @param start_date: Starting date
897         @param duration: Duration between start date and end date
898         @param end_date: Ending Datee
899         @param context: A standard dictionary for contextual values
900         """
901         if context is None:
902             context = {}
903
904         value = {}
905         if not start_date:
906             return value
907         if not end_date and not duration:
908             duration = 1.00
909             value['duration'] = duration
910
911         start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
912         if allday: # For all day event
913             duration = 24.0
914             value['duration'] = duration
915             # change start_date's time to 00:00:00 in the user's timezone
916             user = self.pool.get('res.users').browse(cr, uid, uid)
917             tz = pytz.timezone(user.tz) if user.tz else pytz.utc
918             start = pytz.utc.localize(start).astimezone(tz)     # convert start in user's timezone
919             start = start.replace(hour=0, minute=0, second=0)   # change start's time to 00:00:00
920             start = start.astimezone(pytz.utc)                  # convert start back to utc
921             start_date = start.strftime("%Y-%m-%d %H:%M:%S")
922             value['date'] = start_date
923
924         if end_date and not duration:
925             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
926             diff = end - start
927             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
928             value['duration'] = round(duration, 2)
929         elif not end_date:
930             end = start + timedelta(hours=duration)
931             value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
932         elif end_date and duration and not allday:
933             # we have both, keep them synchronized:
934             # set duration based on end_date (arbitrary decision: this avoid
935             # getting dates like 06:31:48 instead of 06:32:00)
936             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
937             diff = end - start
938             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
939             value['duration'] = round(duration, 2)
940
941         return {'value': value}
942
943     def unlink_events(self, cr, uid, ids, context=None):
944         """
945         This function deletes event which are linked with the event with recurrent_uid
946                 (Removes the events which refers to the same UID value)
947         """
948         if context is None:
949             context = {}
950         for event_id in ids:
951             cr.execute("select id from %s where recurrent_uid=%%s" % (self._table), (event_id,))
952             r_ids = map(lambda x: x[0], cr.fetchall())
953             self.unlink(cr, uid, r_ids, context=context)
954         return True
955
956     def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
957         """
958         Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
959         @param self: The object pointer
960         @param cr: the current row, from the database cursor,
961         @param id: List of calendar event's ids.
962         @param context: A standard dictionary for contextual values
963         @return: dictionary of rrule value.
964         """
965
966         result = {}
967         if not isinstance(ids, list):
968             ids = [ids]
969
970         for datas in self.read(cr, uid, ids, ['id','byday','recurrency', 'month_list','end_date', 'rrule_type', 'select1', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'exrule', 'day', 'week_list' ], context=context):
971             event = datas['id']
972             if datas.get('interval', 0) < 0:
973                 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
974             if datas.get('count', 0) < 0:
975                 raise osv.except_osv(_('Warning!'), _('Count cannot be negative.'))
976             if datas['recurrency']:
977                 result[event] = self.compute_rule_string(datas)
978             else:
979                 result[event] = ""
980         return result
981
982     def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
983         data = self._get_empty_rrule_data()
984         if field_value:
985             data['recurrency'] = True
986             for event in self.browse(cr, uid, ids, context=context):
987                 rdate = rule_date or event.date
988                 update_data = self._parse_rrule(field_value, dict(data), rdate)
989                 data.update(update_data)
990                 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
991         return True
992
993
994     _columns = {
995         'id': fields.integer('ID', readonly=True),
996         'sequence': fields.integer('Sequence'),
997         'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
998         'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True,),
999         'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
1000         'create_date': fields.datetime('Created', readonly=True),
1001         'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1002         'description': fields.text('Description', states={'done': [('readonly', True)]}),
1003         'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1004              ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
1005         'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1006         'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1007                                                 'Show Time as', states={'done': [('readonly', True)]}),
1008         'base_calendar_url': fields.char('Caldav URL', size=264),
1009         'state': fields.selection([('tentative', 'Tentative'),
1010                         ('cancelled', 'Cancelled'),
1011                         ('confirmed', 'Confirmed'),
1012                         ], 'Status', readonly=True),
1013         'exdate': fields.text('Exception Date/Times', help="This property \
1014 defines the list of date/time exceptions for a recurring calendar component."),
1015         'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1016 rule or repeating pattern of time to exclude from the recurring rule."),
1017         'rrule': fields.function(_get_rulestring, type='char', size=124, \
1018                     fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1019         'rrule_type': fields.selection([('none', ''), ('daily', 'Daily'), \
1020                             ('weekly', 'Weekly'), ('monthly', 'Monthly'), \
1021                             ('yearly', 'Yearly'),],
1022                             'Recurrency', states={'done': [('readonly', True)]},
1023                             help="Let the event automatically repeat at that interval"),
1024         'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]},
1025                         help="Set an alarm at this time, before the event occurs" ),
1026         'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1027         'recurrent_uid': fields.integer('Recurrent ID'),
1028         'recurrent_id': fields.datetime('Recurrent ID date'),
1029         'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1030         'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1031         'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with Organizer Attribure of VEvent.
1032         'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1033         'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'),
1034         'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
1035         'count': fields.integer('Repeat', help="Repeat x times"),
1036         'mo': fields.boolean('Mon'),
1037         'tu': fields.boolean('Tue'),
1038         'we': fields.boolean('Wed'),
1039         'th': fields.boolean('Thu'),
1040         'fr': fields.boolean('Fri'),
1041         'sa': fields.boolean('Sat'),
1042         'su': fields.boolean('Sun'),
1043         'select1': fields.selection([('date', 'Date of month'),
1044                                     ('day', 'Day of month')], 'Option'),
1045         'day': fields.integer('Date of month'),
1046         'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), \
1047                                    ('WE', 'Wednesday'), ('TH', 'Thursday'), \
1048                                    ('FR', 'Friday'), ('SA', 'Saturday'), \
1049                                    ('SU', 'Sunday')], 'Weekday'),
1050         'byday': fields.selection([('1', 'First'), ('2', 'Second'), \
1051                                    ('3', 'Third'), ('4', 'Fourth'), \
1052                                    ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
1053         'month_list': fields.selection(months.items(), 'Month'),
1054         'end_date': fields.date('Repeat Until'),
1055         'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1056                                  'event_id', 'attendee_id', 'Attendees'),
1057         'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1058         'active': fields.boolean('Active', help="If the active field is set to \
1059          true, it will allow you to hide the event alarm information without removing it."),
1060         'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1061     }
1062
1063     def default_organizer(self, cr, uid, context=None):
1064         user_pool = self.pool.get('res.users')
1065         user = user_pool.browse(cr, uid, uid, context=context)
1066         res = user.name
1067         if user.email:
1068             res += " <%s>" %(user.email)
1069         return res
1070
1071     _defaults = {
1072             'end_type' : 'count',
1073             'count' : 1,
1074             'rrule_type' : 'none',
1075             'state': 'tentative',
1076             'class': 'public',
1077             'show_as': 'busy',
1078             'select1': 'date',
1079             'interval': 1,
1080             'active': 1,
1081             'user_id': lambda self, cr, uid, ctx: uid,
1082             'organizer': default_organizer,
1083     }
1084
1085     def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1086         """Gives virtual event ids for recurring events based on value of Recurrence Rule
1087         This method gives ids of dates that comes between start date and end date of calendar views
1088         @param self: The object pointer
1089         @param cr: the current row, from the database cursor,
1090         @param uid: the current user’s ID for security checks,
1091         @param limit: The Number of Results to Return """
1092         if not context:
1093             context = {}
1094
1095         result = []
1096         for data in super(calendar_event, self).read(cr, uid, select, context=context):
1097             if not data['rrule']:
1098                 result.append(data['id'])
1099                 continue
1100             event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1101 #                To check: If the start date is replace by event date .. the event date will be changed by that of calendar code
1102
1103             if not data['rrule']:
1104                 continue
1105
1106             exdate = data['exdate'] and data['exdate'].split(',') or []
1107             rrule_str = data['rrule']
1108             new_rrule_str = []
1109             rrule_until_date = False
1110             is_until = False
1111             for rule in rrule_str.split(';'):
1112                 name, value = rule.split('=')
1113                 if name == "UNTIL":
1114                     is_until = True
1115                     value = parser.parse(value)
1116                     rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1117                     value = value.strftime("%Y%m%d%H%M%S")
1118                 new_rule = '%s=%s' % (name, value)
1119                 new_rrule_str.append(new_rule)
1120             new_rrule_str = ';'.join(new_rrule_str)
1121             rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1122             for r_date in rdates:
1123                 ok = True
1124                 for arg in domain:
1125                     if arg[0] in ('date', 'date_deadline'):
1126                         if (arg[1]=='='):
1127                             ok = ok and r_date.strftime('%Y-%m-%d')==arg[2]
1128                         if (arg[1]=='>'):
1129                             ok = ok and r_date.strftime('%Y-%m-%d')>arg[2]
1130                         if (arg[1]=='<'):
1131                             ok = ok and r_date.strftime('%Y-%m-%d')<arg[2]
1132                         if (arg[1]=='>='):
1133                             ok = ok and r_date.strftime('%Y-%m-%d')>=arg[2]
1134                         if (arg[1]=='<='):
1135                             ok = ok and r_date.strftime('%Y-%m-%d')<=arg[2]
1136                 if not ok:
1137                     continue
1138                 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1139                 result.append(idval)
1140
1141         if isinstance(select, (str, int, long)):
1142             return ids and ids[0] or False
1143         else:
1144             ids = list(set(result))
1145         return ids
1146
1147     def compute_rule_string(self, datas):
1148         """
1149         Compute rule string according to value type RECUR of iCalendar from the values given.
1150         @param self: the object pointer
1151         @param datas: dictionary of freq and interval value.
1152         """
1153         def get_week_string(freq, datas):
1154             weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1155             if freq == 'weekly':
1156                 byday = map(lambda x: x.upper(), filter(lambda x: datas.get(x) and x in weekdays, datas))
1157                 if byday:
1158                     return ';BYDAY=' + ','.join(byday)
1159             return ''
1160
1161         def get_month_string(freq, datas):
1162             if freq == 'monthly':
1163                 if datas.get('select1')=='date' and (datas.get('day') < 1 or datas.get('day') > 31):
1164                     raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1165
1166                 if datas.get('select1')=='day':
1167                     return ';BYDAY=' + datas.get('byday') + datas.get('week_list')
1168                 elif datas.get('select1')=='date':
1169                     return ';BYMONTHDAY=' + str(datas.get('day'))
1170             return ''
1171
1172         def get_end_date(datas):
1173             if datas.get('end_date'):
1174                 datas['end_date_new'] = ''.join((re.compile('\d')).findall(datas.get('end_date'))) + 'T235959Z'
1175
1176             return (datas.get('end_type') == 'count' and (';COUNT=' + str(datas.get('count'))) or '') +\
1177                              ((datas.get('end_date_new') and datas.get('end_type') == 'end_date' and (';UNTIL=' + datas.get('end_date_new'))) or '')
1178
1179         freq=datas.get('rrule_type')
1180         if freq == 'none':
1181             return ''
1182
1183         interval_srting = datas.get('interval') and (';INTERVAL=' + str(datas.get('interval'))) or ''
1184
1185         return 'FREQ=' + freq.upper() + get_week_string(freq, datas) + interval_srting + get_end_date(datas) + get_month_string(freq, datas)
1186
1187     def _get_empty_rrule_data(self):
1188         return  {
1189             'byday' : False,
1190             'recurrency' : False,
1191             'end_date' : False,
1192             'rrule_type' : False,
1193             'select1' : False,
1194             'interval' : 0,
1195             'count' : False,
1196             'end_type' : False,
1197             'mo' : False,
1198             'tu' : False,
1199             'we' : False,
1200             'th' : False,
1201             'fr' : False,
1202             'sa' : False,
1203             'su' : False,
1204             'exrule' : False,
1205             'day' : False,
1206             'week_list' : False
1207         }
1208
1209     #def _write_rrule(self, cr, uid, ids, field_value, rule_date=False, context=None):
1210     #    data = self._get_empty_rrule_data()
1211     #
1212     #    if field_value:
1213     #        data['recurrency'] = True
1214     #        for event in self.browse(cr, uid, ids, context=context):
1215     #            rdate = rule_date or event.date
1216     #            update_data = self._parse_rrule(field_value, dict(data), rdate)
1217     #            data.update(update_data)
1218     #            #parse_rrule
1219     #            self.write(cr, uid, event.id, data, context=context)
1220
1221
1222     def _parse_rrule(self, rule, data, date_start):
1223         day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1224         rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1225         r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1226
1227         if r._freq > 0 and r._freq < 4:
1228             data['rrule_type'] = rrule_type[r._freq]
1229
1230         data['count'] = r._count
1231         data['interval'] = r._interval
1232         data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1233         #repeat weekly
1234         if r._byweekday:
1235             for i in xrange(0,7):
1236                 if i in r._byweekday:
1237                     data[day_list[i]] = True
1238             data['rrule_type'] = 'weekly'
1239         #repeat monthly bynweekday ((weekday, weeknumber), )
1240         if r._bynweekday:
1241             data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1242             data['byday'] = r._bynweekday[0][1]
1243             data['select1'] = 'day'
1244             data['rrule_type'] = 'monthly'
1245
1246         if r._bymonthday:
1247             data['day'] = r._bymonthday[0]
1248             data['select1'] = 'date'
1249             data['rrule_type'] = 'monthly'
1250
1251         #yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1252         if r._bymonth:
1253             data['interval'] = data['interval'] * 12
1254
1255         #FIXEME handle forever case
1256         #end of recurrence
1257         #in case of repeat for ever that we do not support right now
1258         if not (data.get('count') or data.get('end_date')):
1259             data['count'] = 100
1260         if data.get('count'):
1261             data['end_type'] = 'count'
1262         else:
1263             data['end_type'] = 'end_date'
1264         return data
1265
1266     def remove_virtual_id(self, ids):
1267         if isinstance(ids, (str, int, long)):
1268             return base_calendar_id2real_id(ids)
1269
1270         if isinstance(ids, (list, tuple)):
1271             res = []
1272             for id in ids:
1273                 res.append(base_calendar_id2real_id(id))
1274             return res
1275
1276     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1277         context = context or {}
1278         args_without_date = []
1279         filter_date = []
1280
1281         for arg in args:
1282             if arg[0] == "id":
1283                 new_id = self.remove_virtual_id(arg[2])
1284                 new_arg = (arg[0], arg[1], new_id)
1285                 args_without_date.append(new_arg)
1286             elif arg[0] not in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1287                 args_without_date.append(arg)
1288             else:
1289                 if context.get('virtual_id', True):
1290                     args_without_date.append('|')
1291                 args_without_date.append(arg)
1292                 if context.get('virtual_id', True):
1293                     args_without_date.append(('recurrency','=',1))
1294                 filter_date.append(arg)
1295
1296         res = super(calendar_event, self).search(cr, uid, args_without_date, \
1297                                  0, 0, order, context, count=False)
1298         if context.get('virtual_id', True):
1299             res = self.get_recurrent_ids(cr, uid, res, args, limit, context=context)
1300
1301         if count:
1302             return len(res)
1303         elif limit:
1304             return res[offset:offset+limit]
1305         else:
1306             return res
1307
1308     def _get_data(self, cr, uid, id, context=None):
1309         res = self.read(cr, uid, [id],['date', 'date_deadline'])
1310         return res[0]
1311
1312     def need_to_update(self, event_id, vals):
1313         split_id = str(event_id).split("-")
1314         if len(split_id) < 2:
1315             return False
1316         else:
1317             date_start = vals.get('date', '')
1318             try:
1319                 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1320                 return date_start == split_id[1]
1321             except Exception:
1322                 return True
1323
1324
1325     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1326         context = context or {}
1327         if isinstance(ids, (str, int, long)):
1328             ids = [ids]
1329         res = False
1330
1331         # Special write of complex IDS
1332         for event_id in ids[:]:
1333             if len(str(event_id).split('-')) == 1:
1334                 continue
1335             ids.remove(event_id)
1336             real_event_id = base_calendar_id2real_id(event_id)
1337             if not vals.get('recurrency', True):
1338                 ids.append(real_event_id)
1339                 continue
1340
1341             #if edit one instance of a reccurrent id
1342             data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1343                                                 'rrule', 'duration', 'exdate'])
1344             if data.get('rrule'):
1345                 data.update(vals)
1346                 data.update({
1347                     'recurrent_uid': real_event_id,
1348                     'recurrent_id': data.get('date'),
1349                     'rrule_type': 'none',
1350                     'rrule': '',
1351                     'recurrency' : False,
1352                     })
1353
1354                 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1355
1356                 date_new = event_id.split('-')[1]
1357                 date_new = time.strftime("%Y%m%dT%H%M%S", \
1358                              time.strptime(date_new, "%Y%m%d%H%M%S"))
1359                 exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1360                 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1361
1362                 context.update({'active_id': new_id, 'active_ids': [new_id]})
1363                 continue
1364
1365         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1366             vals['vtimezone'] = vals['vtimezone'][40:]
1367
1368         res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1369
1370         if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1371                 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1372             alarm_obj = self.pool.get('res.alarm')
1373             alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1374         return res or True and False
1375
1376     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1377         if not context:
1378             context = {}
1379
1380         if 'date' in groupby:
1381             raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1382         virtual_id = context.get('virtual_id', True)
1383         context.update({'virtual_id': False})
1384         res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1385         for re in res:
1386             #remove the count, since the value is not consistent with the result of the search when expand the group
1387             for groupname in groupby:
1388                 if re.get(groupname + "_count"):
1389                     del re[groupname + "_count"]
1390             re.get('__context', {}).update({'virtual_id' : virtual_id})
1391         return res
1392
1393     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1394         # FIXME This whole id mangling has to go!
1395         if context is None:
1396             context = {}
1397         fields2 = fields and fields[:] or None
1398
1399         EXTRAFIELDS = ('class','user_id','duration')
1400         for f in EXTRAFIELDS:
1401             if fields and (f not in fields):
1402                 fields2.append(f)
1403
1404         if isinstance(ids, (str, int, long)):
1405             select = [ids]
1406         else:
1407             select = ids
1408         select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1409         result = []
1410
1411         real_data = super(calendar_event, self).read(cr, uid,
1412                     [real_id for base_calendar_id, real_id in select],
1413                     fields=fields2, context=context, load=load)
1414         real_data = dict(zip([x['id'] for x in real_data], real_data))
1415
1416         for base_calendar_id, real_id in select:
1417             res = real_data[real_id].copy()
1418             ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1419             if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1420                 res['date'] = ls[1]
1421                 res['date_deadline'] = ls[2]
1422             res['id'] = base_calendar_id
1423
1424             result.append(res)
1425
1426         for r in result:
1427             if r['user_id']:
1428                 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1429                 if user_id==uid:
1430                     continue
1431             if r['class']=='private':
1432                 for f in r.keys():
1433                     if f not in ('id','date','date_deadline','duration','user_id','state'):
1434                         if isinstance(r[f], list):
1435                             r[f] = []
1436                         else:    
1437                             r[f] = False
1438                     if f=='name':
1439                         r[f] = _('Busy')
1440
1441         for r in result:
1442             for k in EXTRAFIELDS:
1443                 if (k in r) and ((not fields) or (k not in fields)):
1444                     del r[k]
1445         if isinstance(ids, (str, int, long)):
1446             return result and result[0] or False
1447         return result
1448
1449     def copy(self, cr, uid, id, default=None, context=None):
1450         if context is None:
1451             context = {}
1452
1453         res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1454         alarm_obj = self.pool.get('res.alarm')
1455         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1456         return res
1457
1458     def unlink(self, cr, uid, ids, context=None):
1459         if not isinstance(ids, list):
1460             ids = [ids]
1461         res = False
1462         attendee_obj=self.pool.get('calendar.attendee')
1463         for event_id in ids[:]:
1464             if len(str(event_id).split('-')) == 1:
1465                 continue
1466
1467             real_event_id = base_calendar_id2real_id(event_id)
1468             data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1469             date_new = event_id.split('-')[1]
1470             date_new = time.strftime("%Y%m%dT%H%M%S", \
1471                          time.strptime(date_new, "%Y%m%d%H%M%S"))
1472             exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1473             self.write(cr, uid, [real_event_id], {'exdate': exdate})
1474             ids.remove(event_id)
1475         for event in self.browse(cr, uid, ids, context=context):
1476             if event.attendee_ids:
1477                 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1478
1479         res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1480         self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1481         self.unlink_events(cr, uid, ids, context=context)
1482         return res
1483
1484
1485     def create(self, cr, uid, vals, context=None):
1486         if context is None:
1487             context = {}
1488
1489         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1490             vals['vtimezone'] = vals['vtimezone'][40:]
1491
1492         #updated_vals = self.onchange_dates(cr, uid, [],
1493         #    vals.get('date', False),
1494         #    vals.get('duration', False),
1495         #    vals.get('date_deadline', False),
1496         #    vals.get('allday', False),
1497         #    context=context)
1498         #vals.update(updated_vals.get('value', {}))
1499
1500         res = super(calendar_event, self).create(cr, uid, vals, context)
1501         alarm_obj = self.pool.get('res.alarm')
1502         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1503         return res
1504
1505     def do_tentative(self, cr, uid, ids, context=None, *args):
1506         """ Makes event invitation as Tentative
1507         @param self: The object pointer
1508         @param cr: the current row, from the database cursor,
1509         @param uid: the current user’s ID for security checks,
1510         @param ids: List of Event IDs
1511         @param *args: Get Tupple value
1512         @param context: A standard dictionary for contextual values
1513         """
1514         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1515
1516     def do_cancel(self, cr, uid, ids, context=None, *args):
1517         """ Makes event invitation as Tentative
1518         @param self: The object pointer
1519         @param cr: the current row, from the database cursor,
1520         @param uid: the current user’s ID for security checks,
1521         @param ids: List of Event IDs
1522         @param *args: Get Tupple value
1523         @param context: A standard dictionary for contextual values
1524         """
1525         return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1526
1527     def do_confirm(self, cr, uid, ids, context=None, *args):
1528         """ Makes event invitation as Tentative
1529         @param self: The object pointer
1530         @param cr: the current row, from the database cursor,
1531         @param uid: the current user’s ID for security checks,
1532         @param ids: List of Event IDs
1533         @param *args: Get Tupple value
1534         @param context: A standard dictionary for contextual values
1535         """
1536         return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1537
1538 calendar_event()
1539
1540 class calendar_todo(osv.osv):
1541     """ Calendar Task """
1542
1543     _name = "calendar.todo"
1544     _inherit = "calendar.event"
1545     _description = "Calendar Task"
1546
1547     def _get_date(self, cr, uid, ids, name, arg, context=None):
1548         """
1549         Get Date
1550         @param self: The object pointer
1551         @param cr: the current row, from the database cursor,
1552         @param uid: the current user’s ID for security checks,
1553         @param ids: List of calendar todo's IDs.
1554         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1555         @param context: A standard dictionary for contextual values
1556         """
1557
1558         res = {}
1559         for event in self.browse(cr, uid, ids, context=context):
1560             res[event.id] = event.date_start
1561         return res
1562
1563     def _set_date(self, cr, uid, id, name, value, arg, context=None):
1564         """
1565         Set Date
1566         @param self: The object pointer
1567         @param cr: the current row, from the database cursor,
1568         @param uid: the current user’s ID for security checks,
1569         @param id: calendar's ID.
1570         @param value: Get Value
1571         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1572         @param context: A standard dictionary for contextual values
1573         """
1574
1575         assert name == 'date'
1576         return self.write(cr, uid, id, { 'date_start': value }, context=context)
1577
1578     _columns = {
1579         'date': fields.function(_get_date, fnct_inv=_set_date, \
1580                             string='Duration', store=True, type='datetime'),
1581         'duration': fields.integer('Duration'),
1582     }
1583
1584     __attribute__ = {}
1585
1586
1587 calendar_todo()
1588
1589
1590 class ir_values(osv.osv):
1591     _inherit = 'ir.values'
1592
1593     def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1594             isobject=False, meta=False, preserve_user=False, company=False):
1595         """
1596         Set IR Values
1597         @param self: The object pointer
1598         @param cr: the current row, from the database cursor,
1599         @param uid: the current user’s ID for security checks,
1600         @param model: Get The Model
1601         """
1602
1603         new_model = []
1604         for data in models:
1605             if type(data) in (list, tuple):
1606                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1607             else:
1608                 new_model.append(data)
1609         return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1610                     value, replace, isobject, meta, preserve_user, company)
1611
1612     def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1613              res_id_req=False, without_user=True, key2_req=True):
1614         """
1615         Get IR Values
1616         @param self: The object pointer
1617         @param cr: the current row, from the database cursor,
1618         @param uid: the current user’s ID for security checks,
1619         @param model: Get The Model
1620         """
1621         if context is None:
1622             context = {}
1623         new_model = []
1624         for data in models:
1625             if type(data) in (list, tuple):
1626                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1627             else:
1628                 new_model.append(data)
1629         return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1630                          meta, context, res_id_req, without_user, key2_req)
1631
1632 ir_values()
1633
1634 class ir_model(osv.osv):
1635
1636     _inherit = 'ir.model'
1637
1638     def read(self, cr, uid, ids, fields=None, context=None,
1639             load='_classic_read'):
1640         """
1641         Overrides orm read method.
1642         @param self: The object pointer
1643         @param cr: the current row, from the database cursor,
1644         @param uid: the current user’s ID for security checks,
1645         @param ids: List of IR Model’s IDs.
1646         @param context: A standard dictionary for contextual values
1647         """
1648         new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1649         if context is None:
1650             context = {}
1651         data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1652                         context=context, load=load)
1653         if data:
1654             for val in data:
1655                 val['id'] = base_calendar_id2real_id(val['id'])
1656         return isinstance(ids, (str, int, long)) and data[0] or data
1657
1658 ir_model()
1659
1660 class virtual_report_spool(web_services.report_spool):
1661
1662     def exp_report(self, db, uid, object, ids, datas=None, context=None):
1663         """
1664         Export Report
1665         @param self: The object pointer
1666         @param db: get the current database,
1667         @param uid: the current user’s ID for security checks,
1668         @param context: A standard dictionary for contextual values
1669         """
1670
1671         if object == 'printscreen.list':
1672             return super(virtual_report_spool, self).exp_report(db, uid, \
1673                             object, ids, datas, context)
1674         new_ids = []
1675         for id in ids:
1676             new_ids.append(base_calendar_id2real_id(id))
1677         if datas.get('id', False):
1678             datas['id'] = base_calendar_id2real_id(datas['id'])
1679         return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, datas, context)
1680
1681 virtual_report_spool()
1682
1683 class res_users(osv.osv):
1684     _inherit = 'res.users'
1685
1686     def _get_user_avail(self, cr, uid, ids, context=None):
1687         """
1688         Get User Availability
1689         @param self: The object pointer
1690         @param cr: the current row, from the database cursor,
1691         @param uid: the current user’s ID for security checks,
1692         @param ids: List of res user’s IDs.
1693         @param context: A standard dictionary for contextual values
1694         """
1695
1696         current_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1697         res = {}
1698         attendee_obj = self.pool.get('calendar.attendee')
1699         attendee_ids = attendee_obj.search(cr, uid, [
1700                     ('event_date', '<=', current_datetime), ('event_end_date', '<=', current_datetime),
1701                     ('state', '=', 'accepted'), ('user_id', 'in', ids)
1702                     ])
1703
1704         for attendee_data in attendee_obj.read(cr, uid, attendee_ids, ['user_id']):
1705             user_id = attendee_data['user_id']
1706             status = 'busy'
1707             res.update({user_id:status})
1708
1709         #TOCHECK: Delegated Event
1710         for user_id in ids:
1711             if user_id not in res:
1712                 res[user_id] = 'free'
1713
1714         return res
1715
1716     def _get_user_avail_fun(self, cr, uid, ids, name, args, context=None):
1717         """
1718         Get User Availability Function
1719         @param self: The object pointer
1720         @param cr: the current row, from the database cursor,
1721         @param uid: the current user’s ID for security checks,
1722         @param ids: List of res user’s IDs.
1723         @param context: A standard dictionary for contextual values
1724         """
1725
1726         return self._get_user_avail(cr, uid, ids, context=context)
1727
1728     _columns = {
1729             'availability': fields.function(_get_user_avail_fun, type='selection', \
1730                     selection=[('free', 'Free'), ('busy', 'Busy')], \
1731                     string='Free/Busy'),
1732     }
1733
1734 res_users()
1735
1736
1737 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: