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
30 # O-1 Optional and can come only once
31 # O-n Optional and can come more than once
32 # R-1 Required and can come only once
33 # R-n Required and can come more than once
35 def uid2openobjectid(cr, uidval, oomodel, rdate):
36 __rege = re.compile(r'OpenObject-([\w|\.]+)_([0-9]+)@(\w+)$')
37 wematch = __rege.match(uidval.encode('utf8'))
41 model, id, dbname = wematch.groups()
42 model_obj = pooler.get_pool(cr.dbname).get(model)
43 if (not model == oomodel) or (not dbname == cr.dbname):
45 qry = 'select distinct(id) from %s' % model_obj._table
47 qry += " where recurrent_id='%s'" % (rdate)
53 ids = map(lambda x: str(x[0]), cr.fetchall())
58 def openobjectid2uid(cr, uidval, oomodel):
59 value = 'OpenObject-%s_%s@%s' % (oomodel, uidval, cr.dbname)
62 def get_attribute_mapping(cr, uid, context={}):
63 pool = pooler.get_pool(cr.dbname)
64 field_obj = pool.get('basic.calendar.fields')
65 fids = field_obj.search(cr, uid, [])
67 for field in field_obj.browse(cr, uid, fids):
68 attr = field.attribute
70 res[attr]['field'] = field.field_id.name
71 res[attr]['type'] = field.field_id.ttype
72 if res[attr]['type'] in ('one2many', 'many2many', 'many2one'):
73 res[attr]['object'] = field.field_id.relation
74 elif res[attr]['type'] in ('selection'):
75 res[attr]['mapping'] = field.info
78 def map_data(cr, uid, obj):
80 for map_dict in obj.__attribute__:
81 map_val = obj.ical_get(map_dict, 'value')
82 field = obj.ical_get(map_dict, 'field')
83 field_type = obj.ical_get(map_dict, 'type')
85 if field_type == 'selection':
88 mapping = obj.__attribute__[map_dict].get('mapping', False)
90 map_val = mapping[map_val.lower()]
92 map_val = map_val.lower()
93 if field_type == 'many2many':
98 model = obj.__attribute__[map_dict].get('object', False)
99 modobj = obj.pool.get(model)
100 for map_vall in map_val:
101 id = modobj.create(cr, uid, map_vall)
103 vals[field] = [(6, 0, ids)]
105 if field_type == 'many2one':
107 if not map_val or not isinstance(map_val, dict):
110 model = obj.__attribute__[map_dict].get('object', False)
111 modobj = obj.pool.get(model)
112 id = modobj.create(cr, uid, map_val)
116 vals[field] = map_val
119 class CalDAV(object):
122 def get_recurrent_dates(self, rrulestring, exdate, startdate=None):
124 startdate = datetime.now()
125 rset1 = rrulestr(rrulestring, dtstart=startdate, forceset=True)
128 datetime_obj = todate(date)
129 rset1._exdate.append(datetime_obj)
130 re_dates = map(lambda x:x.strftime('%Y-%m-%d %H:%M:%S'), rset1._iter())
133 def ical_set(self, name, value, type):
134 if name in self.__attribute__ and self.__attribute__[name]:
135 self.__attribute__[name][type] = value
138 def ical_get(self, name, type):
139 if self.__attribute__.get(name):
140 val = self.__attribute__.get(name).get(type, None)
141 valtype = self.__attribute__.get(name).get('type', None)
143 if valtype and valtype == 'datetime' and val:
144 if isinstance(val, list):
145 val = ','.join(map(lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), val))
147 val = val.strftime('%Y-%m-%d %H:%M:%S')
148 if valtype and valtype == 'integer' and val:
152 return self.__attribute__.get(name, None)
154 def ical_reset(self, type):
155 for name in self.__attribute__:
156 if self.__attribute__[name]:
157 self.__attribute__[name][type] = None
160 def export_ical(self, cr, uid, datas, vobj=None, context={}):
161 ical = vobject.iCalendar()
163 vevent = ical.add(vobj)
164 for field in self.__attribute__.keys():
165 map_field = self.ical_get(field, 'field')
166 map_type = self.ical_get(field, 'type')
167 if map_field in data.keys():
169 model = context.get('model', None)
172 uidval = openobjectid2uid(cr, data[map_field], model)
173 model_obj = self.pool.get(model)
174 cr.execute('select id from %s where recurrent_uid=%s'
175 % (model_obj._table, data[map_field]))
176 r_ids = map(lambda x: x[0], cr.fetchall())
178 rdata = self.pool.get(model).read(cr, uid, r_ids)
179 rcal = self.export_ical(cr, uid, rdata, context=context)
180 for revents in rcal.contents['vevent']:
181 ical.contents['vevent'].append(revents)
182 if data.get('recurrent_uid', None):
183 uidval = openobjectid2uid(cr, data['recurrent_uid'], model)
184 vevent.add('uid').value = uidval
185 elif field == 'attendee' and data[map_field]:
186 model = self.__attribute__[field].get('object', False)
187 attendee_obj = self.pool.get('basic.calendar.attendee')
188 vevent = attendee_obj.export_ical(cr, uid, model, \
189 data[map_field], vevent, context=context)
190 elif field == 'valarm' and data[map_field]:
191 model = self.__attribute__[field].get('object', False)
192 alarm_obj = self.pool.get('basic.calendar.alarm')
193 vevent = alarm_obj.export_ical(cr, uid, model, \
194 data[map_field][0], vevent, context=context)
195 elif data[map_field]:
196 if map_type == "text":
197 vevent.add(field).value = str(data[map_field])
198 elif map_type == 'datetime' and data[map_field]:
199 if field in ('exdate'):
200 vevent.add(field).value = [parser.parse(data[map_field])]
202 vevent.add(field).value = parser.parse(data[map_field])
203 elif map_type == "timedelta":
204 vevent.add(field).value = timedelta(hours=data[map_field])
205 elif map_type == "many2one":
206 vevent.add(field).value = [data.get(map_field)[1]]
207 if self.__attribute__.get(field).has_key('mapping'):
208 for key1, val1 in self.ical_get(field, 'mapping').items():
209 if val1 == data[map_field]:
210 vevent.add(field).value = key1
213 def import_ical(self, cr, uid, ical_data):
214 parsedCal = vobject.readOne(ical_data)
217 for child in parsedCal.getChildren():
218 for cal_data in child.getChildren():
219 if cal_data.name.lower() == 'attendee':
220 attendee = self.pool.get('basic.calendar.attendee')
221 att_data.append(attendee.import_ical(cr, uid, cal_data))
222 self.ical_set(cal_data.name.lower(), att_data, 'value')
224 if cal_data.name.lower() == 'valarm':
225 alarm = self.pool.get('basic.calendar.alarm')
226 vals = alarm.import_ical(cr, uid, cal_data)
227 self.ical_set(cal_data.name.lower(), vals, 'value')
229 if cal_data.name.lower() in self.__attribute__:
230 self.ical_set(cal_data.name.lower(), cal_data.value, 'value')
231 if child.name.lower() in ('vevent', 'vtodo'):
232 vals = map_data(cr, uid, self)
236 if vals: res.append(vals)
237 self.ical_reset('value')
241 class Calendar(CalDAV, osv.osv_memory):
242 _name = 'basic.calendar'
244 'prodid': None, # Use: R-1, Type: TEXT, Specifies the identifier for the product that created the iCalendar object.
245 'version': None, # Use: R-1, Type: TEXT, Specifies the identifier corresponding to the highest version number
246 # or the minimum and maximum range of the iCalendar specification
247 # that is required in order to interpret the iCalendar object.
248 'calscale': None, # Use: O-1, Type: TEXT, Defines the calendar scale used for the calendar information specified in the iCalendar object.
249 'method': None, # Use: O-1, Type: TEXT, Defines the iCalendar object method associated with the calendar object.
250 'vevent': None, # Use: O-n, Type: Collection of Event class
251 'vtodo': None, # Use: O-n, Type: Collection of ToDo class
252 'vjournal': None, # Use: O-n, Type: Collection of Journal class
253 'vfreebusy': None, # Use: O-n, Type: Collection of FreeBusy class
254 'vtimezone': None, # Use: O-n, Type: Collection of Timezone class
260 class basic_calendar_fields_type(osv.osv):
261 _name = 'basic.calendar.fields.type'
262 _description = 'Calendar fields type'
265 'name': fields.char('Name', size=64),
266 'object_id': fields.many2one('ir.model', 'Object'),
269 basic_calendar_fields_type()
271 class basic_calendar_fields(osv.osv):
272 _name = 'basic.calendar.fields'
273 _description = 'Calendar fields'
274 _rec_name = 'attribute_id'
277 'attribute_id': fields.many2one('basic.calendar.fields.type', \
278 'Attribute', size=64),
279 'attribute': fields.related('attribute_id', 'name', size=64, \
280 type='char', string='Attribute Name', \
282 'object_id': fields.related('attribute_id', 'object_id', \
283 type='many2one', relation='ir.model', store=True,\
285 'field_id': fields.many2one('ir.model.fields', 'OpenObject Field'),
286 'info': fields.text('Other info'),
287 'value': fields.text('Value', help="For some attribute that \
288 have some default value"),
291 basic_calendar_fields()
293 class Event(CalDAV, osv.osv_memory):
294 _name = 'basic.calendar.event'
296 'class': None, # Use: O-1, Type: TEXT, Defines the access classification for a calendar component like "PUBLIC" / "PRIVATE" / "CONFIDENTIAL"
297 '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.
298 'description': None, # Use: O-1, Type: TEXT, Provides a more complete description of the calendar component, than that provided by the "SUMMARY" property.
299 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
300 'geo': None, # Use: O-1, Type: FLOAT, Specifies information related to the global position for the activity specified by a calendar component.
301 '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.
302 'location': None, # Use: O-1, Type: TEXT Defines the intended venue for the activity defined by a calendar component.
303 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
304 'priority': None, # Use: O-1, Type: INTEGER, Defines the relative priority for a calendar component.
305 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
306 'seq': None, # Use: O-1, Type: INTEGER, Defines the revision sequence number of the calendar component within a sequence of revision.
307 'status': None, # Use: O-1, Type: TEXT, Defines the overall status or confirmation for the calendar component.
308 'summary': None, # Use: O-1, Type: TEXT, Defines a short summary or subject for the calendar component.
309 'transp': None, # Use: O-1, Type: TEXT, Defines whether an event is transparent or not to busy time searches.
310 'uid': None, # Use: O-1, Type: TEXT, Defines the persistent, globally unique identifier for the calendar component.
311 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
313 'attach': None, # Use: O-n, Type: BINARY, Provides the capability to associate a document object with a calendar component.
314 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
315 'categories': None, # Use: O-n, Type: TEXT, Defines the categories for a calendar component.
316 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
317 'contact': None, # Use: O-n, Type: TEXT, Used to represent contact information or alternately a reference to contact information associated with the calendar component.
318 'exdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/time exceptions for a recurring calendar component.
319 'exrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for an exception to a recurrence set.
321 'related': None, # Use: O-n, Specify the relationship of the alarm trigger with respect to the start or end of the calendar component.
322 # like A trigger set 5 minutes after the end of the event or to-do.---> TRIGGER;related=END:PT5M
323 '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
324 'rdate': None, # Use: O-n, Type: DATE-TIME, Defines the list of date/times for a recurrence set.
325 'rrule': None, # Use: O-n, Type: RECUR, Defines a rule or repeating pattern for recurring events, to-dos, or time zone definitions.
327 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
328 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
330 def export_ical(self, cr, uid, datas, vobj='vevent', context={}):
331 return super(Event, self).export_ical(cr, uid, datas, 'vevent', context=context)
335 class ToDo(CalDAV, osv.osv_memory):
336 _name = 'basic.calendar.todo'
373 def export_ical(self, cr, uid, datas, vobj='vevent', context={}):
374 return super(ToDo, self).export_ical(cr, uid, datas, 'vtodo', context=context)
378 class Journal(CalDAV):
382 class FreeBusy(CalDAV):
384 'contact': None, # Use: O-1, Type: Text, Represent contact information or alternately a reference to contact information associated with the calendar component.
385 'dtstart': None, # Use: O-1, Type: DATE-TIME, Specifies when the calendar component begins.
386 'dtend': None, # Use: O-1, Type: DATE-TIME, Specifies the date and time that a calendar component ends.
387 'duration': None, # Use: O-1, Type: DURATION, Specifies a positive duration of time.
388 'dtstamp': None, # Use: O-1, Type: DATE-TIME, Indicates the date/time that the instance of the iCalendar object was created.
389 'organizer': None, # Use: O-1, Type: CAL-ADDRESS, Defines the organizer for a calendar component.
390 'uid': None, # Use: O-1, Type: Text, Defines the persistent, globally unique identifier for the calendar component.
391 'url': None, # Use: O-1, Type: URL, Defines a Uniform Resource Locator (URL) associated with the iCalendar object.
392 'attendee': None, # Use: O-n, Type: CAL-ADDRESS, Defines an "Attendee" within a calendar component.
393 'comment': None, # Use: O-n, Type: TEXT, Specifies non-processing information intended to provide a comment to the calendar user.
394 'freebusy': None, # Use: O-n, Type: PERIOD, Defines one or more free or busy time intervals.
400 class Timezone(CalDAV):
402 'tzid': None, # Use: R-1, Type: Text, Specifies the text value that uniquely identifies the "VTIMEZONE" calendar component.
403 '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.
404 '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.
405 'standardc': {'tzprop': None}, # Use: R-1,
406 'daylightc': {'tzprop': None}, # Use: R-1,
407 'x-prop': None, # Use: O-n, Type: Text,
411 class Alarm(CalDAV, osv.osv_memory):
412 _name = 'basic.calendar.alarm'
414 'action': None, # Use: R-1, Type: Text, defines the action to be invoked when an alarm is triggered LIKE "AUDIO" / "DISPLAY" / "EMAIL" / "PROCEDURE"
415 '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
416 'summary': None, # Use: R-1, Type: Text Which contains the text to be used as the message subject. Use for EMAIL
417 '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
418 '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
419 '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
420 '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
421 '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.
425 def export_ical(self, cr, uid, model, alarm_id, vevent, context={}):
426 valarm = vevent.add('valarm')
427 alarm_object = self.pool.get(model)
428 alarm_data = alarm_object.read(cr, uid, alarm_id, [])
430 # Compute trigger data
431 interval = alarm_data['trigger_interval']
432 occurs = alarm_data['trigger_occurs']
433 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
434 or -(alarm_data['trigger_duration'])
435 related = alarm_data['trigger_related']
436 trigger = valarm.add('TRIGGER')
437 trigger.params['related'] = [related.upper()]
438 if interval == 'days':
439 delta = timedelta(days=duration)
440 if interval == 'hours':
441 delta = timedelta(hours=duration)
442 if interval == 'minutes':
443 delta = timedelta(minutes=duration)
444 trigger.value = delta
446 # Compute other details
447 valarm.add('DESCRIPTION').value = alarm_data['name']
448 valarm.add('ACTION').value = alarm_data['action']
451 def import_ical(self, cr, uid, ical_data):
452 for child in ical_data.getChildren():
453 if child.name.lower() == 'trigger':
454 seconds = child.value.seconds
455 days = child.value.days
456 diff = (days * 86400) + seconds
461 related = days > 0 and 'after' or 'before'
462 elif (abs(diff) / 3600) == 0:
463 duration = abs(diff / 60)
465 related = days >= 0 and 'after' or 'before'
467 duration = abs(diff / 3600)
469 related = days >= 0 and 'after' or 'before'
470 self.ical_set('trigger_interval', interval, 'value')
471 self.ical_set('trigger_duration', duration, 'value')
472 self.ical_set('trigger_occurs', related.lower(), 'value')
474 if child.params.get('related'):
475 self.ical_set('trigger_related', child.params.get('related')[0].lower(), 'value')
477 self.ical_set(child.name.lower(), child.value.lower(), 'value')
478 vals = map_data(cr, uid, self)
483 class Attendee(CalDAV, osv.osv_memory):
484 _name = 'basic.calendar.attendee'
486 'cutype': None, # Use: 0-1 Specify the type of calendar user specified by the property like "INDIVIDUAL"/"GROUP"/"RESOURCE"/"ROOM"/"UNKNOWN".
487 'member': None, # Use: 0-1 Specify the group or list membership of the calendar user specified by the property.
488 '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"
489 '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".
490 '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.
491 'delegated-to': None, # Use: 0-1 Specify the calendar users to whom the calendar user specified by the property has delegated participation.
492 'delegated-from': None, # Use: 0-1 Specify the calendar users that have delegated their participation to the calendar user specified by the property.
493 'sent-by': None, # Use: 0-1 Specify the calendar user that is acting on behalf of the calendar user specified by the property.
494 'cn': None, # Use: 0-1 Specify the common name to be associated with the calendar user specified by the property.
495 'dir': None, # Use: 0-1 Specify reference to a directory entry associated with the calendar user specified by the property.
496 'language': None, # Use: 0-1 Specify the language for text values in a property or property parameter.
499 def import_ical(self, cr, uid, ical_data):
500 for para in ical_data.params:
501 if para.lower() == 'cn':
502 self.ical_set(para.lower(), ical_data.params[para][0]+':'+ \
503 ical_data.value, 'value')
505 self.ical_set(para.lower(), ical_data.params[para][0].lower(), 'value')
506 if not ical_data.params.get('CN'):
507 self.ical_set('cn', ical_data.value, 'value')
508 vals = map_data(cr, uid, self)
511 def export_ical(self, cr, uid, model, attendee_ids, vevent, context={}):
512 attendee_object = self.pool.get(model)
513 for attendee in attendee_object.read(cr, uid, attendee_ids, []):
514 attendee_add = vevent.add('attendee')
515 for a_key, a_val in attendee_object.__attribute__.items():
516 if attendee[a_val['field']] and a_val['field'] != 'cn':
517 if a_val['type'] == 'text':
518 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
519 elif a_val['type'] == 'boolean':
520 attendee_add.params[a_key] = [str(attendee[a_val['field']])]
521 if a_val['field'] == 'cn':
522 cn_val = [str(attendee[a_val['field']])]
523 attendee_add.params['CN'] = cn_val
529 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: