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, context=context):
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 == 'datetime_utc':
168 res[attr]['type'] = 'utc'
169 if field.fn == 'hours':
170 res[attr]['type'] = "timedelta"
171 if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
172 res[attr]['object'] = field.field_id.relation
173 elif res[attr]['type'] in ('selection') and field.mapping:
174 res[attr]['mapping'] = eval(field.mapping)
175 if not res.get('uid', None):
177 res['uid']['field'] = 'id'
178 res['uid']['type'] = "integer"
181 def map_data(cr, uid, obj, context=None):
183 @param self: The object pointer
184 @param cr: the current row, from the database cursor,"""
187 for map_dict in obj.__attribute__:
188 map_val = obj.ical_get(map_dict, 'value')
189 field = obj.ical_get(map_dict, 'field')
190 field_type = obj.ical_get(map_dict, 'type')
192 #ignore write date, this field is resered for the orm
193 if field == 'write_date':
195 if field_type == 'selection':
198 if type(map_val) == list and len(map_val): #TOFIX: why need to check this
200 mapping = obj.__attribute__[map_dict].get('mapping', False)
202 map_val = mapping.get(map_val.lower(), False)
204 map_val = map_val.lower()
205 if field_type == 'many2many':
210 model = obj.__attribute__[map_dict].get('object', False)
211 modobj = obj.pool.get(model)
212 for map_vall in map_val:
213 id = modobj.create(cr, uid, map_vall, context=context)
215 vals[field] = [(6, 0, ids)]
217 if field_type == 'many2one':
219 if not map_val or not isinstance(map_val, dict):
222 model = obj.__attribute__[map_dict].get('object', False)
223 modobj = obj.pool.get(model)
224 # check if the record exists or not
225 key1 = map_val.keys()
226 value1 = map_val.values()
227 domain = [(key1[i], '=', value1[i]) for i in range(len(key1)) if value1[i]]
228 exist_id = modobj.search(cr, uid, domain, context=context)
232 id = modobj.create(cr, uid, map_val, context=context)
235 if field_type == 'timedelta':
237 vals[field] = (map_val.seconds/float(86400) + map_val.days)
238 vals[field] = map_val
241 class CalDAV(object):
243 _logger = logging.getLogger('document.caldav')
245 def ical_set(self, name, value, type):
246 """ set calendar Attribute
247 @param self: The object pointer,
248 @param name: Get Attribute Name
249 @param value: Get Attribute Value
250 @param type: Get Attribute Type
252 if name in self.__attribute__ and self.__attribute__[name]:
253 self.__attribute__[name][type] = value
256 def ical_get(self, name, type):
257 """ Get calendar Attribute
258 @param self: The object pointer,
259 @param name: Get Attribute Name
260 @param type: Get Attribute Type
262 if self.__attribute__.get(name):
263 val = self.__attribute__.get(name).get(type, None)
264 valtype = self.__attribute__.get(name).get('type', None)
266 if valtype and valtype == 'datetime' and val:
267 if isinstance(val, list):
268 val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
270 val = val.strftime('%Y-%m-%d %H:%M:%S')
273 return self.__attribute__.get(name, None)
275 def ical_reset(self, type):
276 """ Reset Calendar Attribute
277 @param self: The object pointer,
278 @param type: Get Attribute Type
280 for name in self.__attribute__:
281 if self.__attribute__[name]:
282 self.__attribute__[name][type] = None
285 def format_date_tz(self, src_date, tz=None):
286 """ This function converts date into specifice timezone value
287 @param src_date: Date to be converted (datetime.datetime)
288 @return: Converted datetime.datetime object for the date
290 format = tools.DEFAULT_SERVER_DATETIME_FORMAT
291 date_str = src_date.strftime('%Y-%m-%d %H:%M:%S')
292 res_date = tools.server_to_local_timestamp(date_str, format, format, tz)
293 return datetime.strptime(res_date, "%Y-%m-%d %H:%M:%S")
295 def parse_ics(self, cr, uid, child, cal_children=None, context=None):
296 """ parse calendaring and scheduling information
297 @param self: The object pointer
298 @param cr: the current row, from the database cursor,
299 @param uid: the current user’s ID for security checks,
300 @param context: A standard dictionary for contextual values """
304 _server_tzinfo = pytz.timezone(tools.get_server_timezone())
305 for cal_data in child.getChildren():
306 if cal_data.name.lower() == 'organizer':
307 dmail = { 'name': cal_data.params.get('CN', ['',])[0],
308 'email': cal_data.value.lower().replace('mailto:',''),
311 self.ical_set(cal_data.name.lower(), mailto2str(dmail), 'value')
313 if cal_data.name.lower() == 'attendee':
316 ctx.update({'model': cal_children[cal_data.name.lower()]})
317 attendee = self.pool.get('basic.calendar.attendee')
318 att_data.append(attendee.import_cal(cr, uid, cal_data, context=ctx))
319 self.ical_set(cal_data.name.lower(), att_data, 'value')
321 if cal_data.name.lower() == 'valarm':
322 alarm = self.pool.get('basic.calendar.alarm')
325 ctx.update({'model': cal_children[cal_data.name.lower()]})
326 vals = alarm.import_cal(cr, uid, cal_data, context=ctx)
327 self.ical_set(cal_data.name.lower(), vals, 'value')
329 if cal_data.name.lower() == 'exdate':
330 exdates += cal_data.value
332 for exdate in exdates:
333 exvals.append(datetime.fromtimestamp(time.mktime(exdate.utctimetuple())).strftime('%Y%m%dT%H%M%S'))
334 self.ical_set(cal_data.name.lower(), ','.join(exvals), 'value')
336 if cal_data.name.lower() in self.__attribute__:
337 if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'):
338 self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value')
339 date_local = cal_data.value.astimezone(_server_tzinfo)
340 self.ical_set(cal_data.name.lower(), date_local, 'value')
342 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
343 vals = map_data(cr, uid, self, context=context)
346 def create_ics(self, cr, uid, datas, name, ical, context=None):
347 """ create calendaring and scheduling information
348 @param self: The object pointer
349 @param cr: the current row, from the database cursor,
350 @param uid: the current user’s ID for security checks,
351 @param context: A standard dictionary for contextual values """
360 vevent = ical.add(name)
361 for field in self.__attribute__.keys():
362 map_field = self.ical_get(field, 'field')
363 map_type = self.ical_get(field, 'type')
364 if map_field in data.keys():
366 model = context.get('model', None)
369 uidval = openobjectid2uid(cr, data[map_field], model)
370 #Computation for getting events with the same UID (RFC4791 Section4.1)
372 model_obj = self.pool.get(model)
374 if model_obj._columns.get('recurrent_uid', None):
375 cr.execute('SELECT id FROM %s WHERE recurrent_uid=%%s' % model_obj._table,
377 r_ids = map(lambda x: x[0], cr.fetchall())
379 r_datas = model_obj.read(cr, uid, r_ids, context=context)
380 rcal = CalDAV.export_cal(self, cr, uid, r_datas, 'vevent', context=context)
381 for revents in rcal.contents.get('vevent', []):
382 ical.contents['vevent'].append(revents)
384 if data.get('recurrent_uid', None):
385 # Change the UID value in case of modified event from any recurrent event
386 uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
387 vevent.add('uid').value = uidval
388 elif field == 'attendee' and data[map_field]:
389 model = self.__attribute__[field].get('object', False)
390 attendee_obj = self.pool.get('basic.calendar.attendee')
391 vevent = attendee_obj.export_cal(cr, uid, model, \
392 data[map_field], vevent, context=context)
393 elif field == 'valarm' and data[map_field]:
394 model = self.__attribute__[field].get('object', False)
396 ctx.update({'model': model})
397 alarm_obj = self.pool.get('basic.calendar.alarm')
398 vevent = alarm_obj.export_cal(cr, uid, model, \
399 data[map_field][0], vevent, context=ctx)
400 elif field == 'vtimezone' and data[map_field]:
401 tzval = data[map_field]
402 if tzval not in timezones:
403 tz_obj = self.pool.get('basic.calendar.timezone')
404 ical = tz_obj.export_cal(cr, uid, None, \
405 data[map_field], ical, context=context)
406 timezones.append(data[map_field])
407 if vevent.contents.get('recurrence-id'):
408 # Convert recurrence-id field value accroding to timezone value
409 recurid_val = vevent.contents.get('recurrence-id')[0].value
410 vevent.contents.get('recurrence-id')[0].params['TZID'] = [tzval.title()]
411 vevent.contents.get('recurrence-id')[0].value = self.format_date_tz(recurid_val, tzval.title())
413 # Set exdates according to timezone value
414 # This is the case when timezone mapping comes after the exdate mapping
415 # and we have exdate value available
416 exfield.params['TZID'] = [tzval.title()]
418 for exdate in exdates:
419 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
420 exfield.value = exdates_updated
421 elif field == 'organizer' and data[map_field]:
422 organizer = str2mailto(data[map_field])
423 event_org = vevent.add('organizer')
424 event_org.params['CN'] = [organizer['name']]
425 event_org.value = 'MAILTO:' + (organizer.get('email') or '')
427 elif data[map_field]:
428 if map_type in ("char", "text"):
429 if field in ('exdate'):
430 exfield = vevent.add(field)
431 exdates = (data[map_field]).split(',')
433 # Set exdates according to timezone value
434 # This is the case when timezone mapping comes before the exdate mapping
435 # and we have timezone value available
436 exfield.params['TZID'] = [tzval.title()]
438 for exdate in exdates:
439 exdates_updated.append(self.format_date_tz(parser.parse(exdate), tzval.title()))
440 exfield.value = exdates_updated
442 vevent.add(field).value = tools.ustr(data[map_field])
443 elif map_type in ('datetime', 'date') and data[map_field]:
444 dtfield = vevent.add(field)
446 # Export the date according to the event timezone value
447 dtfield.params['TZID'] = [tzval.title()]
448 dtfield.value = self.format_date_tz(parser.parse(data[map_field]), tzval.title())
450 dtfield.value = parser.parse(data[map_field])
452 elif map_type == 'utc'and data[map_field]:
454 local = pytz.timezone (tzval.title())
455 naive = datetime.strptime (data[map_field], "%Y-%m-%d %H:%M:%S")
456 local_dt = naive.replace (tzinfo = local)
457 utc_dt = local_dt.astimezone (pytz.utc)
458 vevent.add(field).value = utc_dt
460 utc_timezone = pytz.timezone ('UTC')
461 naive = datetime.strptime (data[map_field], "%Y-%m-%d %H:%M:%S")
462 local_dt = naive.replace (tzinfo = utc_timezone)
463 utc_dt = local_dt.astimezone (pytz.utc)
464 vevent.add(field).value = utc_dt
466 elif map_type == "timedelta":
467 vevent.add(field).value = timedelta(hours=data[map_field])
468 elif map_type == "many2one":
469 vevent.add(field).value = tools.ustr(data.get(map_field)[1])
470 elif map_type in ("float", "integer"):
471 vevent.add(field).value = str(data.get(map_field))
472 elif map_type == "selection":
473 if not self.ical_get(field, 'mapping'):
474 vevent.add(field).value = (tools.ustr(data[map_field])).upper()
476 for key1, val1 in self.ical_get(field, 'mapping').items():
477 if val1 == data[map_field]:
478 vevent.add(field).value = key1.upper()
481 def check_import(self, cr, uid, vals, context=None):
483 @param self: The object pointer
484 @param cr: the current row, from the database cursor,
485 @param uid: the current user’s ID for security checks,
486 @param vals: Get Values
487 @param context: A standard dictionary for contextual values
492 model_obj = self.pool.get(context.get('model'))
496 # Compute value of duration
497 if 'date_deadline' in val and 'duration' not in val:
498 start = datetime.strptime(val['date'], '%Y-%m-%d %H:%M:%S')
499 end = datetime.strptime(val['date_deadline'], '%Y-%m-%d %H:%M:%S')
501 val['duration'] = (diff.seconds/float(86400) + diff.days) * 24
502 exists, r_id = calendar.uid2openobjectid(cr, val['id'], context.get('model'), \
503 val.get('recurrent_id'))
504 if val.has_key('create_date'):
505 val.pop('create_date')
506 u_id = val.get('id', None)
509 val.update({'recurrent_uid': exists})
510 model_obj.write(cr, uid, [r_id], val)
513 model_obj.write(cr, uid, [exists], val)
516 if u_id in recur_pool and val.get('recurrent_id'):
517 val.update({'recurrent_uid': recur_pool[u_id]})
518 revent_id = model_obj.create(cr, uid, val)
519 ids.append(revent_id)
521 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
522 wematch = __rege.match(u_id.encode('utf8'))
524 model, recur_id, dbname = wematch.groups()
525 val.update({'recurrent_uid': recur_id})
526 event_id = model_obj.create(cr, uid, val)
527 recur_pool[u_id] = event_id
533 def export_cal(self, cr, uid, datas, vobj=None, context=None):
535 @param self: The object pointer
536 @param cr: the current row, from the database cursor,
537 @param uid: the current user’s ID for security checks,
538 @param datas: Get Data's for caldav
539 @param context: A standard dictionary for contextual values
542 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
543 ical = vobject.iCalendar()
544 self.create_ics(cr, uid, datas, vobj, ical, context=context)
547 raise # osv.except_osv(('Error !'), (str(e)))
549 def import_cal(self, cr, uid, content, data_id=None, context=None):
551 @param self: The object pointer
552 @param cr: the current row, from the database cursor,
553 @param uid: the current user’s ID for security checks,
554 @param data_id: Get Data’s ID or False
555 @param context: A standard dictionary for contextual values
559 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, context)
560 parsedCal = vobject.readOne(ical_data)
563 for child in parsedCal.getChildren():
564 if child.name.lower() in ('vevent', 'vtodo'):
565 vals = self.parse_ics(cr, uid, child, context=context)
569 if vals: res.append(vals)
570 self.ical_reset('value')
573 class Calendar(CalDAV, osv.osv):
574 _name = 'basic.calendar'
575 _calname = 'calendar'
578 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
579 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
580 # or the minimum and maximum range of the iCalendar specification
581 # that is required in order to interpret the iCalendar object.
582 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
583 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
584 'vevent': None, # Use: O-n, Type: Collection of Event class
585 'vtodo': None, # Use: O-n, Type: Collection of ToDo class
586 'vjournal': None, # Use: O-n, Type: Collection of Journal class
587 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
588 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
591 'name': fields.char("Name", size=64),
592 'user_id': fields.many2one('res.users', 'Owner'),
593 'collection_id': fields.many2one('document.directory', 'Collection', \
595 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO')], \
596 string="Type", size=64),
597 'line_ids': fields.one2many('basic.calendar.lines', 'calendar_id', 'Calendar Lines'),
598 'create_date': fields.datetime('Created Date', readonly=True),
599 'write_date': fields.datetime('Write Date', readonly=True),
600 'description': fields.text("Description"),
601 'calendar_color': fields.char('Color', size=20, help="For supporting clients, the color of the calendar entries"),
602 'calendar_order': fields.integer('Order', help="For supporting clients, the order of this folder among the calendars"),
603 'has_webcal': fields.boolean('WebCal', required=True, help="Also export a <name>.ics entry next to the calendar folder, with WebCal content."),
610 def get_calendar_objects(self, cr, uid, ids, parent=None, domain=None, context=None):
616 ctx_res_id = context.get('res_id', None)
617 ctx_model = context.get('model', None)
618 for cal in self.browse(cr, uid, ids):
619 for line in cal.line_ids:
620 if ctx_model and ctx_model != line.object_id.model:
622 if line.name in ('valarm', 'attendee'):
624 line_domain = eval(line.domain or '[]', context)
625 line_domain += domain
627 line_domain += [('id','=',ctx_res_id)]
628 mod_obj = self.pool.get(line.object_id.model)
629 data_ids = mod_obj.search(cr, uid, line_domain, order="id", context=context)
630 for data in mod_obj.browse(cr, uid, data_ids, context):
631 ctx = parent and parent.context or None
632 if hasattr(data, 'recurrent_uid') and data.recurrent_uid:
633 # Skip for event which is child of other event
635 node = res_node_calendar('%s.ics' %data.id, parent, ctx, data, line.object_id.model, data.id)
640 def get_cal_max_modified(self, cr, uid, ids, parent=None, domain=None, context=None):
646 ctx_res_id = context.get('res_id', None)
647 ctx_model = context.get('model', None)
648 for cal in self.browse(cr, uid, ids):
649 for line in cal.line_ids:
650 if ctx_model and ctx_model != line.object_id.model:
652 if line.name in ('valarm', 'attendee'):
654 line_domain = eval(line.domain or '[]', context)
655 line_domain += domain
657 line_domain += [('id','=',ctx_res_id)]
658 mod_obj = self.pool.get(line.object_id.model)
659 max_data = get_last_modified(mod_obj, cr, uid, line_domain, context=context)
660 if res and res > max_data:
665 def export_cal(self, cr, uid, ids, vobj='vevent', context=None):
667 @param ids: List of calendar’s IDs
668 @param vobj: the type of object to export
669 @return the ical data.
673 ctx_model = context.get('model', None)
674 ctx_res_id = context.get('res_id', None)
675 ical = vobject.iCalendar()
676 for cal in self.browse(cr, uid, ids, context=context):
677 for line in cal.line_ids:
678 if ctx_model and ctx_model != line.object_id.model:
680 if line.name in ('valarm', 'attendee'):
682 domain = eval(line.domain or '[]', context)
684 domain += [('id','=',ctx_res_id)]
685 mod_obj = self.pool.get(line.object_id.model)
686 data_ids = mod_obj.search(cr, uid, domain, context=context)
687 datas = mod_obj.read(cr, uid, data_ids, context=context)
688 context.update({'model': line.object_id.model,
689 'calendar_id': cal.id
691 self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context)
692 self.create_ics(cr, uid, datas, line.name, ical, context=context)
693 return ical.serialize()
695 def import_cal(self, cr, uid, content, data_id=None, context=None):
697 @param self: The object pointer
698 @param cr: the current row, from the database cursor,
699 @param uid: the current user’s ID for security checks,
700 @param data_id: Get Data’s ID or False
701 @param context: A standard dictionary for contextual values
707 parsedCal = vobject.readOne(ical_data)
709 data_id = self.search(cr, uid, [])[0]
710 cal = self.browse(cr, uid, data_id, context=context)
713 for line in cal.line_ids:
714 cal_children[line.name] = line.object_id.model
717 for child in parsedCal.getChildren():
718 if child.name.lower() in cal_children:
719 context.update({'model': cal_children[child.name.lower()],
720 'calendar_id': cal['id']
722 self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context)
723 val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context)
725 objs.append(cal_children[child.name.lower()])
726 elif child.name.upper() == 'CALSCALE':
727 if child.value.upper() != 'GREGORIAN':
728 self._logger.warning('How do I handle %s calendars?',child.value)
729 elif child.name.upper() in ('PRODID', 'VERSION'):
731 elif child.name.upper().startswith('X-'):
732 self._logger.debug("skipping custom node %s", child.name)
734 self._logger.debug("skipping node %s", child.name)
737 for obj_name in list(set(objs)):
738 obj = self.pool.get(obj_name)
739 if hasattr(obj, 'check_import'):
740 r = obj.check_import(cr, uid, vals, context=context)
745 r = self.check_import(cr, uid, vals, context=context)
752 class basic_calendar_line(osv.osv):
753 """ Calendar Lines """
755 _name = 'basic.calendar.lines'
756 _description = 'Calendar Lines'
759 'name': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
760 ('valarm', 'Alarm'), \
761 ('attendee', 'Attendee')], \
762 string="Type", size=64),
763 'object_id': fields.many2one('ir.model', 'Object'),
764 'calendar_id': fields.many2one('basic.calendar', 'Calendar', \
765 required=True, ondelete='cascade'),
766 'domain': fields.char('Domain', size=124),
767 'mapping_ids': fields.one2many('basic.calendar.fields', 'type_id', 'Fields Mapping')
771 'domain': lambda *a: '[]',
774 def create(self, cr, uid, vals, context=None):
775 """ create calendar's line
776 @param self: The object pointer
777 @param cr: the current row, from the database cursor,
778 @param uid: the current user’s ID for security checks,
779 @param vals: Get the Values
780 @param context: A standard dictionary for contextual values
783 cr.execute("SELECT COUNT(id) FROM basic_calendar_lines \
784 WHERE name=%s AND calendar_id=%s",
785 (vals.get('name'), vals.get('calendar_id')))
789 raise osv.except_osv(_('Warning !'), _('Can not create line "%s" more than once') % (vals.get('name')))
790 return super(basic_calendar_line, self).create(cr, uid, vals, context=context)
792 basic_calendar_line()
794 class basic_calendar_alias(osv.osv):
795 """ Mapping of client filenames to ORM ids of calendar records
797 Since some clients insist on putting arbitrary filenames on the .ics data
798 they send us, and they won't respect the redirection "Location:" header,
799 we have to store those filenames and allow clients to call our calendar
801 Note that adding a column to all tables that would possibly hold calendar-
802 mapped data won't work. The user is always allowed to specify more
803 calendars, on any arbitrary ORM object, without need to alter those tables'
806 _name = 'basic.calendar.alias'
808 'name': fields.char('Filename', size=512, required=True, select=1),
809 'cal_line_id': fields.many2one('basic.calendar.lines', 'Calendar', required=True,
810 select=1, help='The calendar/line this mapping applies to'),
811 'res_id': fields.integer('Res. ID', required=True, select=1),
814 _sql_constraints = [ ('name_cal_uniq', 'UNIQUE(cal_line_id, name)',
815 _('The same filename cannot apply to two records!')), ]
817 basic_calendar_alias()
819 class basic_calendar_attribute(osv.osv):
820 _name = 'basic.calendar.attributes'
821 _description = 'Calendar attributes'
823 'name': fields.char("Name", size=64, required=True),
824 'type': fields.selection([('vevent', 'Event'), ('vtodo', 'TODO'), \
825 ('alarm', 'Alarm'), \
826 ('attendee', 'Attendee')], \
827 string="Type", size=64, required=True),
830 basic_calendar_attribute()
833 class basic_calendar_fields(osv.osv):
834 """ Calendar fields """
836 _name = 'basic.calendar.fields'
837 _description = 'Calendar fields'
841 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
842 'name': fields.many2one('basic.calendar.attributes', 'Name', required=True),
843 'type_id': fields.many2one('basic.calendar.lines', 'Type', \
844 required=True, ondelete='cascade'),
845 'expr': fields.char("Expression", size=64),
846 'fn': fields.selection([('field', 'Use the field'),
847 ('const', 'Expression as constant'),
848 ('hours', 'Interval in hours'),
849 ('datetime_utc', 'Datetime In UTC'),
851 'mapping': fields.text('Mapping'),
859 ( 'name_type_uniq', 'UNIQUE(name, type_id)', 'Can not map a field more than once'),
862 def check_line(self, cr, uid, vals, name, context=None):
863 """ check calendar's line
864 @param self: The object pointer
865 @param cr: the current row, from the database cursor,
866 @param uid: the current user’s ID for security checks,
867 @param vals: Get Values
868 @param context: A standard dictionary for contextual values
870 f_obj = self.pool.get('ir.model.fields')
871 field = f_obj.browse(cr, uid, vals['field_id'], context=context)
872 relation = field.relation
873 line_obj = self.pool.get('basic.calendar.lines')
874 l_id = line_obj.search(cr, uid, [('name', '=', name)])
876 line = line_obj.browse(cr, uid, l_id, context=context)[0]
877 line_rel = line.object_id.model
878 if (relation != 'NULL') and (not relation == line_rel):
879 raise osv.except_osv(_('Warning !'), _('Please provide proper configuration of "%s" in Calendar Lines') % (name))
882 def create(self, cr, uid, vals, context=None):
883 """ Create Calendar's fields
884 @param self: The object pointer
885 @param cr: the current row, from the database cursor,
886 @param uid: the current user’s ID for security checks,
887 @param vals: Get Values
888 @param context: A standard dictionary for contextual values
891 cr.execute('SELECT name FROM basic_calendar_attributes \
892 WHERE id=%s', (vals.get('name'),))
895 if name in ('valarm', 'attendee'):
896 self.check_line(cr, uid, vals, name, context=context)
897 return super(basic_calendar_fields, self).create(cr, uid, vals, context=context)
899 def write(self, cr, uid, ids, vals, context=None):
900 """ write Calendar's fields
901 @param self: The object pointer
902 @param cr: the current row, from the database cursor,
903 @param uid: the current user’s ID for security checks,
904 @param vals: Get Values
905 @param context: A standard dictionary for contextual values
911 field = self.browse(cr, uid, id, context=context)
912 name = field.name.name
913 if name in ('valarm', 'attendee'):
914 self.check_line(cr, uid, vals, name, context=context)
915 return super(basic_calendar_fields, self).write(cr, uid, ids, vals, context)
917 basic_calendar_fields()
920 class Event(CalDAV, osv.osv_memory):
921 _name = 'basic.calendar.event'
924 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
925 'created': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the calendar information was created by the calendar user agent in the calendar store.
926 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
927 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
928 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
929 'last-mod': None, # Use: O-1, Type: DATE-TIME Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
930 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component.
931 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
932 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
933 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
934 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
935 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
936 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
937 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
938 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
939 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
941 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
942 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
943 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
944 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
945 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component.
946 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
947 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
949 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
950 # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
951 'resources': None, # Use: O-n, Type: TEXT, Defines the equipment or resources anticipated for an activity specified by a calendar entity like RESOURCES:EASEL,PROJECTOR,VCR, LANGUAGE=fr:1 raton-laveur
952 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
953 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
955 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
956 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
959 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
961 @param self: The object pointer
962 @param cr: the current row, from the database cursor,
963 @param uid: the current user’s ID for security checks,
964 @param datas: Get datas
965 @param context: A standard dictionary for contextual values
968 return super(Event, self).export_cal(cr, uid, datas, 'vevent', context=context)
973 class ToDo(CalDAV, osv.osv_memory):
974 _name = 'basic.calendar.todo'
1012 def export_cal(self, cr, uid, datas, vobj='vevent', context=None):
1014 @param self: The object pointer
1015 @param cr: the current row, from the database cursor,
1016 @param uid: the current user’s ID for security checks,
1017 @param datas: Get datas
1018 @param context: A standard dictionary for contextual values
1021 return super(ToDo, self).export_cal(cr, uid, datas, 'vtodo', context=context)
1026 class Journal(CalDAV):
1031 class FreeBusy(CalDAV):
1033 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component.
1034 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
1035 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
1036 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
1037 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
1038 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
1039 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
1040 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
1041 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
1042 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
1043 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
1049 class Timezone(CalDAV, osv.osv_memory):
1050 _name = 'basic.calendar.timezone'
1051 _calname = 'vtimezone'
1054 'tzid': {'field': 'tzid'}, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
1055 'last-mod': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that the information associated with the calendar component was last revised in the calendar store.
1056 'tzurl': None, # Use: O-1, Type: URI, Provides a means for a VTIMEZONE component to point to a network location that can be used to retrieve an up-to-date version of itself.
1057 'standardc': {'tzprop': None}, # Use: R-1,
1058 'daylightc': {'tzprop': None}, # Use: R-1,
1059 'x-prop': None, # Use: O-n, Type: Text,
1062 def get_name_offset(self, cr, uid, tzid, context=None):
1063 """ Get Name Offset value
1064 @param self: The object pointer
1065 @param cr: the current row, from the database cursor,
1066 @param uid: the current user’s ID for security checks,
1067 @param context: A standard dictionary for contextual values
1070 mytz = pytz.timezone(tzid.title())
1071 mydt = datetime.now(tz=mytz)
1072 offset = mydt.utcoffset()
1073 val = offset.days * 24 + float(offset.seconds) / 3600
1074 realoffset = '%02d%02d' % (math.floor(abs(val)), \
1075 round(abs(val) % 1 + 0.01, 2) * 60)
1076 realoffset = (val < 0 and ('-' + realoffset) or ('+' + realoffset))
1077 return (mydt.tzname(), realoffset)
1079 def export_cal(self, cr, uid, model, tzid, ical, context=None):
1081 @param self: The object pointer
1082 @param cr: the current row, from the database cursor,
1083 @param uid: the current user’s ID for security checks,
1084 @param model: Get Model's name
1085 @param context: A standard dictionary for contextual values
1089 ctx = context.copy()
1090 ctx.update({'model': model})
1091 cal_tz = ical.add('vtimezone')
1092 cal_tz.add('TZID').value = tzid.title()
1093 tz_std = cal_tz.add('STANDARD')
1094 tzname, offset = self.get_name_offset(cr, uid, tzid)
1095 tz_std.add("TZOFFSETFROM").value = offset
1096 tz_std.add("TZOFFSETTO").value = offset
1097 #TODO: Get start date for timezone
1098 tz_std.add("DTSTART").value = datetime.strptime('1970-01-01 00:00:00', '%Y-%m-%d %H:%M:%S')
1099 tz_std.add("TZNAME").value = tzname
1102 def import_cal(self, cr, uid, ical_data, context=None):
1104 @param self: The object pointer
1105 @param cr: the current row, from the database cursor,
1106 @param uid: the current user’s ID for security checks,
1107 @param ical_data: Get calendar's data
1108 @param context: A standard dictionary for contextual values
1111 for child in ical_data.getChildren():
1112 if child.name.lower() == 'tzid':
1113 tzname = child.value
1114 self.ical_set(child.name.lower(), tzname, 'value')
1115 vals = map_data(cr, uid, self, context=context)
1121 class Alarm(CalDAV, osv.osv_memory):
1122 _name = 'basic.calendar.alarm'
1126 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
1127 'description': None, # Type: Text, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property. Use:- R-1 for DISPLAY,Use:- R-1 for EMAIL,Use:- R-1 for PROCEDURE
1128 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL
1129 'attendee': None, # Use: R-n, Type: CAL-ADDRESS, Contain the email address of attendees to receive the message. It can also include one or more. Use for EMAIL
1130 'trigger': None, # Use: R-1, Type: DURATION, The "TRIGGER" property specifies a duration prior to the start of an event or a to-do. The "TRIGGER" edge may be explicitly set to be relative to the "START" or "END" of the event or to-do with the "related" parameter of the "TRIGGER" property. The "TRIGGER" property value type can alternatively be set to an absolute calendar date and time of day value. Use for all action like AUDIO, DISPLAY, EMAIL and PROCEDURE
1131 'duration': None, # Type: DURATION, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
1132 'repeat': None, # Type: INTEGER, Duration' and 'repeat' are both optional, and MUST NOT occur more than once each, but if one occurs, so MUST the other. Use:- 0-1 for AUDIO, EMAIL and PROCEDURE, Use:- 0-n for DISPLAY
1133 'attach': None, # Use:- O-n: which MUST point to a sound resource, which is rendered when the alarm is triggered for AUDIO, Use:- O-n: which are intended to be sent as message attachments for EMAIL, Use:- R-1:which MUST point to a procedure resource, which is invoked when the alarm is triggered for PROCEDURE.
1137 def export_cal(self, cr, uid, model, alarm_id, vevent, context=None):
1139 @param self: The object pointer
1140 @param cr: the current row, from the database cursor,
1141 @param uid: the current user’s ID for security checks,
1142 @param model: Get Model's name
1143 @param alarm_id: Get Alarm's Id
1144 @param context: A standard dictionary for contextual values
1146 valarm = vevent.add('valarm')
1147 alarm_object = self.pool.get(model)
1148 alarm_data = alarm_object.read(cr, uid, alarm_id, [])
1150 # Compute trigger data
1151 interval = alarm_data['trigger_interval']
1152 occurs = alarm_data['trigger_occurs']
1153 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
1154 or -(alarm_data['trigger_duration'])
1155 related = alarm_data['trigger_related']
1156 trigger = valarm.add('TRIGGER')
1157 trigger.params['related'] = [related.upper()]
1158 if interval == 'days':
1159 delta = timedelta(days=duration)
1160 if interval == 'hours':
1161 delta = timedelta(hours=duration)
1162 if interval == 'minutes':
1163 delta = timedelta(minutes=duration)
1164 trigger.value = delta
1166 # Compute other details
1167 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
1168 valarm.add('ACTION').value = alarm_data['action']
1171 def import_cal(self, cr, uid, ical_data, context=None):
1173 @param self: The object pointer
1174 @param cr: the current row, from the database cursor,
1175 @param uid: the current user’s ID for security checks,
1176 @param ical_data: Get calendar's Data
1177 @param context: A standard dictionary for contextual values
1181 ctx = context.copy()
1182 ctx.update({'model': context.get('model', None)})
1183 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1184 for child in ical_data.getChildren():
1185 if child.name.lower() == 'trigger':
1186 if isinstance(child.value, timedelta):
1187 seconds = child.value.seconds
1188 days = child.value.days
1189 diff = (days * 86400) + seconds
1192 elif isinstance(child.value, datetime):
1194 # remember, spec says this datetime is in UTC
1195 raise NotImplementedError("we cannot parse absolute triggers")
1197 duration = abs(days)
1198 related = days > 0 and 'after' or 'before'
1199 elif (abs(diff) / 3600) == 0:
1200 duration = abs(diff / 60)
1201 interval = 'minutes'
1202 related = days >= 0 and 'after' or 'before'
1204 duration = abs(diff / 3600)
1206 related = days >= 0 and 'after' or 'before'
1207 self.ical_set('trigger_interval', interval, 'value')
1208 self.ical_set('trigger_duration', duration, 'value')
1209 self.ical_set('trigger_occurs', related.lower(), 'value')
1211 if child.params.get('related'):
1212 self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
1214 self.ical_set(child.name.lower(), child.value.lower(), 'value')
1215 vals = map_data(cr, uid, self, context=context)
1221 class Attendee(CalDAV, osv.osv_memory):
1222 _name = 'basic.calendar.attendee'
1223 _calname = 'attendee'
1226 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
1227 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property.
1228 'role': None, # Use: 0-1 Specify the participation role for the calendar user specified by the property like "CHAIR"/"REQ-PARTICIPANT"/"OPT-PARTICIPANT"/"NON-PARTICIPANT"
1229 'partstat': None, # Use: 0-1 Specify the participation status for the calendar user specified by the property. like use for VEVENT:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED", use for VTODO:-"NEEDS-ACTION"/"ACCEPTED"/"DECLINED"/"TENTATIVE"/"DELEGATED"/"COMPLETED"/"IN-PROCESS" and use for VJOURNAL:- "NEEDS-ACTION"/"ACCEPTED"/"DECLINED".
1230 'rsvp': None, # Use: 0-1 Specify whether there is an expectation of a favor of a reply from the calendar user specified by the property value like TRUE / FALSE.
1231 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation.
1232 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property.
1233 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property.
1234 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property.
1235 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property.
1236 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter.
1239 def import_cal(self, cr, uid, ical_data, 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 ical_data: Get calendar's Data
1245 @param context: A standard dictionary for contextual values
1249 ctx = context.copy()
1250 ctx.update({'model': context.get('model', None)})
1251 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1252 for para in ical_data.params:
1253 if para.lower() == 'cn':
1254 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
1255 ical_data.value, 'value')
1257 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
1258 if not ical_data.params.get('CN'):
1259 self.ical_set('cn', ical_data.value, 'value')
1260 vals = map_data(cr, uid, self, context=context)
1263 def export_cal(self, cr, uid, model, attendee_ids, vevent, context=None):
1265 @param self: The object pointer
1266 @param cr: the current row, from the database cursor,
1267 @param uid: the current user’s ID for security checks,
1268 @param model: Get model's name
1269 @param attendee_ids: Get Attendee's Id
1270 @param context: A standard dictionary for contextual values
1274 attendee_object = self.pool.get(model)
1275 ctx = context.copy()
1276 ctx.update({'model': model})
1277 self.__attribute__ = get_attribute_mapping(cr, uid, self._calname, ctx)
1278 for attendee in attendee_object.read(cr, uid, attendee_ids, []):
1279 attendee_add = vevent.add('attendee')
1281 for a_key, a_val in self.__attribute__.items():
1282 if attendee[a_val['field']] and a_val['field'] != 'cn':
1283 if a_val['type'] in ('text', 'char', 'selection'):
1284 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1285 elif a_val['type'] == 'boolean':
1286 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
1287 if a_val['field'] == 'cn' and attendee[a_val['field']]:
1288 cn_val = [str(attendee[a_val['field']])]
1290 attendee_add.params['CN'] = cn_val
1291 if not attendee['email']:
1292 attendee_add.value = 'MAILTO:'
1293 #raise osv.except_osv(_('Error !'), _('Attendee must have an Email Id'))
1294 elif attendee['email']:
1295 attendee_add.value = 'MAILTO:' + attendee['email']
1300 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: