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