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, date
23 from dateutil import parser
24 from dateutil import rrule
25 from dateutil.relativedelta import relativedelta
26 from osv import fields, osv
27 from service import web_services
28 from tools.translate import _
35 1: "January", 2: "February", 3: "March", 4: "April", \
36 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", \
37 10: "October", 11: "November", 12: "December"
40 def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None):
42 Get recurrent dates based on Rule string considering exdate and start date
43 @param rrulestring: Rulestring
44 @param exdate: List of exception dates for rrule
45 @param startdate: Startdate for computing recurrent dates
46 @return: List of Recurrent dates
49 val = parser.parse(''.join((re.compile('\d')).findall(date)))
53 startdate = datetime.now()
58 rset1 = rrule.rrulestr(str(rrulestring), dtstart=startdate, forceset=True)
60 datetime_obj = todate(date)
61 rset1._exdate.append(datetime_obj)
64 rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate))
68 def base_calendar_id2real_id(base_calendar_id=None, with_date=False):
70 This function converts virtual event id into real id of actual event
71 @param base_calendar_id: Id of calendar
72 @param with_date: If value passed to this param it will return dates based on value of withdate + base_calendar_id
75 if base_calendar_id and isinstance(base_calendar_id, (str, unicode)):
76 res = base_calendar_id.split('-')
81 real_date = time.strftime("%Y-%m-%d %H:%M:%S", \
82 time.strptime(res[1], "%Y%m%d%H%M%S"))
83 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
84 end = start + timedelta(hours=with_date)
85 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
88 return base_calendar_id and int(base_calendar_id) or base_calendar_id
90 def real_id2base_calendar_id(real_id, recurrent_date):
92 Convert real id of record into virtual id using recurrent_date
93 e.g. real id is 1 and recurrent_date is 01-12-2009 10:00:00 then it will return
95 @return: real id with recurrent date.
98 if real_id and recurrent_date:
99 recurrent_date = time.strftime("%Y%m%d%H%M%S", \
100 time.strptime(recurrent_date, "%Y-%m-%d %H:%M:%S"))
101 return '%d-%s' % (real_id, recurrent_date)
104 def _links_get(self, cr, uid, context=None):
107 @param cr: the current row, from the database cursor,
108 @param uid: the current user’s ID for security checks,
109 @param context: A standard dictionary for contextual values
110 @return: list of dictionary which contain object and name and id.
112 obj = self.pool.get('res.request.link')
113 ids = obj.search(cr, uid, [])
114 res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
115 return [(r['object'], r['name']) for r in res]
117 html_invitation = """
120 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
121 <title>%(name)s</title>
124 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
125 style="font-family: Arial, Sans-serif; font-size: 14">
127 <td width="100%%">Hello,</td>
130 <td width="100%%">You are invited for <i>%(company)s</i> Event.</td>
133 <td width="100%%">Below are the details of event:</td>
137 <table cellspacing="0" cellpadding="5" border="0" summary=""
138 style="width: 90%%; font-family: Arial, Sans-serif; border: 1px Solid #ccc; background-color: #f6f6f6">
139 <tr valign="center" align="center">
140 <td bgcolor="DFDFDF">
146 <table cellpadding="8" cellspacing="0" border="0"
147 style="font-size: 14" summary="Eventdetails" bgcolor="f6f6f6"
151 <div><b>Start Date</b></div>
154 <td>%(start_date)s</td>
156 <div><b>End Date</b></div>
159 <td width="25%%">%(end_date)s</td>
162 <td><b>Description</b></td>
164 <td colspan="3">%(description)s</td>
168 <div><b>Location</b></div>
171 <td colspan="3">%(location)s</td>
175 <div><b>Event Attendees</b></div>
180 <div>%(attendees)s</div>
188 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
189 style="font-family: Arial, Sans-serif; font-size: 14">
191 <td width="100%%">From:</td>
194 <td width="100%%">%(user)s</td>
197 <td width="100%%">-<font color="a7a7a7">-------------------------</font></td>
200 <td width="100%%"> <font color="a7a7a7">%(sign)s</font></td>
207 class calendar_attendee(osv.osv):
209 Calendar Attendee Information
211 _name = 'calendar.attendee'
212 _description = 'Attendee information'
217 def _get_address(self, name=None, email=None):
219 Gives email information in ical CAL-ADDRESS type format
220 @param name: Name for CAL-ADDRESS value
221 @param email: Email address for CAL-ADDRESS value
225 return (name or '') + (email and ('MAILTO:' + email) or '')
227 def _compute_data(self, cr, uid, ids, name, arg, context=None):
229 Compute data on function fields for attendee values .
230 @param cr: the current row, from the database cursor,
231 @param uid: the current user’s ID for security checks,
232 @param ids: List of calendar attendee’s IDs.
233 @param name: name of field.
234 @param context: A standard dictionary for contextual values
235 @return: Dictionary of form {id: {'field Name': value'}}.
239 for attdata in self.browse(cr, uid, ids, context=context):
242 if name == 'sent_by':
243 if not attdata.sent_by_uid:
244 result[id][name] = ''
247 result[id][name] = self._get_address(attdata.sent_by_uid.name, \
248 attdata.sent_by_uid.user_email)
252 result[id][name] = attdata.user_id.name
253 elif attdata.partner_id:
254 result[id][name] = attdata.partner_id.name or False
256 result[id][name] = attdata.email or ''
258 if name == 'delegated_to':
260 for child in attdata.child_ids:
262 todata.append('MAILTO:' + child.email)
263 result[id][name] = ', '.join(todata)
265 if name == 'delegated_from':
267 for parent in attdata.parent_ids:
269 fromdata.append('MAILTO:' + parent.email)
270 result[id][name] = ', '.join(fromdata)
272 if name == 'event_date':
274 result[id][name] = attdata.ref.date
276 result[id][name] = False
278 if name == 'event_end_date':
280 result[id][name] = attdata.ref.date_deadline
282 result[id][name] = False
284 if name == 'sent_by_uid':
286 result[id][name] = (attdata.ref.user_id.id, attdata.ref.user_id.name)
288 result[id][name] = uid
290 if name == 'language':
291 user_obj = self.pool.get('res.users')
292 lang = user_obj.read(cr, uid, uid, ['context_lang'], context=context)['context_lang']
293 result[id][name] = lang.replace('_', '-')
297 def _links_get(self, cr, uid, context=None):
299 Get request link for ref field in calendar attendee.
300 @param cr: the current row, from the database cursor,
301 @param uid: the current user’s ID for security checks,
302 @param context: A standard dictionary for contextual values
303 @return: list of dictionary which contain object and name and id.
305 obj = self.pool.get('res.request.link')
306 ids = obj.search(cr, uid, [])
307 res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
308 return [(r['object'], r['name']) for r in res]
310 def _lang_get(self, cr, uid, context=None):
312 Get language for language selection field.
313 @param cr: the current row, from the database cursor,
314 @param uid: the current user’s ID for security checks,
315 @param context: A standard dictionary for contextual values
316 @return: list of dictionary which contain code and name and id.
318 obj = self.pool.get('res.lang')
319 ids = obj.search(cr, uid, [])
320 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
321 res = [((r['code']).replace('_', '-').lower(), r['name']) for r in res]
325 'cutype': fields.selection([('individual', 'Individual'), \
326 ('group', 'Group'), ('resource', 'Resource'), \
327 ('room', 'Room'), ('unknown', 'Unknown') ], \
328 'Invite Type', help="Specify the type of Invitation"),
329 'member': fields.char('Member', size=124,
330 help="Indicate the groups that the attendee belongs to"),
331 'role': fields.selection([('req-participant', 'Participation required'), \
332 ('chair', 'Chair Person'), \
333 ('opt-participant', 'Optional Participation'), \
334 ('non-participant', 'For information Purpose')], 'Role', \
335 help='Participation role for the calendar user'),
336 'state': fields.selection([('tentative', 'Tentative'),
337 ('needs-action', 'Needs Action'),
338 ('accepted', 'Accepted'),
339 ('declined', 'Declined'),
340 ('delegated', 'Delegated')], 'State', readonly=True, \
341 help="Status of the attendee's participation"),
342 'rsvp': fields.boolean('Required Reply?',
343 help="Indicats whether the favor of a reply is requested"),
344 'delegated_to': fields.function(_compute_data, \
345 string='Delegated To', type="char", size=124, store=True, \
346 multi='delegated_to', help="The users that the original \
347 request was delegated to"),
348 'delegated_from': fields.function(_compute_data, string=\
349 'Delegated From', type="char", store=True, size=124, multi='delegated_from'),
350 'parent_ids': fields.many2many('calendar.attendee', 'calendar_attendee_parent_rel', \
351 'attendee_id', 'parent_id', 'Delegrated From'),
352 'child_ids': fields.many2many('calendar.attendee', 'calendar_attendee_child_rel', \
353 'attendee_id', 'child_id', 'Delegrated To'),
354 'sent_by': fields.function(_compute_data, string='Sent By', \
355 type="char", multi='sent_by', store=True, size=124, \
356 help="Specify the user that is acting on behalf of the calendar user"),
357 'sent_by_uid': fields.function(_compute_data, string='Sent By User', \
358 type="many2one", relation="res.users", multi='sent_by_uid'),
359 'cn': fields.function(_compute_data, string='Common name', \
360 type="char", size=124, multi='cn', store=True),
361 'dir': fields.char('URI Reference', size=124, help="Reference to the URI\
362 that points to the directory information corresponding to the attendee."),
363 'language': fields.function(_compute_data, string='Language', \
364 type="selection", selection=_lang_get, multi='language', \
365 store=True, help="To specify the language for text values in a\
366 property or property parameter."),
367 'user_id': fields.many2one('res.users', 'User'),
368 'partner_id': fields.many2one('res.partner', 'Contact'),
369 'email': fields.char('Email', size=124, help="Email of Invited Person"),
370 'event_date': fields.function(_compute_data, string='Event Date', \
371 type="datetime", multi='event_date'),
372 'event_end_date': fields.function(_compute_data, \
373 string='Event End Date', type="datetime", \
374 multi='event_end_date'),
375 'ref': fields.reference('Event Ref', selection=_links_get, size=128),
376 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
380 'state': 'needs-action',
381 'role': 'req-participant',
383 'cutype': 'individual',
386 def copy(self, cr, uid, id, default=None, context=None):
387 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
389 def get_ics_file(self, cr, uid, event_obj, context=None):
391 Returns iCalendar file for the event invitation
392 @param self: The object pointer
393 @param cr: the current row, from the database cursor,
394 @param uid: the current user’s ID for security checks,
395 @param event_obj: Event object (browse record)
396 @param context: A standard dictionary for contextual values
397 @return: .ics file content
400 def ics_datetime(idate, short=False):
402 if short or len(idate)<=10:
403 return date.fromtimestamp(time.mktime(time.strptime(idate, '%Y-%m-%d')))
405 return datetime.strptime(idate, '%Y-%m-%d %H:%M:%S')
409 # FIXME: why isn't this in CalDAV?
413 cal = vobject.iCalendar()
414 event = cal.add('vevent')
415 if not event_obj.date_deadline or not event_obj.date:
416 raise osv.except_osv(_('Warning !'),_("Couldn't Invite because date is not specified!"))
417 event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S'))
418 event.add('dtstart').value = ics_datetime(event_obj.date)
419 event.add('dtend').value = ics_datetime(event_obj.date_deadline)
420 event.add('summary').value = event_obj.name
421 if event_obj.description:
422 event.add('description').value = event_obj.description
423 if event_obj.location:
424 event.add('location').value = event_obj.location
426 event.add('rrule').value = event_obj.rrule
427 if event_obj.organizer:
428 event_org = event.add('organizer')
429 event_org.params['CN'] = [event_obj.organizer]
430 event_org.value = 'MAILTO:' + (event_obj.organizer)
431 elif event_obj.user_id or event_obj.organizer_id:
432 event_org = event.add('organizer')
433 organizer = event_obj.organizer_id
435 organizer = event_obj.user_id
436 event_org.params['CN'] = [organizer.name]
437 event_org.value = 'MAILTO:' + (organizer.user_email or organizer.name)
439 if event_obj.alarm_id:
440 # computes alarm data
441 valarm = event.add('valarm')
442 alarm_object = self.pool.get('res.alarm')
443 alarm_data = alarm_object.read(cr, uid, event_obj.alarm_id.id, context=context)
444 # Compute trigger data
445 interval = alarm_data['trigger_interval']
446 occurs = alarm_data['trigger_occurs']
447 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
448 or -(alarm_data['trigger_duration'])
449 related = alarm_data['trigger_related']
450 trigger = valarm.add('TRIGGER')
451 trigger.params['related'] = [related.upper()]
452 if interval == 'days':
453 delta = timedelta(days=duration)
454 if interval == 'hours':
455 delta = timedelta(hours=duration)
456 if interval == 'minutes':
457 delta = timedelta(minutes=duration)
458 trigger.value = delta
459 # Compute other details
460 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
462 for attendee in event_obj.attendee_ids:
463 attendee_add = event.add('attendee')
464 attendee_add.params['CUTYPE'] = [str(attendee.cutype)]
465 attendee_add.params['ROLE'] = [str(attendee.role)]
466 attendee_add.params['RSVP'] = [str(attendee.rsvp)]
467 attendee_add.value = 'MAILTO:' + (attendee.email or '')
468 res = cal.serialize()
471 def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
473 Send mail for event invitation to event attendees.
474 @param cr: the current row, from the database cursor,
475 @param uid: the current user’s ID for security checks,
476 @param ids: List of attendee’s IDs.
477 @param email_from: Email address for user sending the mail
478 @param context: A standard dictionary for contextual values
484 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
485 mail_message = self.pool.get('mail.message')
486 for att in self.browse(cr, uid, ids, context=context):
487 sign = att.sent_by_uid and att.sent_by_uid.signature or ''
488 sign = '<br>'.join(sign and sign.split('\n') or [])
493 other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
495 for att2 in self.browse(cr, uid, other_invitation_ids):
496 att_infos.append(((att2.user_id and att2.user_id.name) or \
497 (att2.partner_id and att2.partner_id.name) or \
498 att2.email) + ' - Status: ' + att2.state.title())
499 body_vals = {'name': res_obj.name,
500 'start_date': res_obj.date,
501 'end_date': res_obj.date_deadline or False,
502 'description': res_obj.description or '-',
503 'location': res_obj.location or '-',
504 'attendees': '<br>'.join(att_infos),
505 'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
509 body = html_invitation % body_vals
510 if mail_to and email_from:
511 attach = self.get_ics_file(cr, uid, res_obj, context=context)
512 mail_message.schedule_with_attach(cr, uid,
517 attachments=attach and {'invitation.ics': attach} or None,
524 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
526 Make entry on email and availbility on change of user_id field.
527 @param cr: the current row, from the database cursor,
528 @param uid: the current user’s ID for security checks,
529 @param ids: List of calendar attendee’s IDs.
530 @param user_id: Changed value of User id
531 @return: dictionary of value. which put value in email and availability fields.
535 return {'value': {'email': ''}}
536 usr_obj = self.pool.get('res.users')
537 user = usr_obj.browse(cr, uid, user_id, *args)
538 return {'value': {'email': user.user_email, 'availability':user.availability}}
540 def do_tentative(self, cr, uid, ids, context=None, *args):
541 """ Makes event invitation as Tentative
542 @param self: The object pointer
543 @param cr: the current row, from the database cursor,
544 @param uid: the current user’s ID for security checks,
545 @param ids: List of calendar attendee’s IDs
546 @param *args: Get Tupple value
547 @param context: A standard dictionary for contextual values
549 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
551 def do_accept(self, cr, uid, ids, context=None, *args):
553 Update state of invitation as Accepted and
554 if the invited user is other then event user it will make a copy of this event for invited user
555 @param cr: the current row, from the database cursor,
556 @param uid: the current user’s ID for security checks,
557 @param ids: List of calendar attendee’s IDs.
558 @param context: A standard dictionary for contextual values
564 for vals in self.browse(cr, uid, ids, context=context):
565 if vals.ref and vals.ref.user_id:
566 mod_obj = self.pool.get(vals.ref._name)
567 defaults = {'user_id': vals.user_id.id, 'organizer_id': vals.ref.user_id.id}
568 mod_obj.copy(cr, uid, vals.ref.id, default=defaults, context=context)
569 self.write(cr, uid, vals.id, {'state': 'accepted'}, context)
573 def do_decline(self, cr, uid, ids, context=None, *args):
574 """ Marks event invitation as Declined
575 @param self: The object pointer
576 @param cr: the current row, from the database cursor,
577 @param uid: the current user’s ID for security checks,
578 @param ids: List of calendar attendee’s IDs
579 @param *args: Get Tupple value
580 @param context: A standard dictionary for contextual values """
583 return self.write(cr, uid, ids, {'state': 'declined'}, context)
585 def create(self, cr, uid, vals, context=None):
586 """ Overrides orm create method.
587 @param self: The object pointer
588 @param cr: the current row, from the database cursor,
589 @param uid: the current user’s ID for security checks,
590 @param vals: Get Values
591 @param context: A standard dictionary for contextual values """
595 if not vals.get("email") and vals.get("cn"):
596 cnval = vals.get("cn").split(':')
597 email = filter(lambda x:x.__contains__('@'), cnval)
598 vals['email'] = email and email[0] or ''
599 vals['cn'] = vals.get("cn")
600 res = super(calendar_attendee, self).create(cr, uid, vals, context)
604 class res_alarm(osv.osv):
605 """Resource Alarm """
607 _description = 'Basic Alarm Information'
610 'name':fields.char('Name', size=256, required=True),
611 'trigger_occurs': fields.selection([('before', 'Before'), \
612 ('after', 'After')], \
613 'Triggers', required=True),
614 'trigger_interval': fields.selection([('minutes', 'Minutes'), \
615 ('hours', 'Hours'), \
616 ('days', 'Days')], 'Interval', \
618 'trigger_duration': fields.integer('Duration', required=True),
619 'trigger_related': fields.selection([('start', 'The event starts'), \
620 ('end', 'The event ends')], \
621 'Related to', required=True),
622 'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \
623 are both optional, but if one occurs, so MUST the other"""),
624 'repeat': fields.integer('Repeat'),
625 'active': fields.boolean('Active', help="If the active field is set to \
626 true, it will allow you to hide the event alarm information without removing it.")
629 'trigger_interval': 'minutes',
630 'trigger_duration': 5,
631 'trigger_occurs': 'before',
632 'trigger_related': 'start',
636 def do_alarm_create(self, cr, uid, ids, model, date, context=None):
638 Create Alarm for event.
639 @param cr: the current row, from the database cursor,
640 @param uid: the current user’s ID for security checks,
641 @param ids: List of res alarm’s IDs.
642 @param model: Model name.
643 @param date: Event date
644 @param context: A standard dictionary for contextual values
649 alarm_obj = self.pool.get('calendar.alarm')
650 res_alarm_obj = self.pool.get('res.alarm')
651 ir_obj = self.pool.get('ir.model')
652 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
654 model_obj = self.pool.get(model)
655 for data in model_obj.browse(cr, uid, ids, context=context):
657 basic_alarm = data.alarm_id
658 cal_alarm = data.base_calendar_alarm_id
659 if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm):
661 # Find for existing res.alarm
662 duration = cal_alarm.trigger_duration
663 interval = cal_alarm.trigger_interval
664 occurs = cal_alarm.trigger_occurs
665 related = cal_alarm.trigger_related
666 domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)]
667 alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context)
670 'trigger_duration': duration,
671 'trigger_interval': interval,
672 'trigger_occurs': occurs,
673 'trigger_related': related,
674 'name': str(duration) + ' ' + str(interval) + ' ' + str(occurs)
676 new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context)
678 new_res_alarm = alarm_ids[0]
679 cr.execute('UPDATE %s ' % model_obj._table + \
680 ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \
682 (cal_alarm.id, new_res_alarm, data.id))
684 self.do_alarm_unlink(cr, uid, [data.id], model)
688 'description': data.description,
690 'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))],
691 'trigger_related': basic_alarm.trigger_related,
692 'trigger_duration': basic_alarm.trigger_duration,
693 'trigger_occurs': basic_alarm.trigger_occurs,
694 'trigger_interval': basic_alarm.trigger_interval,
695 'duration': basic_alarm.duration,
696 'repeat': basic_alarm.repeat,
698 'event_date': data[date],
700 'model_id': model_id,
703 alarm_id = alarm_obj.create(cr, uid, vals)
704 cr.execute('UPDATE %s ' % model_obj._table + \
705 ' SET base_calendar_alarm_id=%s, alarm_id=%s '
707 ( alarm_id, basic_alarm.id, data.id) )
710 def do_alarm_unlink(self, cr, uid, ids, model, context=None):
712 Delete alarm specified in ids
713 @param cr: the current row, from the database cursor,
714 @param uid: the current user’s ID for security checks,
715 @param ids: List of res alarm’s IDs.
716 @param model: Model name for which alarm is to be cleared.
721 alarm_obj = self.pool.get('calendar.alarm')
722 ir_obj = self.pool.get('ir.model')
723 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
724 model_obj = self.pool.get(model)
725 for datas in model_obj.browse(cr, uid, ids, context=context):
726 alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', datas.id)])
728 alarm_obj.unlink(cr, uid, alarm_ids)
729 cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\
730 where id=%%s' % model_obj._table,(datas.id,))
735 class calendar_alarm(osv.osv):
736 _name = 'calendar.alarm'
737 _description = 'Event alarm information'
738 _inherit = 'res.alarm'
742 'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'),
743 'name': fields.char('Summary', size=124, help="""Contains the text to be \
744 used as the message subject for email \
745 or contains the text to be used for display"""),
746 'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \
747 ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \
748 required=True, help="Defines the action to be invoked when an alarm is triggered"),
749 'description': fields.text('Description', help='Provides a more complete \
750 description of the calendar component, than that \
751 provided by the "SUMMARY" property'),
752 'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \
753 'alarm_id', 'attendee_id', 'Attendees', readonly=True),
754 'attach': fields.binary('Attachment', help="""* Points to a sound resource,\
755 which is rendered when the alarm is triggered for audio,
756 * File which is intended to be sent as message attachments for email,
757 * Points to a procedure resource, which is invoked when\
758 the alarm is triggered for procedure."""),
759 'res_id': fields.integer('Resource ID'),
760 'model_id': fields.many2one('ir.model', 'Model'),
761 'user_id': fields.many2one('res.users', 'Owner'),
762 'event_date': fields.datetime('Event Date'),
763 'event_end_date': fields.datetime('Event End Date'),
764 'trigger_date': fields.datetime('Trigger Date', readonly="True"),
765 'state':fields.selection([
770 ], 'State', select=True, readonly=True),
778 def create(self, cr, uid, vals, context=None):
780 Overrides orm create method.
781 @param self: The object pointer
782 @param cr: the current row, from the database cursor,
783 @param vals: dictionary of fields value.{‘name_of_the_field’: value, ...}
784 @param context: A standard dictionary for contextual values
785 @return: new record id for calendar_alarm.
789 event_date = vals.get('event_date', False)
791 dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S")
792 if vals['trigger_interval'] == 'days':
793 delta = timedelta(days=vals['trigger_duration'])
794 if vals['trigger_interval'] == 'hours':
795 delta = timedelta(hours=vals['trigger_duration'])
796 if vals['trigger_interval'] == 'minutes':
797 delta = timedelta(minutes=vals['trigger_duration'])
798 trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta)
799 vals['trigger_date'] = trigger_date
800 res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
803 def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
805 """Scheduler for event reminder
806 @param self: The object pointer
807 @param cr: the current row, from the database cursor,
808 @param uid: the current user’s ID for security checks,
809 @param ids: List of calendar alarm’s IDs.
810 @param use_new_cursor: False or the dbname
811 @param context: A standard dictionary for contextual values
815 mail_message = self.pool.get('mail.message')
816 current_datetime = datetime.now()
817 request_obj = self.pool.get('res.request')
818 alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
822 for alarm in self.browse(cr, uid, alarm_ids, context=context):
823 next_trigger_date = None
825 model_obj = self.pool.get(alarm.model_id.model)
826 res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
830 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
831 recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.exdate, event_date, res_obj.exrule)
833 trigger_interval = alarm.trigger_interval
834 if trigger_interval == 'days':
835 delta = timedelta(days=alarm.trigger_duration)
836 if trigger_interval == 'hours':
837 delta = timedelta(hours=alarm.trigger_duration)
838 if trigger_interval == 'minutes':
839 delta = timedelta(minutes=alarm.trigger_duration)
840 delta = alarm.trigger_occurs == 'after' and delta or -delta
842 for rdate in recurrent_dates:
843 if rdate + delta > current_datetime:
845 if rdate + delta <= current_datetime:
846 re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
847 rest_dates = recurrent_dates[len(re_dates):]
848 next_trigger_date = rest_dates and rest_dates[0] or None
851 re_dates = [alarm.trigger_date]
853 for r_date in re_dates:
854 ref = alarm.model_id.model + ',' + str(alarm.res_id)
856 # search for alreay sent requests
857 if request_obj.search(cr, uid, [('trigger_date', '=', r_date), ('ref_doc1', '=', ref)], context=context):
860 if alarm.action == 'display':
863 'act_from': alarm.user_id.id,
864 'act_to': alarm.user_id.id,
865 'body': alarm.description,
866 'trigger_date': r_date,
869 request_id = request_obj.create(cr, uid, value)
870 request_ids = [request_id]
871 for attendee in res_obj.attendee_ids:
873 value['act_to'] = attendee.user_id.id
874 request_id = request_obj.create(cr, uid, value)
875 request_ids.append(request_id)
876 request_obj.request_send(cr, uid, request_ids)
878 if alarm.action == 'email':
879 sub = '[Openobject Reminder] %s' % (alarm.name)
891 """ % (alarm.name, alarm.trigger_date, alarm.description, \
892 alarm.user_id.name, alarm.user_id.signature)
893 mail_to = [alarm.user_id.user_email]
894 for att in alarm.attendee_ids:
895 mail_to.append(att.user_id.user_email)
897 mail_message.schedule_with_attach(cr, uid,
898 tools.config.get('email_from', False),
904 if next_trigger_date:
905 update_vals.update({'trigger_date': next_trigger_date})
907 update_vals.update({'state': 'done'})
908 self.write(cr, uid, [alarm.id], update_vals)
914 class calendar_event(osv.osv):
915 _name = "calendar.event"
916 _description = "Calendar Event"
919 def _tz_get(self, cr, uid, context=None):
920 return [(x.lower(), x) for x in pytz.all_timezones]
922 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
923 """Returns duration and/or end date based on values passed
924 @param self: The object pointer
925 @param cr: the current row, from the database cursor,
926 @param uid: the current user’s ID for security checks,
927 @param ids: List of calendar event’s IDs.
928 @param start_date: Starting date
929 @param duration: Duration between start date and end date
930 @param end_date: Ending Datee
931 @param context: A standard dictionary for contextual values
939 if not end_date and not duration:
941 value['duration'] = duration
943 if allday: # For all day event
944 value = {'duration': 24.0}
947 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
948 start_date = datetime.strftime(datetime(start.year, start.month, start.day, 0,0,0), "%Y-%m-%d %H:%M:%S")
949 value['date'] = start_date
952 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
953 if end_date and not duration:
954 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
956 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
957 value['duration'] = round(duration, 2)
959 end = start + timedelta(hours=duration)
960 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
961 elif end_date and duration and not allday:
962 # we have both, keep them synchronized:
963 # set duration based on end_date (arbitrary decision: this avoid
964 # getting dates like 06:31:48 instead of 06:32:00)
965 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
967 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
968 value['duration'] = round(duration, 2)
970 return {'value': value}
972 def unlink_events(self, cr, uid, ids, context=None):
974 This function deletes event which are linked with the event with recurrent_uid
975 (Removes the events which refers to the same UID value)
980 cr.execute("select id from %s where recurrent_uid=%%s" % (self._table), (event_id,))
981 r_ids = map(lambda x: x[0], cr.fetchall())
982 self.unlink(cr, uid, r_ids, context=context)
985 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
987 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
988 @param self: The object pointer
989 @param cr: the current row, from the database cursor,
990 @param id: List of calendar event's ids.
991 @param context: A standard dictionary for contextual values
992 @return: dictionary of rrule value.
996 if not isinstance(ids, list):
999 for datas in self.read(cr, uid, ids, ['id','byday','recurrency', 'month_list','end_date', 'rrule_type', 'select1', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'exrule', 'day', 'week_list' ], context=context):
1001 if datas.get('interval', 0) < 0:
1002 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative'))
1003 if datas.get('count', 0) < 0:
1004 raise osv.except_osv(_('Warning!'), _('Count cannot be negative'))
1005 if datas['recurrency']:
1006 result[event] = self.compute_rule_string(datas)
1011 def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1012 data = self._get_empty_rrule_data()
1014 data['recurrency'] = True
1015 for event in self.browse(cr, uid, ids, context=context):
1016 rdate = rule_date or event.date
1017 update_data = self._parse_rrule(field_value, dict(data), rdate)
1018 data.update(update_data)
1019 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
1024 'id': fields.integer('ID', readonly=True),
1025 'sequence': fields.integer('Sequence'),
1026 'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1027 'date': fields.datetime('Date', states={'done': [('readonly', True)]}),
1028 'date_deadline': fields.datetime('Deadline', states={'done': [('readonly', True)]}),
1029 'create_date': fields.datetime('Created', readonly=True),
1030 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1031 'description': fields.text('Description', states={'done': [('readonly', True)]}),
1032 'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1033 ('confidential', 'Public for Employees')], 'Mark as', states={'done': [('readonly', True)]}),
1034 'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1035 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1036 'Show as', states={'done': [('readonly', True)]}),
1037 'base_calendar_url': fields.char('Caldav URL', size=264),
1038 'state': fields.selection([('tentative', 'Tentative'),
1039 ('confirmed', 'Confirmed'),
1040 ('cancelled', 'Cancelled')], 'State', readonly=True),
1041 'exdate': fields.text('Exception Date/Times', help="This property \
1042 defines the list of date/time exceptions for a recurring calendar component."),
1043 'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1044 rule or repeating pattern of time to exclude from the recurring rule."),
1045 'rrule': fields.function(_get_rulestring, type='char', size=124, \
1046 fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1047 'rrule_type': fields.selection([('none', ''), ('daily', 'Daily'), \
1048 ('weekly', 'Weekly'), ('monthly', 'Monthly'), \
1049 ('yearly', 'Yearly'),],
1050 'Recurrency', states={'done': [('readonly', True)]},
1051 help="Let the event automatically repeat at that interval"),
1052 'alarm_id': fields.many2one('res.alarm', 'Alarm', states={'done': [('readonly', True)]},
1053 help="Set an alarm at this time, before the event occurs" ),
1054 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1055 'recurrent_uid': fields.integer('Recurrent ID'),
1056 'recurrent_id': fields.datetime('Recurrent ID date'),
1057 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1058 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1059 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with Organizer Attribure of VEvent.
1060 'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1061 'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence termination'),
1062 'interval': fields.integer('Repeat every', help="Repeat every (Days/Week/Month/Year)"),
1063 'count': fields.integer('Repeat', help="Repeat x times"),
1064 'mo': fields.boolean('Mon'),
1065 'tu': fields.boolean('Tue'),
1066 'we': fields.boolean('Wed'),
1067 'th': fields.boolean('Thu'),
1068 'fr': fields.boolean('Fri'),
1069 'sa': fields.boolean('Sat'),
1070 'su': fields.boolean('Sun'),
1071 'select1': fields.selection([('date', 'Date of month'),
1072 ('day', 'Day of month')], 'Option'),
1073 'day': fields.integer('Date of month'),
1074 'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), \
1075 ('WE', 'Wednesday'), ('TH', 'Thursday'), \
1076 ('FR', 'Friday'), ('SA', 'Saturday'), \
1077 ('SU', 'Sunday')], 'Weekday'),
1078 'byday': fields.selection([('1', 'First'), ('2', 'Second'), \
1079 ('3', 'Third'), ('4', 'Fourth'), \
1080 ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
1081 'month_list': fields.selection(months.items(), 'Month'),
1082 'end_date': fields.date('Repeat Until'),
1083 'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1084 'event_id', 'attendee_id', 'Attendees'),
1085 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1086 'active': fields.boolean('Active', help="If the active field is set to \
1087 true, it will allow you to hide the event alarm information without removing it."),
1088 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1091 def default_organizer(self, cr, uid, context=None):
1092 user_pool = self.pool.get('res.users')
1093 user = user_pool.browse(cr, uid, uid, context=context)
1096 res += " <%s>" %(user.user_email)
1100 'end_type' : 'count',
1102 'rrule_type' : 'none',
1103 'state': 'tentative',
1109 'user_id': lambda self, cr, uid, ctx: uid,
1110 'organizer': default_organizer,
1113 def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1114 """Gives virtual event ids for recurring events based on value of Recurrence Rule
1115 This method gives ids of dates that comes between start date and end date of calendar views
1116 @param self: The object pointer
1117 @param cr: the current row, from the database cursor,
1118 @param uid: the current user’s ID for security checks,
1119 @param limit: The Number of Results to Return """
1124 for data in super(calendar_event, self).read(cr, uid, select, context=context):
1125 if not data['rrule']:
1126 result.append(data['id'])
1128 event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1129 # To check: If the start date is replace by event date .. the event date will be changed by that of calendar code
1131 if not data['rrule']:
1134 exdate = data['exdate'] and data['exdate'].split(',') or []
1135 rrule_str = data['rrule']
1137 rrule_until_date = False
1139 for rule in rrule_str.split(';'):
1140 name, value = rule.split('=')
1143 value = parser.parse(value)
1144 rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1145 value = value.strftime("%Y%m%d%H%M%S")
1146 new_rule = '%s=%s' % (name, value)
1147 new_rrule_str.append(new_rule)
1148 new_rrule_str = ';'.join(new_rrule_str)
1149 rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1150 for r_date in rdates:
1153 if arg[0] in ('date', 'date_deadline'):
1155 ok = ok and r_date.strftime('%Y-%m-%d')==arg[2]
1157 ok = ok and r_date.strftime('%Y-%m-%d')>arg[2]
1159 ok = ok and r_date.strftime('%Y-%m-%d')<arg[2]
1161 ok = ok and r_date.strftime('%Y-%m-%d')>=arg[2]
1163 ok = ok and r_date.strftime('%Y-%m-%d')<=arg[2]
1166 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1167 result.append(idval)
1169 if isinstance(select, (str, int, long)):
1170 return ids and ids[0] or False
1172 ids = list(set(result))
1175 def compute_rule_string(self, datas):
1177 Compute rule string according to value type RECUR of iCalendar from the values given.
1178 @param self: the object pointer
1179 @param datas: dictionary of freq and interval value.
1181 def get_week_string(freq, datas):
1182 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1183 if freq == 'weekly':
1184 byday = map(lambda x: x.upper(), filter(lambda x: datas.get(x) and x in weekdays, datas))
1186 return ';BYDAY=' + ','.join(byday)
1189 def get_month_string(freq, datas):
1190 if freq == 'monthly':
1191 if datas.get('select1')=='date' and (datas.get('day') < 1 or datas.get('day') > 31):
1192 raise osv.except_osv(_('Error!'), ("Please select proper Day of month"))
1194 if datas.get('select1')=='day':
1195 return ';BYDAY=' + datas.get('byday') + datas.get('week_list')
1196 elif datas.get('select1')=='date':
1197 return ';BYMONTHDAY=' + str(datas.get('day'))
1200 def get_end_date(datas):
1201 if datas.get('end_date'):
1202 datas['end_date_new'] = ''.join((re.compile('\d')).findall(datas.get('end_date'))) + 'T235959Z'
1204 return (datas.get('end_type') == 'count' and (';COUNT=' + str(datas.get('count'))) or '') +\
1205 ((datas.get('end_date_new') and datas.get('end_type') == 'end_date' and (';UNTIL=' + datas.get('end_date_new'))) or '')
1207 freq=datas.get('rrule_type')
1211 interval_srting = datas.get('interval') and (';INTERVAL=' + str(datas.get('interval'))) or ''
1213 return 'FREQ=' + freq.upper() + get_week_string(freq, datas) + interval_srting + get_end_date(datas) + get_month_string(freq, datas)
1215 def _get_empty_rrule_data(self):
1218 'recurrency' : False,
1220 'rrule_type' : False,
1237 #def _write_rrule(self, cr, uid, ids, field_value, rule_date=False, context=None):
1238 # data = self._get_empty_rrule_data()
1241 # data['recurrency'] = True
1242 # for event in self.browse(cr, uid, ids, context=context):
1243 # rdate = rule_date or event.date
1244 # update_data = self._parse_rrule(field_value, dict(data), rdate)
1245 # data.update(update_data)
1247 # self.write(cr, uid, event.id, data, context=context)
1250 def _parse_rrule(self, rule, data, date_start):
1251 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1252 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1253 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1255 if r._freq > 0 and r._freq < 4:
1256 data['rrule_type'] = rrule_type[r._freq]
1258 data['count'] = r._count
1259 data['interval'] = r._interval
1260 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1263 for i in xrange(0,7):
1264 if i in r._byweekday:
1265 data[day_list[i]] = True
1266 data['rrule_type'] = 'weekly'
1267 #repeat monthly bynweekday ((weekday, weeknumber), )
1269 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1270 data['byday'] = r._bynweekday[0][1]
1271 data['select1'] = 'day'
1272 data['rrule_type'] = 'monthly'
1275 data['day'] = r._bymonthday[0]
1276 data['select1'] = 'date'
1277 data['rrule_type'] = 'monthly'
1279 #yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1281 data['interval'] = data['interval'] * 12
1283 #FIXEME handle forever case
1285 #in case of repeat for ever that we do not support right now
1286 if not (data.get('count') or data.get('end_date')):
1288 if data.get('count'):
1289 data['end_type'] = 'count'
1291 data['end_type'] = 'end_date'
1294 def remove_virtual_id(self, ids):
1295 if isinstance(ids, (str, int, long)):
1296 return base_calendar_id2real_id(ids)
1298 if isinstance(ids, (list, tuple)):
1301 res.append(base_calendar_id2real_id(id))
1304 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1305 context = context or {}
1306 args_without_date = []
1311 new_id = self.remove_virtual_id(arg[2])
1312 new_arg = (arg[0], arg[1], new_id)
1313 args_without_date.append(new_arg)
1314 elif arg[0] not in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1315 args_without_date.append(arg)
1317 if context.get('virtual_id', True):
1318 args_without_date.append('|')
1319 args_without_date.append(arg)
1320 if context.get('virtual_id', True):
1321 args_without_date.append(('recurrency','=',1))
1322 filter_date.append(arg)
1324 res = super(calendar_event, self).search(cr, uid, args_without_date, \
1325 0, 0, order, context, count=False)
1326 if context.get('virtual_id', True):
1327 res = self.get_recurrent_ids(cr, uid, res, args, limit, context=context)
1332 return res[offset:offset+limit]
1336 def _get_data(self, cr, uid, id, context=None):
1337 res = self.read(cr, uid, [id],['date', 'date_deadline'])
1340 def need_to_update(self, event_id, vals):
1341 split_id = str(event_id).split("-")
1342 if len(split_id) < 2:
1345 date_start = vals.get('date', '')
1347 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1348 return date_start == split_id[1]
1353 def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1354 context = context or {}
1355 if isinstance(ids, (str, int, long)):
1359 # Special write of complex IDS
1360 for event_id in ids[:]:
1361 if len(str(event_id).split('-')) == 1:
1363 ids.remove(event_id)
1364 real_event_id = base_calendar_id2real_id(event_id)
1365 if not vals.get('recurrency', True):
1366 ids.append(real_event_id)
1369 #if edit one instance of a reccurrent id
1370 data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1371 'rrule', 'duration', 'exdate'])
1372 if data.get('rrule'):
1375 'recurrent_uid': real_event_id,
1376 'recurrent_id': data.get('date'),
1377 'rrule_type': 'none',
1379 'recurrency' : False,
1382 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1384 date_new = event_id.split('-')[1]
1385 date_new = time.strftime("%Y%m%dT%H%M%S", \
1386 time.strptime(date_new, "%Y%m%d%H%M%S"))
1387 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1388 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1390 context.update({'active_id': new_id, 'active_ids': [new_id]})
1393 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1394 vals['vtimezone'] = vals['vtimezone'][40:]
1396 res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1398 if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1399 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1400 alarm_obj = self.pool.get('res.alarm')
1401 alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1402 return res or True and False
1404 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1408 if 'date' in groupby:
1409 raise osv.except_osv(_('Warning !'), _('Group by date not supported, use the calendar view instead'))
1410 virtual_id = context.get('virtual_id', True)
1411 context.update({'virtual_id': False})
1412 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1414 #remove the count, since the value is not consistent with the result of the search when expand the group
1415 for groupname in groupby:
1416 if re.get(groupname + "_count"):
1417 del re[groupname + "_count"]
1418 re.get('__context', {}).update({'virtual_id' : virtual_id})
1421 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1422 # FIXME This whole id mangling has to go!
1425 fields2 = fields and fields[:] or None
1427 EXTRAFIELDS = ('class','user_id','duration')
1428 for f in EXTRAFIELDS:
1429 if fields and (f not in fields):
1432 if isinstance(ids, (str, int, long)):
1436 select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1439 real_data = super(calendar_event, self).read(cr, uid,
1440 [real_id for base_calendar_id, real_id in select],
1441 fields=fields2, context=context, load=load)
1442 real_data = dict(zip([x['id'] for x in real_data], real_data))
1444 for base_calendar_id, real_id in select:
1445 res = real_data[real_id].copy()
1446 ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1447 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1449 res['date_deadline'] = ls[2]
1450 res['id'] = base_calendar_id
1456 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1459 if r['class']=='private':
1461 if f not in ('id','date','date_deadline','duration','user_id','state'):
1467 for k in EXTRAFIELDS:
1468 if (k in r) and ((not fields) or (k not in fields)):
1470 if isinstance(ids, (str, int, long)):
1471 return result and result[0] or False
1474 def copy(self, cr, uid, id, default=None, context=None):
1478 res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1479 alarm_obj = self.pool.get('res.alarm')
1480 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1483 def unlink(self, cr, uid, ids, context=None):
1484 if not isinstance(ids, list):
1487 attendee_obj=self.pool.get('calendar.attendee')
1488 for event_id in ids[:]:
1489 if len(str(event_id).split('-')) == 1:
1492 real_event_id = base_calendar_id2real_id(event_id)
1493 data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1494 date_new = event_id.split('-')[1]
1495 date_new = time.strftime("%Y%m%dT%H%M%S", \
1496 time.strptime(date_new, "%Y%m%d%H%M%S"))
1497 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1498 self.write(cr, uid, [real_event_id], {'exdate': exdate})
1499 ids.remove(event_id)
1500 for event in self.browse(cr, uid, ids, context=context):
1501 if event.attendee_ids:
1502 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1504 res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1505 self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1506 self.unlink_events(cr, uid, ids, context=context)
1510 def create(self, cr, uid, vals, context=None):
1514 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1515 vals['vtimezone'] = vals['vtimezone'][40:]
1517 #updated_vals = self.onchange_dates(cr, uid, [],
1518 # vals.get('date', False),
1519 # vals.get('duration', False),
1520 # vals.get('date_deadline', False),
1521 # vals.get('allday', False),
1523 #vals.update(updated_vals.get('value', {}))
1525 res = super(calendar_event, self).create(cr, uid, vals, context)
1526 alarm_obj = self.pool.get('res.alarm')
1527 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1530 def do_tentative(self, cr, uid, ids, context=None, *args):
1531 """ Makes event invitation as Tentative
1532 @param self: The object pointer
1533 @param cr: the current row, from the database cursor,
1534 @param uid: the current user’s ID for security checks,
1535 @param ids: List of Event IDs
1536 @param *args: Get Tupple value
1537 @param context: A standard dictionary for contextual values
1539 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1541 def do_cancel(self, cr, uid, ids, context=None, *args):
1542 """ Makes event invitation as Tentative
1543 @param self: The object pointer
1544 @param cr: the current row, from the database cursor,
1545 @param uid: the current user’s ID for security checks,
1546 @param ids: List of Event IDs
1547 @param *args: Get Tupple value
1548 @param context: A standard dictionary for contextual values
1550 return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1552 def do_confirm(self, cr, uid, ids, context=None, *args):
1553 """ Makes event invitation as Tentative
1554 @param self: The object pointer
1555 @param cr: the current row, from the database cursor,
1556 @param uid: the current user’s ID for security checks,
1557 @param ids: List of Event IDs
1558 @param *args: Get Tupple value
1559 @param context: A standard dictionary for contextual values
1561 return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1565 class calendar_todo(osv.osv):
1566 """ Calendar Task """
1568 _name = "calendar.todo"
1569 _inherit = "calendar.event"
1570 _description = "Calendar Task"
1572 def _get_date(self, cr, uid, ids, name, arg, context=None):
1575 @param self: The object pointer
1576 @param cr: the current row, from the database cursor,
1577 @param uid: the current user’s ID for security checks,
1578 @param ids: List of calendar todo's IDs.
1579 @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1580 @param context: A standard dictionary for contextual values
1584 for event in self.browse(cr, uid, ids, context=context):
1585 res[event.id] = event.date_start
1588 def _set_date(self, cr, uid, id, name, value, arg, context=None):
1591 @param self: The object pointer
1592 @param cr: the current row, from the database cursor,
1593 @param uid: the current user’s ID for security checks,
1594 @param id: calendar's ID.
1595 @param value: Get Value
1596 @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1597 @param context: A standard dictionary for contextual values
1600 assert name == 'date'
1601 return self.write(cr, uid, id, { 'date_start': value }, context=context)
1604 'date': fields.function(_get_date, fnct_inv=_set_date, \
1605 string='Duration', store=True, type='datetime'),
1606 'duration': fields.integer('Duration'),
1614 class ir_attachment(osv.osv):
1615 _name = 'ir.attachment'
1616 _inherit = 'ir.attachment'
1618 def search_count(self, cr, user, args, context=None):
1620 @param self: The object pointer
1621 @param cr: the current row, from the database cursor,
1622 @param user: the current user’s ID for security checks,
1623 @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1624 @param context: A standard dictionary for contextual values
1629 args1.append(map(lambda x:str(x).split('-')[0], arg))
1630 return super(ir_attachment, self).search_count(cr, user, args1, context)
1634 def create(self, cr, uid, vals, context=None):
1636 id = context.get('default_res_id', False)
1637 context.update({'default_res_id' : base_calendar_id2real_id(id)})
1638 return super(ir_attachment, self).create(cr, uid, vals, context=context)
1640 def search(self, cr, uid, args, offset=0, limit=None, order=None,
1641 context=None, count=False):
1643 @param self: The object pointer
1644 @param cr: the current row, from the database cursor,
1645 @param uid: the current user’s ID for security checks,
1646 @param args: list of tuples of form [(‘name_of_the_field’, ‘operator’, value), ...].
1647 @param offset: The Number of Results to pass,
1648 @param limit: The Number of Results to Return,
1649 @param context: A standard dictionary for contextual values
1653 for i, arg in enumerate(new_args):
1654 if arg[0] == 'res_id':
1655 new_args[i] = (arg[0], arg[1], base_calendar_id2real_id(arg[2]))
1657 return super(ir_attachment, self).search(cr, uid, new_args, offset=offset,
1658 limit=limit, order=order, context=context, count=False)
1661 class ir_values(osv.osv):
1662 _inherit = 'ir.values'
1664 def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1665 isobject=False, meta=False, preserve_user=False, company=False):
1668 @param self: The object pointer
1669 @param cr: the current row, from the database cursor,
1670 @param uid: the current user’s ID for security checks,
1671 @param model: Get The Model
1676 if type(data) in (list, tuple):
1677 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1679 new_model.append(data)
1680 return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1681 value, replace, isobject, meta, preserve_user, company)
1683 def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1684 res_id_req=False, without_user=True, key2_req=True):
1687 @param self: The object pointer
1688 @param cr: the current row, from the database cursor,
1689 @param uid: the current user’s ID for security checks,
1690 @param model: Get The Model
1696 if type(data) in (list, tuple):
1697 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1699 new_model.append(data)
1700 return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1701 meta, context, res_id_req, without_user, key2_req)
1705 class ir_model(osv.osv):
1707 _inherit = 'ir.model'
1709 def read(self, cr, uid, ids, fields=None, context=None,
1710 load='_classic_read'):
1712 Overrides orm read method.
1713 @param self: The object pointer
1714 @param cr: the current row, from the database cursor,
1715 @param uid: the current user’s ID for security checks,
1716 @param ids: List of IR Model’s IDs.
1717 @param context: A standard dictionary for contextual values
1719 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1722 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1723 context=context, load=load)
1726 val['id'] = base_calendar_id2real_id(val['id'])
1727 return isinstance(ids, (str, int, long)) and data[0] or data
1731 class virtual_report_spool(web_services.report_spool):
1733 def exp_report(self, db, uid, object, ids, datas=None, context=None):
1736 @param self: The object pointer
1737 @param db: get the current database,
1738 @param uid: the current user’s ID for security checks,
1739 @param context: A standard dictionary for contextual values
1742 if object == 'printscreen.list':
1743 return super(virtual_report_spool, self).exp_report(db, uid, \
1744 object, ids, datas, context)
1747 new_ids.append(base_calendar_id2real_id(id))
1748 if datas.get('id', False):
1749 datas['id'] = base_calendar_id2real_id(datas['id'])
1750 return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, datas, context)
1752 virtual_report_spool()
1754 class res_users(osv.osv):
1755 _inherit = 'res.users'
1757 def _get_user_avail(self, cr, uid, ids, context=None):
1759 Get User Availability
1760 @param self: The object pointer
1761 @param cr: the current row, from the database cursor,
1762 @param uid: the current user’s ID for security checks,
1763 @param ids: List of res user’s IDs.
1764 @param context: A standard dictionary for contextual values
1767 current_datetime = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
1769 attendee_obj = self.pool.get('calendar.attendee')
1770 attendee_ids = attendee_obj.search(cr, uid, [
1771 ('event_date', '<=', current_datetime), ('event_end_date', '<=', current_datetime),
1772 ('state', '=', 'accepted'), ('user_id', 'in', ids)
1775 for attendee_data in attendee_obj.read(cr, uid, attendee_ids, ['user_id']):
1776 user_id = attendee_data['user_id']
1778 res.update({user_id:status})
1780 #TOCHECK: Delegated Event
1782 if user_id not in res:
1783 res[user_id] = 'free'
1787 def _get_user_avail_fun(self, cr, uid, ids, name, args, context=None):
1789 Get User Availability Function
1790 @param self: The object pointer
1791 @param cr: the current row, from the database cursor,
1792 @param uid: the current user’s ID for security checks,
1793 @param ids: List of res user’s IDs.
1794 @param context: A standard dictionary for contextual values
1797 return self._get_user_avail(cr, uid, ids, context=context)
1800 'availability': fields.function(_get_user_avail_fun, type='selection', \
1801 selection=[('free', 'Free'), ('busy', 'Busy')], \
1802 string='Free/Busy'),
1808 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: