account.analytic.line: fix on_change_unit_amount()
[odoo/odoo.git] / addons / caldav / 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
23 from dateutil import parser
24 from dateutil.rrule import *
25 from osv import osv, fields
26 from tools.translate import _
27 import math
28 import pooler
29 import pytz
30 import re
31 import tools
32 import time
33 import logging
34 from caldav_node import res_node_calendar
35 from orm_utils import get_last_modified
36 from tools.safe_eval import safe_eval as eval
37
38 try:
39     import vobject
40 except ImportError:
41     raise osv.except_osv('vobject Import Error!','Please install python-vobject \
42                      from http://vobject.skyhouseconsulting.com/')
43
44 # O-1  Optional and can come only once
45 # O-n  Optional and can come more than once
46 # R-1  Required and can come only once
47 # R-n  Required and can come more than once
48
49 def uid2openobjectid(cr, uidval, oomodel, rdate):
50     """ UID To Open Object Id
51         @param cr: the current row, from the database cursor,
52         @param uidval: Get USerId vale
53         @oomodel: Open Object ModelName
54         @param rdate: Get Recurrent Date
55     """
56     __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
57     if not uidval:
58         return (False, None)
59     wematch = __rege.match(uidval.encode('utf8'))
60     if not wematch:
61         return (False, None)
62     else:
63         model, id, dbname = wematch.groups()
64         model_obj = pooler.get_pool(cr.dbname).get(model)
65         if (not model == oomodel) or (not dbname == cr.dbname):
66             return (False, None)
67         qry = 'SELECT DISTINCT(id) FROM %s' % model_obj._table
68         if rdate:
69             qry += " WHERE recurrent_id=%s"
70             cr.execute(qry, (rdate,))
71             r_id = cr.fetchone()
72             if r_id:
73                 return (id, r_id[0])
74             else:
75                 return (False, None)
76         cr.execute(qry)
77         ids = map(lambda x: str(x[0]), cr.fetchall())
78         if id in ids:
79             return (id, None)
80         return (False, None)
81
82 def openobjectid2uid(cr, uidval, oomodel):
83     """ Gives the value of UID for VEVENT
84         @param cr: the current row, from the database cursor,
85         @param uidval: Id value of the Event
86         @oomodel: Open Object ModelName """
87
88     value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname)
89     return value
90
91 def mailto2str(arg):
92     """Take a dict of mail and convert to string.
93     """
94     ret = []
95     if isinstance(arg, dict):
96         args = [arg,]
97     else:
98         args = arg
99
100     for ard in args:
101         rstr = ard.get('name','')
102         if ard.get('company',False):
103             rstr += ' (%s)' % ard.get('company')
104         if ard.get('email'):
105             rstr += ' <%s>' % ard.get('email')
106         ret.append(rstr)
107     return ', '.join(ret)
108
109 def str2mailto(emailstr, multi=False):
110     """Split one email string to a dict of name, company, mail parts
111
112        @param multi Return an array, recognize comma-sep
113     """
114     # TODO: move to tools or sth.
115     mege = re.compile(r'([^\(\<]+) *(\((.*?)\))? *(\< ?(.*?) ?\>)? ?(\((.*?)\))? *$')
116
117     mailz= [emailstr,]
118     retz = []
119     if multi:
120         mailz = emailstr.split(',')
121
122     for mas in mailz:
123         m = mege.match(mas.strip())
124         if not m:
125             # one of the rare non-matching strings is "sad" :(
126             # retz.append({ 'name': mas.strip() })
127             # continue
128             raise ValueError("Invalid email address %r" % mas)
129         rd = {  'name': m.group(1).strip(),
130                 'email': m.group(5), }
131         if m.group(2):
132             rd['company'] = m.group(3).strip()
133         elif m.group(6):
134             rd['company'] = m.group(7).strip()
135
136         if rd['name'].startswith('"') and rd['name'].endswith('"'):
137             rd['name'] = rd['name'][1:-1]
138         retz.append(rd)
139
140     if multi:
141         return retz
142     else:
143         return retz[0]
144
145 def get_attribute_mapping(cr, uid, calname, context=None):
146     """ Attribute Mapping with Basic calendar fields and lines
147         @param cr: the current row, from the database cursor,
148         @param uid: the current user’s ID for security checks,
149         @param calname: Get Calendar name
150         @param context: A standard dictionary for contextual values """
151
152     if not context:
153         context = {}
154     pool = pooler.get_pool(cr.dbname)
155     field_obj = pool.get('basic.calendar.fields')
156     type_obj = pool.get('basic.calendar.lines')
157     domain = [('object_id.model', '=', context.get('model'))]
158     if context.get('calendar_id'):
159         domain.append(('calendar_id', '=', context.get('calendar_id')))
160     type_id = type_obj.search(cr, uid, domain)
161     fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])])
162     res = {}
163     for field in field_obj.browse(cr, uid, fids):
164         attr = field.name.name
165         res[attr] = {}
166         res[attr]['field'] = field.field_id.name
167         res[attr]['type'] = field.field_id.ttype
168         if field.fn == 'hours':
169             res[attr]['type'] = "timedelta"
170         if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
171             res[attr]['object'] = field.field_id.relation
172         elif res[attr]['type'] in ('selection') and field.mapping:
173             res[attr]['mapping'] = eval(field.mapping)
174     if not res.get('uid', None):
175         res['uid'] = {}
176         res['uid']['field'] = 'id'
177         res['uid']['type'] = "integer"
178     return res
179
180 def map_data(cr, uid, obj, context=None):
181     """ Map Data
182         @param self: The object pointer
183         @param cr: the current row, from the database cursor,"""
184
185     vals = {}
186     for map_dict in obj.__attribute__:
187         map_val = obj.ical_get(map_dict, 'value')
188         field = obj.ical_get(map_dict, 'field')
189         field_type = obj.ical_get(map_dict, 'type')
190         if field:
191             if field_type == 'selection':
192                 if not map_val:
193                     continue
194                 if type(map_val) == list and len(map_val): #TOFIX: why need to check this
195                     map_val = map_val[0]
196                 mapping = obj.__attribute__[map_dict].get('mapping', False)
197                 if mapping:
198                     map_val = mapping.get(map_val.lower(), False)
199                 else:
200                     map_val = map_val.lower()
201             if field_type == 'many2many':
202                 ids = []
203                 if not map_val:
204                     vals[field] = ids
205                     continue
206                 model = obj.__attribute__[map_dict].get('object', False)
207                 modobj = obj.pool.get(model)
208                 for map_vall in map_val:
209                     id = modobj.create(cr, uid, map_vall, context=context)
210                     ids.append(id)
211                 vals[field] = [(6, 0, ids)]
212                 continue
213             if field_type == 'many2one':
214                 id = None
215                 if not map_val or not isinstance(map_val, dict):
216                     vals[field] = id
217                     continue
218                 model = obj.__attribute__[map_dict].get('object', False)
219                 modobj = obj.pool.get(model)
220                 # check if the record exists or not
221                 key1 = map_val.keys()
222                 value1 = map_val.values()
223                 domain = [(key1[i], '=', value1[i]) for i in range(len(key1)) if value1[i]]
224                 exist_id = modobj.search(cr, uid, domain, context=context)
225                 if exist_id:
226                     id = exist_id[0]
227                 else:
228                     id = modobj.create(cr, uid, map_val, context=context)
229                 vals[field] = id
230                 continue
231             if field_type == 'timedelta':
232                 if map_val:
233                     vals[field] = (map_val.seconds/float(86400) + map_val.days)
234             vals[field] = map_val
235     return vals
236
237 class CalDAV(object):
238     __attribute__ = {}
239     _logger = logging.getLogger('document.caldav')
240
241     def ical_set(self, name, value, type):
242         """ set calendar Attribute
243          @param self: The object pointer,
244          @param name: Get Attribute Name
245          @param value: Get Attribute Value
246          @param type: Get Attribute Type
247         """
248         if name in self.__attribute__ and self.__attribute__[name]:
249             self.__attribute__[name][type] = value
250         return True
251
252     def ical_get(self, name, type):
253         """ Get calendar Attribute
254          @param self: The object pointer,
255          @param name: Get Attribute Name
256          @param type: Get Attribute Type
257         """
258
259         if self.__attribute__.get(name):
260             val = self.__attribute__.get(name).get(type, None)
261             valtype =  self.__attribute__.get(name).get('type', None)
262             if type == 'value':
263                 if valtype and valtype == 'datetime' and val:
264                     if isinstance(val, list):
265                         val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
266                     else:
267                         val = val.strftime('%Y-%m-%d %H:%M:%S')
268             return  val
269         else:
270             return  self.__attribute__.get(name, None)
271
272     def ical_reset(self, type):
273         """ Reset Calendar Attribute
274          @param self: The object pointer,
275          @param type: Get Attribute Type
276         """
277
278         for name in self.__attribute__:
279             if self.__attribute__[name]:
280                 self.__attribute__[name][type] = None
281         return True
282
283     def format_date_tz(self, src_date, tz=None):
284         """ This function converts date into specifice timezone value
285         @param src_date: Date to be converted (datetime.datetime)
286         @return: Converted datetime.datetime object for the date
287         """
288         format = tools.DEFAULT_SERVER_DATETIME_FORMAT
289         date_str = src_date.strftime('%Y-%m-%d %H:%M:%S')
290         res_date = tools.server_to_local_timestamp(date_str, format, format, tz)
291         return datetime.strptime(res_date, "%Y-%m-%d %H:%M:%S")
292
293     def parse_ics(self, cr, uid, child, cal_children=None, context=None):
294         """ parse calendaring and scheduling information
295         @param self: The object pointer
296         @param cr: the current row, from the database cursor,
297         @param uid: the current user’s ID for security checks,
298         @param context: A standard dictionary for contextual values """
299
300         att_data = []
301         exdates = []
302         _server_tzinfo = pytz.timezone(tools.get_server_timezone())
303
304         for cal_data in child.getChildren():
305             if cal_data.name.lower() == 'organizer':
306                 dmail = { 'name': cal_data.params.get('CN', ['',])[0],
307                             'email': cal_data.value.lower().replace('mailto:',''),
308                             # TODO: company? 
309                 }
310                 self.ical_set(cal_data.name.lower(), mailto2str(dmail), 'value')
311                 continue
312             if cal_data.name.lower() == 'attendee':
313                 ctx = context.copy()
314                 if cal_children:
315                     ctx.update({'model': cal_children[cal_data.name.lower()]})
316                 attendee = self.pool.get('basic.calendar.attendee')
317                 att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx))
318                 self.ical_set(cal_data.name.lower(), att_data, 'value')
319                 continue
320             if cal_data.name.lower() == 'valarm':
321                 alarm = self.pool.get('basic.calendar.alarm')
322                 ctx = context.copy()
323                 if cal_children:
324                     ctx.update({'model': cal_children[cal_data.name.lower()]})
325                 vals = alarm.import_cal(cr, uid, cal_data, context=ctx)
326                 self.ical_set(cal_data.name.lower(), vals, 'value')
327                 continue
328             if cal_data.name.lower() == 'exdate':
329                 exdates += cal_data.value
330                 exvals = []
331                 for exdate in exdates:
332                     exvals.append(datetime.fromtimestamp(time.mktime(exdate.utctimetuple())).strftime('%Y%m%dT%H%M%S'))
333                 self.ical_set(cal_data.name.lower(), ','.join(exvals), 'value')
334                 continue
335             if cal_data.name.lower() in self.__attribute__:
336                 if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
337                     self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
338                     date_local = cal_data.value.astimezone(_server_tzinfo)
339                     self.ical_set(cal_data.name.lower(), date_local, 'value')
340                     continue
341                 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
342         vals = map_data(cr, uid, self, context=context)
343         return vals
344
345     def create_ics(self, cr, uid, datas, name, ical, context=None):
346         """ create calendaring and scheduling information
347         @param self: The object pointer
348         @param cr: the current row, from the database cursor,
349         @param uid: the current user’s ID for security checks,
350         @param context: A standard dictionary for contextual values """
351
352         if not datas:
353             return
354         timezones = []
355         for data in datas:
356             tzval = None
357             exfield = None
358             exdates = []
359             vevent = ical.add(name)
360             for field in self.__attribute__.keys():
361                 map_field = self.ical_get(field, 'field')
362                 map_type = self.ical_get(field, 'type')
363                 if map_field in data.keys():
364                     if field == 'uid':
365                         model = context.get('model', None)
366                         if not model:
367                             continue
368                         uidval = openobjectid2uid(cr, data[map_field], model)
369                         #Computation for getting events with the same UID (RFC4791 Section4.1)
370                         #START
371                         model_obj = self.pool.get(model)
372                         r_ids = []
373                         if model_obj._columns.get('recurrent_uid', None):
374                             cr.execute('SELECT id FROM %s WHERE recurrent_uid=%%s' % model_obj._table,
375                                         (data[map_field],))
376                             r_ids = map(lambda x: x[0], cr.fetchall())
377                         if r_ids:
378                             r_datas = model_obj.read(cr, uid, r_ids, context=context)
379                             rcal = CalDAV.export_cal(self, cr, uid, r_datas, 'vevent', context=context)
380                             for revents in rcal.contents.get('vevent', []):
381                                 ical.contents['vevent'].append(revents)
382                         #END
383                         if data.get('recurrent_uid', None):
384                             # Change the UID value in case of modified event from any recurrent event 
385                             uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
386                         vevent.add('uid').value = uidval
387                     elif field == 'attendee' and data[map_field]:
388                         model = self.__attribute__[field].get('object', False)
389                         attendee_obj = self.pool.get('basic.calendar.attendee')
390                         vevent = attendee_obj.export_cal(cr, uid, model, \
391                                      data[map_field], vevent, context=context)
392                     elif field == 'valarm' and data[map_field]:
393                         model = self.__attribute__[field].get('object', False)
394                         ctx = context.copy()
395                         ctx.update({'model': model})
396                         alarm_obj = self.pool.get('basic.calendar.alarm')
397                         vevent = alarm_obj.export_cal(cr, uid, model, \
398                                     data[map_field][0], vevent, context=ctx)
399                     elif field == 'vtimezone' and data[map_field]:
400                         tzval = data[map_field]
401                         if tzval not in timezones:
402                             tz_obj = self.pool.get('basic.calendar.timezone')
403                             ical = tz_obj.export_cal(cr, uid, None, \
404                                          data[map_field], ical, context=context)
405                             timezones.append(data[map_field])
406                         if vevent.contents.get('recurrence-id'):
407                             # Convert recurrence-id field value accroding to timezone value
408                             recurid_val = vevent.contents.get('recurrence-id')[0].value
409                             vevent.contents.get('recurrence-id')[0].params['TZID'] = [tzval.title()]
410                             vevent.contents.get('recurrence-id')[0].value =  self.format_date_tz(recurid_val, tzval.title())
411                         if exfield:
412                             # Set exdates according to timezone value
413                             # This is the case when timezone mapping comes after the exdate mapping
414                             # and we have exdate value available 
415                             exfield.params['TZID'] = [tzval.title()]
416                             exdates_updated = []
417                             for exdate in exdates:
418                                 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
419                             exfield.value = exdates_updated
420                     elif field == 'organizer' and data[map_field]:
421                         organizer = str2mailto(data[map_field])
422                         event_org = vevent.add('organizer')
423                         event_org.params['CN'] = [organizer['name']]
424                         event_org.value = 'MAILTO:' + (organizer.get('email') or '')
425                         # TODO: company?
426                     elif data[map_field]:
427                         if map_type in ("char", "text"):
428                             if field in ('exdate'):
429                                 exfield = vevent.add(field)
430                                 exdates = (data[map_field]).split(',')
431                                 if tzval:
432                                     # Set exdates according to timezone value
433                                     # This is the case when timezone mapping comes before the exdate mapping
434                                     # and we have timezone value available 
435                                     exfield.params['TZID'] = [tzval.title()]
436                                     exdates_updated = []
437                                     for exdate in exdates:
438                                         exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
439                                     exfield.value = exdates_updated
440                             else:
441                                 vevent.add(field).value = tools.ustr(data[map_field])
442                         elif map_type in ('datetime', 'date') and data[map_field]:
443                             dtfield = vevent.add(field)
444                             if tzval:
445                                 # Export the date according to the event timezone value
446                                 dtfield.params['TZID'] = [tzval.title()]
447                                 dtfield.value = self.format_date_tz(parser.parse(data[map_field]), tzval.title())
448                             else:
449                                 dtfield.value = parser.parse(data[map_field])
450                         elif map_type == "timedelta":
451                             vevent.add(field).value = timedelta(hours=data[map_field])
452                         elif map_type == "many2one":
453                             vevent.add(field).value = tools.ustr(data.get(map_field)[1])
454                         elif map_type in ("float", "integer"):
455                             vevent.add(field).value = str(data.get(map_field))
456                         elif map_type == "selection":
457                             if not self.ical_get(field, 'mapping'):
458                                 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
459                             else:
460                                 for key1, val1 in self.ical_get(field, 'mapping').items():
461                                     if val1 == data[map_field]:
462                                         vevent.add(field).value = key1.upper()
463         return vevent
464
465     def check_import(self, cr, uid, vals, context=None):
466         """
467             @param self: The object pointer
468             @param cr: the current row, from the database cursor,
469             @param uid: the current user’s ID for security checks,
470             @param vals: Get Values
471             @param context: A standard dictionary for contextual values
472         """
473         if not context:
474             context = {}
475         ids = []
476         model_obj = self.pool.get(context.get('model'))
477         recur_pool = {}
478         try:
479             for val in vals:
480                 # Compute value of duration
481                 if 'date_deadline' in val and 'duration' not in val:
482                     start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
483                     end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
484                     diff = end - start
485                     val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
486                 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
487                                                                  val.get('recurrent_id'))
488                 if val.has_key('create_date'):
489                     val.pop('create_date')
490                 u_id = val.get('id', None)
491                 val.pop('id')
492                 if exists and r_id:
493                     val.update({'recurrent_uid': exists})
494                     model_obj.write(cr, uid, [r_id], val)
495                     ids.append(r_id)
496                 elif exists:
497                     model_obj.write(cr, uid, [exists], val)
498                     ids.append(exists)
499                 else:
500                     if u_id in recur_pool and val.get('recurrent_id'):
501                         val.update({'recurrent_uid': recur_pool[u_id]})
502                         revent_id = model_obj.create(cr, uid, val)
503                         ids.append(revent_id)
504                     else:
505                         __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
506                         wematch = __rege.match(u_id.encode('utf8'))
507                         if wematch:
508                             model, recur_id, dbname = wematch.groups()
509                             val.update({'recurrent_uid': recur_id})
510                         event_id = model_obj.create(cr, uid, val)
511                         recur_pool[u_id] = event_id
512                         ids.append(event_id)
513         except Exception:
514             raise
515         return ids
516
517     def export_cal(self, cr, uid, datas, vobj=None, context=None):
518         """ Export Calendar
519             @param self: The object pointer
520             @param cr: the current row, from the database cursor,
521             @param uid: the current user’s ID for security checks,
522             @param datas: Get Data's for caldav
523             @param context: A standard dictionary for contextual values
524         """
525         try:
526             self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
527             ical = vobject.iCalendar()
528             self.create_ics(cr, uid, datas, vobj, ical, context=context)
529             return ical
530         except:
531             raise  # osv.except_osv(('Error !'), (str(e)))
532
533     def import_cal(self, cr, uid, content, data_id=None, context=None):
534         """ Import Calendar
535             @param self: The object pointer
536             @param cr: the current row, from the database cursor,
537             @param uid: the current user’s ID for security checks,
538             @param data_id: Get Data’s ID or False
539             @param context: A standard dictionary for contextual values
540         """
541
542         ical_data = content
543         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
544         parsedCal = vobject.readOne(ical_data)
545         res = []
546         vals = {}
547         for child in parsedCal.getChildren():
548             if child.name.lower() in ('vevent', 'vtodo'):
549                 vals = self.parse_ics(cr, uid, child, context=context)
550             else:
551                 vals = {}
552                 continue
553             if vals: res.append(vals)
554             self.ical_reset('value')
555         return res
556
557 class Calendar(CalDAV, osv.osv):
558     _name = 'basic.calendar'
559     _calname = 'calendar'
560
561     __attribute__ = {
562         'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
563         'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
564                            #             or the minimum and maximum range of the iCalendar specification
565                            #             that is required in order to interpret the iCalendar object.
566         'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
567         'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
568         'vevent': None, # Use: O-n, Type: Collection of Event class
569         'vtodo': None, # Use: O-n, Type: Collection of ToDo class
570         'vjournal': None, # Use: O-n, Type: Collection of Journal class
571         'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
572         'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
573     }
574     _columns = {
575             'name': fields.char("Name", size=64),
576             'user_id': fields.many2one('res.users', 'Owner'),
577             'collection_id': fields.many2one('document.directory', 'Collection', \
578                                            required=True),
579             'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
580                                     string="Type", size=64),
581             'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
582             'create_date': fields.datetime('Created Date', readonly=True),
583             'write_date': fields.datetime('Modifided Date', readonly=True),
584             'description': fields.text("description"),
585             'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"),
586             'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"),
587             'has_webcal': fields.boolean('WebCal', required=True, help="Also export a <name>.ics entry next to the calendar folder, with WebCal content."),
588     }
589     
590     _defaults = {
591         'has_webcal': False,
592     }
593
594     def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
595         if not context:
596             context = {}
597         if not domain:
598             domain = []
599         res = []
600         ctx_res_id = context.get('res_id', None)
601         ctx_model = context.get('model', None)
602         for cal in self.browse(cr, uid, ids):
603             for line in cal.line_ids:
604                 if ctx_model and ctx_model != line.object_id.model:
605                     continue
606                 if line.name in ('valarm', 'attendee'):
607                     continue
608                 line_domain = eval(line.domain or '[]', context)
609                 line_domain += domain
610                 if ctx_res_id:
611                     line_domain += [('id','=',ctx_res_id)]
612                 mod_obj = self.pool.get(line.object_id.model)
613                 data_ids = mod_obj.search(cr, uid, line_domain, order="id", context=context)
614                 for data in mod_obj.browse(cr, uid, data_ids, context):
615                     ctx = parent and parent.context or None
616                     if hasattr(data, 'recurrent_uid') and data.recurrent_uid:
617                         # Skip for event which is child of other event
618                         continue
619                     node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
620                     res.append(node)
621         return res
622         
623
624     def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None):
625         if not context:
626             context = {}
627         if not domain:
628             domain = []
629         res = None
630         ctx_res_id = context.get('res_id', None)
631         ctx_model = context.get('model', None)
632         for cal in self.browse(cr, uid, ids):
633             for line in cal.line_ids:
634                 if ctx_model and ctx_model != line.object_id.model:
635                     continue
636                 if line.name in ('valarm', 'attendee'):
637                     continue
638                 line_domain = eval(line.domain or '[]', context)
639                 line_domain += domain
640                 if ctx_res_id:
641                     line_domain += [('id','=',ctx_res_id)]
642                 mod_obj = self.pool.get(line.object_id.model)
643                 max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context)
644                 if res and res > max_data:
645                     continue
646                 res = max_data
647         return res
648
649     def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
650         """ Export Calendar
651             @param ids: List of calendar’s IDs
652             @param vobj: the type of object to export
653             @return the ical data.
654         """
655         if not context:
656            context = {}
657         ctx_model = context.get('model', None)
658         ctx_res_id = context.get('res_id', None)
659         ical = vobject.iCalendar()
660         for cal in self.browse(cr, uid, ids):
661             for line in cal.line_ids:
662                 if ctx_model and ctx_model != line.object_id.model:
663                     continue
664                 if line.name in ('valarm', 'attendee'):
665                     continue
666                 domain = eval(line.domain or '[]', context)
667                 if ctx_res_id:
668                     domain += [('id','=',ctx_res_id)]
669                 mod_obj = self.pool.get(line.object_id.model)
670                 data_ids = mod_obj.search(cr, uid, domain, context=context)
671                 datas = mod_obj.read(cr, uid, data_ids, context=context)
672                 context.update({'model': line.object_id.model,
673                                         'calendar_id': cal.id
674                                         })
675                 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
676                 self.create_ics(cr, uid, datas, line.name, ical, context=context)
677         return ical.serialize()
678
679     def import_cal(self, cr, uid, content, data_id=None, context=None):
680         """ Import Calendar
681             @param self: The object pointer
682             @param cr: the current row, from the database cursor,
683             @param uid: the current user’s ID for security checks,
684             @param data_id: Get Data’s ID or False
685             @param context: A standard dictionary for contextual values
686         """
687         if not context:
688             context = {}
689         vals = []
690         ical_data = content
691         parsedCal = vobject.readOne(ical_data)
692         if not data_id:
693             data_id = self.search(cr, uid, [])[0]
694         cal = self.browse(cr, uid, data_id, context=context)
695         cal_children = {}
696
697         for line in cal.line_ids:
698             cal_children[line.name] = line.object_id.model
699         objs = []
700         checked = True
701         for child in parsedCal.getChildren():
702             if child.name.lower() in cal_children:
703                 context.update({'model': cal_children[child.name.lower()],
704                                 'calendar_id': cal['id']
705                                 })
706                 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
707                 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
708                 vals.append(val)
709                 objs.append(cal_children[child.name.lower()])
710             elif child.name.upper() == 'CALSCALE':
711                 if child.value.upper() != 'GREGORIAN':
712                     self._logger.warning('How do I handle %s calendars?',child.value)
713             elif child.name.upper() in ('PRODID', 'VERSION'):
714                 pass
715             elif child.name.upper().startswith('X-'):
716                 self._logger.debug("skipping custom node %s", child.name)
717             else:
718                 self._logger.debug("skipping node %s", child.name)
719         
720         res = []
721         for obj_name in list(set(objs)):
722             obj = self.pool.get(obj_name)
723             if hasattr(obj, 'check_import'):
724                 r = obj.check_import(cr, uid, vals, context=context)
725                 checked = True
726                 res.extend(r)
727
728         if not checked:
729             r = self.check_import(cr, uid, vals, context=context)
730             res.extend(r)
731         return res
732
733 Calendar()
734
735
736 class basic_calendar_line(osv.osv):
737     """ Calendar Lines """
738
739     _name = 'basic.calendar.lines'
740     _description = 'Calendar Lines'
741
742     _columns = {
743             'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
744                                     ('valarm', 'Alarm'), \
745                                     ('attendee', 'Attendee')], \
746                                     string="Type", size=64),
747             'object_id': fields.many2one('ir.model', 'Object'),
748             'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
749                                        required=True, ondelete='cascade'),
750             'domain': fields.char('Domain', size=124),
751             'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
752     }
753
754     _defaults = {
755         'domain': lambda *a: '[]',
756     }
757
758     def create(self, cr, uid, vals, context=None):
759         """ create calendar's line
760             @param self: The object pointer
761             @param cr: the current row, from the database cursor,
762             @param uid: the current user’s ID for security checks,
763             @param vals: Get the Values
764             @param context: A standard dictionary for contextual values
765         """
766
767         cr.execute("SELECT COUNT(id) FROM basic_calendar_lines \
768                                 WHERE name=%s AND calendar_id=%s", 
769                                 (vals.get('name'), vals.get('calendar_id')))
770         res = cr.fetchone()
771         if res:
772             if res[0] > 0:
773                 raise osv.except_osv(_('Warning !'), _('Can not create \
774 line "%s" more than once' % (vals.get('name'))))
775         return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
776
777 basic_calendar_line()
778
779 class basic_calendar_alias(osv.osv):
780     """ Mapping of client filenames to ORM ids of calendar records
781     
782         Since some clients insist on putting arbitrary filenames on the .ics data
783         they send us, and they won't respect the redirection "Location:" header, 
784         we have to store those filenames and allow clients to call our calendar
785         records with them.
786         Note that adding a column to all tables that would possibly hold calendar-
787         mapped data won't work. The user is always allowed to specify more 
788         calendars, on any arbitrary ORM object, without need to alter those tables'
789         data or structure
790     """
791     _name = 'basic.calendar.alias'
792     _columns = {
793         'name': fields.char('Filename', size=512, required=True, select=1),
794         'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
795                         select=1, help='The calendar/line this mapping applies to'),
796         'res_id': fields.integer('Res. ID', required=True, select=1),
797         }
798         
799     _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
800                 _('The same filename cannot apply to two records!')), ]
801
802 basic_calendar_alias()
803
804 class basic_calendar_attribute(osv.osv):
805     _name = 'basic.calendar.attributes'
806     _description = 'Calendar attributes'
807     _columns = {
808         'name': fields.char("Name", size=64, required=True),
809         'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
810                                     ('alarm', 'Alarm'), \
811                                     ('attendee', 'Attendee')], \
812                                     string="Type", size=64, required=True),
813     }
814
815 basic_calendar_attribute()
816
817
818 class basic_calendar_fields(osv.osv):
819     """ Calendar fields """
820
821     _name = 'basic.calendar.fields'
822     _description = 'Calendar fields'
823
824     _columns = {
825         'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
826         'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
827         'type_id': fields.many2one('basic.calendar.lines', 'Type', \
828                                    required=True, ondelete='cascade'),
829         'expr': fields.char("Expression", size=64),
830         'fn': fields.selection([('field', 'Use the field'),
831                         ('const', 'Expression as constant'),
832                         ('hours', 'Interval in hours'),
833                         ], 'Function'),
834         'mapping': fields.text('Mapping'),
835     }
836
837     _defaults = {
838         'fn': lambda *a: 'field',
839     }
840
841     _sql_constraints = [
842         ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
843     ]
844
845     def check_line(self, cr, uid, vals, name, context=None):
846         """ check calendar's line
847             @param self: The object pointer
848             @param cr: the current row, from the database cursor,
849             @param uid: the current user’s ID for security checks,
850             @param vals: Get Values
851             @param context: A standard dictionary for contextual values
852         """
853         f_obj = self.pool.get('ir.model.fields')
854         field = f_obj.browse(cr, uid, vals['field_id'], context=context)
855         relation = field.relation
856         line_obj = self.pool.get('basic.calendar.lines')
857         l_id = line_obj.search(cr, uid, [('name', '=', name)])
858         if l_id:
859             line = line_obj.browse(cr, uid, l_id, context=context)[0]
860             line_rel = line.object_id.model
861             if (relation != 'NULL') and (not relation == line_rel):
862                 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name)))
863         return True
864
865     def create(self, cr, uid, vals, context=None):
866         """ Create Calendar's fields
867             @param self: The object pointer
868             @param cr: the current row, from the database cursor,
869             @param uid: the current user’s ID for security checks,
870             @param vals: Get Values
871             @param context: A standard dictionary for contextual values
872         """
873
874         cr.execute('SELECT name FROM basic_calendar_attributes \
875                             WHERE id=%s', (vals.get('name'),))
876         name = cr.fetchone()
877         name = name[0]
878         if name in ('valarm', 'attendee'):
879             self.check_line(cr, uid, vals, name, context=context)
880         return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
881
882     def write(self, cr, uid, ids, vals, context=None):
883         """ write Calendar's fields
884             @param self: The object pointer
885             @param cr: the current row, from the database cursor,
886             @param uid: the current user’s ID for security checks,
887             @param vals: Get Values
888             @param context: A standard dictionary for contextual values
889         """
890
891         if not vals:
892             return
893         for id in ids:
894             field = self.browse(cr, uid, id, context=context)
895             name = field.name.name
896             if name in ('valarm', 'attendee'):
897                 self.check_line(cr, uid, vals, name, context=context)
898         return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
899
900 basic_calendar_fields()
901
902
903 class Event(CalDAV, osv.osv_memory):
904     _name = 'basic.calendar.event'
905     _calname = 'vevent'
906     __attribute__ = {
907         'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar  component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
908         'created': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the calendar information  was created by the calendar user agent in the calendar store.
909         'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
910         'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
911         'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
912         'last-mod': None, # Use: O-1, Type: DATE-TIME        Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
913         'location': None, # Use: O-1, Type: TEXT            Defines the intended venue for the activity defined by a calendar component.
914         'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
915         'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
916         'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
917         'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
918         'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
919         'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
920         'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
921         'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
922         'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
923         'recurid': None,
924         'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
925         'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
926         'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
927         'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
928         'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a  reference to contact information associated with the calendar component.
929         'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
930         'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
931         'rstatus': None,
932         'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
933                                 #  like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
934         'resources': None, # Use: O-n, Type: TEXT, Defines the equipment or resources anticipated for an activity specified by a calendar entity like RESOURCES:EASEL,PROJECTOR,VCR, LANGUAGE=fr:1 raton-laveur
935         'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
936         'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
937         'x-prop': None,
938         'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
939         'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
940     }
941
942     def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
943         """ Export calendar
944             @param self: The object pointer
945             @param cr: the current row, from the database cursor,
946             @param uid: the current user’s ID for security checks,
947             @param datas: Get datas
948             @param context: A standard dictionary for contextual values
949         """
950
951         return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
952
953 Event()
954
955
956 class ToDo(CalDAV, osv.osv_memory):
957     _name = 'basic.calendar.todo'
958     _calname = 'vtodo'
959
960     __attribute__ = {
961                 'class': None,
962                 'completed': None,
963                 'created': None,
964                 'description': None,
965                 'dtstamp': None,
966                 'dtstart': None,
967                 'duration': None,
968                 'due': None,
969                 'geo': None,
970                 'last-mod ': None,
971                 'location': None,
972                 'organizer': None,
973                 'percent': None,
974                 'priority': None,
975                 'recurid': None,
976                 'seq': None,
977                 'status': None,
978                 'summary': None,
979                 'uid': None,
980                 'url': None,
981                 'attach': None,
982                 'attendee': None,
983                 'categories': None,
984                 'comment': None,
985                 'contact': None,
986                 'exdate': None,
987                 'exrule': None,
988                 'rstatus': None,
989                 'related': None,
990                 'resources': None,
991                 'rdate': None,
992                 'rrule': None,
993             }
994
995     def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
996         """ Export Calendar
997             @param self: The object pointer
998             @param cr: the current row, from the database cursor,
999             @param uid: the current user’s ID for security checks,
1000             @param datas: Get datas
1001             @param context: A standard dictionary for contextual values
1002         """
1003
1004         return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
1005
1006 ToDo()
1007
1008
1009 class Journal(CalDAV):
1010     __attribute__ = {
1011     }
1012
1013
1014 class FreeBusy(CalDAV):
1015     __attribute__ = {
1016     'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a  reference to contact information associated with the calendar component.
1017     'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
1018     'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
1019     'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
1020     'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
1021     'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
1022     'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
1023     'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
1024     'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
1025     'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
1026     'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
1027     'rstatus': None,
1028     'X-prop': None,
1029     }
1030
1031
1032 class Timezone(CalDAV, osv.osv_memory):
1033     _name = 'basic.calendar.timezone'
1034     _calname = 'vtimezone'
1035
1036     __attribute__ = {
1037     'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
1038     'last-mod': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
1039     'tzurl': None, # Use: O-1, Type: URI, Provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to-date version of itself.
1040     'standardc': {'tzprop': None}, # Use: R-1,
1041     'daylightc': {'tzprop': None}, # Use: R-1,
1042     'x-prop': None, # Use: O-n, Type: Text,
1043     }
1044
1045     def get_name_offset(self, cr, uid, tzid, context=None):
1046         """ Get Name Offset value
1047             @param self: The object pointer
1048             @param cr: the current row, from the database cursor,
1049             @param uid: the current user’s ID for security checks,
1050             @param context: A standard dictionary for contextual values
1051         """
1052
1053         mytz = pytz.timezone(tzid.title())
1054         mydt = datetime.now(tz=mytz)
1055         offset = mydt.utcoffset()
1056         val = offset.days * 24 + float(offset.seconds) / 3600
1057         realoffset = '%02d%02d' % (math.floor(abs(val)), \
1058                                  round(abs(val) % 1 + 0.01, 2) * 60)
1059         realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
1060         return (mydt.tzname(), realoffset)
1061
1062     def export_cal(self, cr, uid, model, tzid, ical, context=None):
1063         """ Export Calendar
1064             @param self: The object pointer
1065             @param cr: the current row, from the database cursor,
1066             @param uid: the current user’s ID for security checks,
1067             @param model: Get Model's name
1068             @param context: A standard dictionary for contextual values
1069         """
1070         if not context:
1071             context = {}
1072         ctx = context.copy()
1073         ctx.update({'model': model})
1074         cal_tz = ical.add('vtimezone')
1075         cal_tz.add('TZID').value = tzid.title()
1076         tz_std = cal_tz.add('STANDARD')
1077         tzname, offset = self.get_name_offset(cr, uid, tzid)
1078         tz_std.add("TZOFFSETFROM").value = offset
1079         tz_std.add("TZOFFSETTO").value = offset
1080         #TODO: Get start date for timezone
1081         tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1082         tz_std.add("TZNAME").value = tzname
1083         return ical
1084
1085     def import_cal(self, cr, uid, ical_data, context=None):
1086         """ Import Calendar
1087             @param self: The object pointer
1088             @param cr: the current row, from the database cursor,
1089             @param uid: the current user’s ID for security checks,
1090             @param ical_data: Get calendar's data
1091             @param context: A standard dictionary for contextual values
1092         """
1093
1094         for child in ical_data.getChildren():
1095             if child.name.lower() == 'tzid':
1096                 tzname = child.value
1097                 self.ical_set(child.name.lower(), tzname, 'value')
1098         vals = map_data(cr, uid, self, context=context)
1099         return vals
1100
1101 Timezone()
1102
1103
1104 class Alarm(CalDAV, osv.osv_memory):
1105     _name = 'basic.calendar.alarm'
1106     _calname = 'alarm'
1107
1108     __attribute__ = {
1109     'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1110     'description': None, #      Type: Text, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. Use:- R-1 for DISPLAY,Use:- R-1 for EMAIL,Use:- R-1 for PROCEDURE
1111     'summary': None, # Use: R-1, Type: Text        Which contains the text to be used as the message subject. Use for EMAIL
1112     'attendee': None, # Use: R-n, Type: CAL-ADDRESS, Contain the email address of attendees to receive the message. It can also include one or more. Use for EMAIL
1113     'trigger': None, # Use: R-1, Type: DURATION, The "TRIGGER" property specifies a duration prior to the start of an event or a to-do. The "TRIGGER" edge may be explicitly set to be relative to the "START" or "END" of the event or to-do with the "related" parameter of the "TRIGGER" property. The "TRIGGER" property value type can alternatively be set to an absolute calendar date and time of day value. Use for all action like AUDIO, DISPLAY, EMAIL and PROCEDURE
1114     'duration': None, #           Type: DURATION, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
1115     'repeat': None, #           Type: INTEGER, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
1116     'attach': None, # Use:- O-n: which MUST point to a sound resource, which is rendered when the alarm is triggered for AUDIO, Use:- O-n: which are intended to be sent as message attachments for EMAIL, Use:- R-1:which MUST point to a procedure resource, which is invoked when the alarm is triggered for PROCEDURE.
1117     'x-prop': None,
1118     }
1119
1120     def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1121         """ Export Calendar
1122             @param self: The object pointer
1123             @param cr: the current row, from the database cursor,
1124             @param uid: the current user’s ID for security checks,
1125             @param model: Get Model's name
1126             @param alarm_id: Get Alarm's Id
1127             @param context: A standard dictionary for contextual values
1128         """
1129         if not context:
1130             context = {}
1131         valarm = vevent.add('valarm')
1132         alarm_object = self.pool.get(model)
1133         alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1134
1135         # Compute trigger data
1136         interval = alarm_data['trigger_interval']
1137         occurs = alarm_data['trigger_occurs']
1138         duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1139                                         or -(alarm_data['trigger_duration'])
1140         related = alarm_data['trigger_related']
1141         trigger = valarm.add('TRIGGER')
1142         trigger.params['related'] = [related.upper()]
1143         if interval == 'days':
1144             delta = timedelta(days=duration)
1145         if interval == 'hours':
1146             delta = timedelta(hours=duration)
1147         if interval == 'minutes':
1148             delta = timedelta(minutes=duration)
1149         trigger.value = delta
1150
1151         # Compute other details
1152         valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1153         valarm.add('ACTION').value = alarm_data['action']
1154         return vevent
1155
1156     def import_cal(self, cr, uid, ical_data, context=None):
1157         """ Import Calendar
1158             @param self: The object pointer
1159             @param cr: the current row, from the database cursor,
1160             @param uid: the current user’s ID for security checks,
1161             @param ical_data: Get calendar's Data
1162             @param context: A standard dictionary for contextual values
1163         """
1164
1165         ctx = context.copy()
1166         ctx.update({'model': context.get('model', None)})
1167         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1168         for child in ical_data.getChildren():
1169             if child.name.lower() == 'trigger':
1170                 seconds = child.value.seconds
1171                 days = child.value.days
1172                 diff = (days * 86400) +  seconds
1173                 interval = 'days'
1174                 related = 'before'
1175                 if not seconds:
1176                     duration = abs(days)
1177                     related = days > 0 and 'after' or 'before'
1178                 elif (abs(diff) / 3600) == 0:
1179                     duration = abs(diff / 60)
1180                     interval = 'minutes'
1181                     related = days >= 0 and 'after' or 'before'
1182                 else:
1183                     duration = abs(diff / 3600)
1184                     interval = 'hours'
1185                     related = days >= 0 and 'after' or 'before'
1186                 self.ical_set('trigger_interval', interval, 'value')
1187                 self.ical_set('trigger_duration', duration, 'value')
1188                 self.ical_set('trigger_occurs', related.lower(), 'value')
1189                 if child.params:
1190                     if child.params.get('related'):
1191                         self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1192             else:
1193                 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1194         vals = map_data(cr, uid, self, context=context)
1195         return vals
1196
1197 Alarm()
1198
1199
1200 class Attendee(CalDAV, osv.osv_memory):
1201     _name = 'basic.calendar.attendee'
1202     _calname = 'attendee'
1203
1204     __attribute__ = {
1205     'cutype': None, # Use: 0-1    Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1206     'member': None, # Use: 0-1    Specify the group or list membership of the calendar user specified by the property.
1207     'role': None, # Use: 0-1    Specify the participation role for the calendar user specified by the property like "CHAIR"/"REQ-PARTICIPANT"/"OPT-PARTICIPANT"/"NON-PARTICIPANT"
1208     'partstat': None, # Use: 0-1    Specify the participation status for the calendar user specified by the property. like use for VEVENT:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED", use for VTODO:-"NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED"/"COMPLETED"/"IN-PROCESS" and use for VJOURNAL:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED".
1209     'rsvp': None, # Use: 0-1    Specify whether there is an expectation of a favor of a reply from the calendar user specified by the property value like TRUE / FALSE.
1210     'delegated-to': None, # Use: 0-1    Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1211     'delegated-from': None, # Use: 0-1    Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1212     'sent-by': None, # Use: 0-1    Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1213     'cn': None, # Use: 0-1    Specify the common name to be associated with the calendar user specified by the property.
1214     'dir': None, # Use: 0-1    Specify reference to a directory entry associated with the calendar user specified by the property.
1215     'language': None, # Use: 0-1    Specify the language for text values in a property or property parameter.
1216     }
1217
1218     def import_cal(self, cr, uid, ical_data, context=None):
1219         """ Import Calendar
1220             @param self: The object pointer
1221             @param cr: the current row, from the database cursor,
1222             @param uid: the current user’s ID for security checks,
1223             @param ical_data: Get calendar's Data
1224             @param context: A standard dictionary for contextual values
1225         """
1226
1227         ctx = context.copy()
1228         ctx.update({'model': context.get('model', None)})
1229         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1230         for para in ical_data.params:
1231             if para.lower() == 'cn':
1232                 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1233                         ical_data.value, 'value')
1234             else:
1235                 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1236         if not ical_data.params.get('CN'):
1237             self.ical_set('cn', ical_data.value, 'value')
1238         vals = map_data(cr, uid, self, context=context)
1239         return vals
1240
1241     def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1242         """ Export Calendar
1243             @param self: The object pointer
1244             @param cr: the current row, from the database cursor,
1245             @param uid: the current user’s ID for security checks,
1246             @param model: Get model's name
1247             @param attendee_ids: Get Attendee's Id
1248             @param context: A standard dictionary for contextual values
1249         """
1250         if not context:
1251             context = {}
1252         attendee_object = self.pool.get(model)
1253         ctx = context.copy()
1254         ctx.update({'model': model})
1255         self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1256         for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1257             attendee_add = vevent.add('attendee')
1258             cn_val = ''
1259             for a_key, a_val in self.__attribute__.items():
1260                 if attendee[a_val['field']] and a_val['field'] != 'cn':
1261                     if a_val['type'] in ('text', 'char', 'selection'):
1262                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1263                     elif a_val['type'] == 'boolean':
1264                         attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1265                 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1266                     cn_val = [str(attendee[a_val['field']])]
1267                     if cn_val:
1268                         attendee_add.params['CN'] = cn_val
1269             if not attendee['email']:
1270                 attendee_add.value = 'MAILTO:'
1271                 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1272             elif attendee['email']:
1273                 attendee_add.value = 'MAILTO:' + attendee['email']
1274         return vevent
1275
1276 Attendee()
1277
1278 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: