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