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