Launchpad automatic translations update.
[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.user_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, ['context_lang'], context=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([('tentative', 'Tentative'),
337                         ('needs-action', 'Needs Action'),
338                         ('accepted', 'Accepted'),
339                         ('declined', 'Declined'),
340                         ('delegated', 'Delegated')], 'State', 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 !'),_("Couldn't Invite because date is not specified!"))
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.user_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 cr: the current row, from the database cursor,
475         @param uid: the current user’s ID for security checks,
476         @param ids: List of attendee’s IDs.
477         @param email_from: Email address for user sending the mail
478         @param context: A standard dictionary for contextual values
479         @return: True
480         """
481         if context is None:
482             context = {}
483
484         company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
485         mail_message = self.pool.get('mail.message')
486         for att in self.browse(cr, uid, ids, context=context):
487             sign = att.sent_by_uid and att.sent_by_uid.signature or ''
488             sign = '<br>'.join(sign and sign.split('\n') or [])
489             res_obj = att.ref
490             if res_obj:
491                 att_infos = []
492                 sub = res_obj.name
493                 other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
494
495                 for att2 in self.browse(cr, uid, other_invitation_ids):
496                     att_infos.append(((att2.user_id and att2.user_id.name) or \
497                                  (att2.partner_id and att2.partner_id.name) or \
498                                     att2.email) + ' - Status: ' + att2.state.title())
499                 body_vals = {'name': res_obj.name,
500                             'start_date': res_obj.date,
501                             'end_date': res_obj.date_deadline or False,
502                             'description': res_obj.description or '-',
503                             'location': res_obj.location or '-',
504                             'attendees': '<br>'.join(att_infos),
505                             'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
506                             'sign': sign,
507                             'company': company
508                 }
509                 body = html_invitation % body_vals
510                 if mail_to and email_from:
511                     attach = self.get_ics_file(cr, uid, res_obj, context=context)
512                     mail_message.schedule_with_attach(cr, uid,
513                         email_from,
514                         mail_to,
515                         sub,
516                         body,
517                         attachments=attach and {'invitation.ics': attach} or None,
518                         subtype='html',
519                         reply_to=email_from,
520                         context=context
521                     )
522             return True
523
524     def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
525         """
526         Make entry on email and availbility on change of user_id field.
527         @param cr: the current row, from the database cursor,
528         @param uid: the current user’s ID for security checks,
529         @param ids: List of calendar attendee’s IDs.
530         @param user_id: Changed value of User id
531         @return: dictionary of value. which put value in email and availability fields.
532         """
533
534         if not user_id:
535             return {'value': {'email': ''}}
536         usr_obj = self.pool.get('res.users')
537         user = usr_obj.browse(cr, uid, user_id, *args)
538         return {'value': {'email': user.user_email, 'availability':user.availability}}
539
540     def do_tentative(self, cr, uid, ids, context=None, *args):
541         """ Makes event invitation as Tentative
542         @param self: The object pointer
543         @param cr: the current row, from the database cursor,
544         @param uid: the current user’s ID for security checks,
545         @param ids: List of calendar attendee’s IDs
546         @param *args: Get Tupple value
547         @param context: A standard dictionary for contextual values
548         """
549         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
550
551     def do_accept(self, cr, uid, ids, context=None, *args):
552         """
553         Update state of invitation as Accepted and
554         if the invited user is other then event user it will make a copy of this event for invited user
555         @param cr: the current row, from the database cursor,
556         @param uid: the current user’s ID for security checks,
557         @param ids: List of calendar attendee’s IDs.
558         @param context: A standard dictionary for contextual values
559         @return: True
560         """
561         if context is None:
562             context = {}
563
564         for vals in self.browse(cr, uid, ids, context=context):
565             if vals.ref and vals.ref.user_id:
566                 mod_obj = self.pool.get(vals.ref._name)
567                 defaults = {'user_id': vals.user_id.id, 'organizer_id': vals.ref.user_id.id}
568                 mod_obj.copy(cr, uid, vals.ref.id, default=defaults, context=context)
569             self.write(cr, uid, vals.id, {'state': 'accepted'}, context)
570
571         return True
572
573     def do_decline(self, cr, uid, ids, context=None, *args):
574         """ Marks event invitation as Declined
575         @param self: The object pointer
576         @param cr: the current row, from the database cursor,
577         @param uid: the current user’s ID for security checks,
578         @param ids: List of calendar attendee’s IDs
579         @param *args: Get Tupple value
580         @param context: A standard dictionary for contextual values """
581         if context is None:
582             context = {}
583         return self.write(cr, uid, ids, {'state': 'declined'}, context)
584
585     def create(self, cr, uid, vals, context=None):
586         """ Overrides orm create method.
587         @param self: The object pointer
588         @param cr: the current row, from the database cursor,
589         @param uid: the current user’s ID for security checks,
590         @param vals: Get Values
591         @param context: A standard dictionary for contextual values """
592
593         if context is None:
594             context = {}
595         if not vals.get("email") and vals.get("cn"):
596             cnval = vals.get("cn").split(':')
597             email = filter(lambda x:x.__contains__('@'), cnval)
598             vals['email'] = email and email[0] or ''
599             vals['cn'] = vals.get("cn")
600         res = super(calendar_attendee, self).create(cr, uid, vals, context)
601         return res
602 calendar_attendee()
603
604 class res_alarm(osv.osv):
605     """Resource Alarm """
606     _name = 'res.alarm'
607     _description = 'Basic Alarm Information'
608
609     _columns = {
610         'name':fields.char('Name', size=256, required=True),
611         'trigger_occurs': fields.selection([('before', 'Before'), \
612                                             ('after', 'After')], \
613                                         'Triggers', required=True),
614         'trigger_interval': fields.selection([('minutes', 'Minutes'), \
615                                                 ('hours', 'Hours'), \
616                                                 ('days', 'Days')], 'Interval', \
617                                                 required=True),
618         'trigger_duration': fields.integer('Duration', required=True),
619         'trigger_related': fields.selection([('start', 'The event starts'), \
620                                             ('end', 'The event ends')], \
621                                             'Related to', required=True),
622         'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \
623 are both optional, but if one occurs, so MUST the other"""),
624         'repeat': fields.integer('Repeat'),
625         'active': fields.boolean('Active', help="If the active field is set to \
626 true, it will allow you to hide the event alarm information without removing it.")
627     }
628     _defaults = {
629         'trigger_interval': 'minutes',
630         'trigger_duration': 5,
631         'trigger_occurs': 'before',
632         'trigger_related': 'start',
633         'active': 1,
634     }
635
636     def do_alarm_create(self, cr, uid, ids, model, date, context=None):
637         """
638         Create Alarm for event.
639         @param cr: the current row, from the database cursor,
640         @param uid: the current user’s ID for security checks,
641         @param ids: List of res alarm’s IDs.
642         @param model: Model name.
643         @param date: Event date
644         @param context: A standard dictionary for contextual values
645         @return: True
646         """
647         if context is None:
648             context = {}
649         alarm_obj = self.pool.get('calendar.alarm')
650         res_alarm_obj = self.pool.get('res.alarm')
651         ir_obj = self.pool.get('ir.model')
652         model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
653
654         model_obj = self.pool.get(model)
655         for data in model_obj.browse(cr, uid, ids, context=context):
656
657             basic_alarm = data.alarm_id
658             cal_alarm = data.base_calendar_alarm_id
659             if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm):
660                 new_res_alarm = None
661                 # Find for existing res.alarm
662                 duration = cal_alarm.trigger_duration
663                 interval = cal_alarm.trigger_interval
664                 occurs = cal_alarm.trigger_occurs
665                 related = cal_alarm.trigger_related
666                 domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)]
667                 alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context)
668                 if not alarm_ids:
669                     val = {
670                             'trigger_duration': duration,
671                             'trigger_interval': interval,
672                             'trigger_occurs': occurs,
673                             'trigger_related': related,
674                             'name': str(duration) + ' ' + str(interval) + ' '  + str(occurs)
675                            }
676                     new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context)
677                 else:
678                     new_res_alarm = alarm_ids[0]
679                 cr.execute('UPDATE %s ' % model_obj._table + \
680                             ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \
681                             ' WHERE id=%s',
682                             (cal_alarm.id, new_res_alarm, data.id))
683
684             self.do_alarm_unlink(cr, uid, [data.id], model)
685             if basic_alarm:
686                 vals = {
687                     'action': 'display',
688                     'description': data.description,
689                     'name': data.name,
690                     'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))],
691                     'trigger_related': basic_alarm.trigger_related,
692                     'trigger_duration': basic_alarm.trigger_duration,
693                     'trigger_occurs': basic_alarm.trigger_occurs,
694                     'trigger_interval': basic_alarm.trigger_interval,
695                     'duration': basic_alarm.duration,
696                     'repeat': basic_alarm.repeat,
697                     'state': 'run',
698                     'event_date': data[date],
699                     'res_id': data.id,
700                     'model_id': model_id,
701                     'user_id': uid
702                  }
703                 alarm_id = alarm_obj.create(cr, uid, vals)
704                 cr.execute('UPDATE %s ' % model_obj._table + \
705                             ' SET base_calendar_alarm_id=%s, alarm_id=%s '
706                             ' WHERE id=%s', \
707                             ( alarm_id, basic_alarm.id, data.id) )
708         return True
709
710     def do_alarm_unlink(self, cr, uid, ids, model, context=None):
711         """
712         Delete alarm specified in ids
713         @param cr: the current row, from the database cursor,
714         @param uid: the current user’s ID for security checks,
715         @param ids: List of res alarm’s IDs.
716         @param model: Model name for which alarm is to be cleared.
717         @return: True
718         """
719         if context is None:
720             context = {}
721         alarm_obj = self.pool.get('calendar.alarm')
722         ir_obj = self.pool.get('ir.model')
723         model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
724         model_obj = self.pool.get(model)
725         for datas in model_obj.browse(cr, uid, ids, context=context):
726             alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', datas.id)])
727             if alarm_ids:
728                 alarm_obj.unlink(cr, uid, alarm_ids)
729                 cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\
730                             where id=%%s' % model_obj._table,(datas.id,))
731         return True
732
733 res_alarm()
734
735 class calendar_alarm(osv.osv):
736     _name = 'calendar.alarm'
737     _description = 'Event alarm information'
738     _inherit = 'res.alarm'
739     __attribute__ = {}
740
741     _columns = {
742         'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'),
743         'name': fields.char('Summary', size=124, help="""Contains the text to be \
744                      used as the message subject for email \
745                      or contains the text to be used for display"""),
746         'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \
747                 ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \
748                 required=True, help="Defines the action to be invoked when an alarm is triggered"),
749         'description': fields.text('Description', help='Provides a more complete \
750                             description of the calendar component, than that \
751                             provided by the "SUMMARY" property'),
752         'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \
753                                       'alarm_id', 'attendee_id', 'Attendees', readonly=True),
754         'attach': fields.binary('Attachment', help="""* Points to a sound resource,\
755                      which is rendered when the alarm is triggered for audio,
756                     * File which is intended to be sent as message attachments for email,
757                     * Points to a procedure resource, which is invoked when\
758                       the alarm is triggered for procedure."""),
759         'res_id': fields.integer('Resource ID'),
760         'model_id': fields.many2one('ir.model', 'Model'),
761         'user_id': fields.many2one('res.users', 'Owner'),
762         'event_date': fields.datetime('Event Date'),
763         'event_end_date': fields.datetime('Event End Date'),
764         'trigger_date': fields.datetime('Trigger Date', readonly="True"),
765         'state':fields.selection([
766                     ('draft', 'Draft'),
767                     ('run', 'Run'),
768                     ('stop', 'Stop'),
769                     ('done', 'Done'),
770                 ], 'State', select=True, readonly=True),
771      }
772
773     _defaults = {
774         'action': 'email',
775         'state': 'run',
776      }
777
778     def create(self, cr, uid, vals, context=None):
779         """
780         Overrides orm create method.
781         @param self: The object pointer
782         @param cr: the current row, from the database cursor,
783         @param vals: dictionary of fields value.{‘name_of_the_field’: value, ...}
784         @param context: A standard dictionary for contextual values
785         @return: new record id for calendar_alarm.
786         """
787         if context is None:
788             context = {}
789         event_date = vals.get('event_date', False)
790         if event_date:
791             dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S")
792             if vals['trigger_interval'] == 'days':
793                 delta = timedelta(days=vals['trigger_duration'])
794             if vals['trigger_interval'] == 'hours':
795                 delta = timedelta(hours=vals['trigger_duration'])
796             if vals['trigger_interval'] == 'minutes':
797                 delta = timedelta(minutes=vals['trigger_duration'])
798             trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta)
799             vals['trigger_date'] = trigger_date
800         res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
801         return res
802
803     def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
804                        context=None):
805         """Scheduler for event reminder
806         @param self: The object pointer
807         @param cr: the current row, from the database cursor,
808         @param uid: the current user’s ID for security checks,
809         @param ids: List of calendar alarm’s IDs.
810         @param use_new_cursor: False or the dbname
811         @param context: A standard dictionary for contextual values
812         """
813         if context is None:
814             context = {}
815         mail_message = self.pool.get('mail.message')
816         current_datetime = datetime.now()
817         request_obj = self.pool.get('res.request')
818         alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
819
820         mail_to = []
821
822         for alarm in self.browse(cr, uid, alarm_ids, context=context):
823             next_trigger_date = None
824             update_vals = {}
825             model_obj = self.pool.get(alarm.model_id.model)
826             res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
827             re_dates = []
828
829             if res_obj.rrule:
830                 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
831                 recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.exdate, event_date, res_obj.exrule)
832
833                 trigger_interval = alarm.trigger_interval
834                 if trigger_interval == 'days':
835                     delta = timedelta(days=alarm.trigger_duration)
836                 if trigger_interval == 'hours':
837                     delta = timedelta(hours=alarm.trigger_duration)
838                 if trigger_interval == 'minutes':
839                     delta = timedelta(minutes=alarm.trigger_duration)
840                 delta = alarm.trigger_occurs == 'after' and delta or -delta
841
842                 for rdate in recurrent_dates:
843                     if rdate + delta > current_datetime:
844                         break
845                     if rdate + delta <= current_datetime:
846                         re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
847                 rest_dates = recurrent_dates[len(re_dates):]
848                 next_trigger_date = rest_dates and rest_dates[0] or None
849
850             else:
851                 re_dates = [alarm.trigger_date]
852
853             for r_date in re_dates:
854                 ref = alarm.model_id.model + ',' + str(alarm.res_id)
855
856                 # search for alreay sent requests
857                 if request_obj.search(cr, uid, [('trigger_date', '=', r_date), ('ref_doc1', '=', ref)], context=context):
858                     continue
859
860                 if alarm.action == 'display':
861                     value = {
862                        'name': alarm.name,
863                        'act_from': alarm.user_id.id,
864                        'act_to': alarm.user_id.id,
865                        'body': alarm.description,
866                        'trigger_date': r_date,
867                        'ref_doc1': ref
868                     }
869                     request_id = request_obj.create(cr, uid, value)
870                     request_ids = [request_id]
871                     for attendee in res_obj.attendee_ids:
872                         if attendee.user_id:
873                             value['act_to'] = attendee.user_id.id
874                             request_id = request_obj.create(cr, uid, value)
875                             request_ids.append(request_id)
876                     request_obj.request_send(cr, uid, request_ids)
877
878                 if alarm.action == 'email':
879                     sub = '[Openobject Reminder] %s' % (alarm.name)
880                     body = """
881 Event: %s
882 Event Date: %s
883 Description: %s
884
885 From:
886       %s
887
888 ----
889 %s
890
891 """  % (alarm.name, alarm.trigger_date, alarm.description, \
892                         alarm.user_id.name, alarm.user_id.signature)
893                     mail_to = [alarm.user_id.user_email]
894                     for att in alarm.attendee_ids:
895                         mail_to.append(att.user_id.user_email)
896                     if mail_to:
897                         mail_message.schedule_with_attach(cr, uid,
898                             tools.config.get('email_from', False),
899                             mail_to,
900                             sub,
901                             body,
902                             context=context
903                         )
904             if next_trigger_date:
905                 update_vals.update({'trigger_date': next_trigger_date})
906             else:
907                 update_vals.update({'state': 'done'})
908             self.write(cr, uid, [alarm.id], update_vals)
909         return True
910
911 calendar_alarm()
912
913
914 class calendar_event(osv.osv):
915     _name = "calendar.event"
916     _description = "Calendar Event"
917     __attribute__ = {}
918
919     def _tz_get(self, cr, uid, context=None):
920         return [(x.lower(), x) for x in pytz.all_timezones]
921
922     def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
923         """Returns duration and/or end date based on values passed
924         @param self: The object pointer
925         @param cr: the current row, from the database cursor,
926         @param uid: the current user’s ID for security checks,
927         @param ids: List of calendar event’s IDs.
928         @param start_date: Starting date
929         @param duration: Duration between start date and end date
930         @param end_date: Ending Datee
931         @param context: A standard dictionary for contextual values
932         """
933         if context is None:
934             context = {}
935
936         value = {}
937         if not start_date:
938             return value
939         if not end_date and not duration:
940             duration = 1.00
941             value['duration'] = duration
942
943         if allday: # For all day event
944             value = {'duration': 24.0}
945             duration = 24.0
946             if start_date:
947                 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
948                 start_date = datetime.strftime(datetime(start.year, start.month, start.day, 0,0,0), "%Y-%m-%d %H:%M:%S")
949                 value['date'] = start_date
950
951
952         start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
953         if end_date and not duration:
954             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
955             diff = end - start
956             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
957             value['duration'] = round(duration, 2)
958         elif not end_date:
959             end = start + timedelta(hours=duration)
960             value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
961         elif end_date and duration and not allday:
962             # we have both, keep them synchronized:
963             # set duration based on end_date (arbitrary decision: this avoid
964             # getting dates like 06:31:48 instead of 06:32:00)
965             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
966             diff = end - start
967             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
968             value['duration'] = round(duration, 2)
969
970         return {'value': value}
971
972     def unlink_events(self, cr, uid, ids, context=None):
973         """
974         This function deletes event which are linked with the event with recurrent_uid
975                 (Removes the events which refers to the same UID value)
976         """
977         if context is None:
978             context = {}
979         for event_id in ids:
980             cr.execute("select id from %s where recurrent_uid=%%s" % (self._table), (event_id,))
981             r_ids = map(lambda x: x[0], cr.fetchall())
982             self.unlink(cr, uid, r_ids, context=context)
983         return True
984
985     def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
986         """
987         Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
988         @param self: The object pointer
989         @param cr: the current row, from the database cursor,
990         @param id: List of calendar event's ids.
991         @param context: A standard dictionary for contextual values
992         @return: dictionary of rrule value.
993         """
994         
995         result = {}
996         if not isinstance(ids, list):
997             ids = [ids]
998             
999         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):
1000             event = datas['id']
1001             if datas.get('interval', 0) < 0:
1002                 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative'))
1003             if datas.get('count', 0) < 0:
1004                 raise osv.except_osv(_('Warning!'), _('Count cannot be negative'))
1005             if datas['recurrency']:
1006                 result[event] = self.compute_rule_string(datas)
1007             else:
1008                 result[event] = ""
1009         return result
1010
1011     def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1012         data = self._get_empty_rrule_data()
1013         if field_value:
1014             data['recurrency'] = True
1015             for event in self.browse(cr, uid, ids, context=context):
1016                 rdate = rule_date or event.date
1017                 update_data = self._parse_rrule(field_value, dict(data), rdate)
1018                 data.update(update_data)
1019                 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
1020         return True
1021
1022
1023     _columns = {
1024         'id': fields.integer('ID', readonly=True),
1025         'sequence': fields.integer('Sequence'),
1026         'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1027         'date': fields.datetime('Date', states={'done': [('readonly', True)]}),
1028         'date_deadline': fields.datetime('Deadline', states={'done': [('readonly', True)]}),
1029         'create_date': fields.datetime('Created', readonly=True),
1030         'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1031         'description': fields.text('Description', states={'done': [('readonly', True)]}),
1032         'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1033              ('confidential', 'Public for Employees')], 'Mark as', states={'done': [('readonly', True)]}),
1034         'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1035         'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1036                                                 'Show as', states={'done': [('readonly', True)]}),
1037         'base_calendar_url': fields.char('Caldav URL', size=264),
1038         'state': fields.selection([('tentative', 'Tentative'),
1039                         ('confirmed', 'Confirmed'),
1040                         ('cancelled', 'Cancelled')], 'State', readonly=True),
1041         'exdate': fields.text('Exception Date/Times', help="This property \
1042 defines the list of date/time exceptions for a recurring calendar component."),
1043         'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1044 rule or repeating pattern of time to exclude from the recurring rule."),
1045         'rrule': fields.function(_get_rulestring, type='char', size=124, \
1046                     fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1047         'rrule_type': fields.selection([('none', ''), ('daily', 'Daily'), \
1048                             ('weekly', 'Weekly'), ('monthly', 'Monthly'), \
1049                             ('yearly', 'Yearly'),],
1050                             'Recurrency', states={'done': [('readonly', True)]},
1051                             help="Let the event automatically repeat at that interval"),
1052         'alarm_id': fields.many2one('res.alarm', 'Alarm', states={'done': [('readonly', True)]},
1053                         help="Set an alarm at this time, before the event occurs" ),
1054         'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1055         'recurrent_uid': fields.integer('Recurrent ID'),
1056         'recurrent_id': fields.datetime('Recurrent ID date'),
1057         'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1058         'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1059         'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with Organizer Attribure of VEvent.
1060         'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1061         'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence termination'),
1062         'interval': fields.integer('Repeat every', help="Repeat every (Days/Week/Month/Year)"),
1063         'count': fields.integer('Repeat', help="Repeat x times"),
1064         'mo': fields.boolean('Mon'),
1065         'tu': fields.boolean('Tue'),
1066         'we': fields.boolean('Wed'),
1067         'th': fields.boolean('Thu'),
1068         'fr': fields.boolean('Fri'),
1069         'sa': fields.boolean('Sat'),
1070         'su': fields.boolean('Sun'),
1071         'select1': fields.selection([('date', 'Date of month'),
1072                                     ('day', 'Day of month')], 'Option'),
1073         'day': fields.integer('Date of month'),
1074         'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), \
1075                                    ('WE', 'Wednesday'), ('TH', 'Thursday'), \
1076                                    ('FR', 'Friday'), ('SA', 'Saturday'), \
1077                                    ('SU', 'Sunday')], 'Weekday'),
1078         'byday': fields.selection([('1', 'First'), ('2', 'Second'), \
1079                                    ('3', 'Third'), ('4', 'Fourth'), \
1080                                    ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
1081         'month_list': fields.selection(months.items(), 'Month'),
1082         'end_date': fields.date('Repeat Until'),
1083         'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1084                                  'event_id', 'attendee_id', 'Attendees'),
1085         'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1086         'active': fields.boolean('Active', help="If the active field is set to \
1087          true, it will allow you to hide the event alarm information without removing it."),
1088         'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1089     }
1090     
1091     def default_organizer(self, cr, uid, context=None):
1092         user_pool = self.pool.get('res.users')
1093         user = user_pool.browse(cr, uid, uid, context=context)
1094         res = user.name
1095         if user.user_email:
1096             res += " <%s>" %(user.user_email)
1097         return res
1098
1099     _defaults = {
1100             'end_type' : 'count',
1101             'count' : 1,
1102             'rrule_type' : 'none',
1103             'state': 'tentative',
1104             'class': 'public',
1105             'show_as': 'busy',
1106             'select1': 'date',
1107             'interval': 1,
1108             'active': 1,
1109             'user_id': lambda self, cr, uid, ctx: uid,
1110             'organizer': default_organizer,
1111     }
1112
1113     def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1114         """Gives virtual event ids for recurring events based on value of Recurrence Rule
1115         This method gives ids of dates that comes between start date and end date of calendar views
1116         @param self: The object pointer
1117         @param cr: the current row, from the database cursor,
1118         @param uid: the current user’s ID for security checks,
1119         @param limit: The Number of Results to Return """
1120         if not context:
1121             context = {}
1122
1123         result = []
1124         for data in super(calendar_event, self).read(cr, uid, select, context=context):
1125             if not data['rrule']:
1126                 result.append(data['id'])
1127                 continue
1128             event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1129 #                To check: If the start date is replace by event date .. the event date will be changed by that of calendar code
1130
1131             if not data['rrule']:
1132                 continue
1133
1134             exdate = data['exdate'] and data['exdate'].split(',') or []
1135             rrule_str = data['rrule']
1136             new_rrule_str = []
1137             rrule_until_date = False
1138             is_until = False
1139             for rule in rrule_str.split(';'):
1140                 name, value = rule.split('=')
1141                 if name == "UNTIL":
1142                     is_until = True
1143                     value = parser.parse(value)
1144                     rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1145                     value = value.strftime("%Y%m%d%H%M%S")
1146                 new_rule = '%s=%s' % (name, value)
1147                 new_rrule_str.append(new_rule)
1148             new_rrule_str = ';'.join(new_rrule_str)
1149             rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1150             for r_date in rdates:
1151                 ok = True
1152                 for arg in domain:
1153                     if arg[0] in ('date', 'date_deadline'):
1154                         if (arg[1]=='='):
1155                             ok = ok and r_date.strftime('%Y-%m-%d')==arg[2]
1156                         if (arg[1]=='>'):
1157                             ok = ok and r_date.strftime('%Y-%m-%d')>arg[2]
1158                         if (arg[1]=='<'):
1159                             ok = ok and r_date.strftime('%Y-%m-%d')<arg[2]
1160                         if (arg[1]=='>='):
1161                             ok = ok and r_date.strftime('%Y-%m-%d')>=arg[2]
1162                         if (arg[1]=='<='):
1163                             ok = ok and r_date.strftime('%Y-%m-%d')<=arg[2]
1164                 if not ok:
1165                     continue
1166                 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1167                 result.append(idval)
1168
1169         if isinstance(select, (str, int, long)):
1170             return ids and ids[0] or False
1171         else:
1172             ids = list(set(result))
1173         return ids
1174
1175     def compute_rule_string(self, datas):
1176         """
1177         Compute rule string according to value type RECUR of iCalendar from the values given.
1178         @param self: the object pointer
1179         @param datas: dictionary of freq and interval value.
1180         """
1181         def get_week_string(freq, datas):
1182             weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1183             if freq == 'weekly':
1184                 byday = map(lambda x: x.upper(), filter(lambda x: datas.get(x) and x in weekdays, datas))
1185                 if byday:
1186                     return ';BYDAY=' + ','.join(byday)
1187             return ''  
1188         
1189         def get_month_string(freq, datas):
1190             if freq == 'monthly':
1191                 if datas.get('select1')=='date' and (datas.get('day') < 1 or datas.get('day') > 31):
1192                     raise osv.except_osv(_('Error!'), ("Please select proper Day of month"))
1193                 
1194                 if datas.get('select1')=='day':
1195                     return ';BYDAY=' + datas.get('byday') + datas.get('week_list')
1196                 elif datas.get('select1')=='date':
1197                     return ';BYMONTHDAY=' + str(datas.get('day'))
1198             return ''
1199         
1200         def get_end_date(datas):
1201             if datas.get('end_date'):
1202                 datas['end_date_new'] = ''.join((re.compile('\d')).findall(datas.get('end_date'))) + 'T235959Z'
1203             
1204             return (datas.get('end_type') == 'count' and (';COUNT=' + str(datas.get('count'))) or '') +\
1205                              ((datas.get('end_date_new') and datas.get('end_type') == 'end_date' and (';UNTIL=' + datas.get('end_date_new'))) or '')
1206
1207         freq=datas.get('rrule_type')
1208         if freq == 'none':
1209             return ''
1210
1211         interval_srting = datas.get('interval') and (';INTERVAL=' + str(datas.get('interval'))) or ''
1212
1213         return 'FREQ=' + freq.upper() + get_week_string(freq, datas) + interval_srting + get_end_date(datas) + get_month_string(freq, datas)
1214     
1215     def _get_empty_rrule_data(self):
1216         return  {
1217             'byday' : False,
1218             'recurrency' : False,
1219             'end_date' : False, 
1220             'rrule_type' : False, 
1221             'select1' : False, 
1222             'interval' : 0, 
1223             'count' : False, 
1224             'end_type' : False, 
1225             'mo' : False, 
1226             'tu' : False, 
1227             'we' : False, 
1228             'th' : False, 
1229             'fr' : False, 
1230             'sa' : False, 
1231             'su' : False, 
1232             'exrule' : False, 
1233             'day' : False, 
1234             'week_list' : False
1235         }
1236
1237     #def _write_rrule(self, cr, uid, ids, field_value, rule_date=False, context=None):
1238     #    data = self._get_empty_rrule_data()
1239     #    
1240     #    if field_value:
1241     #        data['recurrency'] = True
1242     #        for event in self.browse(cr, uid, ids, context=context):
1243     #            rdate = rule_date or event.date
1244     #            update_data = self._parse_rrule(field_value, dict(data), rdate)
1245     #            data.update(update_data)
1246     #            #parse_rrule
1247     #            self.write(cr, uid, event.id, data, context=context)
1248         
1249
1250     def _parse_rrule(self, rule, data, date_start):
1251         day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1252         rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1253         r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1254         
1255         if r._freq > 0 and r._freq < 4:
1256             data['rrule_type'] = rrule_type[r._freq]
1257             
1258         data['count'] = r._count
1259         data['interval'] = r._interval
1260         data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1261         #repeat weekly
1262         if r._byweekday:
1263             for i in xrange(0,7):
1264                 if i in r._byweekday:
1265                     data[day_list[i]] = True
1266             data['rrule_type'] = 'weekly'
1267         #repeat monthly bynweekday ((weekday, weeknumber), )
1268         if r._bynweekday: 
1269             data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1270             data['byday'] = r._bynweekday[0][1]
1271             data['select1'] = 'day'
1272             data['rrule_type'] = 'monthly' 
1273            
1274         if r._bymonthday:
1275             data['day'] = r._bymonthday[0]
1276             data['select1'] = 'date'
1277             data['rrule_type'] = 'monthly'
1278             
1279         #yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1280         if r._bymonth:
1281             data['interval'] = data['interval'] * 12
1282             
1283         #FIXEME handle forever case
1284         #end of recurrence    
1285         #in case of repeat for ever that we do not support right now
1286         if not (data.get('count') or data.get('end_date')):
1287             data['count'] = 100
1288         if data.get('count'):
1289             data['end_type'] = 'count'
1290         else:
1291             data['end_type'] = 'end_date'  
1292         return data  
1293
1294     def remove_virtual_id(self, ids):
1295         if isinstance(ids, (str, int, long)):
1296             return base_calendar_id2real_id(ids)
1297             
1298         if isinstance(ids, (list, tuple)):
1299             res = []
1300             for id in ids:
1301                 res.append(base_calendar_id2real_id(id))
1302             return res
1303
1304     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1305         context = context or {}
1306         args_without_date = []
1307         filter_date = []
1308
1309         for arg in args:
1310             if arg[0] == "id":
1311                 new_id = self.remove_virtual_id(arg[2])
1312                 new_arg = (arg[0], arg[1], new_id)
1313                 args_without_date.append(new_arg)
1314             elif arg[0] not in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1315                 args_without_date.append(arg)
1316             else:
1317                 if context.get('virtual_id', True):
1318                     args_without_date.append('|')
1319                 args_without_date.append(arg)
1320                 if context.get('virtual_id', True):
1321                     args_without_date.append(('recurrency','=',1))
1322                 filter_date.append(arg)
1323
1324         res = super(calendar_event, self).search(cr, uid, args_without_date, \
1325                                  0, 0, order, context, count=False)
1326         if context.get('virtual_id', True):
1327             res = self.get_recurrent_ids(cr, uid, res, args, limit, context=context)
1328
1329         if count:
1330             return len(res)
1331         elif limit:
1332             return res[offset:offset+limit]
1333         else:
1334             return res
1335
1336     def _get_data(self, cr, uid, id, context=None):
1337         res = self.read(cr, uid, [id],['date', 'date_deadline'])
1338         return res[0]
1339
1340     def need_to_update(self, event_id, vals):
1341         split_id = str(event_id).split("-")
1342         if len(split_id) < 2:
1343             return False 
1344         else:
1345             date_start = vals.get('date', '')
1346             try:
1347                 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1348                 return date_start == split_id[1]
1349             except Exception:
1350                 return True
1351
1352
1353     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1354         context = context or {}
1355         if isinstance(ids, (str, int, long)):
1356             ids = [ids]
1357         res = False
1358
1359         # Special write of complex IDS
1360         for event_id in ids[:]:
1361             if len(str(event_id).split('-')) == 1:
1362                 continue
1363             ids.remove(event_id)
1364             real_event_id = base_calendar_id2real_id(event_id)
1365             if not vals.get('recurrency', True):
1366                 ids.append(real_event_id)
1367                 continue
1368
1369             #if edit one instance of a reccurrent id
1370             data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1371                                                 'rrule', 'duration', 'exdate'])
1372             if data.get('rrule'):
1373                 data.update(vals)
1374                 data.update({
1375                     'recurrent_uid': real_event_id,
1376                     'recurrent_id': data.get('date'),
1377                     'rrule_type': 'none',
1378                     'rrule': '',
1379                     'recurrency' : False,
1380                     })
1381
1382                 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1383
1384                 date_new = event_id.split('-')[1]
1385                 date_new = time.strftime("%Y%m%dT%H%M%S", \
1386                              time.strptime(date_new, "%Y%m%d%H%M%S"))
1387                 exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1388                 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1389
1390                 context.update({'active_id': new_id, 'active_ids': [new_id]})
1391                 continue
1392
1393         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1394             vals['vtimezone'] = vals['vtimezone'][40:]
1395
1396         res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1397
1398         if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1399                 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1400             alarm_obj = self.pool.get('res.alarm')
1401             alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1402         return res or True and False
1403
1404     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1405         if not context:
1406             context = {}
1407             
1408         if 'date' in groupby:
1409             raise osv.except_osv(_('Warning !'), _('Group by date not supported, use the calendar view instead'))
1410         virtual_id = context.get('virtual_id', True)
1411         context.update({'virtual_id': False})
1412         res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1413         for re in res:
1414             #remove the count, since the value is not consistent with the result of the search when expand the group
1415             for groupname in groupby:
1416                 if re.get(groupname + "_count"):
1417                     del re[groupname + "_count"]
1418             re.get('__context', {}).update({'virtual_id' : virtual_id})
1419         return res
1420
1421     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1422         # FIXME This whole id mangling has to go!
1423         if context is None:
1424             context = {}
1425         fields2 = fields and fields[:] or None
1426
1427         EXTRAFIELDS = ('class','user_id','duration')
1428         for f in EXTRAFIELDS:
1429             if fields and (f not in fields):
1430                 fields2.append(f)
1431
1432         if isinstance(ids, (str, int, long)):
1433             select = [ids]
1434         else:
1435             select = ids
1436         select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1437         result = []
1438
1439         real_data = super(calendar_event, self).read(cr, uid,
1440                     [real_id for base_calendar_id, real_id in select],
1441                     fields=fields2, context=context, load=load)
1442         real_data = dict(zip([x['id'] for x in real_data], real_data))
1443
1444         for base_calendar_id, real_id in select:
1445             res = real_data[real_id].copy()
1446             ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1447             if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1448                 res['date'] = ls[1]
1449                 res['date_deadline'] = ls[2]
1450             res['id'] = base_calendar_id
1451
1452             result.append(res)
1453
1454         for r in result:
1455             if r['user_id']:
1456                 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1457                 if user_id==uid:
1458                     continue
1459             if r['class']=='private':
1460                 for f in r.keys():
1461                     if f not in ('id','date','date_deadline','duration','user_id','state'):
1462                         r[f] = False
1463                     if f=='name':
1464                         r[f] = _('Busy')
1465
1466         for r in result:
1467             for k in EXTRAFIELDS:
1468                 if (k in r) and ((not fields) or (k not in fields)):
1469                     del r[k]
1470         if isinstance(ids, (str, int, long)):
1471             return result and result[0] or False
1472         return result
1473
1474     def copy(self, cr, uid, id, default=None, context=None):
1475         if context is None:
1476             context = {}
1477             
1478         res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1479         alarm_obj = self.pool.get('res.alarm')
1480         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1481         return res
1482
1483     def unlink(self, cr, uid, ids, context=None):
1484         if not isinstance(ids, list):
1485             ids = [ids]
1486         res = False
1487         attendee_obj=self.pool.get('calendar.attendee')
1488         for event_id in ids[:]:
1489             if len(str(event_id).split('-')) == 1:
1490                 continue
1491
1492             real_event_id = base_calendar_id2real_id(event_id)
1493             data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1494             date_new = event_id.split('-')[1]
1495             date_new = time.strftime("%Y%m%dT%H%M%S", \
1496                          time.strptime(date_new, "%Y%m%d%H%M%S"))
1497             exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1498             self.write(cr, uid, [real_event_id], {'exdate': exdate})
1499             ids.remove(event_id)
1500         for event in self.browse(cr, uid, ids, context=context):
1501             if event.attendee_ids:
1502                 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1503
1504         res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1505         self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1506         self.unlink_events(cr, uid, ids, context=context)
1507         return res
1508
1509
1510     def create(self, cr, uid, vals, context=None):
1511         if context is None:
1512             context = {}
1513
1514         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1515             vals['vtimezone'] = vals['vtimezone'][40:]
1516
1517         #updated_vals = self.onchange_dates(cr, uid, [],
1518         #    vals.get('date', False),
1519         #    vals.get('duration', False),
1520         #    vals.get('date_deadline', False),
1521         #    vals.get('allday', False),
1522         #    context=context)
1523         #vals.update(updated_vals.get('value', {}))
1524
1525         res = super(calendar_event, self).create(cr, uid, vals, context)
1526         alarm_obj = self.pool.get('res.alarm')
1527         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1528         return res
1529
1530     def do_tentative(self, cr, uid, ids, context=None, *args):
1531         """ Makes event invitation as Tentative
1532         @param self: The object pointer
1533         @param cr: the current row, from the database cursor,
1534         @param uid: the current user’s ID for security checks,
1535         @param ids: List of Event IDs
1536         @param *args: Get Tupple value
1537         @param context: A standard dictionary for contextual values
1538         """
1539         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1540
1541     def do_cancel(self, cr, uid, ids, context=None, *args):
1542         """ Makes event invitation as Tentative
1543         @param self: The object pointer
1544         @param cr: the current row, from the database cursor,
1545         @param uid: the current user’s ID for security checks,
1546         @param ids: List of Event IDs
1547         @param *args: Get Tupple value
1548         @param context: A standard dictionary for contextual values
1549         """
1550         return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1551
1552     def do_confirm(self, cr, uid, ids, context=None, *args):
1553         """ Makes event invitation as Tentative
1554         @param self: The object pointer
1555         @param cr: the current row, from the database cursor,
1556         @param uid: the current user’s ID for security checks,
1557         @param ids: List of Event IDs
1558         @param *args: Get Tupple value
1559         @param context: A standard dictionary for contextual values
1560         """
1561         return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1562
1563 calendar_event()
1564
1565 class calendar_todo(osv.osv):
1566     """ Calendar Task """
1567
1568     _name = "calendar.todo"
1569     _inherit = "calendar.event"
1570     _description = "Calendar Task"
1571
1572     def _get_date(self, cr, uid, ids, name, arg, context=None):
1573         """
1574         Get Date
1575         @param self: The object pointer
1576         @param cr: the current row, from the database cursor,
1577         @param uid: the current user’s ID for security checks,
1578         @param ids: List of calendar todo's IDs.
1579         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1580         @param context: A standard dictionary for contextual values
1581         """
1582
1583         res = {}
1584         for event in self.browse(cr, uid, ids, context=context):
1585             res[event.id] = event.date_start
1586         return res
1587
1588     def _set_date(self, cr, uid, id, name, value, arg, context=None):
1589         """
1590         Set Date
1591         @param self: The object pointer
1592         @param cr: the current row, from the database cursor,
1593         @param uid: the current user’s ID for security checks,
1594         @param id: calendar's ID.
1595         @param value: Get Value
1596         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1597         @param context: A standard dictionary for contextual values
1598         """
1599
1600         assert name == 'date'
1601         return self.write(cr, uid, id, { 'date_start': value }, context=context)
1602
1603     _columns = {
1604         'date': fields.function(_get_date, fnct_inv=_set_date, \
1605                             string='Duration', store=True, type='datetime'),
1606         'duration': fields.integer('Duration'),
1607     }
1608
1609     __attribute__ = {}
1610
1611
1612 calendar_todo()
1613
1614 class ir_attachment(osv.osv):
1615     _name = 'ir.attachment'
1616     _inherit = 'ir.attachment'
1617
1618     def search_count(self, cr, user, args, context=None):
1619         new_args = []
1620         for domain_item in args:
1621             if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
1622                 new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
1623             else:
1624                 new_args.append(domain_item)
1625         return super(ir_attachment, self).search_count(cr, user, new_args, context)
1626
1627     def create(self, cr, uid, vals, context=None):
1628         if context:
1629             id = context.get('default_res_id', False)
1630             context.update({'default_res_id' : base_calendar_id2real_id(id)})
1631         return super(ir_attachment, self).create(cr, uid, vals, context=context)
1632
1633     def search(self, cr, uid, args, offset=0, limit=None, order=None,
1634             context=None, count=False):
1635         new_args = []
1636         for domain_item in args:
1637             if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
1638                 new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
1639             else:
1640                 new_args.append(domain_item)
1641         return super(ir_attachment, self).search(cr, uid, new_args, offset=offset,
1642                             limit=limit, order=order, context=context, count=False)
1643 ir_attachment()
1644
1645 class ir_values(osv.osv):
1646     _inherit = 'ir.values'
1647
1648     def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1649             isobject=False, meta=False, preserve_user=False, company=False):
1650         """
1651         Set IR Values
1652         @param self: The object pointer
1653         @param cr: the current row, from the database cursor,
1654         @param uid: the current user’s ID for security checks,
1655         @param model: Get The Model
1656         """
1657
1658         new_model = []
1659         for data in models:
1660             if type(data) in (list, tuple):
1661                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1662             else:
1663                 new_model.append(data)
1664         return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1665                     value, replace, isobject, meta, preserve_user, company)
1666
1667     def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1668              res_id_req=False, without_user=True, key2_req=True):
1669         """
1670         Get IR Values
1671         @param self: The object pointer
1672         @param cr: the current row, from the database cursor,
1673         @param uid: the current user’s ID for security checks,
1674         @param model: Get The Model
1675         """
1676         if context is None:
1677             context = {}
1678         new_model = []
1679         for data in models:
1680             if type(data) in (list, tuple):
1681                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1682             else:
1683                 new_model.append(data)
1684         return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1685                          meta, context, res_id_req, without_user, key2_req)
1686
1687 ir_values()
1688
1689 class ir_model(osv.osv):
1690
1691     _inherit = 'ir.model'
1692
1693     def read(self, cr, uid, ids, fields=None, context=None,
1694             load='_classic_read'):
1695         """
1696         Overrides orm read method.
1697         @param self: The object pointer
1698         @param cr: the current row, from the database cursor,
1699         @param uid: the current user’s ID for security checks,
1700         @param ids: List of IR Model’s IDs.
1701         @param context: A standard dictionary for contextual values
1702         """
1703         new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1704         if context is None:
1705             context = {}
1706         data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1707                         context=context, load=load)
1708         if data:
1709             for val in data:
1710                 val['id'] = base_calendar_id2real_id(val['id'])
1711         return isinstance(ids, (str, int, long)) and data[0] or data
1712
1713 ir_model()
1714
1715 class virtual_report_spool(web_services.report_spool):
1716
1717     def exp_report(self, db, uid, object, ids, datas=None, context=None):
1718         """
1719         Export Report
1720         @param self: The object pointer
1721         @param db: get the current database,
1722         @param uid: the current user’s ID for security checks,
1723         @param context: A standard dictionary for contextual values
1724         """
1725
1726         if object == 'printscreen.list':
1727             return super(virtual_report_spool, self).exp_report(db, uid, \
1728                             object, ids, datas, context)
1729         new_ids = []
1730         for id in ids:
1731             new_ids.append(base_calendar_id2real_id(id))
1732         if datas.get('id', False):
1733             datas['id'] = base_calendar_id2real_id(datas['id'])
1734         return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, datas, context)
1735
1736 virtual_report_spool()
1737
1738 class res_users(osv.osv):
1739     _inherit = 'res.users'
1740
1741     def _get_user_avail(self, cr, uid, ids, context=None):
1742         """
1743         Get User Availability
1744         @param self: The object pointer
1745         @param cr: the current row, from the database cursor,
1746         @param uid: the current user’s ID for security checks,
1747         @param ids: List of res user’s IDs.
1748         @param context: A standard dictionary for contextual values
1749         """
1750
1751         current_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1752         res = {}
1753         attendee_obj = self.pool.get('calendar.attendee')
1754         attendee_ids = attendee_obj.search(cr, uid, [
1755                     ('event_date', '<=', current_datetime), ('event_end_date', '<=', current_datetime),
1756                     ('state', '=', 'accepted'), ('user_id', 'in', ids)
1757                     ])
1758
1759         for attendee_data in attendee_obj.read(cr, uid, attendee_ids, ['user_id']):
1760             user_id = attendee_data['user_id']
1761             status = 'busy'
1762             res.update({user_id:status})
1763
1764         #TOCHECK: Delegated Event
1765         for user_id in ids:
1766             if user_id not in res:
1767                 res[user_id] = 'free'
1768
1769         return res
1770
1771     def _get_user_avail_fun(self, cr, uid, ids, name, args, context=None):
1772         """
1773         Get User Availability Function
1774         @param self: The object pointer
1775         @param cr: the current row, from the database cursor,
1776         @param uid: the current user’s ID for security checks,
1777         @param ids: List of res user’s IDs.
1778         @param context: A standard dictionary for contextual values
1779         """
1780
1781         return self._get_user_avail(cr, uid, ids, context=context)
1782
1783     _columns = {
1784             'availability': fields.function(_get_user_avail_fun, type='selection', \
1785                     selection=[('free', 'Free'), ('busy', 'Busy')], \
1786                     string='Free/Busy'),
1787     }
1788
1789 res_users()
1790
1791
1792 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: