1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
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 _
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
41 raise osv.except_osv(_('vobject Import Error!'), _('Please install python-vobject from http://vobject.skyhouseconsulting.com/'))
43 # O-1 Optional and can come only once
44 # O-n Optional and can come more than once
45 # R-1 Required and can come only once
46 # R-n Required and can come more than once
48 def uid2openobjectid(cr, uidval, oomodel, rdate):
49 """ UID To Open Object Id
50 @param cr: the current row, from the database cursor,
51 @param uidval: Get USerId vale
52 @oomodel: Open Object ModelName
53 @param rdate: Get Recurrent Date
55 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
58 wematch = __rege.match(uidval.encode('utf8'))
62 model, id, dbname = wematch.groups()
63 model_obj = pooler.get_pool(cr.dbname).get(model)
64 if (not model == oomodel) or (not dbname == cr.dbname):
66 qry = 'SELECT DISTINCT(id) FROM %s' % model_obj._table
68 qry += " WHERE recurrent_id=%s"
69 cr.execute(qry, (rdate,))
76 ids = map(lambda x: str(x[0]), cr.fetchall())
81 def openobjectid2uid(cr, uidval, oomodel):
82 """ Gives the value of UID for VEVENT
83 @param cr: the current row, from the database cursor,
84 @param uidval: Id value of the Event
85 @oomodel: Open Object ModelName """
87 value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname)
91 """Take a dict of mail and convert to string.
94 if isinstance(arg, dict):
100 rstr = ard.get('name','')
101 if ard.get('company',False):
102 rstr += ' (%s)' % ard.get('company')
104 rstr += ' <%s>' % ard.get('email')
106 return ', '.join(ret)
108 def str2mailto(emailstr, multi=False):
109 """Split one email string to a dict of name, company, mail parts
111 @param multi Return an array, recognize comma-sep
113 # TODO: move to tools or sth.
114 mege = re.compile(r'([^\(\<]+) *(\((.*?)\))? *(\< ?(.*?) ?\>)? ?(\((.*?)\))? *$')
119 mailz = emailstr.split(',')
122 m = mege.match(mas.strip())
124 # one of the rare non-matching strings is "sad" :(
125 # retz.append({ 'name': mas.strip() })
127 raise ValueError("Invalid email address %r" % mas)
128 rd = { 'name': m.group(1).strip(),
129 'email': m.group(5), }
131 rd['company'] = m.group(3).strip()
133 rd['company'] = m.group(7).strip()
135 if rd['name'].startswith('"') and rd['name'].endswith('"'):
136 rd['name'] = rd['name'][1:-1]
144 def get_attribute_mapping(cr, uid, calname, context=None):
145 """ Attribute Mapping with Basic calendar fields and lines
146 @param cr: the current row, from the database cursor,
147 @param uid: the current user’s ID for security checks,
148 @param calname: Get Calendar name
149 @param context: A standard dictionary for contextual values """
153 pool = pooler.get_pool(cr.dbname)
154 field_obj = pool.get('basic.calendar.fields')
155 type_obj = pool.get('basic.calendar.lines')
156 domain = [('object_id.model', '=', context.get('model'))]
157 if context.get('calendar_id'):
158 domain.append(('calendar_id', '=', context.get('calendar_id')))
159 type_id = type_obj.search(cr, uid, domain)
160 fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])])
162 for field in field_obj.browse(cr, uid, fids):
163 attr = field.name.name
165 res[attr]['field'] = field.field_id.name
166 res[attr]['type'] = field.field_id.ttype
167 if field.fn == 'hours':
168 res[attr]['type'] = "timedelta"
169 if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
170 res[attr]['object'] = field.field_id.relation
171 elif res[attr]['type'] in ('selection') and field.mapping:
172 res[attr]['mapping'] = eval(field.mapping)
173 if not res.get('uid', None):
175 res['uid']['field'] = 'id'
176 res['uid']['type'] = "integer"
179 def map_data(cr, uid, obj, context=None):
181 @param self: The object pointer
182 @param cr: the current row, from the database cursor,"""
185 for map_dict in obj.__attribute__:
186 map_val = obj.ical_get(map_dict, 'value')
187 field = obj.ical_get(map_dict, 'field')
188 field_type = obj.ical_get(map_dict, 'type')
190 if field_type == 'selection':
193 if type(map_val) == list and len(map_val): #TOFIX: why need to check this
195 mapping = obj.__attribute__[map_dict].get('mapping', False)
197 map_val = mapping.get(map_val.lower(), False)
199 map_val = map_val.lower()
200 if field_type == 'many2many':
205 model = obj.__attribute__[map_dict].get('object', False)
206 modobj = obj.pool.get(model)
207 for map_vall in map_val:
208 id = modobj.create(cr, uid, map_vall, context=context)
210 vals[field] = [(6, 0, ids)]
212 if field_type == 'many2one':
214 if not map_val or not isinstance(map_val, dict):
217 model = obj.__attribute__[map_dict].get('object', False)
218 modobj = obj.pool.get(model)
219 # check if the record exists or not
220 key1 = map_val.keys()
221 value1 = map_val.values()
222 domain = [(key1[i], '=', value1[i]) for i in range(len(key1)) if value1[i]]
223 exist_id = modobj.search(cr, uid, domain, context=context)
227 id = modobj.create(cr, uid, map_val, context=context)
230 if field_type == 'timedelta':
232 vals[field] = (map_val.seconds/float(86400) + map_val.days)
233 vals[field] = map_val
236 class CalDAV(object):
238 _logger = logging.getLogger('document.caldav')
240 def ical_set(self, name, value, type):
241 """ set calendar Attribute
242 @param self: The object pointer,
243 @param name: Get Attribute Name
244 @param value: Get Attribute Value
245 @param type: Get Attribute Type
247 if name in self.__attribute__ and self.__attribute__[name]:
248 self.__attribute__[name][type] = value
251 def ical_get(self, name, type):
252 """ Get calendar Attribute
253 @param self: The object pointer,
254 @param name: Get Attribute Name
255 @param type: Get Attribute Type
258 if self.__attribute__.get(name):
259 val = self.__attribute__.get(name).get(type, None)
260 valtype = self.__attribute__.get(name).get('type', None)
262 if valtype and valtype == 'datetime' and val:
263 if isinstance(val, list):
264 val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
266 val = val.strftime('%Y-%m-%d %H:%M:%S')
269 return self.__attribute__.get(name, None)
271 def ical_reset(self, type):
272 """ Reset Calendar Attribute
273 @param self: The object pointer,
274 @param type: Get Attribute Type
277 for name in self.__attribute__:
278 if self.__attribute__[name]:
279 self.__attribute__[name][type] = None
282 def format_date_tz(self, src_date, tz=None):
283 """ This function converts date into specifice timezone value
284 @param src_date: Date to be converted (datetime.datetime)
285 @return: Converted datetime.datetime object for the date
287 format = tools.DEFAULT_SERVER_DATETIME_FORMAT
288 date_str = src_date.strftime('%Y-%m-%d %H:%M:%S')
289 res_date = tools.server_to_local_timestamp(date_str, format, format, tz)
290 return datetime.strptime(res_date, "%Y-%m-%d %H:%M:%S")
292 def parse_ics(self, cr, uid, child, cal_children=None, context=None):
293 """ parse calendaring and scheduling information
294 @param self: The object pointer
295 @param cr: the current row, from the database cursor,
296 @param uid: the current user’s ID for security checks,
297 @param context: A standard dictionary for contextual values """
301 _server_tzinfo = pytz.timezone(tools.get_server_timezone())
303 for cal_data in child.getChildren():
304 if cal_data.name.lower() == 'organizer':
305 dmail = { 'name': cal_data.params.get('CN', ['',])[0],
306 'email': cal_data.value.lower().replace('mailto:',''),
309 self.ical_set(cal_data.name.lower(), mailto2str(dmail), 'value')
311 if cal_data.name.lower() == 'attendee':
314 ctx.update({'model': cal_children[cal_data.name.lower()]})
315 attendee = self.pool.get('basic.calendar.attendee')
316 att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx))
317 self.ical_set(cal_data.name.lower(), att_data, 'value')
319 if cal_data.name.lower() == 'valarm':
320 alarm = self.pool.get('basic.calendar.alarm')
323 ctx.update({'model': cal_children[cal_data.name.lower()]})
324 vals = alarm.import_cal(cr, uid, cal_data, context=ctx)
325 self.ical_set(cal_data.name.lower(), vals, 'value')
327 if cal_data.name.lower() == 'exdate':
328 exdates += cal_data.value
330 for exdate in exdates:
331 exvals.append(datetime.fromtimestamp(time.mktime(exdate.utctimetuple())).strftime('%Y%m%dT%H%M%S'))
332 self.ical_set(cal_data.name.lower(), ','.join(exvals), 'value')
334 if cal_data.name.lower() in self.__attribute__:
335 if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
336 self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
337 date_local = cal_data.value.astimezone(_server_tzinfo)
338 self.ical_set(cal_data.name.lower(), date_local, 'value')
340 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
341 vals = map_data(cr, uid, self, context=context)
344 def create_ics(self, cr, uid, datas, name, ical, context=None):
345 """ create calendaring and scheduling information
346 @param self: The object pointer
347 @param cr: the current row, from the database cursor,
348 @param uid: the current user’s ID for security checks,
349 @param context: A standard dictionary for contextual values """
358 vevent = ical.add(name)
359 for field in self.__attribute__.keys():
360 map_field = self.ical_get(field, 'field')
361 map_type = self.ical_get(field, 'type')
362 if map_field in data.keys():
364 model = context.get('model', None)
367 uidval = openobjectid2uid(cr, data[map_field], model)
368 #Computation for getting events with the same UID (RFC4791 Section4.1)
370 model_obj = self.pool.get(model)
372 if model_obj._columns.get('recurrent_uid', None):
373 cr.execute('SELECT id FROM %s WHERE recurrent_uid=%%s' % model_obj._table,
375 r_ids = map(lambda x: x[0], cr.fetchall())
377 r_datas = model_obj.read(cr, uid, r_ids, context=context)
378 rcal = CalDAV.export_cal(self, cr, uid, r_datas, 'vevent', context=context)
379 for revents in rcal.contents.get('vevent', []):
380 ical.contents['vevent'].append(revents)
382 if data.get('recurrent_uid', None):
383 # Change the UID value in case of modified event from any recurrent event
384 uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
385 vevent.add('uid').value = uidval
386 elif field == 'attendee' and data[map_field]:
387 model = self.__attribute__[field].get('object', False)
388 attendee_obj = self.pool.get('basic.calendar.attendee')
389 vevent = attendee_obj.export_cal(cr, uid, model, \
390 data[map_field], vevent, context=context)
391 elif field == 'valarm' and data[map_field]:
392 model = self.__attribute__[field].get('object', False)
394 ctx.update({'model': model})
395 alarm_obj = self.pool.get('basic.calendar.alarm')
396 vevent = alarm_obj.export_cal(cr, uid, model, \
397 data[map_field][0], vevent, context=ctx)
398 elif field == 'vtimezone' and data[map_field]:
399 tzval = data[map_field]
400 if tzval not in timezones:
401 tz_obj = self.pool.get('basic.calendar.timezone')
402 ical = tz_obj.export_cal(cr, uid, None, \
403 data[map_field], ical, context=context)
404 timezones.append(data[map_field])
405 if vevent.contents.get('recurrence-id'):
406 # Convert recurrence-id field value accroding to timezone value
407 recurid_val = vevent.contents.get('recurrence-id')[0].value
408 vevent.contents.get('recurrence-id')[0].params['TZID'] = [tzval.title()]
409 vevent.contents.get('recurrence-id')[0].value = self.format_date_tz(recurid_val, tzval.title())
411 # Set exdates according to timezone value
412 # This is the case when timezone mapping comes after the exdate mapping
413 # and we have exdate value available
414 exfield.params['TZID'] = [tzval.title()]
416 for exdate in exdates:
417 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
418 exfield.value = exdates_updated
419 elif field == 'organizer' and data[map_field]:
420 organizer = str2mailto(data[map_field])
421 event_org = vevent.add('organizer')
422 event_org.params['CN'] = [organizer['name']]
423 event_org.value = 'MAILTO:' + (organizer.get('email') or '')
425 elif data[map_field]:
426 if map_type in ("char", "text"):
427 if field in ('exdate'):
428 exfield = vevent.add(field)
429 exdates = (data[map_field]).split(',')
431 # Set exdates according to timezone value
432 # This is the case when timezone mapping comes before the exdate mapping
433 # and we have timezone value available
434 exfield.params['TZID'] = [tzval.title()]
436 for exdate in exdates:
437 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
438 exfield.value = exdates_updated
440 vevent.add(field).value = tools.ustr(data[map_field])
441 elif map_type in ('datetime', 'date') and data[map_field]:
442 dtfield = vevent.add(field)
444 # Export the date according to the event timezone value
445 dtfield.params['TZID'] = [tzval.title()]
446 dtfield.value = self.format_date_tz(parser.parse(data[map_field]), tzval.title())
448 dtfield.value = parser.parse(data[map_field])
449 elif map_type == "timedelta":
450 vevent.add(field).value = timedelta(hours=data[map_field])
451 elif map_type == "many2one":
452 vevent.add(field).value = tools.ustr(data.get(map_field)[1])
453 elif map_type in ("float", "integer"):
454 vevent.add(field).value = str(data.get(map_field))
455 elif map_type == "selection":
456 if not self.ical_get(field, 'mapping'):
457 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
459 for key1, val1 in self.ical_get(field, 'mapping').items():
460 if val1 == data[map_field]:
461 vevent.add(field).value = key1.upper()
464 def check_import(self, cr, uid, vals, context=None):
466 @param self: The object pointer
467 @param cr: the current row, from the database cursor,
468 @param uid: the current user’s ID for security checks,
469 @param vals: Get Values
470 @param context: A standard dictionary for contextual values
475 model_obj = self.pool.get(context.get('model'))
479 # Compute value of duration
480 if 'date_deadline' in val and 'duration' not in val:
481 start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
482 end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
484 val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
485 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
486 val.get('recurrent_id'))
487 if val.has_key('create_date'):
488 val.pop('create_date')
489 u_id = val.get('id', None)
492 val.update({'recurrent_uid': exists})
493 model_obj.write(cr, uid, [r_id], val)
496 model_obj.write(cr, uid, [exists], val)
499 if u_id in recur_pool and val.get('recurrent_id'):
500 val.update({'recurrent_uid': recur_pool[u_id]})
501 revent_id = model_obj.create(cr, uid, val)
502 ids.append(revent_id)
504 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
505 wematch = __rege.match(u_id.encode('utf8'))
507 model, recur_id, dbname = wematch.groups()
508 val.update({'recurrent_uid': recur_id})
509 event_id = model_obj.create(cr, uid, val)
510 recur_pool[u_id] = event_id
516 def export_cal(self, cr, uid, datas, vobj=None, context=None):
518 @param self: The object pointer
519 @param cr: the current row, from the database cursor,
520 @param uid: the current user’s ID for security checks,
521 @param datas: Get Data's for caldav
522 @param context: A standard dictionary for contextual values
525 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
526 ical = vobject.iCalendar()
527 self.create_ics(cr, uid, datas, vobj, ical, context=context)
530 raise # osv.except_osv(('Error !'), (str(e)))
532 def import_cal(self, cr, uid, content, data_id=None, context=None):
534 @param self: The object pointer
535 @param cr: the current row, from the database cursor,
536 @param uid: the current user’s ID for security checks,
537 @param data_id: Get Data’s ID or False
538 @param context: A standard dictionary for contextual values
542 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
543 parsedCal = vobject.readOne(ical_data)
546 for child in parsedCal.getChildren():
547 if child.name.lower() in ('vevent', 'vtodo'):
548 vals = self.parse_ics(cr, uid, child, context=context)
552 if vals: res.append(vals)
553 self.ical_reset('value')
556 class Calendar(CalDAV, osv.osv):
557 _name = 'basic.calendar'
558 _calname = 'calendar'
561 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
562 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
563 # or the minimum and maximum range of the iCalendar specification
564 # that is required in order to interpret the iCalendar object.
565 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
566 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
567 'vevent': None, # Use: O-n, Type: Collection of Event class
568 'vtodo': None, # Use: O-n, Type: Collection of ToDo class
569 'vjournal': None, # Use: O-n, Type: Collection of Journal class
570 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
571 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
574 'name': fields.char("Name", size=64),
575 'user_id': fields.many2one('res.users', 'Owner'),
576 'collection_id': fields.many2one('document.directory', 'Collection', \
578 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
579 string="Type", size=64),
580 'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
581 'create_date': fields.datetime('Created Date', readonly=True),
582 'write_date': fields.datetime('Modifided Date', readonly=True),
583 'description': fields.text("description"),
584 'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"),
585 'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"),
586 'has_webcal': fields.boolean('WebCal', required=True, help="Also export a <name>.ics entry next to the calendar folder, with WebCal content."),
593 def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
599 ctx_res_id = context.get('res_id', None)
600 ctx_model = context.get('model', None)
601 for cal in self.browse(cr, uid, ids):
602 for line in cal.line_ids:
603 if ctx_model and ctx_model != line.object_id.model:
605 if line.name in ('valarm', 'attendee'):
607 line_domain = eval(line.domain or '[]', context)
608 line_domain += domain
610 line_domain += [('id','=',ctx_res_id)]
611 mod_obj = self.pool.get(line.object_id.model)
612 data_ids = mod_obj.search(cr, uid, line_domain, order="id", context=context)
613 for data in mod_obj.browse(cr, uid, data_ids, context):
614 ctx = parent and parent.context or None
615 if hasattr(data, 'recurrent_uid') and data.recurrent_uid:
616 # Skip for event which is child of other event
618 node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
623 def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None):
629 ctx_res_id = context.get('res_id', None)
630 ctx_model = context.get('model', None)
631 for cal in self.browse(cr, uid, ids):
632 for line in cal.line_ids:
633 if ctx_model and ctx_model != line.object_id.model:
635 if line.name in ('valarm', 'attendee'):
637 line_domain = eval(line.domain or '[]', context)
638 line_domain += domain
640 line_domain += [('id','=',ctx_res_id)]
641 mod_obj = self.pool.get(line.object_id.model)
642 max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context)
643 if res and res > max_data:
648 def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
650 @param ids: List of calendar’s IDs
651 @param vobj: the type of object to export
652 @return the ical data.
656 ctx_model = context.get('model', None)
657 ctx_res_id = context.get('res_id', None)
658 ical = vobject.iCalendar()
659 for cal in self.browse(cr, uid, ids):
660 for line in cal.line_ids:
661 if ctx_model and ctx_model != line.object_id.model:
663 if line.name in ('valarm', 'attendee'):
665 domain = eval(line.domain or '[]', context)
667 domain += [('id','=',ctx_res_id)]
668 mod_obj = self.pool.get(line.object_id.model)
669 data_ids = mod_obj.search(cr, uid, domain, context=context)
670 datas = mod_obj.read(cr, uid, data_ids, context=context)
671 context.update({'model': line.object_id.model,
672 'calendar_id': cal.id
674 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
675 self.create_ics(cr, uid, datas, line.name, ical, context=context)
676 return ical.serialize()
678 def import_cal(self, cr, uid, content, data_id=None, context=None):
680 @param self: The object pointer
681 @param cr: the current row, from the database cursor,
682 @param uid: the current user’s ID for security checks,
683 @param data_id: Get Data’s ID or False
684 @param context: A standard dictionary for contextual values
690 parsedCal = vobject.readOne(ical_data)
692 data_id = self.search(cr, uid, [])[0]
693 cal = self.browse(cr, uid, data_id, context=context)
696 for line in cal.line_ids:
697 cal_children[line.name] = line.object_id.model
700 for child in parsedCal.getChildren():
701 if child.name.lower() in cal_children:
702 context.update({'model': cal_children[child.name.lower()],
703 'calendar_id': cal['id']
705 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
706 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
708 objs.append(cal_children[child.name.lower()])
709 elif child.name.upper() == 'CALSCALE':
710 if child.value.upper() != 'GREGORIAN':
711 self._logger.warning('How do I handle %s calendars?',child.value)
712 elif child.name.upper() in ('PRODID', 'VERSION'):
714 elif child.name.upper().startswith('X-'):
715 self._logger.debug("skipping custom node %s", child.name)
717 self._logger.debug("skipping node %s", child.name)
720 for obj_name in list(set(objs)):
721 obj = self.pool.get(obj_name)
722 if hasattr(obj, 'check_import'):
723 r = obj.check_import(cr, uid, vals, context=context)
728 r = self.check_import(cr, uid, vals, context=context)
735 class basic_calendar_line(osv.osv):
736 """ Calendar Lines """
738 _name = 'basic.calendar.lines'
739 _description = 'Calendar Lines'
742 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
743 ('valarm', 'Alarm'), \
744 ('attendee', 'Attendee')], \
745 string="Type", size=64),
746 'object_id': fields.many2one('ir.model', 'Object'),
747 'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
748 required=True, ondelete='cascade'),
749 'domain': fields.char('Domain', size=124),
750 'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
754 'domain': lambda *a: '[]',
757 def create(self, cr, uid, vals, context=None):
758 """ create calendar's line
759 @param self: The object pointer
760 @param cr: the current row, from the database cursor,
761 @param uid: the current user’s ID for security checks,
762 @param vals: Get the Values
763 @param context: A standard dictionary for contextual values
766 cr.execute("SELECT COUNT(id) FROM basic_calendar_lines \
767 WHERE name=%s AND calendar_id=%s",
768 (vals.get('name'), vals.get('calendar_id')))
772 raise osv.except_osv(_('Warning !'), _('Can not create line "%s" more than once') % (vals.get('name')))
773 return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
775 basic_calendar_line()
777 class basic_calendar_alias(osv.osv):
778 """ Mapping of client filenames to ORM ids of calendar records
780 Since some clients insist on putting arbitrary filenames on the .ics data
781 they send us, and they won't respect the redirection "Location:" header,
782 we have to store those filenames and allow clients to call our calendar
784 Note that adding a column to all tables that would possibly hold calendar-
785 mapped data won't work. The user is always allowed to specify more
786 calendars, on any arbitrary ORM object, without need to alter those tables'
789 _name = 'basic.calendar.alias'
791 'name': fields.char('Filename', size=512, required=True, select=1),
792 'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
793 select=1, help='The calendar/line this mapping applies to'),
794 'res_id': fields.integer('Res. ID', required=True, select=1),
797 _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
798 _('The same filename cannot apply to two records!')), ]
800 basic_calendar_alias()
802 class basic_calendar_attribute(osv.osv):
803 _name = 'basic.calendar.attributes'
804 _description = 'Calendar attributes'
806 'name': fields.char("Name", size=64, required=True),
807 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
808 ('alarm', 'Alarm'), \
809 ('attendee', 'Attendee')], \
810 string="Type", size=64, required=True),
813 basic_calendar_attribute()
816 class basic_calendar_fields(osv.osv):
817 """ Calendar fields """
819 _name = 'basic.calendar.fields'
820 _description = 'Calendar fields'
823 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
824 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
825 'type_id': fields.many2one('basic.calendar.lines', 'Type', \
826 required=True, ondelete='cascade'),
827 'expr': fields.char("Expression", size=64),
828 'fn': fields.selection([('field', 'Use the field'),
829 ('const', 'Expression as constant'),
830 ('hours', 'Interval in hours'),
832 'mapping': fields.text('Mapping'),
836 'fn': lambda *a: 'field',
840 ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
843 def check_line(self, cr, uid, vals, name, context=None):
844 """ check calendar's line
845 @param self: The object pointer
846 @param cr: the current row, from the database cursor,
847 @param uid: the current user’s ID for security checks,
848 @param vals: Get Values
849 @param context: A standard dictionary for contextual values
851 f_obj = self.pool.get('ir.model.fields')
852 field = f_obj.browse(cr, uid, vals['field_id'], context=context)
853 relation = field.relation
854 line_obj = self.pool.get('basic.calendar.lines')
855 l_id = line_obj.search(cr, uid, [('name', '=', name)])
857 line = line_obj.browse(cr, uid, l_id, context=context)[0]
858 line_rel = line.object_id.model
859 if (relation != 'NULL') and (not relation == line_rel):
860 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines') % (name))
863 def create(self, cr, uid, vals, context=None):
864 """ Create Calendar's fields
865 @param self: The object pointer
866 @param cr: the current row, from the database cursor,
867 @param uid: the current user’s ID for security checks,
868 @param vals: Get Values
869 @param context: A standard dictionary for contextual values
872 cr.execute('SELECT name FROM basic_calendar_attributes \
873 WHERE id=%s', (vals.get('name'),))
876 if name in ('valarm', 'attendee'):
877 self.check_line(cr, uid, vals, name, context=context)
878 return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
880 def write(self, cr, uid, ids, vals, context=None):
881 """ write Calendar's fields
882 @param self: The object pointer
883 @param cr: the current row, from the database cursor,
884 @param uid: the current user’s ID for security checks,
885 @param vals: Get Values
886 @param context: A standard dictionary for contextual values
892 field = self.browse(cr, uid, id, context=context)
893 name = field.name.name
894 if name in ('valarm', 'attendee'):
895 self.check_line(cr, uid, vals, name, context=context)
896 return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
898 basic_calendar_fields()
901 class Event(CalDAV, osv.osv_memory):
902 _name = 'basic.calendar.event'
905 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
906 '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.
907 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
908 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
909 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
910 '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.
911 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component.
912 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
913 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
914 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
915 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
916 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
917 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
918 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
919 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
920 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
922 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
923 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
924 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
925 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
926 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component.
927 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
928 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
930 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
931 # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
932 '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
933 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
934 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
936 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
937 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
940 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
942 @param self: The object pointer
943 @param cr: the current row, from the database cursor,
944 @param uid: the current user’s ID for security checks,
945 @param datas: Get datas
946 @param context: A standard dictionary for contextual values
949 return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
954 class ToDo(CalDAV, osv.osv_memory):
955 _name = 'basic.calendar.todo'
993 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
995 @param self: The object pointer
996 @param cr: the current row, from the database cursor,
997 @param uid: the current user’s ID for security checks,
998 @param datas: Get datas
999 @param context: A standard dictionary for contextual values
1002 return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
1007 class Journal(CalDAV):
1012 class FreeBusy(CalDAV):
1014 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component.
1015 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
1016 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
1017 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
1018 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
1019 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
1020 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
1021 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
1022 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
1023 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
1024 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
1030 class Timezone(CalDAV, osv.osv_memory):
1031 _name = 'basic.calendar.timezone'
1032 _calname = 'vtimezone'
1035 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
1036 '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.
1037 '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.
1038 'standardc': {'tzprop': None}, # Use: R-1,
1039 'daylightc': {'tzprop': None}, # Use: R-1,
1040 'x-prop': None, # Use: O-n, Type: Text,
1043 def get_name_offset(self, cr, uid, tzid, context=None):
1044 """ Get Name Offset value
1045 @param self: The object pointer
1046 @param cr: the current row, from the database cursor,
1047 @param uid: the current user’s ID for security checks,
1048 @param context: A standard dictionary for contextual values
1051 mytz = pytz.timezone(tzid.title())
1052 mydt = datetime.now(tz=mytz)
1053 offset = mydt.utcoffset()
1054 val = offset.days * 24 + float(offset.seconds) / 3600
1055 realoffset = '%02d%02d' % (math.floor(abs(val)), \
1056 round(abs(val) % 1 + 0.01, 2) * 60)
1057 realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
1058 return (mydt.tzname(), realoffset)
1060 def export_cal(self, cr, uid, model, tzid, ical, context=None):
1062 @param self: The object pointer
1063 @param cr: the current row, from the database cursor,
1064 @param uid: the current user’s ID for security checks,
1065 @param model: Get Model's name
1066 @param context: A standard dictionary for contextual values
1070 ctx = context.copy()
1071 ctx.update({'model': model})
1072 cal_tz = ical.add('vtimezone')
1073 cal_tz.add('TZID').value = tzid.title()
1074 tz_std = cal_tz.add('STANDARD')
1075 tzname, offset = self.get_name_offset(cr, uid, tzid)
1076 tz_std.add("TZOFFSETFROM").value = offset
1077 tz_std.add("TZOFFSETTO").value = offset
1078 #TODO: Get start date for timezone
1079 tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1080 tz_std.add("TZNAME").value = tzname
1083 def import_cal(self, cr, uid, ical_data, context=None):
1085 @param self: The object pointer
1086 @param cr: the current row, from the database cursor,
1087 @param uid: the current user’s ID for security checks,
1088 @param ical_data: Get calendar's data
1089 @param context: A standard dictionary for contextual values
1092 for child in ical_data.getChildren():
1093 if child.name.lower() == 'tzid':
1094 tzname = child.value
1095 self.ical_set(child.name.lower(), tzname, 'value')
1096 vals = map_data(cr, uid, self, context=context)
1102 class Alarm(CalDAV, osv.osv_memory):
1103 _name = 'basic.calendar.alarm'
1107 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1108 '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
1109 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL
1110 '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
1111 '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
1112 '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
1113 '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
1114 '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.
1118 def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1120 @param self: The object pointer
1121 @param cr: the current row, from the database cursor,
1122 @param uid: the current user’s ID for security checks,
1123 @param model: Get Model's name
1124 @param alarm_id: Get Alarm's Id
1125 @param context: A standard dictionary for contextual values
1129 valarm = vevent.add('valarm')
1130 alarm_object = self.pool.get(model)
1131 alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1133 # Compute trigger data
1134 interval = alarm_data['trigger_interval']
1135 occurs = alarm_data['trigger_occurs']
1136 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1137 or -(alarm_data['trigger_duration'])
1138 related = alarm_data['trigger_related']
1139 trigger = valarm.add('TRIGGER')
1140 trigger.params['related'] = [related.upper()]
1141 if interval == 'days':
1142 delta = timedelta(days=duration)
1143 if interval == 'hours':
1144 delta = timedelta(hours=duration)
1145 if interval == 'minutes':
1146 delta = timedelta(minutes=duration)
1147 trigger.value = delta
1149 # Compute other details
1150 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1151 valarm.add('ACTION').value = alarm_data['action']
1154 def import_cal(self, cr, uid, ical_data, context=None):
1156 @param self: The object pointer
1157 @param cr: the current row, from the database cursor,
1158 @param uid: the current user’s ID for security checks,
1159 @param ical_data: Get calendar's Data
1160 @param context: A standard dictionary for contextual values
1163 ctx = context.copy()
1164 ctx.update({'model': context.get('model', None)})
1165 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1166 for child in ical_data.getChildren():
1167 if child.name.lower() == 'trigger':
1168 seconds = child.value.seconds
1169 days = child.value.days
1170 diff = (days * 86400) + seconds
1174 duration = abs(days)
1175 related = days > 0 and 'after' or 'before'
1176 elif (abs(diff) / 3600) == 0:
1177 duration = abs(diff / 60)
1178 interval = 'minutes'
1179 related = days >= 0 and 'after' or 'before'
1181 duration = abs(diff / 3600)
1183 related = days >= 0 and 'after' or 'before'
1184 self.ical_set('trigger_interval', interval, 'value')
1185 self.ical_set('trigger_duration', duration, 'value')
1186 self.ical_set('trigger_occurs', related.lower(), 'value')
1188 if child.params.get('related'):
1189 self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1191 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1192 vals = map_data(cr, uid, self, context=context)
1198 class Attendee(CalDAV, osv.osv_memory):
1199 _name = 'basic.calendar.attendee'
1200 _calname = 'attendee'
1203 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1204 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property.
1205 '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"
1206 '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".
1207 '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.
1208 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1209 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1210 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1211 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property.
1212 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property.
1213 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter.
1216 def import_cal(self, cr, uid, ical_data, context=None):
1218 @param self: The object pointer
1219 @param cr: the current row, from the database cursor,
1220 @param uid: the current user’s ID for security checks,
1221 @param ical_data: Get calendar's Data
1222 @param context: A standard dictionary for contextual values
1225 ctx = context.copy()
1226 ctx.update({'model': context.get('model', None)})
1227 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1228 for para in ical_data.params:
1229 if para.lower() == 'cn':
1230 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1231 ical_data.value, 'value')
1233 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1234 if not ical_data.params.get('CN'):
1235 self.ical_set('cn', ical_data.value, 'value')
1236 vals = map_data(cr, uid, self, context=context)
1239 def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
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 model: Get model's name
1245 @param attendee_ids: Get Attendee's Id
1246 @param context: A standard dictionary for contextual values
1250 attendee_object = self.pool.get(model)
1251 ctx = context.copy()
1252 ctx.update({'model': model})
1253 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1254 for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1255 attendee_add = vevent.add('attendee')
1257 for a_key, a_val in self.__attribute__.items():
1258 if attendee[a_val['field']] and a_val['field'] != 'cn':
1259 if a_val['type'] in ('text', 'char', 'selection'):
1260 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1261 elif a_val['type'] == 'boolean':
1262 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1263 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1264 cn_val = [str(attendee[a_val['field']])]
1266 attendee_add.params['CN'] = cn_val
1267 if not attendee['email']:
1268 attendee_add.value = 'MAILTO:'
1269 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1270 elif attendee['email']:
1271 attendee_add.value = 'MAILTO:' + attendee['email']
1276 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: