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