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