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