[IMP] sale order line invisible type
[odoo/odoo.git] / addons / base_calendar / base_calendar.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import datetime, timedelta, date
23 from dateutil import parser
24 from dateutil import rrule
25 from dateutil.relativedelta import relativedelta
26 from osv import fields, osv
27 from service import web_services
28 from tools.translate import _
29 import pytz
30 import re
31 import time
32 import tools
33
34 months = {
35     1: "January", 2: "February", 3: "March", 4: "April", \
36     5: "May", 6: "June", 7: "July", 8: "August", 9: "September", \
37     10: "October", 11: "November", 12: "December"
38 }
39
40 def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None):
41     """
42     Get recurrent dates based on Rule string considering exdate and start date
43     @param rrulestring: Rulestring
44     @param exdate: List of exception dates for rrule
45     @param startdate: Startdate for computing recurrent dates
46     @return: List of Recurrent dates
47     """
48     def todate(date):
49         val = parser.parse(''.join((re.compile('\d')).findall(date)))
50         return val
51
52     if not startdate:
53         startdate = datetime.now()
54
55     if not exdate:
56         exdate = []
57
58     rset1 = rrule.rrulestr(str(rrulestring), dtstart=startdate, forceset=True)
59     for date in exdate:
60         datetime_obj = todate(date)
61         rset1._exdate.append(datetime_obj)
62
63     if exrule:
64         rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate))
65
66     return list(rset1)
67
68 def base_calendar_id2real_id(base_calendar_id=None, with_date=False):
69     """
70     This function converts virtual event id into real id of actual event
71     @param base_calendar_id: Id of calendar
72     @param with_date: If value passed to this param it will return dates based on value of withdate + base_calendar_id
73     """
74
75     if base_calendar_id and isinstance(base_calendar_id, (str, unicode)):
76         res = base_calendar_id.split('-')
77
78         if len(res) >= 2:
79             real_id = res[0]
80             if with_date:
81                 real_date = time.strftime("%Y-%m-%d %H:%M:%S", \
82                                  time.strptime(res[1], "%Y%m%d%H%M%S"))
83                 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
84                 end = start + timedelta(hours=with_date)
85                 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
86             return int(real_id)
87
88     return base_calendar_id and int(base_calendar_id) or base_calendar_id
89
90 def real_id2base_calendar_id(real_id, recurrent_date):
91     """
92     Convert  real id of record into virtual id using recurrent_date
93     e.g. real id is 1 and recurrent_date is 01-12-2009 10:00:00 then it will return
94         1-20091201100000
95     @return: real id with recurrent date.
96     """
97
98     if real_id and recurrent_date:
99         recurrent_date = time.strftime("%Y%m%d%H%M%S", \
100                             time.strptime(recurrent_date, "%Y-%m-%d %H:%M:%S"))
101         return '%d-%s' % (real_id, recurrent_date)
102     return real_id
103
104 def _links_get(self, cr, uid, context=None):
105     """
106     Get request link.
107     @param cr: the current row, from the database cursor,
108     @param uid: the current user’s ID for security checks,
109     @param context: A standard dictionary for contextual values
110     @return: list of dictionary which contain object and name and id.
111     """
112     obj = self.pool.get('res.request.link')
113     ids = obj.search(cr, uid, [])
114     res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
115     return [(r['object'], r['name']) for r in res]
116
117 html_invitation = """
118 <html>
119 <head>
120 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
121 <title>%(name)s</title>
122 </head>
123 <body>
124 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
125     style="font-family: Arial, Sans-serif; font-size: 14">
126     <tr>
127         <td width="100%%">Hello,</td>
128     </tr>
129     <tr>
130         <td width="100%%">You are invited for <i>%(company)s</i> Event.</td>
131     </tr>
132     <tr>
133         <td width="100%%">Below are the details of event:</td>
134     </tr>
135 </table>
136
137 <table cellspacing="0" cellpadding="5" border="0" summary=""
138     style="width: 90%%; font-family: Arial, Sans-serif; border: 1px Solid #ccc; background-color: #f6f6f6">
139     <tr valign="center" align="center">
140         <td bgcolor="DFDFDF">
141         <h3>%(name)s</h3>
142         </td>
143     </tr>
144     <tr>
145         <td>
146         <table cellpadding="8" cellspacing="0" border="0"
147             style="font-size: 14" summary="Eventdetails" bgcolor="f6f6f6"
148             width="90%%">
149             <tr>
150                 <td width="21%%">
151                 <div><b>Start Date</b></div>
152                 </td>
153                 <td><b>:</b></td>
154                 <td>%(start_date)s</td>
155                 <td width="15%%">
156                 <div><b>End Date</b></div>
157                 </td>
158                 <td><b>:</b></td>
159                 <td width="25%%">%(end_date)s</td>
160             </tr>
161             <tr valign="top">
162                 <td><b>Description</b></td>
163                 <td><b>:</b></td>
164                 <td colspan="3">%(description)s</td>
165             </tr>
166             <tr valign="top">
167                 <td>
168                 <div><b>Location</b></div>
169                 </td>
170                 <td><b>:</b></td>
171                 <td colspan="3">%(location)s</td>
172             </tr>
173             <tr valign="top">
174                 <td>
175                 <div><b>Event Attendees</b></div>
176                 </td>
177                 <td><b>:</b></td>
178                 <td colspan="3">
179                 <div>
180                 <div>%(attendees)s</div>
181                 </div>
182                 </td>
183             </tr>
184         </table>
185         </td>
186     </tr>
187 </table>
188 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
189     style="font-family: Arial, Sans-serif; font-size: 14">
190     <tr>
191         <td width="100%%">From:</td>
192     </tr>
193     <tr>
194         <td width="100%%">%(user)s</td>
195     </tr>
196     <tr valign="top">
197         <td width="100%%">-<font color="a7a7a7">-------------------------</font></td>
198     </tr>
199     <tr>
200         <td width="100%%"> <font color="a7a7a7">%(sign)s</font></td>
201     </tr>
202 </table>
203 </body>
204 </html>
205 """
206
207 class calendar_attendee(osv.osv):
208     """
209     Calendar Attendee Information
210     """
211     _name = 'calendar.attendee'
212     _description = 'Attendee information'
213     _rec_name = 'cutype'
214
215     __attribute__ = {}
216
217     def _get_address(self, name=None, email=None):
218         """
219         Gives email information in ical CAL-ADDRESS type format
220         @param name: Name for CAL-ADDRESS value
221         @param email: Email address for CAL-ADDRESS value
222         """
223         if name and email:
224             name += ':'
225         return (name or '') + (email and ('MAILTO:' + email) or '')
226
227     def _compute_data(self, cr, uid, ids, name, arg, context=None):
228         """
229         Compute data on function fields for attendee values .
230         @param cr: the current row, from the database cursor,
231         @param uid: the current user’s ID for security checks,
232         @param ids: List of calendar attendee’s IDs.
233         @param name: name of field.
234         @param context: A standard dictionary for contextual values
235         @return: Dictionary of form {id: {'field Name': value'}}.
236         """
237         name = name[0]
238         result = {}
239         for attdata in self.browse(cr, uid, ids, context=context):
240             id = attdata.id
241             result[id] = {}
242             if name == 'sent_by':
243                 if not attdata.sent_by_uid:
244                     result[id][name] = ''
245                     continue
246                 else:
247                     result[id][name] = self._get_address(attdata.sent_by_uid.name, \
248                                         attdata.sent_by_uid.email)
249
250             if name == 'cn':
251                 if attdata.user_id:
252                     result[id][name] = attdata.user_id.name
253                 elif attdata.partner_id:
254                     result[id][name] = attdata.partner_id.name or False
255                 else:
256                     result[id][name] = attdata.email or ''
257
258             if name == 'delegated_to':
259                 todata = []
260                 for child in attdata.child_ids:
261                     if child.email:
262                         todata.append('MAILTO:' + child.email)
263                 result[id][name] = ', '.join(todata)
264
265             if name == 'delegated_from':
266                 fromdata = []
267                 for parent in attdata.parent_ids:
268                     if parent.email:
269                         fromdata.append('MAILTO:' + parent.email)
270                 result[id][name] = ', '.join(fromdata)
271
272             if name == 'event_date':
273                 if attdata.ref:
274                     result[id][name] = attdata.ref.date
275                 else:
276                     result[id][name] = False
277
278             if name == 'event_end_date':
279                 if attdata.ref:
280                     result[id][name] = attdata.ref.date_deadline
281                 else:
282                     result[id][name] = False
283
284             if name == 'sent_by_uid':
285                 if attdata.ref:
286                     result[id][name] = (attdata.ref.user_id.id, attdata.ref.user_id.name)
287                 else:
288                     result[id][name] = uid
289
290             if name == 'language':
291                 user_obj = self.pool.get('res.users')
292                 lang = user_obj.read(cr, uid, uid, ['lang'], context=context)['lang']
293                 result[id][name] = lang.replace('_', '-')
294
295         return result
296
297     def _links_get(self, cr, uid, context=None):
298         """
299         Get request link for ref field in calendar attendee.
300         @param cr: the current row, from the database cursor,
301         @param uid: the current user’s ID for security checks,
302         @param context: A standard dictionary for contextual values
303         @return: list of dictionary which contain object and name and id.
304         """
305         obj = self.pool.get('res.request.link')
306         ids = obj.search(cr, uid, [])
307         res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
308         return [(r['object'], r['name']) for r in res]
309
310     def _lang_get(self, cr, uid, context=None):
311         """
312         Get language for language selection field.
313         @param cr: the current row, from the database cursor,
314         @param uid: the current user’s ID for security checks,
315         @param context: A standard dictionary for contextual values
316         @return: list of dictionary which contain code and name and id.
317         """
318         obj = self.pool.get('res.lang')
319         ids = obj.search(cr, uid, [])
320         res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
321         res = [((r['code']).replace('_', '-').lower(), r['name']) for r in res]
322         return res
323
324     _columns = {
325         'cutype': fields.selection([('individual', 'Individual'), \
326                     ('group', 'Group'), ('resource', 'Resource'), \
327                     ('room', 'Room'), ('unknown', 'Unknown') ], \
328                     'Invite Type', help="Specify the type of Invitation"),
329         'member': fields.char('Member', size=124,
330                     help="Indicate the groups that the attendee belongs to"),
331         'role': fields.selection([('req-participant', 'Participation required'), \
332                     ('chair', 'Chair Person'), \
333                     ('opt-participant', 'Optional Participation'), \
334                     ('non-participant', 'For information Purpose')], 'Role', \
335                     help='Participation role for the calendar user'),
336         'state': fields.selection([('needs-action', 'Needs Action'),
337                         ('tentative', 'Tentative'),
338                         ('declined', 'Declined'),
339                         ('accepted', 'Accepted'),
340                         ('delegated', 'Delegated')], 'Status', readonly=True, \
341                         help="Status of the attendee's participation"),
342         'rsvp':  fields.boolean('Required Reply?',
343                     help="Indicats whether the favor of a reply is requested"),
344         'delegated_to': fields.function(_compute_data, \
345                 string='Delegated To', type="char", size=124, store=True, \
346                 multi='delegated_to', help="The users that the original \
347 request was delegated to"),
348         'delegated_from': fields.function(_compute_data, string=\
349             'Delegated From', type="char", store=True, size=124, multi='delegated_from'),
350         'parent_ids': fields.many2many('calendar.attendee', 'calendar_attendee_parent_rel', \
351                                     'attendee_id', 'parent_id', 'Delegrated From'),
352         'child_ids': fields.many2many('calendar.attendee', 'calendar_attendee_child_rel', \
353                                       'attendee_id', 'child_id', 'Delegrated To'),
354         'sent_by': fields.function(_compute_data, string='Sent By', \
355                         type="char", multi='sent_by', store=True, size=124, \
356                         help="Specify the user that is acting on behalf of the calendar user"),
357         'sent_by_uid': fields.function(_compute_data, string='Sent By User', \
358                             type="many2one", relation="res.users", multi='sent_by_uid'),
359         'cn': fields.function(_compute_data, string='Common name', \
360                             type="char", size=124, multi='cn', store=True),
361         'dir': fields.char('URI Reference', size=124, help="Reference to the URI\
362 that points to the directory information corresponding to the attendee."),
363         'language': fields.function(_compute_data, string='Language', \
364                     type="selection", selection=_lang_get, multi='language', \
365                     store=True, help="To specify the language for text values in a\
366 property or property parameter."),
367         'user_id': fields.many2one('res.users', 'User'),
368         'partner_id': fields.many2one('res.partner', 'Contact'),
369         'email': fields.char('Email', size=124, help="Email of Invited Person"),
370         'event_date': fields.function(_compute_data, string='Event Date', \
371                             type="datetime", multi='event_date'),
372         'event_end_date': fields.function(_compute_data, \
373                             string='Event End Date', type="datetime", \
374                             multi='event_end_date'),
375         'ref': fields.reference('Event Ref', selection=_links_get, size=128),
376         'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
377     }
378
379     _defaults = {
380         'state': 'needs-action',
381         'role': 'req-participant',
382         'rsvp':  True,
383         'cutype': 'individual',
384     }
385
386     def copy(self, cr, uid, id, default=None, context=None):
387         raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
388
389     def get_ics_file(self, cr, uid, event_obj, context=None):
390         """
391         Returns iCalendar file for the event invitation
392         @param self: The object pointer
393         @param cr: the current row, from the database cursor,
394         @param uid: the current user’s ID for security checks,
395         @param event_obj: Event object (browse record)
396         @param context: A standard dictionary for contextual values
397         @return: .ics file content
398         """
399         res = None
400         def ics_datetime(idate, short=False):
401             if idate:
402                 if short or len(idate)<=10:
403                     return date.fromtimestamp(time.mktime(time.strptime(idate, '%Y-%m-%d')))
404                 else:
405                     return datetime.strptime(idate, '%Y-%m-%d %H:%M:%S')
406             else:
407                 return False
408         try:
409             # FIXME: why isn't this in CalDAV?
410             import vobject
411         except ImportError:
412             return res
413         cal = vobject.iCalendar()
414         event = cal.add('vevent')
415         if not event_obj.date_deadline or not event_obj.date:
416             raise osv.except_osv(_('Warning!'),_("First you have to specify the date of the invitation."))
417         event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S'))
418         event.add('dtstart').value = ics_datetime(event_obj.date)
419         event.add('dtend').value = ics_datetime(event_obj.date_deadline)
420         event.add('summary').value = event_obj.name
421         if  event_obj.description:
422             event.add('description').value = event_obj.description
423         if event_obj.location:
424             event.add('location').value = event_obj.location
425         if event_obj.rrule:
426             event.add('rrule').value = event_obj.rrule
427         if event_obj.organizer:
428             event_org = event.add('organizer')
429             event_org.params['CN'] = [event_obj.organizer]
430             event_org.value = 'MAILTO:' + (event_obj.organizer)
431         elif event_obj.user_id or event_obj.organizer_id:
432             event_org = event.add('organizer')
433             organizer = event_obj.organizer_id
434             if not organizer:
435                 organizer = event_obj.user_id
436             event_org.params['CN'] = [organizer.name]
437             event_org.value = 'MAILTO:' + (organizer.email or organizer.name)
438
439         if event_obj.alarm_id:
440             # computes alarm data
441             valarm = event.add('valarm')
442             alarm_object = self.pool.get('res.alarm')
443             alarm_data = alarm_object.read(cr, uid, event_obj.alarm_id.id, context=context)
444             # Compute trigger data
445             interval = alarm_data['trigger_interval']
446             occurs = alarm_data['trigger_occurs']
447             duration = (occurs == 'after' and alarm_data['trigger_duration']) \
448                                             or -(alarm_data['trigger_duration'])
449             related = alarm_data['trigger_related']
450             trigger = valarm.add('TRIGGER')
451             trigger.params['related'] = [related.upper()]
452             if interval == 'days':
453                 delta = timedelta(days=duration)
454             if interval == 'hours':
455                 delta = timedelta(hours=duration)
456             if interval == 'minutes':
457                 delta = timedelta(minutes=duration)
458             trigger.value = delta
459             # Compute other details
460             valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
461
462         for attendee in event_obj.attendee_ids:
463             attendee_add = event.add('attendee')
464             attendee_add.params['CUTYPE'] = [str(attendee.cutype)]
465             attendee_add.params['ROLE'] = [str(attendee.role)]
466             attendee_add.params['RSVP'] = [str(attendee.rsvp)]
467             attendee_add.value = 'MAILTO:' + (attendee.email or '')
468         res = cal.serialize()
469         return res
470
471     def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
472         """
473         Send mail for event invitation to event attendees.
474         @param 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                         content_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.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.email]
895                     for att in alarm.attendee_ids:
896                         mail_to.append(att.user_id.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         start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
945         if allday: # For all day event
946             duration = 24.0
947             value['duration'] = duration
948             # change start_date's time to 00:00:00 in the user's timezone
949             user = self.pool.get('res.users').browse(cr, uid, uid)
950             tz = pytz.timezone(user.tz) if user.tz else pytz.utc
951             start = pytz.utc.localize(start).astimezone(tz)     # convert start in user's timezone
952             start = start.replace(hour=0, minute=0, second=0)   # change start's time to 00:00:00
953             start = start.astimezone(pytz.utc)                  # convert start back to utc
954             start_date = start.strftime("%Y-%m-%d %H:%M:%S")
955             value['date'] = start_date
956
957         if end_date and not duration:
958             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
959             diff = end - start
960             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
961             value['duration'] = round(duration, 2)
962         elif not end_date:
963             end = start + timedelta(hours=duration)
964             value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
965         elif end_date and duration and not allday:
966             # we have both, keep them synchronized:
967             # set duration based on end_date (arbitrary decision: this avoid
968             # getting dates like 06:31:48 instead of 06:32:00)
969             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
970             diff = end - start
971             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
972             value['duration'] = round(duration, 2)
973
974         return {'value': value}
975
976     def unlink_events(self, cr, uid, ids, context=None):
977         """
978         This function deletes event which are linked with the event with recurrent_uid
979                 (Removes the events which refers to the same UID value)
980         """
981         if context is None:
982             context = {}
983         for event_id in ids:
984             cr.execute("select id from %s where recurrent_uid=%%s" % (self._table), (event_id,))
985             r_ids = map(lambda x: x[0], cr.fetchall())
986             self.unlink(cr, uid, r_ids, context=context)
987         return True
988
989     def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
990         """
991         Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
992         @param self: The object pointer
993         @param cr: the current row, from the database cursor,
994         @param id: List of calendar event's ids.
995         @param context: A standard dictionary for contextual values
996         @return: dictionary of rrule value.
997         """
998
999         result = {}
1000         if not isinstance(ids, list):
1001             ids = [ids]
1002
1003         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):
1004             event = datas['id']
1005             if datas.get('interval', 0) < 0:
1006                 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
1007             if datas.get('count', 0) < 0:
1008                 raise osv.except_osv(_('Warning!'), _('Count cannot be negative.'))
1009             if datas['recurrency']:
1010                 result[event] = self.compute_rule_string(datas)
1011             else:
1012                 result[event] = ""
1013         return result
1014
1015     def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1016         data = self._get_empty_rrule_data()
1017         if field_value:
1018             data['recurrency'] = True
1019             for event in self.browse(cr, uid, ids, context=context):
1020                 rdate = rule_date or event.date
1021                 update_data = self._parse_rrule(field_value, dict(data), rdate)
1022                 data.update(update_data)
1023                 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
1024         return True
1025
1026
1027     _columns = {
1028         'id': fields.integer('ID', readonly=True),
1029         'sequence': fields.integer('Sequence'),
1030         'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1031         'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True,),
1032         'date_deadline': fields.datetime('Deadline', states={'done': [('readonly', True)]}, required=True,),
1033         'create_date': fields.datetime('Created', readonly=True),
1034         'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1035         'description': fields.text('Description', states={'done': [('readonly', True)]}),
1036         'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1037              ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
1038         'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1039         'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1040                                                 'Show Time as', states={'done': [('readonly', True)]}),
1041         'base_calendar_url': fields.char('Caldav URL', size=264),
1042         'state': fields.selection([('tentative', 'Tentative'),
1043                         ('cancelled', 'Cancelled'),
1044                         ('confirmed', 'Confirmed'),
1045                         ], 'Status', readonly=True),
1046         'exdate': fields.text('Exception Date/Times', help="This property \
1047 defines the list of date/time exceptions for a recurring calendar component."),
1048         'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1049 rule or repeating pattern of time to exclude from the recurring rule."),
1050         'rrule': fields.function(_get_rulestring, type='char', size=124, \
1051                     fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1052         'rrule_type': fields.selection([('none', ''), ('daily', 'Daily'), \
1053                             ('weekly', 'Weekly'), ('monthly', 'Monthly'), \
1054                             ('yearly', 'Yearly'),],
1055                             'Recurrency', states={'done': [('readonly', True)]},
1056                             help="Let the event automatically repeat at that interval"),
1057         'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]},
1058                         help="Set an alarm at this time, before the event occurs" ),
1059         'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1060         'recurrent_uid': fields.integer('Recurrent ID'),
1061         'recurrent_id': fields.datetime('Recurrent ID date'),
1062         'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1063         'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1064         'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with Organizer Attribure of VEvent.
1065         'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1066         'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'),
1067         'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
1068         'count': fields.integer('Repeat', help="Repeat x times"),
1069         'mo': fields.boolean('Mon'),
1070         'tu': fields.boolean('Tue'),
1071         'we': fields.boolean('Wed'),
1072         'th': fields.boolean('Thu'),
1073         'fr': fields.boolean('Fri'),
1074         'sa': fields.boolean('Sat'),
1075         'su': fields.boolean('Sun'),
1076         'select1': fields.selection([('date', 'Date of month'),
1077                                     ('day', 'Day of month')], 'Option'),
1078         'day': fields.integer('Date of month'),
1079         'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), \
1080                                    ('WE', 'Wednesday'), ('TH', 'Thursday'), \
1081                                    ('FR', 'Friday'), ('SA', 'Saturday'), \
1082                                    ('SU', 'Sunday')], 'Weekday'),
1083         'byday': fields.selection([('1', 'First'), ('2', 'Second'), \
1084                                    ('3', 'Third'), ('4', 'Fourth'), \
1085                                    ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
1086         'month_list': fields.selection(months.items(), 'Month'),
1087         'end_date': fields.date('Repeat Until'),
1088         'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1089                                  'event_id', 'attendee_id', 'Attendees'),
1090         'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1091         'active': fields.boolean('Active', help="If the active field is set to \
1092          true, it will allow you to hide the event alarm information without removing it."),
1093         'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1094     }
1095
1096     def default_organizer(self, cr, uid, context=None):
1097         user_pool = self.pool.get('res.users')
1098         user = user_pool.browse(cr, uid, uid, context=context)
1099         res = user.name
1100         if user.email:
1101             res += " <%s>" %(user.email)
1102         return res
1103
1104     _defaults = {
1105             'end_type' : 'count',
1106             'count' : 1,
1107             'rrule_type' : 'none',
1108             'state': 'tentative',
1109             'class': 'public',
1110             'show_as': 'busy',
1111             'select1': 'date',
1112             'interval': 1,
1113             'active': 1,
1114             'user_id': lambda self, cr, uid, ctx: uid,
1115             'organizer': default_organizer,
1116     }
1117
1118     def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1119         """Gives virtual event ids for recurring events based on value of Recurrence Rule
1120         This method gives ids of dates that comes between start date and end date of calendar views
1121         @param self: The object pointer
1122         @param cr: the current row, from the database cursor,
1123         @param uid: the current user’s ID for security checks,
1124         @param limit: The Number of Results to Return """
1125         if not context:
1126             context = {}
1127
1128         result = []
1129         for data in super(calendar_event, self).read(cr, uid, select, context=context):
1130             if not data['rrule']:
1131                 result.append(data['id'])
1132                 continue
1133             event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1134 #                To check: If the start date is replace by event date .. the event date will be changed by that of calendar code
1135
1136             if not data['rrule']:
1137                 continue
1138
1139             exdate = data['exdate'] and data['exdate'].split(',') or []
1140             rrule_str = data['rrule']
1141             new_rrule_str = []
1142             rrule_until_date = False
1143             is_until = False
1144             for rule in rrule_str.split(';'):
1145                 name, value = rule.split('=')
1146                 if name == "UNTIL":
1147                     is_until = True
1148                     value = parser.parse(value)
1149                     rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1150                     value = value.strftime("%Y%m%d%H%M%S")
1151                 new_rule = '%s=%s' % (name, value)
1152                 new_rrule_str.append(new_rule)
1153             new_rrule_str = ';'.join(new_rrule_str)
1154             rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1155             for r_date in rdates:
1156                 ok = True
1157                 for arg in domain:
1158                     if arg[0] in ('date', 'date_deadline'):
1159                         if (arg[1]=='='):
1160                             ok = ok and r_date.strftime('%Y-%m-%d')==arg[2]
1161                         if (arg[1]=='>'):
1162                             ok = ok and r_date.strftime('%Y-%m-%d')>arg[2]
1163                         if (arg[1]=='<'):
1164                             ok = ok and r_date.strftime('%Y-%m-%d')<arg[2]
1165                         if (arg[1]=='>='):
1166                             ok = ok and r_date.strftime('%Y-%m-%d')>=arg[2]
1167                         if (arg[1]=='<='):
1168                             ok = ok and r_date.strftime('%Y-%m-%d')<=arg[2]
1169                 if not ok:
1170                     continue
1171                 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1172                 result.append(idval)
1173
1174         if isinstance(select, (str, int, long)):
1175             return ids and ids[0] or False
1176         else:
1177             ids = list(set(result))
1178         return ids
1179
1180     def compute_rule_string(self, datas):
1181         """
1182         Compute rule string according to value type RECUR of iCalendar from the values given.
1183         @param self: the object pointer
1184         @param datas: dictionary of freq and interval value.
1185         """
1186         def get_week_string(freq, datas):
1187             weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1188             if freq == 'weekly':
1189                 byday = map(lambda x: x.upper(), filter(lambda x: datas.get(x) and x in weekdays, datas))
1190                 if byday:
1191                     return ';BYDAY=' + ','.join(byday)
1192             return ''
1193
1194         def get_month_string(freq, datas):
1195             if freq == 'monthly':
1196                 if datas.get('select1')=='date' and (datas.get('day') < 1 or datas.get('day') > 31):
1197                     raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1198
1199                 if datas.get('select1')=='day':
1200                     return ';BYDAY=' + datas.get('byday') + datas.get('week_list')
1201                 elif datas.get('select1')=='date':
1202                     return ';BYMONTHDAY=' + str(datas.get('day'))
1203             return ''
1204
1205         def get_end_date(datas):
1206             if datas.get('end_date'):
1207                 datas['end_date_new'] = ''.join((re.compile('\d')).findall(datas.get('end_date'))) + 'T235959Z'
1208
1209             return (datas.get('end_type') == 'count' and (';COUNT=' + str(datas.get('count'))) or '') +\
1210                              ((datas.get('end_date_new') and datas.get('end_type') == 'end_date' and (';UNTIL=' + datas.get('end_date_new'))) or '')
1211
1212         freq=datas.get('rrule_type')
1213         if freq == 'none':
1214             return ''
1215
1216         interval_srting = datas.get('interval') and (';INTERVAL=' + str(datas.get('interval'))) or ''
1217
1218         return 'FREQ=' + freq.upper() + get_week_string(freq, datas) + interval_srting + get_end_date(datas) + get_month_string(freq, datas)
1219
1220     def _get_empty_rrule_data(self):
1221         return  {
1222             'byday' : False,
1223             'recurrency' : False,
1224             'end_date' : False,
1225             'rrule_type' : False,
1226             'select1' : False,
1227             'interval' : 0,
1228             'count' : False,
1229             'end_type' : False,
1230             'mo' : False,
1231             'tu' : False,
1232             'we' : False,
1233             'th' : False,
1234             'fr' : False,
1235             'sa' : False,
1236             'su' : False,
1237             'exrule' : False,
1238             'day' : False,
1239             'week_list' : False
1240         }
1241
1242     #def _write_rrule(self, cr, uid, ids, field_value, rule_date=False, context=None):
1243     #    data = self._get_empty_rrule_data()
1244     #
1245     #    if field_value:
1246     #        data['recurrency'] = True
1247     #        for event in self.browse(cr, uid, ids, context=context):
1248     #            rdate = rule_date or event.date
1249     #            update_data = self._parse_rrule(field_value, dict(data), rdate)
1250     #            data.update(update_data)
1251     #            #parse_rrule
1252     #            self.write(cr, uid, event.id, data, context=context)
1253
1254
1255     def _parse_rrule(self, rule, data, date_start):
1256         day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1257         rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1258         r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1259
1260         if r._freq > 0 and r._freq < 4:
1261             data['rrule_type'] = rrule_type[r._freq]
1262
1263         data['count'] = r._count
1264         data['interval'] = r._interval
1265         data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1266         #repeat weekly
1267         if r._byweekday:
1268             for i in xrange(0,7):
1269                 if i in r._byweekday:
1270                     data[day_list[i]] = True
1271             data['rrule_type'] = 'weekly'
1272         #repeat monthly bynweekday ((weekday, weeknumber), )
1273         if r._bynweekday:
1274             data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1275             data['byday'] = r._bynweekday[0][1]
1276             data['select1'] = 'day'
1277             data['rrule_type'] = 'monthly'
1278
1279         if r._bymonthday:
1280             data['day'] = r._bymonthday[0]
1281             data['select1'] = 'date'
1282             data['rrule_type'] = 'monthly'
1283
1284         #yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1285         if r._bymonth:
1286             data['interval'] = data['interval'] * 12
1287
1288         #FIXEME handle forever case
1289         #end of recurrence
1290         #in case of repeat for ever that we do not support right now
1291         if not (data.get('count') or data.get('end_date')):
1292             data['count'] = 100
1293         if data.get('count'):
1294             data['end_type'] = 'count'
1295         else:
1296             data['end_type'] = 'end_date'
1297         return data
1298
1299     def remove_virtual_id(self, ids):
1300         if isinstance(ids, (str, int, long)):
1301             return base_calendar_id2real_id(ids)
1302
1303         if isinstance(ids, (list, tuple)):
1304             res = []
1305             for id in ids:
1306                 res.append(base_calendar_id2real_id(id))
1307             return res
1308
1309     def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1310         context = context or {}
1311         args_without_date = []
1312         filter_date = []
1313
1314         for arg in args:
1315             if arg[0] == "id":
1316                 new_id = self.remove_virtual_id(arg[2])
1317                 new_arg = (arg[0], arg[1], new_id)
1318                 args_without_date.append(new_arg)
1319             elif arg[0] not in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1320                 args_without_date.append(arg)
1321             else:
1322                 if context.get('virtual_id', True):
1323                     args_without_date.append('|')
1324                 args_without_date.append(arg)
1325                 if context.get('virtual_id', True):
1326                     args_without_date.append(('recurrency','=',1))
1327                 filter_date.append(arg)
1328
1329         res = super(calendar_event, self).search(cr, uid, args_without_date, \
1330                                  0, 0, order, context, count=False)
1331         if context.get('virtual_id', True):
1332             res = self.get_recurrent_ids(cr, uid, res, args, limit, context=context)
1333
1334         if count:
1335             return len(res)
1336         elif limit:
1337             return res[offset:offset+limit]
1338         else:
1339             return res
1340
1341     def _get_data(self, cr, uid, id, context=None):
1342         res = self.read(cr, uid, [id],['date', 'date_deadline'])
1343         return res[0]
1344
1345     def need_to_update(self, event_id, vals):
1346         split_id = str(event_id).split("-")
1347         if len(split_id) < 2:
1348             return False
1349         else:
1350             date_start = vals.get('date', '')
1351             try:
1352                 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1353                 return date_start == split_id[1]
1354             except Exception:
1355                 return True
1356
1357
1358     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1359         context = context or {}
1360         if isinstance(ids, (str, int, long)):
1361             ids = [ids]
1362         res = False
1363
1364         # Special write of complex IDS
1365         for event_id in ids[:]:
1366             if len(str(event_id).split('-')) == 1:
1367                 continue
1368             ids.remove(event_id)
1369             real_event_id = base_calendar_id2real_id(event_id)
1370             if not vals.get('recurrency', True):
1371                 ids.append(real_event_id)
1372                 continue
1373
1374             #if edit one instance of a reccurrent id
1375             data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1376                                                 'rrule', 'duration', 'exdate'])
1377             if data.get('rrule'):
1378                 data.update(vals)
1379                 data.update({
1380                     'recurrent_uid': real_event_id,
1381                     'recurrent_id': data.get('date'),
1382                     'rrule_type': 'none',
1383                     'rrule': '',
1384                     'recurrency' : False,
1385                     })
1386
1387                 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1388
1389                 date_new = event_id.split('-')[1]
1390                 date_new = time.strftime("%Y%m%dT%H%M%S", \
1391                              time.strptime(date_new, "%Y%m%d%H%M%S"))
1392                 exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1393                 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1394
1395                 context.update({'active_id': new_id, 'active_ids': [new_id]})
1396                 continue
1397
1398         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1399             vals['vtimezone'] = vals['vtimezone'][40:]
1400
1401         res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1402
1403         if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1404                 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1405             alarm_obj = self.pool.get('res.alarm')
1406             alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1407         return res or True and False
1408
1409     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1410         if not context:
1411             context = {}
1412
1413         if 'date' in groupby:
1414             raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1415         virtual_id = context.get('virtual_id', True)
1416         context.update({'virtual_id': False})
1417         res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1418         for re in res:
1419             #remove the count, since the value is not consistent with the result of the search when expand the group
1420             for groupname in groupby:
1421                 if re.get(groupname + "_count"):
1422                     del re[groupname + "_count"]
1423             re.get('__context', {}).update({'virtual_id' : virtual_id})
1424         return res
1425
1426     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1427         # FIXME This whole id mangling has to go!
1428         if context is None:
1429             context = {}
1430         fields2 = fields and fields[:] or None
1431
1432         EXTRAFIELDS = ('class','user_id','duration')
1433         for f in EXTRAFIELDS:
1434             if fields and (f not in fields):
1435                 fields2.append(f)
1436
1437         if isinstance(ids, (str, int, long)):
1438             select = [ids]
1439         else:
1440             select = ids
1441         select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1442         result = []
1443
1444         real_data = super(calendar_event, self).read(cr, uid,
1445                     [real_id for base_calendar_id, real_id in select],
1446                     fields=fields2, context=context, load=load)
1447         real_data = dict(zip([x['id'] for x in real_data], real_data))
1448
1449         for base_calendar_id, real_id in select:
1450             res = real_data[real_id].copy()
1451             ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1452             if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1453                 res['date'] = ls[1]
1454                 res['date_deadline'] = ls[2]
1455             res['id'] = base_calendar_id
1456
1457             result.append(res)
1458
1459         for r in result:
1460             if r['user_id']:
1461                 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1462                 if user_id==uid:
1463                     continue
1464             if r['class']=='private':
1465                 for f in r.keys():
1466                     if f not in ('id','date','date_deadline','duration','user_id','state'):
1467                         r[f] = False
1468                     if f=='name':
1469                         r[f] = _('Busy')
1470
1471         for r in result:
1472             for k in EXTRAFIELDS:
1473                 if (k in r) and ((not fields) or (k not in fields)):
1474                     del r[k]
1475         if isinstance(ids, (str, int, long)):
1476             return result and result[0] or False
1477         return result
1478
1479     def copy(self, cr, uid, id, default=None, context=None):
1480         if context is None:
1481             context = {}
1482
1483         res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1484         alarm_obj = self.pool.get('res.alarm')
1485         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1486         return res
1487
1488     def unlink(self, cr, uid, ids, context=None):
1489         if not isinstance(ids, list):
1490             ids = [ids]
1491         res = False
1492         attendee_obj=self.pool.get('calendar.attendee')
1493         for event_id in ids[:]:
1494             if len(str(event_id).split('-')) == 1:
1495                 continue
1496
1497             real_event_id = base_calendar_id2real_id(event_id)
1498             data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1499             date_new = event_id.split('-')[1]
1500             date_new = time.strftime("%Y%m%dT%H%M%S", \
1501                          time.strptime(date_new, "%Y%m%d%H%M%S"))
1502             exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1503             self.write(cr, uid, [real_event_id], {'exdate': exdate})
1504             ids.remove(event_id)
1505         for event in self.browse(cr, uid, ids, context=context):
1506             if event.attendee_ids:
1507                 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1508
1509         res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1510         self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1511         self.unlink_events(cr, uid, ids, context=context)
1512         return res
1513
1514
1515     def create(self, cr, uid, vals, context=None):
1516         if context is None:
1517             context = {}
1518
1519         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1520             vals['vtimezone'] = vals['vtimezone'][40:]
1521
1522         #updated_vals = self.onchange_dates(cr, uid, [],
1523         #    vals.get('date', False),
1524         #    vals.get('duration', False),
1525         #    vals.get('date_deadline', False),
1526         #    vals.get('allday', False),
1527         #    context=context)
1528         #vals.update(updated_vals.get('value', {}))
1529
1530         res = super(calendar_event, self).create(cr, uid, vals, context)
1531         alarm_obj = self.pool.get('res.alarm')
1532         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1533         return res
1534
1535     def do_tentative(self, cr, uid, ids, context=None, *args):
1536         """ Makes event invitation as Tentative
1537         @param self: The object pointer
1538         @param cr: the current row, from the database cursor,
1539         @param uid: the current user’s ID for security checks,
1540         @param ids: List of Event IDs
1541         @param *args: Get Tupple value
1542         @param context: A standard dictionary for contextual values
1543         """
1544         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1545
1546     def do_cancel(self, cr, uid, ids, context=None, *args):
1547         """ Makes event invitation as Tentative
1548         @param self: The object pointer
1549         @param cr: the current row, from the database cursor,
1550         @param uid: the current user’s ID for security checks,
1551         @param ids: List of Event IDs
1552         @param *args: Get Tupple value
1553         @param context: A standard dictionary for contextual values
1554         """
1555         return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1556
1557     def do_confirm(self, cr, uid, ids, context=None, *args):
1558         """ Makes event invitation as Tentative
1559         @param self: The object pointer
1560         @param cr: the current row, from the database cursor,
1561         @param uid: the current user’s ID for security checks,
1562         @param ids: List of Event IDs
1563         @param *args: Get Tupple value
1564         @param context: A standard dictionary for contextual values
1565         """
1566         return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1567
1568 calendar_event()
1569
1570 class calendar_todo(osv.osv):
1571     """ Calendar Task """
1572
1573     _name = "calendar.todo"
1574     _inherit = "calendar.event"
1575     _description = "Calendar Task"
1576
1577     def _get_date(self, cr, uid, ids, name, arg, context=None):
1578         """
1579         Get Date
1580         @param self: The object pointer
1581         @param cr: the current row, from the database cursor,
1582         @param uid: the current user’s ID for security checks,
1583         @param ids: List of calendar todo's IDs.
1584         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1585         @param context: A standard dictionary for contextual values
1586         """
1587
1588         res = {}
1589         for event in self.browse(cr, uid, ids, context=context):
1590             res[event.id] = event.date_start
1591         return res
1592
1593     def _set_date(self, cr, uid, id, name, value, arg, context=None):
1594         """
1595         Set Date
1596         @param self: The object pointer
1597         @param cr: the current row, from the database cursor,
1598         @param uid: the current user’s ID for security checks,
1599         @param id: calendar's ID.
1600         @param value: Get Value
1601         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1602         @param context: A standard dictionary for contextual values
1603         """
1604
1605         assert name == 'date'
1606         return self.write(cr, uid, id, { 'date_start': value }, context=context)
1607
1608     _columns = {
1609         'date': fields.function(_get_date, fnct_inv=_set_date, \
1610                             string='Duration', store=True, type='datetime'),
1611         'duration': fields.integer('Duration'),
1612     }
1613
1614     __attribute__ = {}
1615
1616
1617 calendar_todo()
1618
1619 class ir_attachment(osv.osv):
1620     _name = 'ir.attachment'
1621     _inherit = 'ir.attachment'
1622
1623     def search_count(self, cr, user, args, context=None):
1624         new_args = []
1625         for domain_item in args:
1626             if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
1627                 new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
1628             else:
1629                 new_args.append(domain_item)
1630         return super(ir_attachment, self).search_count(cr, user, new_args, context)
1631
1632     def create(self, cr, uid, vals, context=None):
1633         if context:
1634             id = context.get('default_res_id', False)
1635             context.update({'default_res_id' : base_calendar_id2real_id(id)})
1636         return super(ir_attachment, self).create(cr, uid, vals, context=context)
1637
1638     def search(self, cr, uid, args, offset=0, limit=None, order=None,
1639             context=None, count=False):
1640         new_args = []
1641         for domain_item in args:
1642             if isinstance(domain_item, (list, tuple)) and len(domain_item) == 3 and domain_item[0] == 'res_id':
1643                 new_args.append((domain_item[0], domain_item[1], base_calendar_id2real_id(domain_item[2])))
1644             else:
1645                 new_args.append(domain_item)
1646         return super(ir_attachment, self).search(cr, uid, new_args, offset=offset,
1647                             limit=limit, order=order, context=context, count=False)
1648 ir_attachment()
1649
1650 class ir_values(osv.osv):
1651     _inherit = 'ir.values'
1652
1653     def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1654             isobject=False, meta=False, preserve_user=False, company=False):
1655         """
1656         Set IR Values
1657         @param self: The object pointer
1658         @param cr: the current row, from the database cursor,
1659         @param uid: the current user’s ID for security checks,
1660         @param model: Get The Model
1661         """
1662
1663         new_model = []
1664         for data in models:
1665             if type(data) in (list, tuple):
1666                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1667             else:
1668                 new_model.append(data)
1669         return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1670                     value, replace, isobject, meta, preserve_user, company)
1671
1672     def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1673              res_id_req=False, without_user=True, key2_req=True):
1674         """
1675         Get IR Values
1676         @param self: The object pointer
1677         @param cr: the current row, from the database cursor,
1678         @param uid: the current user’s ID for security checks,
1679         @param model: Get The Model
1680         """
1681         if context is None:
1682             context = {}
1683         new_model = []
1684         for data in models:
1685             if type(data) in (list, tuple):
1686                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1687             else:
1688                 new_model.append(data)
1689         return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1690                          meta, context, res_id_req, without_user, key2_req)
1691
1692 ir_values()
1693
1694 class ir_model(osv.osv):
1695
1696     _inherit = 'ir.model'
1697
1698     def read(self, cr, uid, ids, fields=None, context=None,
1699             load='_classic_read'):
1700         """
1701         Overrides orm read method.
1702         @param self: The object pointer
1703         @param cr: the current row, from the database cursor,
1704         @param uid: the current user’s ID for security checks,
1705         @param ids: List of IR Model’s IDs.
1706         @param context: A standard dictionary for contextual values
1707         """
1708         new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1709         if context is None:
1710             context = {}
1711         data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1712                         context=context, load=load)
1713         if data:
1714             for val in data:
1715                 val['id'] = base_calendar_id2real_id(val['id'])
1716         return isinstance(ids, (str, int, long)) and data[0] or data
1717
1718 ir_model()
1719
1720 class virtual_report_spool(web_services.report_spool):
1721
1722     def exp_report(self, db, uid, object, ids, datas=None, context=None):
1723         """
1724         Export Report
1725         @param self: The object pointer
1726         @param db: get the current database,
1727         @param uid: the current user’s ID for security checks,
1728         @param context: A standard dictionary for contextual values
1729         """
1730
1731         if object == 'printscreen.list':
1732             return super(virtual_report_spool, self).exp_report(db, uid, \
1733                             object, ids, datas, context)
1734         new_ids = []
1735         for id in ids:
1736             new_ids.append(base_calendar_id2real_id(id))
1737         if datas.get('id', False):
1738             datas['id'] = base_calendar_id2real_id(datas['id'])
1739         return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, datas, context)
1740
1741 virtual_report_spool()
1742
1743 class res_users(osv.osv):
1744     _inherit = 'res.users'
1745
1746     def _get_user_avail(self, cr, uid, ids, context=None):
1747         """
1748         Get User Availability
1749         @param self: The object pointer
1750         @param cr: the current row, from the database cursor,
1751         @param uid: the current user’s ID for security checks,
1752         @param ids: List of res user’s IDs.
1753         @param context: A standard dictionary for contextual values
1754         """
1755
1756         current_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1757         res = {}
1758         attendee_obj = self.pool.get('calendar.attendee')
1759         attendee_ids = attendee_obj.search(cr, uid, [
1760                     ('event_date', '<=', current_datetime), ('event_end_date', '<=', current_datetime),
1761                     ('state', '=', 'accepted'), ('user_id', 'in', ids)
1762                     ])
1763
1764         for attendee_data in attendee_obj.read(cr, uid, attendee_ids, ['user_id']):
1765             user_id = attendee_data['user_id']
1766             status = 'busy'
1767             res.update({user_id:status})
1768
1769         #TOCHECK: Delegated Event
1770         for user_id in ids:
1771             if user_id not in res:
1772                 res[user_id] = 'free'
1773
1774         return res
1775
1776     def _get_user_avail_fun(self, cr, uid, ids, name, args, context=None):
1777         """
1778         Get User Availability Function
1779         @param self: The object pointer
1780         @param cr: the current row, from the database cursor,
1781         @param uid: the current user’s ID for security checks,
1782         @param ids: List of res user’s IDs.
1783         @param context: A standard dictionary for contextual values
1784         """
1785
1786         return self._get_user_avail(cr, uid, ids, context=context)
1787
1788     _columns = {
1789             'availability': fields.function(_get_user_avail_fun, type='selection', \
1790                     selection=[('free', 'Free'), ('busy', 'Busy')], \
1791                     string='Free/Busy'),
1792     }
1793
1794 res_users()
1795
1796
1797 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: