[MERGE] Fix the pickle problem
[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.address_id.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, method=True, \
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, method=True, 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, method=True, 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, method=True, string='Sent By User', \
358                             type="many2one", relation="res.users", multi='sent_by_uid'),
359         'cn': fields.function(_compute_data, method=True, 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, method=True, 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, method=True, string='Event Date', \
373                             type="datetime", multi='event_date'),
374         'event_end_date': fields.function(_compute_data, method=True, \
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!'), _('Can not Duplicate'))
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         for att in self.browse(cr, uid, ids, context=context):
488             sign = att.sent_by_uid and att.sent_by_uid.signature or ''
489             sign = '<br>'.join(sign and sign.split('\n') or [])
490             res_obj = att.ref
491             if res_obj:
492                 att_infos = []
493                 sub = res_obj.name
494                 other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
495
496                 for att2 in self.browse(cr, uid, other_invitation_ids):
497                     att_infos.append(((att2.user_id and att2.user_id.name) or \
498                                  (att2.partner_id and att2.partner_id.name) or \
499                                     att2.email) + ' - Status: ' + att2.state.title())
500                 body_vals = {'name': res_obj.name,
501                             'start_date': res_obj.date,
502                             'end_date': res_obj.date_deadline or False,
503                             'description': res_obj.description or '-',
504                             'location': res_obj.location or '-',
505                             'attendees': '<br>'.join(att_infos),
506                             'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
507                             'sign': sign,
508                             'company': company
509                 }
510                 body = html_invitation % body_vals
511                 if mail_to and email_from:
512                     attach = self.get_ics_file(cr, uid, res_obj, context=context)
513                     tools.email_send(
514                         email_from,
515                         mail_to,
516                         sub,
517                         body,
518                         attach=attach and [('invitation.ics', attach)] or None,
519                         subtype='html',
520                         reply_to=email_from
521                     )
522             return True
523
524     def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
525         """
526         Make entry on email and availbility on change of user_id field.
527         @param cr: the current row, from the database cursor,
528         @param uid: the current user’s ID for security checks,
529         @param ids: List of calendar attendee’s IDs.
530         @param user_id: Changed value of User id
531         @return: dictionary of value. which put value in email and availability fields.
532         """
533
534         if not user_id:
535             return {'value': {'email': ''}}
536         usr_obj = self.pool.get('res.users')
537         user = usr_obj.browse(cr, uid, user_id, *args)
538         return {'value': {'email': user.address_id.email, 'availability':user.availability}}
539
540     def do_tentative(self, cr, uid, ids, context=None, *args):
541         """ Makes event invitation as Tentative
542         @param self: The object pointer
543         @param cr: the current row, from the database cursor,
544         @param uid: the current user’s ID for security checks,
545         @param ids: List of calendar attendee’s IDs
546         @param *args: Get Tupple value
547         @param context: A standard dictionary for contextual values
548         """
549         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
550
551     def do_accept(self, cr, uid, ids, context=None, *args):
552         """
553         Update state of invitation as Accepted and
554         if the invited user is other then event user it will make a copy of this event for invited user
555         @param cr: the current row, from the database cursor,
556         @param uid: the current user’s ID for security checks,
557         @param ids: List of calendar attendee’s IDs.
558         @param context: A standard dictionary for contextual values
559         @return: True
560         """
561         if context is None:
562             context = {}
563
564         for vals in self.browse(cr, uid, ids, context=context):
565             if vals.ref and vals.ref.user_id:
566                 mod_obj = self.pool.get(vals.ref._name)
567                 defaults = {'user_id': vals.user_id.id, 'organizer_id': vals.ref.user_id.id}
568                 mod_obj.copy(cr, uid, vals.ref.id, default=defaults, context=context)
569             self.write(cr, uid, vals.id, {'state': 'accepted'}, context)
570
571         return True
572
573     def do_decline(self, cr, uid, ids, context=None, *args):
574         """ Marks event invitation as Declined
575         @param self: The object pointer
576         @param cr: the current row, from the database cursor,
577         @param uid: the current user’s ID for security checks,
578         @param ids: List of calendar attendee’s IDs
579         @param *args: Get Tupple value
580         @param context: A standard dictionary for contextual values """
581         if context is None:
582             context = {}
583         return self.write(cr, uid, ids, {'state': 'declined'}, context)
584
585     def create(self, cr, uid, vals, context=None):
586         """ Overrides orm create method.
587         @param self: The object pointer
588         @param cr: the current row, from the database cursor,
589         @param uid: the current user’s ID for security checks,
590         @param vals: Get Values
591         @param context: A standard dictionary for contextual values """
592
593         if context is None:
594             context = {}
595         if not vals.get("email") and vals.get("cn"):
596             cnval = vals.get("cn").split(':')
597             email = filter(lambda x:x.__contains__('@'), cnval)
598             vals['email'] = email and email[0] or ''
599             vals['cn'] = vals.get("cn")
600         res = super(calendar_attendee, self).create(cr, uid, vals, context)
601         return res
602 calendar_attendee()
603
604 class res_alarm(osv.osv):
605     """Resource Alarm """
606     _name = 'res.alarm'
607     _description = 'Basic Alarm Information'
608
609     _columns = {
610         'name':fields.char('Name', size=256, required=True),
611         'trigger_occurs': fields.selection([('before', 'Before'), \
612                                             ('after', 'After')], \
613                                         'Triggers', required=True),
614         'trigger_interval': fields.selection([('minutes', 'Minutes'), \
615                                                 ('hours', 'Hours'), \
616                                                 ('days', 'Days')], 'Interval', \
617                                                 required=True),
618         'trigger_duration': fields.integer('Duration', required=True),
619         'trigger_related': fields.selection([('start', 'The event starts'), \
620                                             ('end', 'The event ends')], \
621                                             'Related to', required=True),
622         'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \
623 are both optional, but if one occurs, so MUST the other"""),
624         'repeat': fields.integer('Repeat'),
625         'active': fields.boolean('Active', help="If the active field is set to \
626 true, it will allow you to hide the event alarm information without removing it.")
627     }
628     _defaults = {
629         'trigger_interval': 'minutes',
630         'trigger_duration': 5,
631         'trigger_occurs': 'before',
632         'trigger_related': 'start',
633         'active': 1,
634     }
635
636     def do_alarm_create(self, cr, uid, ids, model, date, context=None):
637         """
638         Create Alarm for event.
639         @param cr: the current row, from the database cursor,
640         @param uid: the current user’s ID for security checks,
641         @param ids: List of res alarm’s IDs.
642         @param model: Model name.
643         @param date: Event date
644         @param context: A standard dictionary for contextual values
645         @return: True
646         """
647         if context is None:
648             context = {}
649         alarm_obj = self.pool.get('calendar.alarm')
650         res_alarm_obj = self.pool.get('res.alarm')
651         ir_obj = self.pool.get('ir.model')
652         model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
653
654         model_obj = self.pool.get(model)
655         for data in model_obj.browse(cr, uid, ids, context=context):
656
657             basic_alarm = data.alarm_id
658             cal_alarm = data.base_calendar_alarm_id
659             if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm):
660                 new_res_alarm = None
661                 # Find for existing res.alarm
662                 duration = cal_alarm.trigger_duration
663                 interval = cal_alarm.trigger_interval
664                 occurs = cal_alarm.trigger_occurs
665                 related = cal_alarm.trigger_related
666                 domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)]
667                 alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context)
668                 if not alarm_ids:
669                     val = {
670                             'trigger_duration': duration,
671                             'trigger_interval': interval,
672                             'trigger_occurs': occurs,
673                             'trigger_related': related,
674                             'name': str(duration) + ' ' + str(interval) + ' '  + str(occurs)
675                            }
676                     new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context)
677                 else:
678                     new_res_alarm = alarm_ids[0]
679                 cr.execute('UPDATE %s ' % model_obj._table + \
680                             ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \
681                             ' WHERE id=%s',
682                             (cal_alarm.id, new_res_alarm, data.id))
683
684             self.do_alarm_unlink(cr, uid, [data.id], model)
685             if basic_alarm:
686                 vals = {
687                     'action': 'display',
688                     'description': data.description,
689                     'name': data.name,
690                     'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))],
691                     'trigger_related': basic_alarm.trigger_related,
692                     'trigger_duration': basic_alarm.trigger_duration,
693                     'trigger_occurs': basic_alarm.trigger_occurs,
694                     'trigger_interval': basic_alarm.trigger_interval,
695                     'duration': basic_alarm.duration,
696                     'repeat': basic_alarm.repeat,
697                     'state': 'run',
698                     'event_date': data[date],
699                     'res_id': data.id,
700                     'model_id': model_id,
701                     'user_id': uid
702                  }
703                 alarm_id = alarm_obj.create(cr, uid, vals)
704                 cr.execute('UPDATE %s ' % model_obj._table + \
705                             ' SET base_calendar_alarm_id=%s, alarm_id=%s '
706                             ' WHERE id=%s', \
707                             ( alarm_id, basic_alarm.id, data.id) )
708         return True
709
710     def do_alarm_unlink(self, cr, uid, ids, model, context=None):
711         """
712         Delete alarm specified in ids
713         @param cr: the current row, from the database cursor,
714         @param uid: the current user’s ID for security checks,
715         @param ids: List of res alarm’s IDs.
716         @param model: Model name for which alarm is to be cleared.
717         @return: True
718         """
719         if context is None:
720             context = {}
721         alarm_obj = self.pool.get('calendar.alarm')
722         ir_obj = self.pool.get('ir.model')
723         model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
724         model_obj = self.pool.get(model)
725         for datas in model_obj.browse(cr, uid, ids, context=context):
726             alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', datas.id)])
727             if alarm_ids:
728                 alarm_obj.unlink(cr, uid, alarm_ids)
729                 cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\
730                             where id=%%s' % model_obj._table,(datas.id,))
731         return True
732
733 res_alarm()
734
735 class calendar_alarm(osv.osv):
736     _name = 'calendar.alarm'
737     _description = 'Event alarm information'
738     _inherit = 'res.alarm'
739     __attribute__ = {}
740
741     _columns = {
742         'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'),
743         'name': fields.char('Summary', size=124, help="""Contains the text to be \
744                      used as the message subject for email \
745                      or contains the text to be used for display"""),
746         'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \
747                 ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \
748                 required=True, help="Defines the action to be invoked when an alarm is triggered"),
749         'description': fields.text('Description', help='Provides a more complete \
750                             description of the calendar component, than that \
751                             provided by the "SUMMARY" property'),
752         'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \
753                                       'alarm_id', 'attendee_id', 'Attendees', readonly=True),
754         'attach': fields.binary('Attachment', help="""* Points to a sound resource,\
755                      which is rendered when the alarm is triggered for audio,
756                     * File which is intended to be sent as message attachments for email,
757                     * Points to a procedure resource, which is invoked when\
758                       the alarm is triggered for procedure."""),
759         'res_id': fields.integer('Resource ID'),
760         'model_id': fields.many2one('ir.model', 'Model'),
761         'user_id': fields.many2one('res.users', 'Owner'),
762         'event_date': fields.datetime('Event Date'),
763         'event_end_date': fields.datetime('Event End Date'),
764         'trigger_date': fields.datetime('Trigger Date', readonly="True"),
765         'state':fields.selection([
766                     ('draft', 'Draft'),
767                     ('run', 'Run'),
768                     ('stop', 'Stop'),
769                     ('done', 'Done'),
770                 ], 'State', select=True, readonly=True),
771      }
772
773     _defaults = {
774         'action': 'email',
775         'state': 'run',
776      }
777
778     def create(self, cr, uid, vals, context=None):
779         """
780         Overrides orm create method.
781         @param self: The object pointer
782         @param cr: the current row, from the database cursor,
783         @param vals: dictionary of fields value.{‘name_of_the_field’: value, ...}
784         @param context: A standard dictionary for contextual values
785         @return: new record id for calendar_alarm.
786         """
787         if context is None:
788             context = {}
789         event_date = vals.get('event_date', False)
790         if event_date:
791             dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S")
792             if vals['trigger_interval'] == 'days':
793                 delta = timedelta(days=vals['trigger_duration'])
794             if vals['trigger_interval'] == 'hours':
795                 delta = timedelta(hours=vals['trigger_duration'])
796             if vals['trigger_interval'] == 'minutes':
797                 delta = timedelta(minutes=vals['trigger_duration'])
798             trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta)
799             vals['trigger_date'] = trigger_date
800         res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
801         return res
802
803     def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
804                        context=None):
805         """Scheduler for event reminder
806         @param self: The object pointer
807         @param cr: the current row, from the database cursor,
808         @param uid: the current user’s ID for security checks,
809         @param ids: List of calendar alarm’s IDs.
810         @param use_new_cursor: False or the dbname
811         @param context: A standard dictionary for contextual values
812         """
813         return True # XXX FIXME REMOVE THIS AFTER FIXING get_recurrent_dates!!
814         if context is None:
815             context = {}
816         current_datetime = datetime.now()
817         request_obj = self.pool.get('res.request')
818         alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
819
820         mail_to = []
821
822         for alarm in self.browse(cr, uid, alarm_ids, context=context):
823             next_trigger_date = None
824             update_vals = {}
825             model_obj = self.pool.get(alarm.model_id.model)
826             res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
827             re_dates = []
828
829             if res_obj.rrule:
830                 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
831                 recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.exdate, event_date, res_obj.exrule)
832
833                 trigger_interval = alarm.trigger_interval
834                 if trigger_interval == 'days':
835                     delta = timedelta(days=alarm.trigger_duration)
836                 if trigger_interval == 'hours':
837                     delta = timedelta(hours=alarm.trigger_duration)
838                 if trigger_interval == 'minutes':
839                     delta = timedelta(minutes=alarm.trigger_duration)
840                 delta = alarm.trigger_occurs == 'after' and delta or -delta
841
842                 for rdate in recurrent_dates:
843                     if rdate + delta > current_datetime:
844                         break
845                     if rdate + delta <= current_datetime:
846                         re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
847                 rest_dates = recurrent_dates[len(re_dates):]
848                 next_trigger_date = rest_dates and rest_dates[0] or None
849
850             else:
851                 re_dates = [alarm.trigger_date]
852
853             for r_date in re_dates:
854                 ref = alarm.model_id.model + ',' + str(alarm.res_id)
855
856                 # search for alreay sent requests
857                 if request_obj.search(cr, uid, [('trigger_date', '=', r_date), ('ref_doc1', '=', ref)], context=context):
858                     continue
859
860                 if alarm.action == 'display':
861                     value = {
862                        'name': alarm.name,
863                        'act_from': alarm.user_id.id,
864                        'act_to': alarm.user_id.id,
865                        'body': alarm.description,
866                        'trigger_date': r_date,
867                        'ref_doc1': ref
868                     }
869                     request_id = request_obj.create(cr, uid, value)
870                     request_ids = [request_id]
871                     for attendee in res_obj.attendee_ids:
872                         if attendee.user_id:
873                             value['act_to'] = attendee.user_id.id
874                             request_id = request_obj.create(cr, uid, value)
875                             request_ids.append(request_id)
876                     request_obj.request_send(cr, uid, request_ids)
877
878                 if alarm.action == 'email':
879                     sub = '[Openobject Reminder] %s' % (alarm.name)
880                     body = """
881 Event: %s
882 Event Date: %s
883 Description: %s
884
885 From:
886       %s
887
888 ----
889 %s
890
891 """  % (alarm.name, alarm.trigger_date, alarm.description, \
892                         alarm.user_id.name, alarm.user_id.signature)
893                     mail_to = [alarm.user_id.address_id.email]
894                     for att in alarm.attendee_ids:
895                         mail_to.append(att.user_id.address_id.email)
896                     if mail_to:
897                         tools.email_send(
898                             tools.config.get('email_from', False),
899                             mail_to,
900                             sub,
901                             body
902                         )
903             if next_trigger_date:
904                 update_vals.update({'trigger_date': next_trigger_date})
905             else:
906                 update_vals.update({'state': 'done'})
907             self.write(cr, uid, [alarm.id], update_vals)
908         return True
909
910 calendar_alarm()
911
912
913 class calendar_event(osv.osv):
914     _name = "calendar.event"
915     _description = "Calendar Event"
916     __attribute__ = {}
917
918     def _tz_get(self, cr, uid, context=None):
919         return [(x.lower(), x) for x in pytz.all_timezones]
920
921     def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
922         """Returns duration and/or end date based on values passed
923         @param self: The object pointer
924         @param cr: the current row, from the database cursor,
925         @param uid: the current user’s ID for security checks,
926         @param ids: List of calendar event’s IDs.
927         @param start_date: Starting date
928         @param duration: Duration between start date and end date
929         @param end_date: Ending Datee
930         @param context: A standard dictionary for contextual values
931         """
932         if context is None:
933             context = {}
934
935         value = {}
936         if not start_date:
937             return value
938         if not end_date and not duration:
939             duration = 1.00
940             value['duration'] = duration
941
942         if allday: # For all day event
943             value = {'duration': 24.0}
944             duration = 24.0
945             if start_date:
946                 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
947                 start_date = datetime.strftime(datetime(start.year, start.month, start.day, 0,0,0), "%Y-%m-%d %H:%M:%S")
948                 value['date'] = start_date
949
950
951         start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
952         if end_date and not duration:
953             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
954             diff = end - start
955             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
956             value['duration'] = round(duration, 2)
957         elif not end_date:
958             end = start + timedelta(hours=duration)
959             value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
960         elif end_date and duration and not allday:
961             # we have both, keep them synchronized:
962             # set duration based on end_date (arbitrary decision: this avoid
963             # getting dates like 06:31:48 instead of 06:32:00)
964             end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
965             diff = end - start
966             duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
967             value['duration'] = round(duration, 2)
968
969         return {'value': value}
970
971     def unlink_events(self, cr, uid, ids, context=None):
972         """
973         This function deletes event which are linked with the event with recurrent_uid
974                 (Removes the events which refers to the same UID value)
975         """
976         if context is None:
977             context = {}
978         for event_id in ids:
979             cr.execute("select id from %s where recurrent_uid=%%s" % (self._table), (event_id,))
980             r_ids = map(lambda x: x[0], cr.fetchall())
981             self.unlink(cr, uid, r_ids, context=context)
982         return True
983
984     def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
985         """
986         Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
987         @param self: The object pointer
988         @param cr: the current row, from the database cursor,
989         @param id: List of calendar event's ids.
990         @param context: A standard dictionary for contextual values
991         @return: dictionary of rrule value.
992         """
993         
994         result = {}
995         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):
996             event = datas['id']
997             if datas.get('interval', 0) < 0:
998                 raise osv.except_osv(_('Warning!'), _('Interval can not be Negative'))
999             if datas.get('count', 0) < 0:
1000                 raise osv.except_osv(_('Warning!'), _('Count can not be Negative'))
1001             result[event] = self.compute_rule_string(datas)
1002         return result
1003
1004     _columns = {
1005         'id': fields.integer('ID'),
1006         'sequence': fields.integer('Sequence'),
1007         'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1008         'date': fields.datetime('Date', states={'done': [('readonly', True)]}),
1009         'date_deadline': fields.datetime('Deadline', states={'done': [('readonly', True)]}),
1010         'create_date': fields.datetime('Created', readonly=True),
1011         'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1012         'description': fields.text('Description', states={'done': [('readonly', True)]}),
1013         'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1014              ('confidential', 'Confidential')], 'Mark as', states={'done': [('readonly', True)]}),
1015         'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1016         'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1017                                                 'Show as', states={'done': [('readonly', True)]}),
1018         'base_calendar_url': fields.char('Caldav URL', size=264),
1019         'state': fields.selection([('tentative', 'Tentative'),
1020                         ('confirmed', 'Confirmed'),
1021                         ('cancelled', 'Cancelled')], 'State', readonly=True),
1022         'exdate': fields.text('Exception Date/Times', help="This property \
1023 defines the list of date/time exceptions for a recurring calendar component."),
1024         'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1025 rule or repeating pattern of time to exclude from the recurring rule."),
1026         'rrule': fields.function(_get_rulestring, type='char', size=124, method=True, \
1027                                     string='Recurrent Rule'),
1028         'rrule_type': fields.selection([('none', ''), ('daily', 'Daily'), \
1029                             ('weekly', 'Weekly'), ('monthly', 'Monthly'), \
1030                             ('yearly', 'Yearly'),],
1031                             'Recurrency', states={'done': [('readonly', True)]},
1032                             help="Let the event automatically repeat at that interval"),
1033         'alarm_id': fields.many2one('res.alarm', 'Alarm', states={'done': [('readonly', True)]},
1034                         help="Set an alarm at this time, before the event occurs" ),
1035         'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1036         'recurrent_uid': fields.integer('Recurrent ID'),
1037         'recurrent_id': fields.datetime('Recurrent ID date'),
1038         'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1039         'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1040         'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with Organizer Attribure of VEvent.
1041         'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1042         'end_type' : fields.selection([('count', 'Fix amout of times'), ('end_date','End date')], 'Way to end reccurency'),
1043         'interval': fields.integer('Repeat every', help="Repeat every (Days/Week/Month/Year)"),
1044         'count': fields.integer('Repeat', help="Repeat x times"),
1045         'mo': fields.boolean('Mon'),
1046         'tu': fields.boolean('Tue'),
1047         'we': fields.boolean('Wed'),
1048         'th': fields.boolean('Thu'),
1049         'fr': fields.boolean('Fri'),
1050         'sa': fields.boolean('Sat'),
1051         'su': fields.boolean('Sun'),
1052         'select1': fields.selection([('date', 'Date of month'),
1053                                     ('day', 'Day of month')], 'Option'),
1054         'day': fields.integer('Date of month'),
1055         'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), \
1056                                    ('WE', 'Wednesday'), ('TH', 'Thursday'), \
1057                                    ('FR', 'Friday'), ('SA', 'Saturday'), \
1058                                    ('SU', 'Sunday')], 'Weekday'),
1059         'byday': fields.selection([('1', 'First'), ('2', 'Second'), \
1060                                    ('3', 'Third'), ('4', 'Fourth'), \
1061                                    ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
1062         'month_list': fields.selection(months.items(), 'Month'),
1063         'end_date': fields.date('Repeat Until'),
1064         'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1065                                  'event_id', 'attendee_id', 'Attendees'),
1066         'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1067         'active': fields.boolean('Active', help="If the active field is set to \
1068          true, it will allow you to hide the event alarm information without removing it."),
1069         'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1070         'edit_all': fields.boolean('Edit All', help="Edit all Occurrences  of recurrent Meeting."),
1071     }
1072     def default_organizer(self, cr, uid, context=None):
1073         user_pool = self.pool.get('res.users')
1074         user = user_pool.browse(cr, uid, uid, context=context)
1075         res = user.name
1076         if user.user_email:
1077             res += " <%s>" %(user.user_email)
1078         return res
1079
1080     _defaults = {
1081             'end_type' : 'count',
1082             'count' : 1,
1083             'rrule_type' : 'none',
1084             'state': 'tentative',
1085             'class': 'public',
1086             'show_as': 'busy',
1087             'select1': 'date',
1088             'interval': 1,
1089             'active': 1,
1090             'user_id': lambda self, cr, uid, ctx: uid,
1091             'organizer': default_organizer,
1092             'edit_all' : False,
1093     }
1094
1095     def get_recurrent_ids(self, cr, uid, select, base_start_date, base_until_date, limit=100, context=None):
1096         """Gives virtual event ids for recurring events based on value of Recurrence Rule
1097         This method gives ids of dates that comes between start date and end date of calendar views
1098         @param self: The object pointer
1099         @param cr: the current row, from the database cursor,
1100         @param uid: the current user’s ID for security checks,
1101         @param base_start_date: Get Start Date
1102         @param base_until_date: Get End Date
1103         @param limit: The Number of Results to Return """
1104         if not context:
1105             context = {}
1106
1107         virtual_id = context and context.get('virtual_id', False) or False
1108
1109         if isinstance(select, (str, int, long)):
1110             ids = [select]
1111         else:
1112             ids = select
1113         result = []
1114         recur_dict = []
1115         if ids and virtual_id:
1116             for data in super(calendar_event, self).read(cr, uid, ids, context=context):
1117                 start_date = base_start_date and datetime.strptime(base_start_date[:10]+ ' 00:00:00' , "%Y-%m-%d %H:%M:%S") or False
1118                 until_date = base_until_date and datetime.strptime(base_until_date[:10]+ ' 23:59:59', "%Y-%m-%d %H:%M:%S") or False
1119                 event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1120 #                To check: If the start date is replace by event date .. the event date will be changed by that of calendar code
1121                 
1122                 if not data['rrule']:
1123                     if start_date and (event_date < start_date):
1124                         continue
1125                     if until_date and (event_date > until_date):
1126                         continue
1127                     idval = data['id']
1128                     result.append(idval)                        
1129                 else:
1130                     start_date = event_date
1131                     exdate = data['exdate'] and data['exdate'].split(',') or []
1132                     rrule_str = data['rrule']
1133                     new_rrule_str = []
1134                     rrule_until_date = False
1135                     is_until = False
1136                     for rule in rrule_str.split(';'):
1137                         name, value = rule.split('=')
1138                         if name == "UNTIL":
1139                             is_until = True
1140                             value = parser.parse(value)
1141                             rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1142                             if until_date and until_date >= rrule_until_date:
1143                                 until_date = rrule_until_date
1144                             if until_date:
1145                                 value = until_date.strftime("%Y%m%d%H%M%S")
1146                             else:
1147                                 value = value.strftime("%Y%m%d%H%M%S")
1148                         new_rule = '%s=%s' % (name, value)
1149                         new_rrule_str.append(new_rule)
1150                     if not is_until and until_date:
1151                         value = until_date.strftime("%Y%m%d%H%M%S")
1152                         name = "UNTIL"
1153                         new_rule = '%s=%s' % (name, value)
1154                         new_rrule_str.append(new_rule)
1155                     new_rrule_str = ';'.join(new_rrule_str)
1156                     rdates = get_recurrent_dates(str(new_rrule_str), exdate, start_date, data['exrule'])
1157                     
1158                     for r_date in rdates:
1159                         if start_date and r_date < start_date:
1160                             continue
1161                         if until_date and r_date > until_date:
1162                             continue
1163                         idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1164                         result.append(idval)
1165
1166         if result:
1167             ids = list(set(result))
1168         if isinstance(select, (str, int, long)):
1169             return ids and ids[0] or False
1170         return ids
1171
1172     def compute_rule_string(self, datas):
1173         """
1174         Compute rule string according to value type RECUR of iCalendar from the values given.
1175         @param self: the object pointer
1176         @param datas: dictionary of freq and interval value.
1177         """
1178         
1179         def get_week_string(freq, datas):
1180             weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1181             if freq == 'weekly':
1182                 byday = map(lambda x: x.upper(), filter(lambda x: datas.get(x) and x in weekdays, datas))
1183                 if byday:
1184                     return ';BYDAY=' + ','.join(byday)
1185             return ''  
1186         
1187         def get_month_string(freq, datas):
1188             if freq == 'monthly':
1189                 if datas.get('select1')=='date' and (datas.get('day') < 1 or datas.get('day') > 31):
1190                     raise osv.except_osv(_('Error!'), ("Please select proper Day of month"))
1191                 
1192                 if datas.get('select1')=='day':
1193                     return ';BYDAY=' + datas.get('byday') + datas.get('week_list')
1194                 elif datas.get('select1')=='date':
1195                     return ';BYMONTHDAY=' + str(datas.get('day'))
1196             return ''
1197         
1198         def get_end_date(datas):
1199             if datas.get('end_date'):
1200                 datas['end_date_new'] = ''.join((re.compile('\d')).findall(datas.get('end_date'))) + 'T235959Z'
1201             
1202             return (datas.get('end_type') == 'count' and (';COUNT=' + str(datas.get('count'))) or '') +\
1203                              ((datas.get('end_date_new') and datas.get('end_type') == 'end_date' and (';UNTIL=' + datas.get('end_date_new'))) or '')
1204        
1205         freq=datas.get('rrule_type')
1206         if freq == 'none':
1207             return ''
1208         interval_srting = datas.get('interval') and (';INTERVAL=' + str(datas.get('interval'))) or ''
1209         return 'FREQ=' + freq.upper() + get_week_string(freq, datas) + interval_srting + get_end_date(datas) + get_month_string(freq, datas)
1210
1211
1212     def remove_virtual_id(self, ids):
1213         if isinstance(ids, (str, int)):
1214             return base_calendar_id2real_id(ids)
1215             
1216         if isinstance(ids, (list, tuple)):
1217             res = []
1218             for id in ids:
1219                 res.append(base_calendar_id2real_id(id))
1220             return res
1221                 
1222     def search(self, cr, uid, args, offset=0, limit=0, order=None,
1223             context=None, count=False):
1224         args_without_date = []
1225         start_date = False
1226         until_date = False
1227
1228         for arg in args:
1229             if arg[0] == "id":
1230                 new_id = self.remove_virtual_id(arg[2])
1231                 new_arg = (arg[0], arg[1], new_id)
1232                 args_without_date.append(new_arg)
1233             elif arg[0] not in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1234                 args_without_date.append(arg)
1235             else:
1236                 if arg[1] in ('>', '>='):
1237                     if start_date:
1238                         continue
1239                     start_date = arg[2]
1240                 elif arg[1] in ('<', '<='):
1241                     if until_date:
1242                         continue
1243                     until_date = arg[2]
1244         
1245         res = super(calendar_event, self).search(cr, uid, args_without_date, \
1246                                  0, 0, order, context, count=False)
1247         res = self.get_recurrent_ids(cr, uid, res, start_date, until_date, limit, context=context)
1248         if count:
1249             return len(res)
1250         elif limit:
1251             return res[offset:offset+limit]
1252         else:
1253             return res
1254
1255
1256     def get_edit_all(self, cr, uid, id, vals=None):
1257         """
1258             return true if we have to edit all meeting from the same recurrent
1259             or only on occurency
1260         """
1261         meeting = self.read(cr,uid, id, ['edit_all', 'recurrency'] )
1262         if(vals and 'edit_all' in vals): #we jsut check edit_all
1263             return vals['edit_all']
1264         else: #it's a recurrent event and edit_all is already check
1265             return meeting['recurrency'] and meeting['edit_all']
1266
1267     def _get_data(self, cr, uid, id, context=None):
1268         res = self.read(cr, uid, [id],['date', 'date_deadline'])
1269         return res[0]
1270
1271     def need_to_update(self, event_id, vals):
1272         split_id = str(event_id).split("-")
1273         if len(split_id) < 2:
1274             return False 
1275         else:
1276             date_start = vals.get('date', '')
1277             try:
1278                 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1279                 return date_start == split_id[1]
1280             except Exception:
1281                 return True
1282   
1283     def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1284         if context is None:
1285             context = {}
1286         if isinstance(ids, (str, int, long)):
1287             select = [ids]
1288         else:
1289             select = ids
1290         new_ids = []
1291         res = False
1292         for event_id in select:
1293             real_event_id = base_calendar_id2real_id(event_id)
1294             
1295             edit_all = self.get_edit_all(cr, uid, event_id, vals=vals)
1296             if edit_all:
1297                 if self.need_to_update(event_id, vals):
1298                     res = self._get_data(cr, uid, real_event_id, context=context)
1299                     vals.update(res)
1300                 event_id = real_event_id
1301
1302             #if edit one instance of a reccurrent id
1303             if len(str(event_id).split('-')) > 1 and not edit_all:
1304                 data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1305                                                     'rrule', 'duration', 'exdate'])
1306                 if data.get('rrule'):
1307                     data.update(vals)
1308                     data.update({
1309                         'recurrent_uid': real_event_id,
1310                         'recurrent_id': data.get('date'),
1311                         'rrule_type': 'none',
1312                         'rrule': '',
1313                         'edit_all': False,
1314                         'recurrency' : False,
1315                         })
1316
1317                     new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1318
1319                     date_new = event_id.split('-')[1]
1320                     date_new = time.strftime("%Y%m%dT%H%M%S", \
1321                                  time.strptime(date_new, "%Y%m%d%H%M%S"))
1322                     exdate = (data['exdate'] and (data['exdate'] + ',')  or '') + date_new
1323                     res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1324
1325                     context.update({'active_id': new_id, 'active_ids': [new_id]})
1326                     continue
1327             if not real_event_id in new_ids:
1328                 new_ids.append(real_event_id)
1329
1330         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1331             vals['vtimezone'] = vals['vtimezone'][40:]
1332
1333         updated_vals = self.onchange_dates(cr, uid, new_ids,
1334             vals.get('date', False),
1335             vals.get('duration', False),
1336             vals.get('date_deadline', False),
1337             vals.get('allday', False),
1338             context=context)
1339         vals.update(updated_vals.get('value', {}))
1340         if new_ids:
1341             res = super(calendar_event, self).write(cr, uid, new_ids, vals, context=context)
1342
1343         if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1344                 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1345             # change alarm details
1346             alarm_obj = self.pool.get('res.alarm')
1347             alarm_obj.do_alarm_create(cr, uid, new_ids, self._name, 'date', context=context)
1348         return res or True and False
1349
1350     def browse(self, cr, uid, ids, context=None, list_class=None, fields_process=None):
1351         if isinstance(ids, (str, int, long)):
1352             select = [ids]
1353         else:
1354             select = ids
1355         select = map(lambda x: base_calendar_id2real_id(x), select)
1356         res = super(calendar_event, self).browse(cr, uid, select, context, \
1357                                                     list_class, fields_process)
1358         if isinstance(ids, (str, int, long)):
1359             return res and res[0] or False
1360
1361         return res
1362
1363     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1364         if not context:
1365             context = {}
1366             
1367         if 'date' in groupby:
1368             raise osv.except_osv(_('Warning !'), _('Group by date not supported, use the calendar view instead'))
1369         
1370         context.update({'virtual_id': False})
1371         res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1372         for re in res:
1373             #remove the count, since the value is not consistent with the result of the search when expand the group
1374             for groupname in groupby:
1375                 if re.get(groupname + "_count"):
1376                     del re[groupname + "_count"]
1377             re.get('__context').update({'virtual_id' : virtual_id})
1378         return res
1379
1380     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1381         # FIXME This whole id mangling has to go!
1382         if context is None:
1383             context = {}
1384         
1385
1386
1387         if isinstance(ids, (str, int, long)):
1388             select = [ids]
1389         else:
1390             select = ids
1391         select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1392         result = []
1393         if fields and 'date' not in fields:
1394             fields.append('date')
1395         if fields and 'duration' not in fields:
1396             fields.append('duration')
1397
1398
1399         for base_calendar_id, real_id in select:                
1400             #REVET: Revision ID: olt@tinyerp.com-20100924131709-cqsd1ut234ni6txn
1401             res = super(calendar_event, self).read(cr, uid, real_id, fields=fields, context=context, load=load)
1402            
1403             if not res :
1404                 continue
1405             ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1406             if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1407                 res['date'] = ls[1]
1408                 res['date_deadline'] = ls[2]
1409             res['id'] = base_calendar_id
1410
1411             result.append(res)
1412         if isinstance(ids, (str, int, long)):
1413             return result and result[0] or False
1414         
1415         return result
1416
1417     def copy(self, cr, uid, id, default=None, context=None):
1418         if context is None:
1419             context = {}
1420             
1421         res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1422         alarm_obj = self.pool.get('res.alarm')
1423         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1424         return res
1425     
1426     
1427     def unlink(self, cr, uid, ids, context=None):
1428         if not isinstance(ids, list):
1429             ids = [ids]
1430             
1431         res = False
1432         for id in ids:
1433             event_datas = self.read(cr, uid, [id], ['date', 'rrule', 'exdate'], context=context)[0]
1434             event_id = event_datas['id']
1435
1436             if self.get_edit_all(cr, uid, event_id, vals=None):
1437                 event_id = base_calendar_id2real_id(event_id)
1438
1439             if isinstance(event_id, (int, long)):
1440                 res = super(calendar_event, self).unlink(cr, uid, event_id, context=context)
1441                 self.pool.get('res.alarm').do_alarm_unlink(cr, uid, [event_id], self._name)
1442                 self.unlink_events(cr, uid, [event_id], context=context)
1443             else:
1444                 str_event, date_new = event_id.split('-')
1445                 event_id = int(str_event)
1446                 if event_datas['rrule']:
1447                     # Remove one of the recurrent event
1448                     date_new = time.strftime("%Y%m%dT%H%M%S", \
1449                                  time.strptime(date_new, "%Y%m%d%H%M%S"))
1450                     exdate = (event_datas['exdate'] and (event_datas['exdate'] + ',')  or '') + date_new
1451                     res = self.write(cr, uid, [event_id], {'exdate': exdate})
1452                 else:
1453                     res = super(calendar_event, self).unlink(cr, uid, [event_id], context=context)
1454                     self.pool.get('res.alarm').do_alarm_unlink(cr, uid, [event_id], self._name)
1455                     self.unlink_events(cr, uid, [event_id], context=context)
1456         return res
1457
1458     def create(self, cr, uid, vals, context=None):
1459         if context is None:
1460             context = {}
1461
1462         if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1463             vals['vtimezone'] = vals['vtimezone'][40:]
1464
1465         updated_vals = self.onchange_dates(cr, uid, [],
1466             vals.get('date', False),
1467             vals.get('duration', False),
1468             vals.get('date_deadline', False),
1469             vals.get('allday', False),
1470             context=context)
1471         vals.update(updated_vals.get('value', {}))
1472         res = super(calendar_event, self).create(cr, uid, vals, context)
1473         alarm_obj = self.pool.get('res.alarm')
1474         alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1475        
1476         return res
1477     
1478     
1479
1480     def do_tentative(self, cr, uid, ids, context=None, *args):
1481         """ Makes event invitation as Tentative
1482         @param self: The object pointer
1483         @param cr: the current row, from the database cursor,
1484         @param uid: the current user’s ID for security checks,
1485         @param ids: List of Event IDs
1486         @param *args: Get Tupple value
1487         @param context: A standard dictionary for contextual values
1488         """
1489         return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1490
1491     def do_cancel(self, cr, uid, ids, context=None, *args):
1492         """ Makes event invitation as Tentative
1493         @param self: The object pointer
1494         @param cr: the current row, from the database cursor,
1495         @param uid: the current user’s ID for security checks,
1496         @param ids: List of Event IDs
1497         @param *args: Get Tupple value
1498         @param context: A standard dictionary for contextual values
1499         """
1500         return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1501
1502     def do_confirm(self, cr, uid, ids, context=None, *args):
1503         """ Makes event invitation as Tentative
1504         @param self: The object pointer
1505         @param cr: the current row, from the database cursor,
1506         @param uid: the current user’s ID for security checks,
1507         @param ids: List of Event IDs
1508         @param *args: Get Tupple value
1509         @param context: A standard dictionary for contextual values
1510         """
1511         return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1512
1513 calendar_event()
1514
1515 class calendar_todo(osv.osv):
1516     """ Calendar Task """
1517
1518     _name = "calendar.todo"
1519     _inherit = "calendar.event"
1520     _description = "Calendar Task"
1521
1522     def _get_date(self, cr, uid, ids, name, arg, context=None):
1523         """
1524         Get Date
1525         @param self: The object pointer
1526         @param cr: the current row, from the database cursor,
1527         @param uid: the current user’s ID for security checks,
1528         @param ids: List of calendar todo's IDs.
1529         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1530         @param context: A standard dictionary for contextual values
1531         """
1532
1533         res = {}
1534         for event in self.browse(cr, uid, ids, context=context):
1535             res[event.id] = event.date_start
1536         return res
1537
1538     def _set_date(self, cr, uid, id, name, value, arg, context=None):
1539         """
1540         Set Date
1541         @param self: The object pointer
1542         @param cr: the current row, from the database cursor,
1543         @param uid: the current user’s ID for security checks,
1544         @param id: calendar's ID.
1545         @param value: Get Value
1546         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1547         @param context: A standard dictionary for contextual values
1548         """
1549
1550         assert name == 'date'
1551         return self.write(cr, uid, id, { 'date_start': value }, context=context)
1552
1553     _columns = {
1554         'date': fields.function(_get_date, method=True, fnct_inv=_set_date, \
1555                             string='Duration', store=True, type='datetime'),
1556         'duration': fields.integer('Duration'),
1557     }
1558
1559     __attribute__ = {}
1560
1561
1562 calendar_todo()
1563
1564 class ir_attachment(osv.osv):
1565     _name = 'ir.attachment'
1566     _inherit = 'ir.attachment'
1567
1568     def search_count(self, cr, user, args, context=None):
1569         """
1570         @param self: The object pointer
1571         @param cr: the current row, from the database cursor,
1572         @param user: the current user’s ID for security checks,
1573         @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1574         @param context: A standard dictionary for contextual values
1575         """
1576
1577         args1 = []
1578         for arg in args:
1579             args1.append(map(lambda x:str(x).split('-')[0], arg))
1580         return super(ir_attachment, self).search_count(cr, user, args1, context)
1581
1582
1583
1584     def create(self, cr, uid, vals, context=None):
1585         if context:
1586             id = context.get('default_res_id', False)
1587             context.update({'default_res_id' : base_calendar_id2real_id(id)})
1588         return super(ir_attachment, self).create(cr, uid, vals, context=context)
1589
1590     def search(self, cr, uid, args, offset=0, limit=None, order=None,
1591             context=None, count=False):
1592         """
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 args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1597         @param offset: The Number of Results to pass,
1598         @param limit: The Number of Results to Return,
1599         @param context: A standard dictionary for contextual values
1600         """
1601
1602         new_args = args
1603         for i, arg in enumerate(new_args):
1604             if arg[0] == 'res_id':
1605                 new_args[i] = (arg[0], arg[1], base_calendar_id2real_id(arg[2]))
1606
1607         return super(ir_attachment, self).search(cr, uid, new_args, offset=offset,
1608                             limit=limit, order=order, context=context, count=False)
1609 ir_attachment()
1610
1611 class ir_values(osv.osv):
1612     _inherit = 'ir.values'
1613
1614     def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1615             isobject=False, meta=False, preserve_user=False, company=False):
1616         """
1617         Set IR Values
1618         @param self: The object pointer
1619         @param cr: the current row, from the database cursor,
1620         @param uid: the current user’s ID for security checks,
1621         @param model: Get The Model
1622         """
1623
1624         new_model = []
1625         for data in models:
1626             if type(data) in (list, tuple):
1627                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1628             else:
1629                 new_model.append(data)
1630         return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1631                     value, replace, isobject, meta, preserve_user, company)
1632
1633     def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1634              res_id_req=False, without_user=True, key2_req=True):
1635         """
1636         Get IR Values
1637         @param self: The object pointer
1638         @param cr: the current row, from the database cursor,
1639         @param uid: the current user’s ID for security checks,
1640         @param model: Get The Model
1641         """
1642         if context is None:
1643             context = {}
1644         new_model = []
1645         for data in models:
1646             if type(data) in (list, tuple):
1647                 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1648             else:
1649                 new_model.append(data)
1650         return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1651                          meta, context, res_id_req, without_user, key2_req)
1652
1653 ir_values()
1654
1655 class ir_model(osv.osv):
1656
1657     _inherit = 'ir.model'
1658
1659     def read(self, cr, uid, ids, fields=None, context=None,
1660             load='_classic_read'):
1661         """
1662         Overrides orm read method.
1663         @param self: The object pointer
1664         @param cr: the current row, from the database cursor,
1665         @param uid: the current user’s ID for security checks,
1666         @param ids: List of IR Model’s IDs.
1667         @param context: A standard dictionary for contextual values
1668         """
1669         new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1670         if context is None:
1671             context = {}
1672         data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1673                         context=context, load=load)
1674         if data:
1675             for val in data:
1676                 val['id'] = base_calendar_id2real_id(val['id'])
1677         return isinstance(ids, (str, int, long)) and data[0] or data
1678
1679 ir_model()
1680
1681 class virtual_report_spool(web_services.report_spool):
1682
1683     def exp_report(self, db, uid, object, ids, datas=None, context=None):
1684         """
1685         Export Report
1686         @param self: The object pointer
1687         @param db: get the current database,
1688         @param uid: the current user’s ID for security checks,
1689         @param context: A standard dictionary for contextual values
1690         """
1691
1692         if object == 'printscreen.list':
1693             return super(virtual_report_spool, self).exp_report(db, uid, \
1694                             object, ids, datas, context)
1695         new_ids = []
1696         for id in ids:
1697             new_ids.append(base_calendar_id2real_id(id))
1698         if datas.get('id', False):
1699             datas['id'] = base_calendar_id2real_id(datas['id'])
1700         return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, datas, context)
1701
1702 virtual_report_spool()
1703
1704 class res_users(osv.osv):
1705     _inherit = 'res.users'
1706
1707     def _get_user_avail(self, cr, uid, ids, context=None):
1708         """
1709         Get User Availability
1710         @param self: The object pointer
1711         @param cr: the current row, from the database cursor,
1712         @param uid: the current user’s ID for security checks,
1713         @param ids: List of res user’s IDs.
1714         @param context: A standard dictionary for contextual values
1715         """
1716
1717         current_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1718         res = {}
1719         attendee_obj = self.pool.get('calendar.attendee')
1720         attendee_ids = attendee_obj.search(cr, uid, [
1721                     ('event_date', '<=', current_datetime), ('event_end_date', '<=', current_datetime),
1722                     ('state', '=', 'accepted'), ('user_id', 'in', ids)
1723                     ])
1724
1725         for attendee_data in attendee_obj.read(cr, uid, attendee_ids, ['user_id']):
1726             user_id = attendee_data['user_id']
1727             status = 'busy'
1728             res.update({user_id:status})
1729
1730         #TOCHECK: Delegated Event
1731         for user_id in ids:
1732             if user_id not in res:
1733                 res[user_id] = 'free'
1734
1735         return res
1736
1737     def _get_user_avail_fun(self, cr, uid, ids, name, args, context=None):
1738         """
1739         Get User Availability Function
1740         @param self: The object pointer
1741         @param cr: the current row, from the database cursor,
1742         @param uid: the current user’s ID for security checks,
1743         @param ids: List of res user’s IDs.
1744         @param context: A standard dictionary for contextual values
1745         """
1746
1747         return self._get_user_avail(cr, uid, ids, context=context)
1748
1749     _columns = {
1750             'availability': fields.function(_get_user_avail_fun, type='selection', \
1751                     selection=[('free', 'Free'), ('busy', 'Busy')], \
1752                     string='Free/Busy', method=True),
1753     }
1754
1755 res_users()
1756
1757
1758 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: