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 _
33 from caldav_node import res_node_calendar
38 raise osv.except_osv('vobject Import Error!','Please install python-vobject \
39 from http://vobject.skyhouseconsulting.com/')
41 # O-1 Optional and can come only once
42 # O-n Optional and can come more than once
43 # R-1 Required and can come only once
44 # R-n Required and can come more than once
46 def uid2openobjectid(cr, uidval, oomodel, rdate):
47 """ UID To Open Object Id
48 @param cr: the current row, from the database cursor,
49 @param uidval: Get USerId vale
50 @oomodel: Open Object ModelName
51 @param rdate: Get Recurrent Date
53 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
56 wematch = __rege.match(uidval.encode('utf8'))
60 model, id, dbname = wematch.groups()
61 model_obj = pooler.get_pool(cr.dbname).get(model)
62 if (not model == oomodel) or (not dbname == cr.dbname):
64 qry = 'select distinct(id) from %s' % model_obj._table
66 qry += " where recurrent_id='%s'" % (rdate)
72 ids = map(lambda x: str(x[0]), cr.fetchall())
77 def openobjectid2uid(cr, uidval, oomodel):
78 """ Gives the value of UID for VEVENT
79 @param cr: the current row, from the database cursor,
80 @param uidval: Id value of the Event
81 @oomodel: Open Object ModelName """
83 value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname)
87 """Take a dict of mail and convert to string.
90 if isinstance(arg, dict):
96 rstr = ard.get('name','')
97 if ard.get('company',False):
98 rstr += ' (%s)' % ard.get('company')
100 rstr += ' <%s>' % ard.get('email')
102 return ', '.join(ret)
104 def str2mailto(emailstr, multi=False):
105 """Split one email string to a dict of name, company, mail parts
107 @param multi Return an array, recognize comma-sep
109 # TODO: move to tools or sth.
110 mege = re.compile(r'([^\(\<]+) *(\((.*?)\))? *(\< ?(.*?) ?\>)? ?(\((.*?)\))? *$')
115 mailz = emailstr.split(',')
118 m = mege.match(mas.strip())
120 # one of the rare non-matching strings is "sad" :(
121 # retz.append({ 'name': mas.strip() })
123 raise ValueError("Invalid email address %r" % mas)
124 rd = { 'name': m.group(1).strip(),
125 'email': m.group(5), }
127 rd['company'] = m.group(3).strip()
129 rd['company'] = m.group(7).strip()
131 if rd['name'].startswith('"') and rd['name'].endswith('"'):
132 rd['name'] = rd['name'][1:-1]
140 def get_attribute_mapping(cr, uid, calname, context=None):
141 """ Attribute Mapping with Basic calendar fields and lines
142 @param cr: the current row, from the database cursor,
143 @param uid: the current user’s ID for security checks,
144 @param calname: Get Calendar name
145 @param context: A standard dictionary for contextual values """
149 pool = pooler.get_pool(cr.dbname)
150 field_obj = pool.get('basic.calendar.fields')
151 type_obj = pool.get('basic.calendar.lines')
152 domain = [('object_id.model', '=', context.get('model'))]
153 if context.get('calendar_id'):
154 domain.append(('calendar_id', '=', context.get('calendar_id')))
155 type_id = type_obj.search(cr, uid, domain)
156 fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])])
158 for field in field_obj.browse(cr, uid, fids):
159 attr = field.name.name
161 res[attr]['field'] = field.field_id.name
162 res[attr]['type'] = field.field_id.ttype
163 if field.fn == 'hours':
164 res[attr]['type'] = "timedelta"
165 if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
166 res[attr]['object'] = field.field_id.relation
167 elif res[attr]['type'] in ('selection') and field.mapping:
168 res[attr]['mapping'] = eval(field.mapping)
169 if not res.get('uid', None):
171 res['uid']['field'] = 'id'
172 res['uid']['type'] = "integer"
175 def map_data(cr, uid, obj, context=None):
177 @param self: The object pointer
178 @param cr: the current row, from the database cursor,"""
181 for map_dict in obj.__attribute__:
182 map_val = obj.ical_get(map_dict, 'value')
183 field = obj.ical_get(map_dict, 'field')
184 field_type = obj.ical_get(map_dict, 'type')
186 if field_type == 'selection':
189 mapping = obj.__attribute__[map_dict].get('mapping', False)
191 map_val = mapping.get(map_val.lower(), False)
193 map_val = map_val.lower()
194 if field_type == 'many2many':
199 model = obj.__attribute__[map_dict].get('object', False)
200 modobj = obj.pool.get(model)
201 for map_vall in map_val:
202 id = modobj.create(cr, uid, map_vall, context=context)
204 vals[field] = [(6, 0, ids)]
206 if field_type == 'many2one':
208 if not map_val or not isinstance(map_val, dict):
211 model = obj.__attribute__[map_dict].get('object', False)
212 modobj = obj.pool.get(model)
213 # check if the record exists or not
214 key1 = map_val.keys()
215 value1 = map_val.values()
216 domain = [(key1[i], '=', value1[i]) for i in range(len(key1)) if value1[i]]
217 exist_id = modobj.search(cr, uid, domain, context=context)
221 id = modobj.create(cr, uid, map_val, context=context)
224 if field_type == 'timedelta':
226 vals[field] = (map_val.seconds/float(86400) + map_val.days)
227 vals[field] = map_val
230 class CalDAV(object):
235 def ical_set(self, name, value, type):
236 """ set calendar Attribute
237 @param self: The object pointer,
238 @param name: Get Attribute Name
239 @param value: Get Attribute Value
240 @param type: Get Attribute Type
242 if name in self.__attribute__ and self.__attribute__[name]:
243 self.__attribute__[name][type] = value
246 def ical_get(self, name, type):
247 """ Get calendar Attribute
248 @param self: The object pointer,
249 @param name: Get Attribute Name
250 @param type: Get Attribute Type
253 if self.__attribute__.get(name):
254 val = self.__attribute__.get(name).get(type, None)
255 valtype = self.__attribute__.get(name).get('type', None)
257 if valtype and valtype == 'datetime' and val:
258 if isinstance(val, list):
259 val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
261 val = val.strftime('%Y-%m-%d %H:%M:%S')
264 return self.__attribute__.get(name, None)
266 def ical_reset(self, type):
267 """ Reset Calendar Attribute
268 @param self: The object pointer,
269 @param type: Get Attribute Type
272 for name in self.__attribute__:
273 if self.__attribute__[name]:
274 self.__attribute__[name][type] = None
277 def format_date_tz(self, date, tz=None):
278 format = tools.DEFAULT_SERVER_DATETIME_FORMAT
279 return tools.server_to_local_timestamp(date, format, format, tz)
281 def parse_ics(self, cr, uid, child, cal_children=None, context=None):
282 """ parse calendaring and scheduling information
283 @param self: The object pointer
284 @param cr: the current row, from the database cursor,
285 @param uid: the current user’s ID for security checks,
286 @param context: A standard dictionary for contextual values """
290 _server_tzinfo = pytz.timezone(tools.get_server_timezone())
292 for cal_data in child.getChildren():
293 if cal_data.name.lower() == 'organizer':
294 dmail = { 'name': cal_data.params.get('CN', ['',])[0],
295 'email': cal_data.value.replace('MAILTO:',''),
298 self.ical_set(cal_data.name.lower(), mailto2str(dmail), 'value')
300 if cal_data.name.lower() == 'attendee':
303 ctx.update({'model': cal_children[cal_data.name.lower()]})
304 attendee = self.pool.get('basic.calendar.attendee')
305 att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx))
306 self.ical_set(cal_data.name.lower(), att_data, 'value')
308 if cal_data.name.lower() == 'valarm':
309 alarm = self.pool.get('basic.calendar.alarm')
312 ctx.update({'model': cal_children[cal_data.name.lower()]})
313 vals = alarm.import_cal(cr, uid, cal_data, context=ctx)
314 self.ical_set(cal_data.name.lower(), vals, 'value')
316 if cal_data.name.lower() == 'exdate':
317 exdates += cal_data.value
319 for exdate in exdates:
320 exvals.append(datetime.fromtimestamp(time.mktime(exdate.utctimetuple())).strftime('%Y%m%dT%H%M%S'))
321 self.ical_set(cal_data.name.lower(), ','.join(exvals), 'value')
323 if cal_data.name.lower() in self.__attribute__:
324 if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
325 self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
326 date_utc = cal_data.value.astimezone(pytz.utc)
327 self.ical_set(cal_data.name.lower(), date_utc, 'value')
329 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
330 vals = map_data(cr, uid, self, context=context)
333 def create_ics(self, cr, uid, datas, name, ical, context=None):
334 """ create calendaring and scheduling information
335 @param self: The object pointer
336 @param cr: the current row, from the database cursor,
337 @param uid: the current user’s ID for security checks,
338 @param context: A standard dictionary for contextual values """
347 vevent = ical.add(name)
348 for field in self.__attribute__.keys():
349 map_field = self.ical_get(field, 'field')
350 map_type = self.ical_get(field, 'type')
351 if map_field in data.keys():
353 model = context.get('model', None)
356 uidval = openobjectid2uid(cr, data[map_field], model)
357 #Computation for getting events with the same UID (RFC4791 Section4.1)
359 model_obj = self.pool.get(model)
361 if model_obj._columns.get('recurrent_uid', None):
362 cr.execute('select id from %s where recurrent_uid=%s'
363 % (model_obj._table, data[map_field]))
364 r_ids = map(lambda x: x[0], cr.fetchall())
366 r_datas = model_obj.read(cr, uid, r_ids, context=context)
367 rcal = CalDAV.export_cal(self, cr, uid, r_datas, 'vevent', context=context)
368 for revents in rcal.contents.get('vevent', []):
369 ical.contents['vevent'].append(revents)
371 if data.get('recurrent_uid', None):
372 # Change the UID value in case of modified event from any recurrent event
373 uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
374 vevent.add('uid').value = uidval
375 elif field == 'attendee' and data[map_field]:
376 model = self.__attribute__[field].get('object', False)
377 attendee_obj = self.pool.get('basic.calendar.attendee')
378 vevent = attendee_obj.export_cal(cr, uid, model, \
379 data[map_field], vevent, context=context)
380 elif field == 'valarm' and data[map_field]:
381 model = self.__attribute__[field].get('object', False)
383 ctx.update({'model': model})
384 alarm_obj = self.pool.get('basic.calendar.alarm')
385 vevent = alarm_obj.export_cal(cr, uid, model, \
386 data[map_field][0], vevent, context=ctx)
387 elif field == 'vtimezone' and data[map_field]:
388 tzval = data[map_field]
389 if tzval not in timezones:
390 tz_obj = self.pool.get('basic.calendar.timezone')
391 ical = tz_obj.export_cal(cr, uid, None, \
392 data[map_field], ical, context=context)
393 timezones.append(data[map_field])
395 # Set exdates according to timezone value
396 # This is the case when timezone mapping comes after the exdate mapping
397 # and we have exdate value available
398 exfield.params['TZID'] = [tzval.title()]
400 for exdate in exdates:
401 date1 = (datetime.strptime(exdate, "%Y%m%dT%H%M%S")).strftime('%Y-%m-%d %H:%M:%S')
402 dest_date = self.format_date_tz(date1, tzval.title())
403 ex_date = (datetime.strptime(dest_date, "%Y-%m-%d %H:%M:%S")).strftime('%Y%m%dT%H%M%S')
404 exdates_updated.append(ex_date)
405 exfield.value = map(parser.parse, exdates_updated)
406 elif field == 'organizer' and data[map_field]:
407 organizer = str2mailto(data[map_field])
408 event_org = vevent.add('organizer')
409 event_org.params['CN'] = [organizer['name']]
410 event_org.value = 'MAILTO:' + (organizer.get('email') or '')
412 elif data[map_field]:
413 if map_type in ("char", "text"):
414 if field in ('exdate'):
415 exfield = vevent.add(field)
416 exdates = (data[map_field]).split(',')
418 # Set exdates according to timezone value
419 # This is the case when timezone mapping comes before the exdate mapping
420 # and we have timezone value available
421 exfield.params['TZID'] = [tzval.title()]
423 for exdate in exdates:
424 date1 = (datetime.strptime(exdate, "%Y%m%dT%H%M%S")).strftime('%Y-%m-%d %H:%M:%S')
425 dest_date = self.format_date_tz(date1, tzval.title())
426 ex_date = (datetime.strptime(dest_date, "%Y-%m-%d %H:%M:%S")).strftime('%Y%m%dT%H%M%S')
427 exdates_updated.append(ex_date)
428 exdates = exdates_updated
429 exfield.value = map(parser.parse, exdates)
431 vevent.add(field).value = tools.ustr(data[map_field])
432 elif map_type in ('datetime', 'date') and data[map_field]:
433 dtfield = vevent.add(field)
435 # Export the date according to the event timezone value
436 dest_date = self.format_date_tz(data[map_field], tzval.title())
437 dtfield.params['TZID'] = [tzval.title()]
438 dtfield.value = parser.parse(dest_date)
440 dtfield.value = parser.parse(data[map_field])
441 elif map_type == "timedelta":
442 vevent.add(field).value = timedelta(hours=data[map_field])
443 elif map_type == "many2one":
444 vevent.add(field).value = tools.ustr(data.get(map_field)[1])
445 elif map_type in ("float", "integer"):
446 vevent.add(field).value = str(data.get(map_field))
447 elif map_type == "selection":
448 if not self.ical_get(field, 'mapping'):
449 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
451 for key1, val1 in self.ical_get(field, 'mapping').items():
452 if val1 == data[map_field]:
453 vevent.add(field).value = key1
456 def check_import(self, cr, uid, vals, context=None):
458 @param self: The object pointer
459 @param cr: the current row, from the database cursor,
460 @param uid: the current user’s ID for security checks,
461 @param vals: Get Values
462 @param context: A standard dictionary for contextual values
467 model_obj = self.pool.get(context.get('model'))
471 # Compute value of duration
472 if 'date_deadline' in val and 'duration' not in val:
473 start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
474 end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
476 val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
477 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
478 val.get('recurrent_id'))
479 if val.has_key('create_date'):
480 val.pop('create_date')
481 u_id = val.get('id', None)
484 val.update({'recurrent_uid': exists})
485 model_obj.write(cr, uid, [r_id], val)
488 model_obj.write(cr, uid, [exists], val)
491 if u_id in recur_pool and val.get('recurrent_id'):
492 val.update({'recurrent_uid': recur_pool[u_id]})
493 revent_id = model_obj.create(cr, uid, val)
494 ids.append(revent_id)
496 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
497 wematch = __rege.match(u_id.encode('utf8'))
499 model, recur_id, dbname = wematch.groups()
500 val.update({'recurrent_uid': recur_id})
501 event_id = model_obj.create(cr, uid, val)
502 recur_pool[u_id] = event_id
508 def export_cal(self, cr, uid, datas, vobj=None, context=None):
510 @param self: The object pointer
511 @param cr: the current row, from the database cursor,
512 @param uid: the current user’s ID for security checks,
513 @param datas: Get Data's for caldav
514 @param context: A standard dictionary for contextual values
517 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
518 ical = vobject.iCalendar()
519 self.create_ics(cr, uid, datas, vobj, ical, context=context)
522 raise # osv.except_osv(('Error !'), (str(e)))
524 def import_cal(self, cr, uid, content, data_id=None, context=None):
526 @param self: The object pointer
527 @param cr: the current row, from the database cursor,
528 @param uid: the current user’s ID for security checks,
529 @param data_id: Get Data’s ID or False
530 @param context: A standard dictionary for contextual values
534 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
535 parsedCal = vobject.readOne(ical_data)
538 for child in parsedCal.getChildren():
539 if child.name.lower() in ('vevent', 'vtodo'):
540 vals = self.parse_ics(cr, uid, child, context=context)
544 if vals: res.append(vals)
545 self.ical_reset('value')
548 class Calendar(CalDAV, osv.osv):
549 _name = 'basic.calendar'
550 _calname = 'calendar'
553 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
554 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
555 # or the minimum and maximum range of the iCalendar specification
556 # that is required in order to interpret the iCalendar object.
557 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
558 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
559 'vevent': None, # Use: O-n, Type: Collection of Event class
560 'vtodo': None, # Use: O-n, Type: Collection of ToDo class
561 'vjournal': None, # Use: O-n, Type: Collection of Journal class
562 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
563 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
566 'name': fields.char("Name", size=64),
567 'user_id': fields.many2one('res.users', 'Owner'),
568 'collection_id': fields.many2one('document.directory', 'Collection', \
570 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
571 string="Type", size=64),
572 'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
573 'create_date': fields.datetime('Created Date', readonly=True),
574 'write_date': fields.datetime('Modifided Date', readonly=True),
575 'description': fields.text("description"),
578 def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
584 ctx_res_id = context.get('res_id', None)
585 ctx_model = context.get('model', None)
586 for cal in self.browse(cr, uid, ids):
587 for line in cal.line_ids:
588 if ctx_model and ctx_model != line.object_id.model:
590 if line.name in ('valarm', 'attendee'):
592 line_domain = eval(line.domain or '[]')
593 line_domain += domain
595 line_domain += [('id','=',ctx_res_id)]
596 mod_obj = self.pool.get(line.object_id.model)
597 data_ids = mod_obj.search(cr, uid, line_domain, order="id", context=context)
598 for data in mod_obj.browse(cr, uid, data_ids, context):
599 ctx = parent and parent.context or None
600 if data.recurrent_uid:
601 # Skip for event which is child of other event
603 node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
607 def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
609 @param ids: List of calendar’s IDs
610 @param vobj: the type of object to export
611 @return the ical data.
615 ctx_model = context.get('model', None)
616 ctx_res_id = context.get('res_id', None)
617 ical = vobject.iCalendar()
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:
622 if line.name in ('valarm', 'attendee'):
624 domain = eval(line.domain or '[]')
626 domain += [('id','=',ctx_res_id)]
627 mod_obj = self.pool.get(line.object_id.model)
628 data_ids = mod_obj.search(cr, uid, domain, context=context)
629 datas = mod_obj.read(cr, uid, data_ids, context=context)
630 context.update({'model': line.object_id.model,
631 'calendar_id': cal.id
633 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
634 self.create_ics(cr, uid, datas, line.name, ical, context=context)
635 return ical.serialize()
637 def import_cal(self, cr, uid, content, data_id=None, context=None):
639 @param self: The object pointer
640 @param cr: the current row, from the database cursor,
641 @param uid: the current user’s ID for security checks,
642 @param data_id: Get Data’s ID or False
643 @param context: A standard dictionary for contextual values
649 parsedCal = vobject.readOne(ical_data)
651 data_id = self.search(cr, uid, [])[0]
652 cal = self.browse(cr, uid, data_id, context=context)
655 for line in cal.line_ids:
656 cal_children[line.name] = line.object_id.model
659 for child in parsedCal.getChildren():
660 if child.name.lower() in cal_children:
661 context.update({'model': cal_children[child.name.lower()],
662 'calendar_id': cal['id']
664 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
665 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
667 objs.append(cal_children[child.name.lower()])
670 for obj_name in list(set(objs)):
671 obj = self.pool.get(obj_name)
672 if hasattr(obj, 'check_import'):
673 r = obj.check_import(cr, uid, vals, context=context)
678 r = self.check_import(cr, uid, vals, context=context)
685 class basic_calendar_line(osv.osv):
686 """ Calendar Lines """
688 _name = 'basic.calendar.lines'
689 _description = 'Calendar Lines'
692 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
693 ('valarm', 'Alarm'), \
694 ('attendee', 'Attendee')], \
695 string="Type", size=64),
696 'object_id': fields.many2one('ir.model', 'Object'),
697 'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
698 required=True, ondelete='cascade'),
699 'domain': fields.char('Domain', size=124),
700 'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
704 'domain': lambda *a: '[]',
707 def create(self, cr, uid, vals, context=None):
708 """ create calendar's line
709 @param self: The object pointer
710 @param cr: the current row, from the database cursor,
711 @param uid: the current user’s ID for security checks,
712 @param vals: Get the Values
713 @param context: A standard dictionary for contextual values
716 cr.execute("Select count(id) from basic_calendar_lines \
717 where name='%s' and calendar_id=%s" % (vals.get('name'), vals.get('calendar_id')))
721 raise osv.except_osv(_('Warning !'), _('Can not create \
722 line "%s" more than once' % (vals.get('name'))))
723 return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
725 basic_calendar_line()
728 class basic_calendar_attribute(osv.osv):
729 _name = 'basic.calendar.attributes'
730 _description = 'Calendar attributes'
732 'name': fields.char("Name", size=64, required=True),
733 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
734 ('alarm', 'Alarm'), \
735 ('attendee', 'Attendee')], \
736 string="Type", size=64, required=True),
739 basic_calendar_attribute()
742 class basic_calendar_fields(osv.osv):
743 """ Calendar fields """
745 _name = 'basic.calendar.fields'
746 _description = 'Calendar fields'
749 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
750 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
751 'type_id': fields.many2one('basic.calendar.lines', 'Type', \
752 required=True, ondelete='cascade'),
753 'expr': fields.char("Expression", size=64),
754 'fn': fields.selection([('field', 'Use the field'),
755 ('const', 'Expression as constant'),
756 ('hours', 'Interval in hours'),
758 'mapping': fields.text('Mapping'),
762 'fn': lambda *a: 'field',
766 ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
769 def check_line(self, cr, uid, vals, name, context=None):
770 """ check calendar's line
771 @param self: The object pointer
772 @param cr: the current row, from the database cursor,
773 @param uid: the current user’s ID for security checks,
774 @param vals: Get Values
775 @param context: A standard dictionary for contextual values
777 f_obj = self.pool.get('ir.model.fields')
778 field = f_obj.browse(cr, uid, vals['field_id'], context=context)
779 relation = field.relation
780 line_obj = self.pool.get('basic.calendar.lines')
781 l_id = line_obj.search(cr, uid, [('name', '=', name)])
783 line = line_obj.browse(cr, uid, l_id, context=context)[0]
784 line_rel = line.object_id.model
785 if (relation != 'NULL') and (not relation == line_rel):
786 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name)))
789 def create(self, cr, uid, vals, context=None):
790 """ Create Calendar's fields
791 @param self: The object pointer
792 @param cr: the current row, from the database cursor,
793 @param uid: the current user’s ID for security checks,
794 @param vals: Get Values
795 @param context: A standard dictionary for contextual values
798 cr.execute('select name from basic_calendar_attributes \
799 where id=%s' % (vals.get('name')))
802 if name in ('valarm', 'attendee'):
803 self.check_line(cr, uid, vals, name, context=context)
804 return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
806 def write(self, cr, uid, ids, vals, context=None):
807 """ write Calendar's fields
808 @param self: The object pointer
809 @param cr: the current row, from the database cursor,
810 @param uid: the current user’s ID for security checks,
811 @param vals: Get Values
812 @param context: A standard dictionary for contextual values
818 field = self.browse(cr, uid, id, context=context)
819 name = field.name.name
820 if name in ('valarm', 'attendee'):
821 self.check_line(cr, uid, vals, name, context=context)
822 return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
824 basic_calendar_fields()
827 class Event(CalDAV, osv.osv_memory):
828 _name = 'basic.calendar.event'
831 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
832 '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.
833 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
834 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
835 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
836 '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.
837 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component.
838 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
839 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
840 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
841 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
842 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
843 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
844 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
845 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
846 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
848 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
849 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
850 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
851 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
852 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component.
853 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
854 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
856 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
857 # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
858 '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
859 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
860 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
862 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
863 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
866 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
868 @param self: The object pointer
869 @param cr: the current row, from the database cursor,
870 @param uid: the current user’s ID for security checks,
871 @param datas: Get datas
872 @param context: A standard dictionary for contextual values
875 return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
880 class ToDo(CalDAV, osv.osv_memory):
881 _name = 'basic.calendar.todo'
919 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
921 @param self: The object pointer
922 @param cr: the current row, from the database cursor,
923 @param uid: the current user’s ID for security checks,
924 @param datas: Get datas
925 @param context: A standard dictionary for contextual values
928 return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
933 class Journal(CalDAV):
938 class FreeBusy(CalDAV):
940 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component.
941 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
942 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
943 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
944 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
945 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
946 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
947 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
948 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
949 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
950 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
956 class Timezone(CalDAV, osv.osv_memory):
957 _name = 'basic.calendar.timezone'
958 _calname = 'vtimezone'
961 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
962 '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.
963 '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.
964 'standardc': {'tzprop': None}, # Use: R-1,
965 'daylightc': {'tzprop': None}, # Use: R-1,
966 'x-prop': None, # Use: O-n, Type: Text,
969 def get_name_offset(self, cr, uid, tzid, context=None):
970 """ Get Name Offset value
971 @param self: The object pointer
972 @param cr: the current row, from the database cursor,
973 @param uid: the current user’s ID for security checks,
974 @param context: A standard dictionary for contextual values
977 mytz = pytz.timezone(tzid.title())
978 mydt = datetime.now(tz=mytz)
979 offset = mydt.utcoffset()
980 val = offset.days * 24 + float(offset.seconds) / 3600
981 realoffset = '%02d%02d' % (math.floor(abs(val)), \
982 round(abs(val) % 1 + 0.01, 2) * 60)
983 realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
984 return (mydt.tzname(), realoffset)
986 def export_cal(self, cr, uid, model, tzid, ical, context=None):
988 @param self: The object pointer
989 @param cr: the current row, from the database cursor,
990 @param uid: the current user’s ID for security checks,
991 @param model: Get Model's name
992 @param context: A standard dictionary for contextual values
997 ctx.update({'model': model})
998 cal_tz = ical.add('vtimezone')
999 cal_tz.add('TZID').value = tzid.title()
1000 tz_std = cal_tz.add('STANDARD')
1001 tzname, offset = self.get_name_offset(cr, uid, tzid)
1002 tz_std.add("TZOFFSETFROM").value = offset
1003 tz_std.add("TZOFFSETTO").value = offset
1004 #TODO: Get start date for timezone
1005 tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1006 tz_std.add("TZNAME").value = tzname
1009 def import_cal(self, cr, uid, ical_data, context=None):
1011 @param self: The object pointer
1012 @param cr: the current row, from the database cursor,
1013 @param uid: the current user’s ID for security checks,
1014 @param ical_data: Get calendar's data
1015 @param context: A standard dictionary for contextual values
1018 for child in ical_data.getChildren():
1019 if child.name.lower() == 'tzid':
1020 tzname = child.value
1021 self.ical_set(child.name.lower(), tzname, 'value')
1022 vals = map_data(cr, uid, self, context=context)
1028 class Alarm(CalDAV, osv.osv_memory):
1029 _name = 'basic.calendar.alarm'
1033 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1034 '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
1035 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL
1036 '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
1037 '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
1038 '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
1039 '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
1040 '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.
1044 def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1046 @param self: The object pointer
1047 @param cr: the current row, from the database cursor,
1048 @param uid: the current user’s ID for security checks,
1049 @param model: Get Model's name
1050 @param alarm_id: Get Alarm's Id
1051 @param context: A standard dictionary for contextual values
1055 valarm = vevent.add('valarm')
1056 alarm_object = self.pool.get(model)
1057 alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1059 # Compute trigger data
1060 interval = alarm_data['trigger_interval']
1061 occurs = alarm_data['trigger_occurs']
1062 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1063 or -(alarm_data['trigger_duration'])
1064 related = alarm_data['trigger_related']
1065 trigger = valarm.add('TRIGGER')
1066 trigger.params['related'] = [related.upper()]
1067 if interval == 'days':
1068 delta = timedelta(days=duration)
1069 if interval == 'hours':
1070 delta = timedelta(hours=duration)
1071 if interval == 'minutes':
1072 delta = timedelta(minutes=duration)
1073 trigger.value = delta
1075 # Compute other details
1076 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1077 valarm.add('ACTION').value = alarm_data['action']
1080 def import_cal(self, cr, uid, ical_data, context=None):
1082 @param self: The object pointer
1083 @param cr: the current row, from the database cursor,
1084 @param uid: the current user’s ID for security checks,
1085 @param ical_data: Get calendar's Data
1086 @param context: A standard dictionary for contextual values
1089 ctx = context.copy()
1090 ctx.update({'model': context.get('model', None)})
1091 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1092 for child in ical_data.getChildren():
1093 if child.name.lower() == 'trigger':
1094 seconds = child.value.seconds
1095 days = child.value.days
1096 diff = (days * 86400) + seconds
1100 duration = abs(days)
1101 related = days > 0 and 'after' or 'before'
1102 elif (abs(diff) / 3600) == 0:
1103 duration = abs(diff / 60)
1104 interval = 'minutes'
1105 related = days >= 0 and 'after' or 'before'
1107 duration = abs(diff / 3600)
1109 related = days >= 0 and 'after' or 'before'
1110 self.ical_set('trigger_interval', interval, 'value')
1111 self.ical_set('trigger_duration', duration, 'value')
1112 self.ical_set('trigger_occurs', related.lower(), 'value')
1114 if child.params.get('related'):
1115 self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1117 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1118 vals = map_data(cr, uid, self, context=context)
1124 class Attendee(CalDAV, osv.osv_memory):
1125 _name = 'basic.calendar.attendee'
1126 _calname = 'attendee'
1129 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1130 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property.
1131 '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"
1132 '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".
1133 '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.
1134 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1135 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1136 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1137 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property.
1138 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property.
1139 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter.
1142 def import_cal(self, cr, uid, ical_data, context=None):
1144 @param self: The object pointer
1145 @param cr: the current row, from the database cursor,
1146 @param uid: the current user’s ID for security checks,
1147 @param ical_data: Get calendar's Data
1148 @param context: A standard dictionary for contextual values
1151 ctx = context.copy()
1152 ctx.update({'model': context.get('model', None)})
1153 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1154 for para in ical_data.params:
1155 if para.lower() == 'cn':
1156 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1157 ical_data.value, 'value')
1159 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1160 if not ical_data.params.get('CN'):
1161 self.ical_set('cn', ical_data.value, 'value')
1162 vals = map_data(cr, uid, self, context=context)
1165 def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1167 @param self: The object pointer
1168 @param cr: the current row, from the database cursor,
1169 @param uid: the current user’s ID for security checks,
1170 @param model: Get model's name
1171 @param attendee_ids: Get Attendee's Id
1172 @param context: A standard dictionary for contextual values
1176 attendee_object = self.pool.get(model)
1177 ctx = context.copy()
1178 ctx.update({'model': model})
1179 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1180 for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1181 attendee_add = vevent.add('attendee')
1183 for a_key, a_val in self.__attribute__.items():
1184 if attendee[a_val['field']] and a_val['field'] != 'cn':
1185 if a_val['type'] in ('text', 'char', 'selection'):
1186 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1187 elif a_val['type'] == 'boolean':
1188 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1189 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1190 cn_val = [str(attendee[a_val['field']])]
1192 attendee_add.params['CN'] = cn_val
1193 if not attendee['email']:
1194 attendee_add.value = 'MAILTO:'
1195 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1196 elif attendee['email']:
1197 attendee_add.value = 'MAILTO:' + attendee['email']
1202 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: