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