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