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 model_obj = self.pool.get(model)
359 if model_obj._columns.get('recurrent_uid', None):
360 cr.execute('select id from %s where recurrent_uid=%s'
361 % (model_obj._table, data[map_field]))
362 r_ids = map(lambda x: x[0], cr.fetchall())
364 rcal = self.export_cal(cr, uid, r_ids, 'vevent', context=context)
365 if data.get('recurrent_uid', None):
366 uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
367 vevent.add('uid').value = uidval
368 elif field == 'attendee' and data[map_field]:
369 model = self.__attribute__[field].get('object', False)
370 attendee_obj = self.pool.get('basic.calendar.attendee')
371 vevent = attendee_obj.export_cal(cr, uid, model, \
372 data[map_field], vevent, context=context)
373 elif field == 'valarm' and data[map_field]:
374 model = self.__attribute__[field].get('object', False)
376 ctx.update({'model': model})
377 alarm_obj = self.pool.get('basic.calendar.alarm')
378 vevent = alarm_obj.export_cal(cr, uid, model, \
379 data[map_field][0], vevent, context=ctx)
380 elif field == 'vtimezone' and data[map_field]:
381 tzval = data[map_field]
382 if tzval not in timezones:
383 tz_obj = self.pool.get('basic.calendar.timezone')
384 ical = tz_obj.export_cal(cr, uid, None, \
385 data[map_field], ical, context=context)
386 timezones.append(data[map_field])
388 exfield.params['TZID'] = [tzval.title()]
390 for exdate in exdates:
391 date1 = (datetime.strptime(exdate, "%Y%m%dT%H%M%S")).strftime('%Y-%m-%d %H:%M:%S')
392 dest_date = self.format_date_tz(date1, tzval.title())
393 ex_date = (datetime.strptime(dest_date, "%Y-%m-%d %H:%M:%S")).strftime('%Y%m%dT%H%M%S')
394 exdates_updated.append(ex_date)
395 exfield.value = map(parser.parse, exdates_updated)
396 elif field == 'organizer' and data[map_field]:
397 organizer = str2mailto(data[map_field])
398 event_org = vevent.add('organizer')
399 event_org.params['CN'] = [organizer['name']]
400 event_org.value = 'MAILTO:' + (organizer.get('email') or '')
402 elif data[map_field]:
403 if map_type in ("char", "text"):
404 if field in ('exdate'):
405 exfield = vevent.add(field)
406 exdates = (data[map_field]).split(',')
408 exfield.params['TZID'] = [tzval.title()]
410 for exdate in exdates:
411 date1 = (datetime.strptime(exdate, "%Y%m%dT%H%M%S")).strftime('%Y-%m-%d %H:%M:%S')
412 dest_date = self.format_date_tz(date1, tzval.title())
413 ex_date = (datetime.strptime(dest_date, "%Y-%m-%d %H:%M:%S")).strftime('%Y%m%dT%H%M%S')
414 exdates_updated.append(ex_date)
415 exdates = exdates_updated
416 exfield.value = map(parser.parse, exdates)
418 vevent.add(field).value = tools.ustr(data[map_field])
419 elif map_type in ('datetime', 'date') and data[map_field]:
420 dtfield = vevent.add(field)
422 dest_date = self.format_date_tz(data[map_field], tzval.title())
423 dtfield.params['TZID'] = [tzval.title()]
424 dtfield.value = parser.parse(dest_date)
426 dtfield.value = parser.parse(data[map_field])
427 elif map_type == "timedelta":
428 vevent.add(field).value = timedelta(hours=data[map_field])
429 elif map_type == "many2one":
430 vevent.add(field).value = tools.ustr(data.get(map_field)[1])
431 elif map_type in ("float", "integer"):
432 vevent.add(field).value = str(data.get(map_field))
433 elif map_type == "selection":
434 if not self.ical_get(field, 'mapping'):
435 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
437 for key1, val1 in self.ical_get(field, 'mapping').items():
438 if val1 == data[map_field]:
439 vevent.add(field).value = key1
442 def check_import(self, cr, uid, vals, context=None):
444 @param self: The object pointer
445 @param cr: the current row, from the database cursor,
446 @param uid: the current user’s ID for security checks,
447 @param vals: Get Values
448 @param context: A standard dictionary for contextual values
453 model_obj = self.pool.get(context.get('model'))
457 # Compute value of duration
458 if 'date_deadline' in val and 'duration' not in val:
459 start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
460 end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
462 val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
463 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
464 val.get('recurrent_id'))
465 if val.has_key('create_date'):
466 val.pop('create_date')
467 u_id = val.get('id', None)
470 val.update({'recurrent_uid': exists})
471 model_obj.write(cr, uid, [r_id], val)
474 model_obj.write(cr, uid, [exists], val)
477 if u_id in recur_pool and val.get('recurrent_id'):
478 val.update({'recurrent_uid': recur_pool[u_id]})
479 revent_id = model_obj.create(cr, uid, val)
480 ids.append(revent_id)
482 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
483 wematch = __rege.match(u_id.encode('utf8'))
485 model, recur_id, dbname = wematch.groups()
486 val.update({'recurrent_uid': recur_id})
487 event_id = model_obj.create(cr, uid, val)
488 recur_pool[u_id] = event_id
494 def export_cal(self, cr, uid, datas, vobj=None, context=None):
496 @param self: The object pointer
497 @param cr: the current row, from the database cursor,
498 @param uid: the current user’s ID for security checks,
499 @param datas: Get Data's for caldav
500 @param context: A standard dictionary for contextual values
504 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
505 ical = vobject.iCalendar()
506 self.create_ics(cr, uid, datas, vobj, ical, context=context)
509 raise # osv.except_osv(('Error !'), (str(e)))
511 def import_cal(self, cr, uid, content, data_id=None, context=None):
513 @param self: The object pointer
514 @param cr: the current row, from the database cursor,
515 @param uid: the current user’s ID for security checks,
516 @param data_id: Get Data’s ID or False
517 @param context: A standard dictionary for contextual values
521 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
522 parsedCal = vobject.readOne(ical_data)
525 for child in parsedCal.getChildren():
526 if child.name.lower() in ('vevent', 'vtodo'):
527 vals = self.parse_ics(cr, uid, child, context=context)
531 if vals: res.append(vals)
532 self.ical_reset('value')
535 class Calendar(CalDAV, osv.osv):
536 _name = 'basic.calendar'
537 _calname = 'calendar'
540 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
541 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
542 # or the minimum and maximum range of the iCalendar specification
543 # that is required in order to interpret the iCalendar object.
544 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
545 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
546 'vevent': None, # Use: O-n, Type: Collection of Event class
547 'vtodo': None, # Use: O-n, Type: Collection of ToDo class
548 'vjournal': None, # Use: O-n, Type: Collection of Journal class
549 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
550 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
553 'name': fields.char("Name", size=64),
554 'user_id': fields.many2one('res.users', 'Owner'),
555 'collection_id': fields.many2one('document.directory', 'Collection', \
557 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
558 string="Type", size=64),
559 'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
560 'create_date': fields.datetime('Created Date', readonly=True),
561 'write_date': fields.datetime('Modifided Date', readonly=True),
562 'description': fields.text("description"),
565 def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
571 ctx_res_id = context.get('res_id', None)
572 ctx_model = context.get('model', None)
573 for cal in self.browse(cr, uid, ids):
574 for line in cal.line_ids:
575 if ctx_model and ctx_model != line.object_id.model:
577 if line.name in ('valarm', 'attendee'):
579 line_domain = eval(line.domain or '[]')
580 line_domain += domain
582 line_domain += [('id','=',ctx_res_id)]
583 mod_obj = self.pool.get(line.object_id.model)
584 data_ids = mod_obj.search(cr, uid, line_domain, context=context)
585 for data in mod_obj.browse(cr, uid, data_ids, context):
586 ctx = parent and parent.context or None
587 node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
591 def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
593 @param ids: List of calendar’s IDs
594 @param vobj: the type of object to export
596 @return the ical data.
600 ctx_model = context.get('model', None)
601 ctx_res_id = context.get('res_id', None)
602 ical = vobject.iCalendar()
603 for cal in self.browse(cr, uid, ids):
604 for line in cal.line_ids:
605 if ctx_model and ctx_model != line.object_id.model:
607 if line.name in ('valarm', 'attendee'):
609 domain = eval(line.domain or '[]')
611 domain += [('id','=',ctx_res_id)]
612 mod_obj = self.pool.get(line.object_id.model)
613 data_ids = mod_obj.search(cr, uid, domain, context=context)
614 datas = mod_obj.read(cr, uid, data_ids, context=context)
615 context.update({'model': line.object_id.model,
616 'calendar_id': cal.id
618 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
619 self.create_ics(cr, uid, datas, line.name, ical, context=context)
620 return ical.serialize()
622 def import_cal(self, cr, uid, content, data_id=None, context=None):
624 @param self: The object pointer
625 @param cr: the current row, from the database cursor,
626 @param uid: the current user’s ID for security checks,
627 @param data_id: Get Data’s ID or False
628 @param context: A standard dictionary for contextual values
635 parsedCal = vobject.readOne(ical_data)
637 data_id = self.search(cr, uid, [])[0]
638 cal = self.browse(cr, uid, data_id, context=context)
641 for line in cal.line_ids:
642 cal_children[line.name] = line.object_id.model
645 for child in parsedCal.getChildren():
646 if child.name.lower() in cal_children:
647 context.update({'model': cal_children[child.name.lower()],
648 'calendar_id': cal['id']
650 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
651 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
653 objs.append(cal_children[child.name.lower()])
656 for obj_name in list(set(objs)):
657 obj = self.pool.get(obj_name)
658 if hasattr(obj, 'check_import'):
659 r = obj.check_import(cr, uid, vals, context=context)
664 r = self.check_import(cr, uid, vals, context=context)
670 class basic_calendar_line(osv.osv):
671 """ Calendar Lines """
673 _name = 'basic.calendar.lines'
674 _description = 'Calendar Lines'
677 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
678 ('valarm', 'Alarm'), \
679 ('attendee', 'Attendee')], \
680 string="Type", size=64),
681 'object_id': fields.many2one('ir.model', 'Object'),
682 'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
683 required=True, ondelete='cascade'),
684 'domain': fields.char('Domain', size=124),
685 'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
689 'domain': lambda *a: '[]',
692 def create(self, cr, uid, vals, context=None):
693 """ create calendar's line
694 @param self: The object pointer
695 @param cr: the current row, from the database cursor,
696 @param uid: the current user’s ID for security checks,
697 @param vals: Get the Values
698 @param context: A standard dictionary for contextual values
701 cr.execute("Select count(id) from basic_calendar_lines \
702 where name='%s' and calendar_id=%s" % (vals.get('name'), vals.get('calendar_id')))
706 raise osv.except_osv(_('Warning !'), _('Can not create \
707 line "%s" more than once' % (vals.get('name'))))
708 return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
710 basic_calendar_line()
713 class basic_calendar_attribute(osv.osv):
714 _name = 'basic.calendar.attributes'
715 _description = 'Calendar attributes'
717 'name': fields.char("Name", size=64, required=True),
718 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
719 ('alarm', 'Alarm'), \
720 ('attendee', 'Attendee')], \
721 string="Type", size=64, required=True),
724 basic_calendar_attribute()
727 class basic_calendar_fields(osv.osv):
728 """ Calendar fields """
730 _name = 'basic.calendar.fields'
731 _description = 'Calendar fields'
734 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
735 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
736 'type_id': fields.many2one('basic.calendar.lines', 'Type', \
737 required=True, ondelete='cascade'),
738 'expr': fields.char("Expression", size=64),
739 'fn': fields.selection([('field', 'Use the field'),
740 ('const', 'Expression as constant'),
741 ('hours', 'Interval in hours'),
743 'mapping': fields.text('Mapping'),
747 'fn': lambda *a: 'field',
751 ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
754 def check_line(self, cr, uid, vals, name, context=None):
755 """ check calendar's line
756 @param self: The object pointer
757 @param cr: the current row, from the database cursor,
758 @param uid: the current user’s ID for security checks,
759 @param vals: Get Values
760 @param context: A standard dictionary for contextual values
762 f_obj = self.pool.get('ir.model.fields')
763 field = f_obj.browse(cr, uid, vals['field_id'], context=context)
764 relation = field.relation
765 line_obj = self.pool.get('basic.calendar.lines')
766 l_id = line_obj.search(cr, uid, [('name', '=', name)])
768 line = line_obj.browse(cr, uid, l_id, context=context)[0]
769 line_rel = line.object_id.model
770 if (relation != 'NULL') and (not relation == line_rel):
771 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines' % (name)))
774 def create(self, cr, uid, vals, context=None):
775 """ Create Calendar's fields
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 Values
780 @param context: A standard dictionary for contextual values
783 cr.execute('select name from basic_calendar_attributes \
784 where id=%s' % (vals.get('name')))
787 if name in ('valarm', 'attendee'):
788 self.check_line(cr, uid, vals, name, context=context)
789 return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
791 def write(self, cr, uid, ids, vals, context=None):
792 """ write Calendar's fields
793 @param self: The object pointer
794 @param cr: the current row, from the database cursor,
795 @param uid: the current user’s ID for security checks,
796 @param vals: Get Values
797 @param context: A standard dictionary for contextual values
803 field = self.browse(cr, uid, id, context=context)
804 name = field.name.name
805 if name in ('valarm', 'attendee'):
806 self.check_line(cr, uid, vals, name, context=context)
807 return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
809 basic_calendar_fields()
812 class Event(CalDAV, osv.osv_memory):
813 _name = 'basic.calendar.event'
816 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
817 '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.
818 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
819 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
820 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
821 '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.
822 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component.
823 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
824 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
825 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
826 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
827 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
828 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
829 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
830 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
831 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
833 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
834 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
835 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
836 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
837 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component.
838 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
839 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
841 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
842 # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
843 '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
844 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
845 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
847 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
848 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
851 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
853 @param self: The object pointer
854 @param cr: the current row, from the database cursor,
855 @param uid: the current user’s ID for security checks,
856 @param datas: Get datas
857 @param context: A standard dictionary for contextual values
860 return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
865 class ToDo(CalDAV, osv.osv_memory):
866 _name = 'basic.calendar.todo'
904 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
906 @param self: The object pointer
907 @param cr: the current row, from the database cursor,
908 @param uid: the current user’s ID for security checks,
909 @param datas: Get datas
910 @param context: A standard dictionary for contextual values
913 return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
918 class Journal(CalDAV):
923 class FreeBusy(CalDAV):
925 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component.
926 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
927 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
928 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
929 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
930 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
931 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
932 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
933 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
934 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
935 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
941 class Timezone(CalDAV, osv.osv_memory):
942 _name = 'basic.calendar.timezone'
943 _calname = 'vtimezone'
946 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
947 '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.
948 '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.
949 'standardc': {'tzprop': None}, # Use: R-1,
950 'daylightc': {'tzprop': None}, # Use: R-1,
951 'x-prop': None, # Use: O-n, Type: Text,
954 def get_name_offset(self, cr, uid, tzid, context=None):
955 """ Get Name Offset value
956 @param self: The object pointer
957 @param cr: the current row, from the database cursor,
958 @param uid: the current user’s ID for security checks,
959 @param context: A standard dictionary for contextual values
962 mytz = pytz.timezone(tzid.title())
963 mydt = datetime.now(tz=mytz)
964 offset = mydt.utcoffset()
965 val = offset.days * 24 + float(offset.seconds) / 3600
966 realoffset = '%02d%02d' % (math.floor(abs(val)), \
967 round(abs(val) % 1 + 0.01, 2) * 60)
968 realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
969 return (mydt.tzname(), realoffset)
971 def export_cal(self, cr, uid, model, tzid, ical, context=None):
973 @param self: The object pointer
974 @param cr: the current row, from the database cursor,
975 @param uid: the current user’s ID for security checks,
976 @param model: Get Model's name
977 @param context: A standard dictionary for contextual values
982 ctx.update({'model': model})
983 cal_tz = ical.add('vtimezone')
984 cal_tz.add('TZID').value = tzid.title()
985 tz_std = cal_tz.add('STANDARD')
986 tzname, offset = self.get_name_offset(cr, uid, tzid)
987 tz_std.add("TZOFFSETFROM").value = offset
988 tz_std.add("TZOFFSETTO").value = offset
989 #TODO: Get start date for timezone
990 tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
991 tz_std.add("TZNAME").value = tzname
994 def import_cal(self, cr, uid, ical_data, context=None):
996 @param self: The object pointer
997 @param cr: the current row, from the database cursor,
998 @param uid: the current user’s ID for security checks,
999 @param ical_data: Get calendar's data
1000 @param context: A standard dictionary for contextual values
1003 for child in ical_data.getChildren():
1004 if child.name.lower() == 'tzid':
1005 tzname = child.value
1006 self.ical_set(child.name.lower(), tzname, 'value')
1007 vals = map_data(cr, uid, self, context=context)
1013 class Alarm(CalDAV, osv.osv_memory):
1014 _name = 'basic.calendar.alarm'
1018 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1019 '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
1020 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL
1021 '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
1022 '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
1023 '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
1024 '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
1025 '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.
1029 def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1031 @param self: The object pointer
1032 @param cr: the current row, from the database cursor,
1033 @param uid: the current user’s ID for security checks,
1034 @param model: Get Model's name
1035 @param alarm_id: Get Alarm's Id
1036 @param context: A standard dictionary for contextual values
1040 valarm = vevent.add('valarm')
1041 alarm_object = self.pool.get(model)
1042 alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1044 # Compute trigger data
1045 interval = alarm_data['trigger_interval']
1046 occurs = alarm_data['trigger_occurs']
1047 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1048 or -(alarm_data['trigger_duration'])
1049 related = alarm_data['trigger_related']
1050 trigger = valarm.add('TRIGGER')
1051 trigger.params['related'] = [related.upper()]
1052 if interval == 'days':
1053 delta = timedelta(days=duration)
1054 if interval == 'hours':
1055 delta = timedelta(hours=duration)
1056 if interval == 'minutes':
1057 delta = timedelta(minutes=duration)
1058 trigger.value = delta
1060 # Compute other details
1061 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1062 valarm.add('ACTION').value = alarm_data['action']
1065 def import_cal(self, cr, uid, ical_data, context=None):
1067 @param self: The object pointer
1068 @param cr: the current row, from the database cursor,
1069 @param uid: the current user’s ID for security checks,
1070 @param ical_data: Get calendar's Data
1071 @param context: A standard dictionary for contextual values
1074 ctx = context.copy()
1075 ctx.update({'model': context.get('model', None)})
1076 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1077 for child in ical_data.getChildren():
1078 if child.name.lower() == 'trigger':
1079 seconds = child.value.seconds
1080 days = child.value.days
1081 diff = (days * 86400) + seconds
1085 duration = abs(days)
1086 related = days > 0 and 'after' or 'before'
1087 elif (abs(diff) / 3600) == 0:
1088 duration = abs(diff / 60)
1089 interval = 'minutes'
1090 related = days >= 0 and 'after' or 'before'
1092 duration = abs(diff / 3600)
1094 related = days >= 0 and 'after' or 'before'
1095 self.ical_set('trigger_interval', interval, 'value')
1096 self.ical_set('trigger_duration', duration, 'value')
1097 self.ical_set('trigger_occurs', related.lower(), 'value')
1099 if child.params.get('related'):
1100 self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1102 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1103 vals = map_data(cr, uid, self, context=context)
1109 class Attendee(CalDAV, osv.osv_memory):
1110 _name = 'basic.calendar.attendee'
1111 _calname = 'attendee'
1114 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1115 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property.
1116 '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"
1117 '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".
1118 '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.
1119 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1120 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1121 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1122 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property.
1123 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property.
1124 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter.
1127 def import_cal(self, cr, uid, ical_data, context=None):
1129 @param self: The object pointer
1130 @param cr: the current row, from the database cursor,
1131 @param uid: the current user’s ID for security checks,
1132 @param ical_data: Get calendar's Data
1133 @param context: A standard dictionary for contextual values
1136 ctx = context.copy()
1137 ctx.update({'model': context.get('model', None)})
1138 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1139 for para in ical_data.params:
1140 if para.lower() == 'cn':
1141 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1142 ical_data.value, 'value')
1144 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1145 if not ical_data.params.get('CN'):
1146 self.ical_set('cn', ical_data.value, 'value')
1147 vals = map_data(cr, uid, self, context=context)
1150 def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1152 @param self: The object pointer
1153 @param cr: the current row, from the database cursor,
1154 @param uid: the current user’s ID for security checks,
1155 @param model: Get model's name
1156 @param attendee_ids: Get Attendee's Id
1157 @param context: A standard dictionary for contextual values
1161 attendee_object = self.pool.get(model)
1162 ctx = context.copy()
1163 ctx.update({'model': model})
1164 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1165 for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1166 attendee_add = vevent.add('attendee')
1168 for a_key, a_val in self.__attribute__.items():
1169 if attendee[a_val['field']] and a_val['field'] != 'cn':
1170 if a_val['type'] in ('text', 'char', 'selection'):
1171 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1172 elif a_val['type'] == 'boolean':
1173 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1174 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1175 cn_val = [str(attendee[a_val['field']])]
1177 attendee_add.params['CN'] = cn_val
1178 if not attendee['email']:
1179 attendee_add.value = 'MAILTO:'
1180 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1181 elif attendee['email']:
1182 attendee_add.value = 'MAILTO:' + attendee['email']
1187 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: