[MERGE] lp:~openerp-dev/openobject-addons/trunk-wizard-remove-cancel-editable-tch
[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([('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 !'),_("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                 ], 'Status', 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         alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
818
819         mail_to = []
820
821         for alarm in self.browse(cr, uid, alarm_ids, context=context):
822             next_trigger_date = None
823             update_vals = {}
824             model_obj = self.pool.get(alarm.model_id.model)
825             res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
826             re_dates = []
827
828             if res_obj.rrule:
829                 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
830                 recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.exdate, event_date, res_obj.exrule)
831
832                 trigger_interval = alarm.trigger_interval
833                 if trigger_interval == 'days':
834                     delta = timedelta(days=alarm.trigger_duration)
835                 if trigger_interval == 'hours':
836                     delta = timedelta(hours=alarm.trigger_duration)
837                 if trigger_interval == 'minutes':
838                     delta = timedelta(minutes=alarm.trigger_duration)
839                 delta = alarm.trigger_occurs == 'after' and delta or -delta
840
841                 for rdate in recurrent_dates:
842                     if rdate + delta > current_datetime:
843                         break
844                     if rdate + delta <= current_datetime:
845                         re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
846                 rest_dates = recurrent_dates[len(re_dates):]
847                 next_trigger_date = rest_dates and rest_dates[0] or None
848
849             else:
850                 re_dates = [alarm.trigger_date]
851
852             for r_date in re_dates:
853                 ref = alarm.model_id.model + ',' + str(alarm.res_id)
854
855                 # search for alreay sent requests
856                 #if request_obj.search(cr, uid, [('trigger_date', '=', r_date), ('ref_doc1', '=', ref)], context=context):
857                     #continue
858
859                 # Deactivated because of the removing of res.request
860                 # TODO: when cleaning calendar module, re-add this in a new mechanism
861                 #if alarm.action == 'display':
862                     #value = {
863                        #'name': alarm.name,
864                        #'act_from': alarm.user_id.id,
865                        #'act_to': alarm.user_id.id,
866                        #'body': alarm.description,
867                        #'trigger_date': r_date,
868                        #'ref_doc1': ref
869                     #}
870                     #request_id = request_obj.create(cr, uid, value)
871                     #request_ids = [request_id]
872                     #for attendee in res_obj.attendee_ids:
873                         #if attendee.user_id:
874                             #value['act_to'] = attendee.user_id.id
875                             #request_id = request_obj.create(cr, uid, value)
876                             #request_ids.append(request_id)
877                     #request_obj.request_send(cr, uid, request_ids)
878
879                 if alarm.action == 'email':
880                     sub = '[Openobject Reminder] %s' % (alarm.name)
881                     body = """
882 Event: %s
883 Event Date: %s
884 Description: %s
885
886 From:
887       %s
888
889 ----
890 %s
891
892 """  % (alarm.name, alarm.trigger_date, alarm.description, \
893                         alarm.user_id.name, alarm.user_id.signature)
894                     mail_to = [alarm.user_id.user_email]
895                     for att in alarm.attendee_ids:
896                         mail_to.append(att.user_id.user_email)
897                     if mail_to:
898                         mail_message.schedule_with_attach(cr, uid,
899                             tools.config.get('email_from', False),
900                             mail_to,
901                             sub,
902                             body,
903                             context=context
904                         )
905             if next_trigger_date:
906                 update_vals.update({'trigger_date': next_trigger_date})
907             else:
908                 update_vals.update({'state': 'done'})
909             self.write(cr, uid, [alarm.id], update_vals)
910         return True
911
912 calendar_alarm()
913
914
915 class calendar_event(osv.osv):
916     _name = "calendar.event"
917     _description = "Calendar Event"
918     __attribute__ = {}
919
920     def _tz_get(self, cr, uid, context=None):
921         return [(x.lower(), x) for x in pytz.all_timezones]
922
923     def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
924         """Returns duration and/or end date based on values passed
925         @param self: The object pointer
926         @param cr: the current row, from the database cursor,
927         @param uid: the current user’s ID for security checks,
928         @param ids: List of calendar event’s IDs.
929         @param start_date: Starting date
930         @param duration: Duration between start date and end date
931         @param end_date: Ending Datee
932         @param context: A standard dictionary for contextual values
933         """
934         if context is None:
935             context = {}
936
937         value = {}
938         if not start_date:
939             return value
940         if not end_date and not duration:
941             duration = 1.00
942             value['duration'] = duration
943
944         if allday: # For all day event
945             value = {'duration': 24.0}
946             duration = 24.0
947             if start_date:
948                 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
949                 start_date = datetime.strftime(datetime(start.year, start.month, start.day, 0,0,0), "%Y-%m-%d %H:%M:%S")
950                 value['date'] = start_date
951
952
953         start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
954         if end_date and not duration:
955             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
956             diff = end - start
957             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
958             value['duration'] = round(duration, 2)
959         elif not end_date:
960             end = start + timedelta(hours=duration)
961             value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
962         elif end_date and duration and not allday:
963             # we have both, keep them synchronized:
964             # set duration based on end_date (arbitrary decision: this avoid
965             # getting dates like 06:31:48 instead of 06:32:00)
966             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
967             diff = end - start
968             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
969             value['duration'] = round(duration, 2)
970
971         return {'value': value}
972
973     def unlink_events(self, cr, uid, ids, context=None):
974         """
975         This function deletes event which are linked with the event with recurrent_uid
976                 (Removes the events which refers to the same UID value)
977         """
978         if context is None:
979             context = {}
980         for event_id in ids:
981             cr.execute("select id from %s where recurrent_uid=%%s" % (self._table), (event_id,))
982             r_ids = map(lambda x: x[0], cr.fetchall())
983             self.unlink(cr, uid, r_ids, context=context)
984         return True
985
986     def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
987         """
988         Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
989         @param self: The object pointer
990         @param cr: the current row, from the database cursor,
991         @param id: List of calendar event's ids.
992         @param context: A standard dictionary for contextual values
993         @return: dictionary of rrule value.
994         """
995         
996         result = {}
997         if not isinstance(ids, list):
998             ids = [ids]
999             
1000         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):
1001             event = datas['id']
1002             if datas.get('interval', 0) < 0:
1003                 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative'))
1004             if datas.get('count', 0) < 0:
1005                 raise osv.except_osv(_('Warning!'), _('Count cannot be negative'))
1006             if datas['recurrency']:
1007                 result[event] = self.compute_rule_string(datas)
1008             else:
1009                 result[event] = ""
1010         return result
1011
1012     def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1013         data = self._get_empty_rrule_data()
1014         if field_value:
1015             data['recurrency'] = True
1016             for event in self.browse(cr, uid, ids, context=context):
1017                 rdate = rule_date or event.date
1018                 update_data = self._parse_rrule(field_value, dict(data), rdate)
1019                 data.update(update_data)
1020                 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
1021         return True
1022
1023
1024     _columns = {
1025         'id': fields.integer('ID', readonly=True),
1026         'sequence': fields.integer('Sequence'),
1027         'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1028         'date': fields.datetime('Date', states={'done': [('readonly', True)]}),
1029         'date_deadline': fields.datetime('Deadline', states={'done': [('readonly', True)]}),
1030         'create_date': fields.datetime('Created', readonly=True),
1031         'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1032         'description': fields.text('Description', states={'done': [('readonly', True)]}),
1033         'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1034              ('confidential', 'Public for Employees')], 'Mark as', states={'done': [('readonly', True)]}),
1035         'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1036         'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1037                                                 'Show as', states={'done': [('readonly', True)]}),
1038         'base_calendar_url': fields.char('Caldav URL', size=264),
1039         'state': fields.selection([('tentative', 'Tentative'),
1040                         ('cancelled', 'Cancelled'),
1041                         ('confirmed', 'Confirmed'),
1042                         ], 'Status', readonly=True),
1043         'exdate': fields.text('Exception Date/Times', help="This property \
1044 defines the list of date/time exceptions for a recurring calendar component."),
1045         'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1046 rule or repeating pattern of time to exclude from the recurring rule."),
1047         'rrule': fields.function(_get_rulestring, type='char', size=124, \
1048                     fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1049         'rrule_type': fields.selection([('none', ''), ('daily', 'Daily'), \
1050                             ('weekly', 'Weekly'), ('monthly', 'Monthly'), \
1051                             ('yearly', 'Yearly'),],
1052                             'Recurrency', states={'done': [('readonly', True)]},
1053                             help="Let the event automatically repeat at that interval"),
1054         'alarm_id': fields.many2one('res.alarm', 'Alarm', states={'done': [('readonly', True)]},
1055                         help="Set an alarm at this time, before the event occurs" ),
1056         'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1057         'recurrent_uid': fields.integer('Recurrent ID'),
1058         'recurrent_id': fields.datetime('Recurrent ID date'),
1059         'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1060         'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1061         'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with Organizer Attribure of VEvent.
1062         'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1063         'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence termination'),
1064         'interval': fields.integer('Repeat every', help="Repeat every (Days/Week/Month/Year)"),
1065         'count': fields.integer('Repeat', help="Repeat x times"),
1066         'mo': fields.boolean('Mon'),
1067         'tu': fields.boolean('Tue'),
1068         'we': fields.boolean('Wed'),
1069         'th': fields.boolean('Thu'),
1070         'fr': fields.boolean('Fri'),
1071         'sa': fields.boolean('Sat'),
1072         'su': fields.boolean('Sun'),
1073         'select1': fields.selection([('date', 'Date of month'),
1074                                     ('day', 'Day of month')], 'Option'),
1075         'day': fields.integer('Date of month'),
1076         'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), \
1077                                    ('WE', 'Wednesday'), ('TH', 'Thursday'), \
1078                                    ('FR', 'Friday'), ('SA', 'Saturday'), \
1079                                    ('SU', 'Sunday')], 'Weekday'),
1080         'byday': fields.selection([('1', 'First'), ('2', 'Second'), \
1081                                    ('3', 'Third'), ('4', 'Fourth'), \
1082                                    ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
1083         'month_list': fields.selection(months.items(), 'Month'),
1084         'end_date': fields.date('Repeat Until'),
1085         'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1086                                  'event_id', 'attendee_id', 'Attendees'),
1087         'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1088         'active': fields.boolean('Active', help="If the active field is set to \
1089          true, it will allow you to hide the event alarm information without removing it."),
1090         'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1091     }
1092     
1093     def default_organizer(self, cr, uid, context=None):
1094         user_pool = self.pool.get('res.users')
1095         user = user_pool.browse(cr, uid, uid, context=context)
1096         res = user.name
1097         if user.user_email:
1098             res += " <%s>" %(user.user_email)
1099         return res
1100
1101     _defaults = {
1102             'end_type' : 'count',
1103             'count' : 1,
1104             'rrule_type' : 'none',
1105             'state': 'tentative',
1106             'class': 'public',
1107             'show_as': 'busy',
1108             'select1': 'date',
1109             'interval': 1,
1110             'active': 1,
1111             'user_id': lambda self, cr, uid, ctx: uid,
1112             'organizer': default_organizer,
1113     }
1114
1115     def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1116         """Gives virtual event ids for recurring events based on value of Recurrence Rule
1117         This method gives ids of dates that comes between start date and end date of calendar views
1118         @param self: The object pointer
1119         @param cr: the current row, from the database cursor,
1120         @param uid: the current user’s ID for security checks,
1121         @param limit: The Number of Results to Return """
1122         if not context:
1123             context = {}
1124
1125         result = []
1126         for data in super(calendar_event, self).read(cr, uid, select, context=context):
1127             if not data['rrule']:
1128                 result.append(data['id'])
1129                 continue
1130             event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1131 #                To check: If the start date is replace by event date .. the event date will be changed by that of calendar code
1132
1133             if not data['rrule']:
1134                 continue
1135
1136             exdate = data['exdate'] and data['exdate'].split(',') or []
1137             rrule_str = data['rrule']
1138             new_rrule_str = []
1139             rrule_until_date = False
1140             is_until = False
1141             for rule in rrule_str.split(';'):
1142                 name, value = rule.split('=')
1143                 if name == "UNTIL":
1144                     is_until = True
1145                     value = parser.parse(value)
1146                     rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1147                     value = value.strftime("%Y%m%d%H%M%S")
1148                 new_rule = '%s=%s' % (name, value)
1149                 new_rrule_str.append(new_rule)
1150             new_rrule_str = ';'.join(new_rrule_str)
1151             rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1152             for r_date in rdates:
1153                 ok = True
1154                 for arg in domain:
1155                     if arg[0] in ('date', 'date_deadline'):
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 (arg[1]=='<='):
1165                             ok = ok and r_date.strftime('%Y-%m-%d')<=arg[2]
1166                 if not ok:
1167                     continue
1168                 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1169                 result.append(idval)
1170
1171         if isinstance(select, (str, int, long)):
1172             return ids and ids[0] or False
1173         else:
1174             ids = list(set(result))
1175         return ids
1176
1177     def compute_rule_string(self, datas):
1178         """
1179         Compute rule string according to value type RECUR of iCalendar from the values given.
1180         @param self: the object pointer
1181         @param datas: dictionary of freq and interval value.
1182         """
1183         def get_week_string(freq, datas):
1184             weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1185             if freq == 'weekly':
1186                 byday = map(lambda x: x.upper(), filter(lambda x: datas.get(x) and x in weekdays, datas))
1187                 if byday:
1188                     return ';BYDAY=' + ','.join(byday)
1189             return ''  
1190         
1191         def get_month_string(freq, datas):
1192             if freq == 'monthly':
1193                 if datas.get('select1')=='date' and (datas.get('day') < 1 or datas.get('day') > 31):
1194                     raise osv.except_osv(_('Error!'), ("Please select proper Day of month"))
1195                 
1196                 if datas.get('select1')=='day':
1197                     return ';BYDAY=' + datas.get('byday') + datas.get('week_list')
1198                 elif datas.get('select1')=='date':
1199                     return ';BYMONTHDAY=' + str(datas.get('day'))
1200             return ''
1201         
1202         def get_end_date(datas):
1203             if datas.get('end_date'):
1204                 datas['end_date_new'] = ''.join((re.compile('\d')).findall(datas.get('end_date'))) + 'T235959Z'
1205             
1206             return (datas.get('end_type') == 'count' and (';COUNT=' + str(datas.get('count'))) or '') +\
1207                              ((datas.get('end_date_new') and datas.get('end_type') == 'end_date' and (';UNTIL=' + datas.get('end_date_new'))) or '')
1208
1209         freq=datas.get('rrule_type')
1210         if freq == 'none':
1211             return ''
1212
1213         interval_srting = datas.get('interval') and (';INTERVAL=' + str(datas.get('interval'))) or ''
1214
1215         return 'FREQ=' + freq.upper() + get_week_string(freq, datas) + interval_srting + get_end_date(datas) + get_month_string(freq, datas)
1216     
1217     def _get_empty_rrule_data(self):
1218         return  {
1219             'byday' : False,
1220             'recurrency' : False,
1221             'end_date' : False, 
1222             'rrule_type' : False, 
1223             'select1' : False, 
1224             'interval' : 0, 
1225             'count' : False, 
1226             'end_type' : False, 
1227             'mo' : False, 
1228             'tu' : False, 
1229             'we' : False, 
1230             'th' : False, 
1231             'fr' : False, 
1232             'sa' : False, 
1233             'su' : False, 
1234             'exrule' : False, 
1235             'day' : False, 
1236             'week_list' : False
1237         }
1238
1239     #def _write_rrule(self, cr, uid, ids, field_value, rule_date=False, context=None):
1240     #    data = self._get_empty_rrule_data()
1241     #    
1242     #    if field_value:
1243     #        data['recurrency'] = True
1244     #        for event in self.browse(cr, uid, ids, context=context):
1245     #            rdate = rule_date or event.date
1246     #            update_data = self._parse_rrule(field_value, dict(data), rdate)
1247     #            data.update(update_data)
1248     #            #parse_rrule
1249     #            self.write(cr, uid, event.id, data, context=context)
1250         
1251
1252     def _parse_rrule(self, rule, data, date_start):
1253         day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1254         rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1255         r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1256         
1257         if r._freq > 0 and r._freq < 4:
1258             data['rrule_type'] = rrule_type[r._freq]
1259             
1260         data['count'] = r._count
1261         data['interval'] = r._interval
1262         data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1263         #repeat weekly
1264         if r._byweekday:
1265             for i in xrange(0,7):
1266                 if i in r._byweekday:
1267                     data[day_list[i]] = True
1268             data['rrule_type'] = 'weekly'
1269         #repeat monthly bynweekday ((weekday, weeknumber), )
1270         if r._bynweekday: 
1271             data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1272             data['byday'] = r._bynweekday[0][1]
1273             data['select1'] = 'day'
1274             data['rrule_type'] = 'monthly' 
1275            
1276         if r._bymonthday:
1277             data['day'] = r._bymonthday[0]
1278             data['select1'] = 'date'
1279             data['rrule_type'] = 'monthly'
1280             
1281         #yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1282         if r._bymonth:
1283             data['interval'] = data['interval'] * 12
1284             
1285         #FIXEME handle forever case
1286         #end of recurrence    
1287         #in case of repeat for ever that we do not support right now
1288         if not (data.get('count') or data.get('end_date')):
1289             data['count'] = 100
1290         if data.get('count'):
1291             data['end_type'] = 'count'
1292         else:
1293             data['end_type'] = 'end_date'  
1294         return data  
1295
1296     def remove_virtual_id(self, ids):
1297         if isinstance(ids, (str, int, long)):
1298             return base_calendar_id2real_id(ids)
1299             
1300         if isinstance(ids, (list, tuple)):
1301             res = []
1302             for id in ids:
1303                 res.append(base_calendar_id2real_id(id))
1304             return res
1305
1306     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1307         context = context or {}
1308         args_without_date = []
1309         filter_date = []
1310
1311         for arg in args:
1312             if arg[0] == "id":
1313                 new_id = self.remove_virtual_id(arg[2])
1314                 new_arg = (arg[0], arg[1], new_id)
1315                 args_without_date.append(new_arg)
1316             elif arg[0] not in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1317                 args_without_date.append(arg)
1318             else:
1319                 if context.get('virtual_id', True):
1320                     args_without_date.append('|')
1321                 args_without_date.append(arg)
1322                 if context.get('virtual_id', True):
1323                     args_without_date.append(('recurrency','=',1))
1324                 filter_date.append(arg)
1325
1326         res = super(calendar_event, self).search(cr, uid, args_without_date, \
1327                                  0, 0, order, context, count=False)
1328         if context.get('virtual_id', True):
1329             res = self.get_recurrent_ids(cr, uid, res, args, limit, context=context)
1330
1331         if count:
1332             return len(res)
1333         elif limit:
1334             return res[offset:offset+limit]
1335         else:
1336             return res
1337
1338     def _get_data(self, cr, uid, id, context=None):
1339         res = self.read(cr, uid, [id],['date', 'date_deadline'])
1340         return res[0]
1341
1342     def need_to_update(self, event_id, vals):
1343         split_id = str(event_id).split("-")
1344         if len(split_id) < 2:
1345             return False 
1346         else:
1347             date_start = vals.get('date', '')
1348             try:
1349                 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1350                 return date_start == split_id[1]
1351             except Exception:
1352                 return True
1353
1354
1355     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1356         context = context or {}
1357         if isinstance(ids, (str, int, long)):
1358             ids = [ids]
1359         res = False
1360
1361         # Special write of complex IDS
1362         for event_id in ids[:]:
1363             if len(str(event_id).split('-')) == 1:
1364                 continue
1365             ids.remove(event_id)
1366             real_event_id = base_calendar_id2real_id(event_id)
1367             if not vals.get('recurrency', True):
1368                 ids.append(real_event_id)
1369                 continue
1370
1371             #if edit one instance of a reccurrent id
1372             data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1373                                                 'rrule', 'duration', 'exdate'])
1374             if data.get('rrule'):
1375                 data.update(vals)
1376                 data.update({
1377                     'recurrent_uid': real_event_id,
1378                     'recurrent_id': data.get('date'),
1379                     'rrule_type': 'none',
1380                     'rrule': '',
1381                     'recurrency' : False,
1382                     })
1383
1384                 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1385
1386                 date_new = event_id.split('-')[1]
1387                 date_new = time.strftime("%Y%m%dT%H%M%S", \
1388                              time.strptime(date_new, "%Y%m%d%H%M%S"))
1389                 exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1390                 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1391
1392                 context.update({'active_id': new_id, 'active_ids': [new_id]})
1393                 continue
1394
1395         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1396             vals['vtimezone'] = vals['vtimezone'][40:]
1397
1398         res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1399
1400         if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1401                 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1402             alarm_obj = self.pool.get('res.alarm')
1403             alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1404         return res or True and False
1405
1406     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1407         if not context:
1408             context = {}
1409             
1410         if 'date' in groupby:
1411             raise osv.except_osv(_('Warning !'), _('Group by date not supported, use the calendar view instead'))
1412         virtual_id = context.get('virtual_id', True)
1413         context.update({'virtual_id': False})
1414         res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1415         for re in res:
1416             #remove the count, since the value is not consistent with the result of the search when expand the group
1417             for groupname in groupby:
1418                 if re.get(groupname + "_count"):
1419                     del re[groupname + "_count"]
1420             re.get('__context', {}).update({'virtual_id' : virtual_id})
1421         return res
1422
1423     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1424         # FIXME This whole id mangling has to go!
1425         if context is None:
1426             context = {}
1427         fields2 = fields and fields[:] or None
1428
1429         EXTRAFIELDS = ('class','user_id','duration')
1430         for f in EXTRAFIELDS:
1431             if fields and (f not in fields):
1432                 fields2.append(f)
1433
1434         if isinstance(ids, (str, int, long)):
1435             select = [ids]
1436         else:
1437             select = ids
1438         select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1439         result = []
1440
1441         real_data = super(calendar_event, self).read(cr, uid,
1442                     [real_id for base_calendar_id, real_id in select],
1443                     fields=fields2, context=context, load=load)
1444         real_data = dict(zip([x['id'] for x in real_data], real_data))
1445
1446         for base_calendar_id, real_id in select:
1447             res = real_data[real_id].copy()
1448             ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1449             if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1450                 res['date'] = ls[1]
1451                 res['date_deadline'] = ls[2]
1452             res['id'] = base_calendar_id
1453
1454             result.append(res)
1455
1456         for r in result:
1457             if r['user_id']:
1458                 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1459                 if user_id==uid:
1460                     continue
1461             if r['class']=='private':
1462                 for f in r.keys():
1463                     if f not in ('id','date','date_deadline','duration','user_id','state'):
1464                         r[f] = False
1465                     if f=='name':
1466                         r[f] = _('Busy')
1467
1468         for r in result:
1469             for k in EXTRAFIELDS:
1470                 if (k in r) and ((not fields) or (k not in fields)):
1471                     del r[k]
1472         if isinstance(ids, (str, int, long)):
1473             return result and result[0] or False
1474         return result
1475
1476     def copy(self, cr, uid, id, default=None, context=None):
1477         if context is None:
1478             context = {}
1479             
1480         res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1481         alarm_obj = self.pool.get('res.alarm')
1482         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1483         return res
1484
1485     def unlink(self, cr, uid, ids, context=None):
1486         if not isinstance(ids, list):
1487             ids = [ids]
1488         res = False
1489         attendee_obj=self.pool.get('calendar.attendee')
1490         for event_id in ids[:]:
1491             if len(str(event_id).split('-')) == 1:
1492                 continue
1493
1494             real_event_id = base_calendar_id2real_id(event_id)
1495             data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1496             date_new = event_id.split('-')[1]
1497             date_new = time.strftime("%Y%m%dT%H%M%S", \
1498                          time.strptime(date_new, "%Y%m%d%H%M%S"))
1499             exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1500             self.write(cr, uid, [real_event_id], {'exdate': exdate})
1501             ids.remove(event_id)
1502         for event in self.browse(cr, uid, ids, context=context):
1503             if event.attendee_ids:
1504                 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1505
1506         res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1507         self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1508         self.unlink_events(cr, uid, ids, context=context)
1509         return res
1510
1511
1512     def create(self, cr, uid, vals, context=None):
1513         if context is None:
1514             context = {}
1515
1516         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1517             vals['vtimezone'] = vals['vtimezone'][40:]
1518
1519         #updated_vals = self.onchange_dates(cr, uid, [],
1520         #    vals.get('date', False),
1521         #    vals.get('duration', False),
1522         #    vals.get('date_deadline', False),
1523         #    vals.get('allday', False),
1524         #    context=context)
1525         #vals.update(updated_vals.get('value', {}))
1526
1527         res = super(calendar_event, self).create(cr, uid, vals, context)
1528         alarm_obj = self.pool.get('res.alarm')
1529         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1530         return res
1531
1532     def do_tentative(self, cr, uid, ids, context=None, *args):
1533         """ Makes event invitation as Tentative
1534         @param self: The object pointer
1535         @param cr: the current row, from the database cursor,
1536         @param uid: the current user’s ID for security checks,
1537         @param ids: List of Event IDs
1538         @param *args: Get Tupple value
1539         @param context: A standard dictionary for contextual values
1540         """
1541         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1542
1543     def do_cancel(self, cr, uid, ids, context=None, *args):
1544         """ Makes event invitation as Tentative
1545         @param self: The object pointer
1546         @param cr: the current row, from the database cursor,
1547         @param uid: the current user’s ID for security checks,
1548         @param ids: List of Event IDs
1549         @param *args: Get Tupple value
1550         @param context: A standard dictionary for contextual values
1551         """
1552         return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1553
1554     def do_confirm(self, cr, uid, ids, context=None, *args):
1555         """ Makes event invitation as Tentative
1556         @param self: The object pointer
1557         @param cr: the current row, from the database cursor,
1558         @param uid: the current user’s ID for security checks,
1559         @param ids: List of Event IDs
1560         @param *args: Get Tupple value
1561         @param context: A standard dictionary for contextual values
1562         """
1563         return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1564
1565 calendar_event()
1566
1567 class calendar_todo(osv.osv):
1568     """ Calendar Task """
1569
1570     _name = "calendar.todo"
1571     _inherit = "calendar.event"
1572     _description = "Calendar Task"
1573
1574     def _get_date(self, cr, uid, ids, name, arg, context=None):
1575         """
1576         Get Date
1577         @param self: The object pointer
1578         @param cr: the current row, from the database cursor,
1579         @param uid: the current user’s ID for security checks,
1580         @param ids: List of calendar todo's IDs.
1581         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1582         @param context: A standard dictionary for contextual values
1583         """
1584
1585         res = {}
1586         for event in self.browse(cr, uid, ids, context=context):
1587             res[event.id] = event.date_start
1588         return res
1589
1590     def _set_date(self, cr, uid, id, name, value, arg, context=None):
1591         """
1592         Set Date
1593         @param self: The object pointer
1594         @param cr: the current row, from the database cursor,
1595         @param uid: the current user’s ID for security checks,
1596         @param id: calendar's ID.
1597         @param value: Get Value
1598         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1599         @param context: A standard dictionary for contextual values
1600         """
1601
1602         assert name == 'date'
1603         return self.write(cr, uid, id, { 'date_start': value }, context=context)
1604
1605     _columns = {
1606         'date': fields.function(_get_date, fnct_inv=_set_date, \
1607                             string='Duration', store=True, type='datetime'),
1608         'duration': fields.integer('Duration'),
1609     }
1610
1611     __attribute__ = {}
1612
1613
1614 calendar_todo()
1615
1616 class ir_attachment(osv.osv):
1617     _name = 'ir.attachment'
1618     _inherit = 'ir.attachment'
1619
1620     def search_count(self, cr, user, args, context=None):
1621         new_args = []
1622         for domain_item in args:
1623             if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
1624                 new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
1625             else:
1626                 new_args.append(domain_item)
1627         return super(ir_attachment, self).search_count(cr, user, new_args, context)
1628
1629     def create(self, cr, uid, vals, context=None):
1630         if context:
1631             id = context.get('default_res_id', False)
1632             context.update({'default_res_id' : base_calendar_id2real_id(id)})
1633         return super(ir_attachment, self).create(cr, uid, vals, context=context)
1634
1635     def search(self, cr, uid, args, offset=0, limit=None, order=None,
1636             context=None, count=False):
1637         new_args = []
1638         for domain_item in args:
1639             if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
1640                 new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
1641             else:
1642                 new_args.append(domain_item)
1643         return super(ir_attachment, self).search(cr, uid, new_args, offset=offset,
1644                             limit=limit, order=order, context=context, count=False)
1645 ir_attachment()
1646
1647 class ir_values(osv.osv):
1648     _inherit = 'ir.values'
1649
1650     def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1651             isobject=False, meta=False, preserve_user=False, company=False):
1652         """
1653         Set IR Values
1654         @param self: The object pointer
1655         @param cr: the current row, from the database cursor,
1656         @param uid: the current user’s ID for security checks,
1657         @param model: Get The Model
1658         """
1659
1660         new_model = []
1661         for data in models:
1662             if type(data) in (list, tuple):
1663                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1664             else:
1665                 new_model.append(data)
1666         return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1667                     value, replace, isobject, meta, preserve_user, company)
1668
1669     def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1670              res_id_req=False, without_user=True, key2_req=True):
1671         """
1672         Get IR Values
1673         @param self: The object pointer
1674         @param cr: the current row, from the database cursor,
1675         @param uid: the current user’s ID for security checks,
1676         @param model: Get The Model
1677         """
1678         if context is None:
1679             context = {}
1680         new_model = []
1681         for data in models:
1682             if type(data) in (list, tuple):
1683                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1684             else:
1685                 new_model.append(data)
1686         return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1687                          meta, context, res_id_req, without_user, key2_req)
1688
1689 ir_values()
1690
1691 class ir_model(osv.osv):
1692
1693     _inherit = 'ir.model'
1694
1695     def read(self, cr, uid, ids, fields=None, context=None,
1696             load='_classic_read'):
1697         """
1698         Overrides orm read method.
1699         @param self: The object pointer
1700         @param cr: the current row, from the database cursor,
1701         @param uid: the current user’s ID for security checks,
1702         @param ids: List of IR Model’s IDs.
1703         @param context: A standard dictionary for contextual values
1704         """
1705         new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1706         if context is None:
1707             context = {}
1708         data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1709                         context=context, load=load)
1710         if data:
1711             for val in data:
1712                 val['id'] = base_calendar_id2real_id(val['id'])
1713         return isinstance(ids, (str, int, long)) and data[0] or data
1714
1715 ir_model()
1716
1717 class virtual_report_spool(web_services.report_spool):
1718
1719     def exp_report(self, db, uid, object, ids, datas=None, context=None):
1720         """
1721         Export Report
1722         @param self: The object pointer
1723         @param db: get the current database,
1724         @param uid: the current user’s ID for security checks,
1725         @param context: A standard dictionary for contextual values
1726         """
1727
1728         if object == 'printscreen.list':
1729             return super(virtual_report_spool, self).exp_report(db, uid, \
1730                             object, ids, datas, context)
1731         new_ids = []
1732         for id in ids:
1733             new_ids.append(base_calendar_id2real_id(id))
1734         if datas.get('id', False):
1735             datas['id'] = base_calendar_id2real_id(datas['id'])
1736         return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, datas, context)
1737
1738 virtual_report_spool()
1739
1740 class res_users(osv.osv):
1741     _inherit = 'res.users'
1742
1743     def _get_user_avail(self, cr, uid, ids, context=None):
1744         """
1745         Get User Availability
1746         @param self: The object pointer
1747         @param cr: the current row, from the database cursor,
1748         @param uid: the current user’s ID for security checks,
1749         @param ids: List of res user’s IDs.
1750         @param context: A standard dictionary for contextual values
1751         """
1752
1753         current_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1754         res = {}
1755         attendee_obj = self.pool.get('calendar.attendee')
1756         attendee_ids = attendee_obj.search(cr, uid, [
1757                     ('event_date', '<=', current_datetime), ('event_end_date', '<=', current_datetime),
1758                     ('state', '=', 'accepted'), ('user_id', 'in', ids)
1759                     ])
1760
1761         for attendee_data in attendee_obj.read(cr, uid, attendee_ids, ['user_id']):
1762             user_id = attendee_data['user_id']
1763             status = 'busy'
1764             res.update({user_id:status})
1765
1766         #TOCHECK: Delegated Event
1767         for user_id in ids:
1768             if user_id not in res:
1769                 res[user_id] = 'free'
1770
1771         return res
1772
1773     def _get_user_avail_fun(self, cr, uid, ids, name, args, context=None):
1774         """
1775         Get User Availability Function
1776         @param self: The object pointer
1777         @param cr: the current row, from the database cursor,
1778         @param uid: the current user’s ID for security checks,
1779         @param ids: List of res user’s IDs.
1780         @param context: A standard dictionary for contextual values
1781         """
1782
1783         return self._get_user_avail(cr, uid, ids, context=context)
1784
1785     _columns = {
1786             'availability': fields.function(_get_user_avail_fun, type='selection', \
1787                     selection=[('free', 'Free'), ('busy', 'Busy')], \
1788                     string='Free/Busy'),
1789     }
1790
1791 res_users()
1792
1793
1794 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: