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