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 openerp.osv import fields, osv
27 from openerp.tools.translate import _
32 from openerp import tools, SUPERUSER_ID
33 import openerp.service.report
36 1: "January", 2: "February", 3: "March", 4: "April", \
37 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", \
38 10: "October", 11: "November", 12: "December"
41 def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None):
43 Get recurrent dates based on Rule string considering exdate and start date.
44 @param rrulestring: rulestring
45 @param exdate: list of exception dates for rrule
46 @param startdate: startdate for computing recurrent dates
47 @return: list of Recurrent dates
50 val = parser.parse(''.join((re.compile('\d')).findall(date)))
54 startdate = datetime.now()
59 rset1 = rrule.rrulestr(str(rrulestring), dtstart=startdate, forceset=True)
61 datetime_obj = todate(date)
62 rset1._exdate.append(datetime_obj)
65 rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate))
69 def base_calendar_id2real_id(base_calendar_id=None, with_date=False):
71 Convert a "virtual/recurring event id" (type string) into a real event id (type int).
72 E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
73 @param base_calendar_id: id of calendar
74 @param with_date: if a value is passed to this param it will return dates based on value of withdate + base_calendar_id
75 @return: real event id
77 if base_calendar_id and isinstance(base_calendar_id, (str, unicode)):
78 res = base_calendar_id.split('-')
83 real_date = time.strftime("%Y-%m-%d %H:%M:%S", \
84 time.strptime(res[1], "%Y%m%d%H%M%S"))
85 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
86 end = start + timedelta(hours=with_date)
87 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
90 return base_calendar_id and int(base_calendar_id) or base_calendar_id
92 def get_real_ids(ids):
93 if isinstance(ids, (str, int, long)):
94 return base_calendar_id2real_id(ids)
96 if isinstance(ids, (list, tuple)):
99 res.append(base_calendar_id2real_id(id))
102 def real_id2base_calendar_id(real_id, recurrent_date):
104 Convert a real event id (type int) into a "virtual/recurring event id" (type string).
105 E.g. real event id is 1 and recurrent_date is set to 01-12-2009 10:00:00, so
106 it will return 1-20091201100000.
107 @param real_id: real event id
108 @param recurrent_date: real event recurrent date
109 @return: string containing the real id and the recurrent date
111 if real_id and recurrent_date:
112 recurrent_date = time.strftime("%Y%m%d%H%M%S", \
113 time.strptime(recurrent_date, "%Y-%m-%d %H:%M:%S"))
114 return '%d-%s' % (real_id, recurrent_date)
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. Hours and dates expressed in %(timezone)s time.</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.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, ['lang'], context=context)['lang']
293 result[id][name] = lang.replace('_', '-') if lang else False
297 def _lang_get(self, cr, uid, context=None):
299 Get language for language selection field.
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 code and name and id
305 obj = self.pool.get('res.lang')
306 ids = obj.search(cr, uid, [])
307 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
308 res = [((r['code']).replace('_', '-').lower(), r['name']) for r in res]
312 'cutype': fields.selection([('individual', 'Individual'), \
313 ('group', 'Group'), ('resource', 'Resource'), \
314 ('room', 'Room'), ('unknown', 'Unknown') ], \
315 'Invite Type', help="Specify the type of Invitation"),
316 'member': fields.char('Member', size=124,
317 help="Indicate the groups that the attendee belongs to"),
318 'role': fields.selection([('req-participant', 'Participation required'), \
319 ('chair', 'Chair Person'), \
320 ('opt-participant', 'Optional Participation'), \
321 ('non-participant', 'For information Purpose')], 'Role', \
322 help='Participation role for the calendar user'),
323 'state': fields.selection([('needs-action', 'Needs Action'),
324 ('tentative', 'Uncertain'),
325 ('declined', 'Declined'),
326 ('accepted', 'Accepted'),
327 ('delegated', 'Delegated')], 'Status', readonly=True, \
328 help="Status of the attendee's participation"),
329 'rsvp': fields.boolean('Required Reply?',
330 help="Indicats whether the favor of a reply is requested"),
331 'delegated_to': fields.function(_compute_data, \
332 string='Delegated To', type="char", size=124, store=True, \
333 multi='delegated_to', help="The users that the original \
334 request was delegated to"),
335 'delegated_from': fields.function(_compute_data, string=\
336 'Delegated From', type="char", store=True, size=124, multi='delegated_from'),
337 'parent_ids': fields.many2many('calendar.attendee', 'calendar_attendee_parent_rel', \
338 'attendee_id', 'parent_id', 'Delegrated From'),
339 'child_ids': fields.many2many('calendar.attendee', 'calendar_attendee_child_rel', \
340 'attendee_id', 'child_id', 'Delegrated To'),
341 'sent_by': fields.function(_compute_data, string='Sent By', \
342 type="char", multi='sent_by', store=True, size=124, \
343 help="Specify the user that is acting on behalf of the calendar user"),
344 'sent_by_uid': fields.function(_compute_data, string='Sent By User', \
345 type="many2one", relation="res.users", multi='sent_by_uid'),
346 'cn': fields.function(_compute_data, string='Common name', \
347 type="char", size=124, multi='cn', store=True),
348 'dir': fields.char('URI Reference', size=124, help="Reference to the URI\
349 that points to the directory information corresponding to the attendee."),
350 'language': fields.function(_compute_data, string='Language', \
351 type="selection", selection=_lang_get, multi='language', \
352 store=True, help="To specify the language for text values in a\
353 property or property parameter."),
354 'user_id': fields.many2one('res.users', 'User'),
355 'partner_id': fields.many2one('res.partner', 'Contact'),
356 'email': fields.char('Email', size=124, help="Email of Invited Person"),
357 'event_date': fields.function(_compute_data, string='Event Date', \
358 type="datetime", multi='event_date'),
359 'event_end_date': fields.function(_compute_data, \
360 string='Event End Date', type="datetime", \
361 multi='event_end_date'),
362 'ref': fields.reference('Event Ref', selection=openerp.addons.base.res.res_request.referencable_models, size=128),
363 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
366 'state': 'needs-action',
367 'role': 'req-participant',
369 'cutype': 'individual',
373 def copy(self, cr, uid, id, default=None, context=None):
374 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
376 def onchange_partner_id(self, cr, uid, ids, partner_id,context=None):
378 Make entry on email and availbility on change of partner_id field.
379 @param cr: the current row, from the database cursor
380 @param uid: the current user's ID for security checks
381 @param ids: list of calendar attendee's IDs
382 @param partner_id: changed value of partner id
383 @param context: a standard dictionary for contextual values
384 @return: dictionary of values which put value in email and availability fields
388 return {'value': {'email': ''}}
389 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
390 return {'value': {'email': partner.email}}
392 def get_ics_file(self, cr, uid, event_obj, context=None):
394 Returns iCalendar file for the event invitation.
395 @param self: the object pointer
396 @param cr: the current row, from the database cursor
397 @param uid: the current user's id for security checks
398 @param event_obj: event object (browse record)
399 @param context: a standard dictionary for contextual values
400 @return: .ics file content
403 def ics_datetime(idate, short=False):
405 #returns the datetime as UTC, because it is stored as it in the database
406 return datetime.strptime(idate, '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.timezone('UTC'))
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!'),_("First you have to specify the date of the invitation."))
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
428 if event_obj.user_id or event_obj.organizer_id:
429 event_org = event.add('organizer')
430 organizer = event_obj.organizer_id
432 organizer = event_obj.user_id
433 event_org.params['CN'] = [organizer.name]
434 event_org.value = 'MAILTO:' + (organizer.email or organizer.name)
436 if event_obj.alarm_id:
437 # computes alarm data
438 valarm = event.add('valarm')
439 alarm_object = self.pool.get('res.alarm')
440 alarm_data = alarm_object.read(cr, uid, event_obj.alarm_id.id, context=context)
441 # Compute trigger data
442 interval = alarm_data['trigger_interval']
443 occurs = alarm_data['trigger_occurs']
444 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
445 or -(alarm_data['trigger_duration'])
446 related = alarm_data['trigger_related']
447 trigger = valarm.add('TRIGGER')
448 trigger.params['related'] = [related.upper()]
449 if interval == 'days':
450 delta = timedelta(days=duration)
451 if interval == 'hours':
452 delta = timedelta(hours=duration)
453 if interval == 'minutes':
454 delta = timedelta(minutes=duration)
455 trigger.value = delta
456 # Compute other details
457 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
459 for attendee in event_obj.attendee_ids:
460 attendee_add = event.add('attendee')
461 attendee_add.params['CUTYPE'] = [str(attendee.cutype)]
462 attendee_add.params['ROLE'] = [str(attendee.role)]
463 attendee_add.params['RSVP'] = [str(attendee.rsvp)]
464 attendee_add.value = 'MAILTO:' + (attendee.email or '')
465 res = cal.serialize()
468 def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
470 Send mail for event invitation to event attendees.
471 @param email_from: email address for user sending the mail
474 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
475 for att in self.browse(cr, uid, ids, context=context):
476 sign = att.sent_by_uid and att.sent_by_uid.signature or ''
477 sign = '<br>'.join(sign and sign.split('\n') or [])
482 other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
484 for att2 in self.browse(cr, uid, other_invitation_ids):
485 att_infos.append(((att2.user_id and att2.user_id.name) or \
486 (att2.partner_id and att2.partner_id.name) or \
487 att2.email) + ' - Status: ' + att2.state.title())
488 #dates and times are gonna be expressed in `tz` time (local timezone of the `uid`)
489 tz = context.get('tz', pytz.timezone('UTC'))
490 #res_obj.date and res_obj.date_deadline are in UTC in database so we use context_timestamp() to transform them in the `tz` timezone
491 date_start = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
493 if res_obj.date_deadline:
494 date_stop = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
495 body_vals = {'name': res_obj.name,
496 'start_date': date_start,
497 'end_date': date_stop,
499 'description': res_obj.description or '-',
500 'location': res_obj.location or '-',
501 'attendees': '<br>'.join(att_infos),
502 'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
506 body = html_invitation % body_vals
507 if mail_to and email_from:
508 ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
509 vals = {'email_from': email_from,
516 vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
517 'datas_fname': 'invitation.ics',
518 'datas': str(ics_file).encode('base64')})]
519 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
522 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
524 Make entry on email and availbility on change of user_id field.
525 @param cr: the current row, from the database cursor
526 @param uid: the current user's ID for security checks
527 @param ids: list of calendar attendee's IDs
528 @param user_id: changed value of User id
529 @return: dictionary of values which put value in email and availability fields
533 return {'value': {'email': ''}}
534 usr_obj = self.pool.get('res.users')
535 user = usr_obj.browse(cr, uid, user_id, *args)
536 return {'value': {'email': user.email, 'availability':user.availability}}
538 def do_tentative(self, cr, uid, ids, context=None, *args):
540 Makes event invitation as Tentative.
541 @param self: the object pointer
542 @param cr: the current row, from the database cursor
543 @param uid: the current user's ID for security checks
544 @param ids: list of calendar attendee's IDs
545 @param *args: get Tupple value
546 @param context: a standard dictionary for contextual values
548 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
550 def do_accept(self, cr, uid, ids, context=None, *args):
552 Marks event invitation as Accepted.
553 @param cr: the current row, from the database cursor
554 @param uid: the current user's ID for security checks
555 @param ids: list of calendar attendee's IDs
556 @param context: a standard dictionary for contextual values
562 return self.write(cr, uid, ids, {'state': 'accepted'}, context)
564 def do_decline(self, cr, uid, ids, context=None, *args):
566 Marks event invitation as Declined.
567 @param self: the object pointer
568 @param cr: the current row, from the database cursor
569 @param uid: the current user's ID for security checks
570 @param ids: list of calendar attendee's IDs
571 @param *args: get Tupple value
572 @param context: a standard dictionary for contextual values
576 return self.write(cr, uid, ids, {'state': 'declined'}, context)
578 def create(self, cr, uid, vals, context=None):
580 Overrides orm create method.
581 @param self: The object pointer
582 @param cr: the current row, from the database cursor
583 @param uid: the current user's ID for security checks
584 @param vals: get Values
585 @param context: a standard dictionary for contextual values
589 if not vals.get("email") and vals.get("cn"):
590 cnval = vals.get("cn").split(':')
591 email = filter(lambda x:x.__contains__('@'), cnval)
592 vals['email'] = email and email[0] or ''
593 vals['cn'] = vals.get("cn")
594 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
598 class res_alarm(osv.osv):
599 """Resource Alarm """
601 _description = 'Basic Alarm Information'
604 'name':fields.char('Name', size=256, required=True),
605 'trigger_occurs': fields.selection([('before', 'Before'), \
606 ('after', 'After')], \
607 'Triggers', required=True),
608 'trigger_interval': fields.selection([('minutes', 'Minutes'), \
609 ('hours', 'Hours'), \
610 ('days', 'Days')], 'Interval', \
612 'trigger_duration': fields.integer('Duration', required=True),
613 'trigger_related': fields.selection([('start', 'The event starts'), \
614 ('end', 'The event ends')], \
615 'Related to', required=True),
616 'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \
617 are both optional, but if one occurs, so MUST the other"""),
618 'repeat': fields.integer('Repeat'),
619 'active': fields.boolean('Active', help="If the active field is set to \
620 true, it will allow you to hide the event alarm information without removing it.")
623 'trigger_interval': 'minutes',
624 'trigger_duration': 5,
625 'trigger_occurs': 'before',
626 'trigger_related': 'start',
630 def do_alarm_create(self, cr, uid, ids, model, date, context=None):
632 Create Alarm for event.
633 @param cr: the current row, from the database cursor,
634 @param uid: the current user's ID for security checks,
635 @param ids: List of res alarm's IDs.
636 @param model: Model name.
637 @param date: Event date
638 @param context: A standard dictionary for contextual values
643 alarm_obj = self.pool.get('calendar.alarm')
644 res_alarm_obj = self.pool.get('res.alarm')
645 ir_obj = self.pool.get('ir.model')
646 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
648 model_obj = self.pool[model]
649 for data in model_obj.browse(cr, uid, ids, context=context):
651 basic_alarm = data.alarm_id
652 cal_alarm = data.base_calendar_alarm_id
653 if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm):
655 # Find for existing res.alarm
656 duration = cal_alarm.trigger_duration
657 interval = cal_alarm.trigger_interval
658 occurs = cal_alarm.trigger_occurs
659 related = cal_alarm.trigger_related
660 domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)]
661 alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context)
664 'trigger_duration': duration,
665 'trigger_interval': interval,
666 'trigger_occurs': occurs,
667 'trigger_related': related,
668 'name': str(duration) + ' ' + str(interval) + ' ' + str(occurs)
670 new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context)
672 new_res_alarm = alarm_ids[0]
673 cr.execute('UPDATE %s ' % model_obj._table + \
674 ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \
676 (cal_alarm.id, new_res_alarm, data.id))
678 self.do_alarm_unlink(cr, uid, [data.id], model)
682 'description': data.description,
684 'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))],
685 'trigger_related': basic_alarm.trigger_related,
686 'trigger_duration': basic_alarm.trigger_duration,
687 'trigger_occurs': basic_alarm.trigger_occurs,
688 'trigger_interval': basic_alarm.trigger_interval,
689 'duration': basic_alarm.duration,
690 'repeat': basic_alarm.repeat,
692 'event_date': data[date],
694 'model_id': model_id,
697 alarm_id = alarm_obj.create(cr, uid, vals)
698 cr.execute('UPDATE %s ' % model_obj._table + \
699 ' SET base_calendar_alarm_id=%s, alarm_id=%s '
701 ( alarm_id, basic_alarm.id, data.id) )
704 def do_alarm_unlink(self, cr, uid, ids, model, context=None):
706 Delete alarm specified in ids
707 @param cr: the current row, from the database cursor,
708 @param uid: the current user's ID for security checks,
709 @param ids: List of res alarm's IDs.
710 @param model: Model name for which alarm is to be cleared.
715 alarm_obj = self.pool.get('calendar.alarm')
716 ir_obj = self.pool.get('ir.model')
717 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
718 model_obj = self.pool[model]
719 for data in model_obj.browse(cr, uid, ids, context=context):
720 alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', data.id)])
722 alarm_obj.unlink(cr, uid, alarm_ids)
723 cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\
724 where id=%%s' % model_obj._table,(data.id,))
728 class calendar_alarm(osv.osv):
729 _name = 'calendar.alarm'
730 _description = 'Event alarm information'
731 _inherit = 'res.alarm'
735 'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'),
736 'name': fields.char('Summary', size=124, help="""Contains the text to be \
737 used as the message subject for email \
738 or contains the text to be used for display"""),
739 'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \
740 ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \
741 required=True, help="Defines the action to be invoked when an alarm is triggered"),
742 'description': fields.text('Description', help='Provides a more complete \
743 description of the calendar component, than that \
744 provided by the "SUMMARY" property'),
745 'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \
746 'alarm_id', 'attendee_id', 'Attendees', readonly=True),
747 'attach': fields.binary('Attachment', help="""* Points to a sound resource,\
748 which is rendered when the alarm is triggered for audio,
749 * File which is intended to be sent as message attachments for email,
750 * Points to a procedure resource, which is invoked when\
751 the alarm is triggered for procedure."""),
752 'res_id': fields.integer('Resource ID'),
753 'model_id': fields.many2one('ir.model', 'Model'),
754 'user_id': fields.many2one('res.users', 'Owner'),
755 'event_date': fields.datetime('Event Date'),
756 'event_end_date': fields.datetime('Event End Date'),
757 'trigger_date': fields.datetime('Trigger Date', readonly="True"),
758 'state':fields.selection([
763 ], 'Status', select=True, readonly=True),
771 def create(self, cr, uid, vals, context=None):
773 Overrides orm create method.
774 @param self: The object pointer
775 @param cr: the current row, from the database cursor,
776 @param vals: dictionary of fields value.{'name_of_the_field': value, ...}
777 @param context: A standard dictionary for contextual values
778 @return: new record id for calendar_alarm.
782 event_date = vals.get('event_date', False)
784 dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S")
785 if vals['trigger_interval'] == 'days':
786 delta = timedelta(days=vals['trigger_duration'])
787 if vals['trigger_interval'] == 'hours':
788 delta = timedelta(hours=vals['trigger_duration'])
789 if vals['trigger_interval'] == 'minutes':
790 delta = timedelta(minutes=vals['trigger_duration'])
791 trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta)
792 vals['trigger_date'] = trigger_date
793 res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
796 def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
798 """Scheduler for event reminder
799 @param self: The object pointer
800 @param cr: the current row, from the database cursor,
801 @param uid: the current user's ID for security checks,
802 @param ids: List of calendar alarm's IDs.
803 @param use_new_cursor: False or the dbname
804 @param context: A standard dictionary for contextual values
808 current_datetime = datetime.now()
809 alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
813 for alarm in self.browse(cr, uid, alarm_ids, context=context):
814 next_trigger_date = None
816 model_obj = self.pool[alarm.model_id.model]
817 res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
820 if hasattr(res_obj, 'rrule') and res_obj.rrule:
821 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
822 #exdate is a string and we need a list
823 exdate = res_obj.exdate and res_obj.exdate.split(',') or []
824 recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule)
826 trigger_interval = alarm.trigger_interval
827 if trigger_interval == 'days':
828 delta = timedelta(days=alarm.trigger_duration)
829 if trigger_interval == 'hours':
830 delta = timedelta(hours=alarm.trigger_duration)
831 if trigger_interval == 'minutes':
832 delta = timedelta(minutes=alarm.trigger_duration)
833 delta = alarm.trigger_occurs == 'after' and delta or -delta
835 for rdate in recurrent_dates:
836 if rdate + delta > current_datetime:
838 if rdate + delta <= current_datetime:
839 re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
840 rest_dates = recurrent_dates[len(re_dates):]
841 next_trigger_date = rest_dates and rest_dates[0] or None
844 re_dates = [alarm.trigger_date]
847 if alarm.action == 'email':
848 sub = '[OpenERP Reminder] %s' % (alarm.name)
860 """ % (alarm.name, alarm.trigger_date, alarm.description, \
861 alarm.user_id.name, alarm.user_id.signature)
862 mail_to = alarm.user_id.email
863 for att in alarm.attendee_ids:
864 mail_to = mail_to + " " + att.user_id.email
871 'email_from': tools.config.get('email_from', mail_to),
873 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
874 if next_trigger_date:
875 update_vals.update({'trigger_date': next_trigger_date})
877 update_vals.update({'state': 'done'})
878 self.write(cr, uid, [alarm.id], update_vals)
883 class calendar_event(osv.osv):
884 _name = "calendar.event"
885 _description = "Calendar Event"
888 def _tz_get(self, cr, uid, context=None):
889 return [(x.lower(), x) for x in pytz.all_timezones]
891 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
892 """Returns duration and/or end date based on values passed
893 @param self: The object pointer
894 @param cr: the current row, from the database cursor,
895 @param uid: the current user's ID for security checks,
896 @param ids: List of calendar event's IDs.
897 @param start_date: Starting date
898 @param duration: Duration between start date and end date
899 @param end_date: Ending Datee
900 @param context: A standard dictionary for contextual values
908 if not end_date and not duration:
910 value['duration'] = duration
912 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
913 if allday: # For all day event
915 value['duration'] = duration
916 # change start_date's time to 00:00:00 in the user's timezone
917 user = self.pool.get('res.users').browse(cr, uid, uid)
918 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
919 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
920 start = start.replace(hour=0, minute=0, second=0) # change start's time to 00:00:00
921 start = start.astimezone(pytz.utc) # convert start back to utc
922 start_date = start.strftime("%Y-%m-%d %H:%M:%S")
923 value['date'] = start_date
925 if end_date and not duration:
926 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
928 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
929 value['duration'] = round(duration, 2)
931 end = start + timedelta(hours=duration)
932 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
933 elif end_date and duration and not allday:
934 # we have both, keep them synchronized:
935 # set duration based on end_date (arbitrary decision: this avoid
936 # getting dates like 06:31:48 instead of 06:32:00)
937 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
939 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
940 value['duration'] = round(duration, 2)
942 return {'value': value}
944 def unlink_events(self, cr, uid, ids, context=None):
946 This function deletes event which are linked with the event with recurrent_id
947 (Removes the events which refers to the same UID value)
952 cr.execute("select id from %s where recurrent_id=%%s" % (self._table), (event_id,))
953 r_ids = map(lambda x: x[0], cr.fetchall())
954 self.unlink(cr, uid, r_ids, context=context)
957 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
959 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
960 @param self: The object pointer
961 @param cr: the current row, from the database cursor,
962 @param id: List of calendar event's ids.
963 @param context: A standard dictionary for contextual values
964 @return: dictionary of rrule value.
968 if not isinstance(ids, list):
972 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
973 data = self.read(cr, SUPERUSER_ID, id, ['interval', 'count'], context=context)
974 if data.get('interval', 0) < 0:
975 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
976 if data.get('count', 0) <= 0:
977 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
978 data = self.read(cr, uid, id, ['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)
980 if data['recurrency']:
981 result[event] = self.compute_rule_string(data)
986 def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
987 data = self._get_empty_rrule_data()
989 data['recurrency'] = True
990 for event in self.browse(cr, uid, ids, context=context):
991 rdate = rule_date or event.date
992 update_data = self._parse_rrule(field_value, dict(data), rdate)
993 data.update(update_data)
994 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
998 'id': fields.integer('ID', readonly=True),
999 'sequence': fields.integer('Sequence'),
1000 'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1001 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True,),
1002 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
1003 'create_date': fields.datetime('Created', readonly=True),
1004 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1005 'description': fields.text('Description', states={'done': [('readonly', True)]}),
1006 'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1007 ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
1008 'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1009 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1010 'Show Time as', states={'done': [('readonly', True)]}),
1011 'base_calendar_url': fields.char('Caldav URL', size=264),
1012 'state': fields.selection([
1013 ('tentative', 'Uncertain'),
1014 ('cancelled', 'Cancelled'),
1015 ('confirmed', 'Confirmed'),
1016 ], 'Status', readonly=True),
1017 'exdate': fields.text('Exception Date/Times', help="This property \
1018 defines the list of date/time exceptions for a recurring calendar component."),
1019 'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1020 rule or repeating pattern of time to exclude from the recurring rule."),
1021 'rrule': fields.function(_get_rulestring, type='char', size=124, \
1022 fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1023 'rrule_type': fields.selection([
1024 ('daily', 'Day(s)'),
1025 ('weekly', 'Week(s)'),
1026 ('monthly', 'Month(s)'),
1027 ('yearly', 'Year(s)')
1028 ], 'Recurrency', states={'done': [('readonly', True)]},
1029 help="Let the event automatically repeat at that interval"),
1030 'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]},
1031 help="Set an alarm at this time, before the event occurs" ),
1032 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1033 'recurrent_id': fields.integer('Recurrent ID'),
1034 'recurrent_id_date': fields.datetime('Recurrent ID date'),
1035 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1036 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1037 'organizer': fields.char("Organizer (deprecated)", size=256, states={'done': [('readonly', True)]},
1038 deprecated='Will be removed with OpenERP v8; use organizer_id field instead'), # Map with organizer attribute of VEvent.
1039 'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1040 'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'),
1041 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
1042 'count': fields.integer('Repeat', help="Repeat x times"),
1043 'mo': fields.boolean('Mon'),
1044 'tu': fields.boolean('Tue'),
1045 'we': fields.boolean('Wed'),
1046 'th': fields.boolean('Thu'),
1047 'fr': fields.boolean('Fri'),
1048 'sa': fields.boolean('Sat'),
1049 'su': fields.boolean('Sun'),
1050 'select1': fields.selection([('date', 'Date of month'),
1051 ('day', 'Day of month')], 'Option'),
1052 'day': fields.integer('Date of month'),
1053 'week_list': fields.selection([
1056 ('WE', 'Wednesday'),
1060 ('SU', 'Sunday')], 'Weekday'),
1061 'byday': fields.selection([
1067 ('-1', 'Last')], 'By day'),
1068 'month_list': fields.selection(months.items(), 'Month'),
1069 'end_date': fields.date('Repeat Until'),
1070 'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1071 'event_id', 'attendee_id', 'Attendees'),
1072 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1073 'active': fields.boolean('Active', help="If the active field is set to \
1074 true, it will allow you to hide the event alarm information without removing it."),
1075 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1076 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
1079 def create_attendees(self, cr, uid, ids, context):
1080 att_obj = self.pool.get('calendar.attendee')
1081 user_obj = self.pool.get('res.users')
1082 current_user = user_obj.browse(cr, uid, uid, context=context)
1083 for event in self.browse(cr, uid, ids, context):
1085 for att in event.attendee_ids:
1086 attendees[att.partner_id.id] = True
1089 for partner in event.partner_ids:
1090 if partner.id in attendees:
1092 local_context = context.copy()
1093 local_context.pop('default_state', None)
1094 att_id = self.pool.get('calendar.attendee').create(cr, uid, {
1095 'partner_id': partner.id,
1096 'user_id': partner.user_ids and partner.user_ids[0].id or False,
1097 'ref': self._name+','+str(event.id),
1098 'email': partner.email
1099 }, context=local_context)
1101 mail_to = mail_to + " " + partner.email
1102 self.write(cr, uid, [event.id], {
1103 'attendee_ids': [(4, att_id)]
1105 new_attendees.append(att_id)
1107 if mail_to and current_user.email:
1108 att_obj._send_mail(cr, uid, new_attendees, mail_to,
1109 email_from = current_user.email, context=context)
1112 def default_organizer(self, cr, uid, context=None):
1115 Use organizer_id field and its default value instead.
1117 user_pool = self.pool.get('res.users')
1118 user = user_pool.browse(cr, uid, uid, context=context)
1121 res += " <%s>" %(user.email)
1125 'end_type': 'count',
1127 'rrule_type': False,
1128 'state': 'tentative',
1134 'user_id': lambda self, cr, uid, ctx: uid,
1137 def _check_closing_date(self, cr, uid, ids, context=None):
1138 for event in self.browse(cr, uid, ids, context=context):
1139 if event.date_deadline < event.date:
1144 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
1147 def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1148 """Gives virtual event ids for recurring events based on value of Recurrence Rule
1149 This method gives ids of dates that comes between start date and end date of calendar views
1150 @param self: The object pointer
1151 @param cr: the current row, from the database cursor,
1152 @param uid: the current user's ID for security checks,
1153 @param limit: The Number of Results to Return """
1158 for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'recurrency', 'exdate', 'exrule', 'date'], context=context):
1159 if not data['recurrency'] or not data['rrule']:
1160 result.append(data['id'])
1162 event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1164 # TOCHECK: the start date should be replaced by event date; the event date will be changed by that of calendar code
1166 if not data['rrule']:
1169 exdate = data['exdate'] and data['exdate'].split(',') or []
1170 rrule_str = data['rrule']
1172 rrule_until_date = False
1174 for rule in rrule_str.split(';'):
1175 name, value = rule.split('=')
1178 value = parser.parse(value)
1179 rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1180 value = value.strftime("%Y%m%d%H%M%S")
1181 new_rule = '%s=%s' % (name, value)
1182 new_rrule_str.append(new_rule)
1183 new_rrule_str = ';'.join(new_rrule_str)
1184 rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1185 for r_date in rdates:
1186 # fix domain evaluation
1187 # step 1: check date and replace expression by True or False, replace other expressions by True
1188 # step 2: evaluation of & and |
1189 # check if there are one False
1192 if str(arg[0]) in (str('date'), str('date_deadline')):
1194 ok = r_date.strftime('%Y-%m-%d')==arg[2]
1196 ok = r_date.strftime('%Y-%m-%d')>arg[2]
1198 ok = r_date.strftime('%Y-%m-%d')<arg[2]
1199 if (arg[1] == '>='):
1200 ok = r_date.strftime('%Y-%m-%d')>=arg[2]
1201 if (arg[1] == '<='):
1202 ok = r_date.strftime('%Y-%m-%d')<=arg[2]
1204 elif str(arg) == str('&') or str(arg) == str('|'):
1211 if not isinstance(item, basestring):
1213 elif str(item) == str('&'):
1214 first = new_pile.pop()
1215 second = new_pile.pop()
1216 res = first and second
1217 elif str(item) == str('|'):
1218 first = new_pile.pop()
1219 second = new_pile.pop()
1220 res = first or second
1221 new_pile.append(res)
1223 if [True for item in new_pile if not item]:
1225 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1226 result.append(idval)
1228 if isinstance(select, (str, int, long)):
1229 return ids and ids[0] or False
1231 ids = list(set(result))
1234 def compute_rule_string(self, data):
1236 Compute rule string according to value type RECUR of iCalendar from the values given.
1237 @param self: the object pointer
1238 @param data: dictionary of freq and interval value
1239 @return: string containing recurring rule (empty if no rule)
1241 def get_week_string(freq, data):
1242 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1243 if freq == 'weekly':
1244 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1246 return ';BYDAY=' + ','.join(byday)
1249 def get_month_string(freq, data):
1250 if freq == 'monthly':
1251 if data.get('select1')=='date' and (data.get('day') < 1 or data.get('day') > 31):
1252 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1254 if data.get('select1')=='day':
1255 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1256 elif data.get('select1')=='date':
1257 return ';BYMONTHDAY=' + str(data.get('day'))
1260 def get_end_date(data):
1261 if data.get('end_date'):
1262 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1264 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1265 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1267 freq = data.get('rrule_type', False)
1270 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1271 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1275 def _get_empty_rrule_data(self):
1278 'recurrency' : False,
1280 'rrule_type' : False,
1297 def _parse_rrule(self, rule, data, date_start):
1298 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1299 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1300 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1302 if r._freq > 0 and r._freq < 4:
1303 data['rrule_type'] = rrule_type[r._freq]
1305 data['count'] = r._count
1306 data['interval'] = r._interval
1307 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1310 for i in xrange(0,7):
1311 if i in r._byweekday:
1312 data[day_list[i]] = True
1313 data['rrule_type'] = 'weekly'
1314 #repeat monthly by nweekday ((weekday, weeknumber), )
1316 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1317 data['byday'] = r._bynweekday[0][1]
1318 data['select1'] = 'day'
1319 data['rrule_type'] = 'monthly'
1322 data['day'] = r._bymonthday[0]
1323 data['select1'] = 'date'
1324 data['rrule_type'] = 'monthly'
1326 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1328 data['interval'] = data['interval'] * 12
1330 #FIXEME handle forever case
1332 #in case of repeat for ever that we do not support right now
1333 if not (data.get('count') or data.get('end_date')):
1335 if data.get('count'):
1336 data['end_type'] = 'count'
1338 data['end_type'] = 'end_date'
1341 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1348 if arg[0] in ('date_deadline', unicode('date_deadline')):
1349 if context.get('virtual_id', True):
1350 new_args += ['|','&',('recurrency','=',1),('end_date', arg[1], arg[2])]
1351 elif arg[0] == "id":
1352 new_id = get_real_ids(arg[2])
1353 new_arg = (arg[0], arg[1], new_id)
1354 new_args.append(new_arg)
1355 #offset, limit and count must be treated separately as we may need to deal with virtual ids
1356 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=order, context=context, count=False)
1357 if context.get('virtual_id', True):
1358 res = self.get_recurrent_ids(cr, uid, res, args, limit, context=context)
1362 return res[offset:offset+limit]
1365 def _get_data(self, cr, uid, id, context=None):
1366 return self.read(cr, uid, id,['date', 'date_deadline'])
1368 def need_to_update(self, event_id, vals):
1369 split_id = str(event_id).split("-")
1370 if len(split_id) < 2:
1373 date_start = vals.get('date', '')
1375 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1376 return date_start == split_id[1]
1380 def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1381 def _only_changes_to_apply_on_real_ids(field_names):
1382 ''' return True if changes are only to be made on the real ids'''
1383 for field in field_names:
1384 if field not in ['message_follower_ids']:
1388 context = context or {}
1389 if isinstance(ids, (str, int, long)):
1393 # Special write of complex IDS
1394 for event_id in ids[:]:
1395 if len(str(event_id).split('-')) == 1:
1397 ids.remove(event_id)
1398 real_event_id = base_calendar_id2real_id(event_id)
1400 # if we are setting the recurrency flag to False or if we are only changing fields that
1401 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1402 # then set real ids to be updated.
1403 if not vals.get('recurrency', True) or _only_changes_to_apply_on_real_ids(vals.keys()):
1404 ids.append(real_event_id)
1407 #if edit one instance of a reccurrent id
1408 data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1409 'rrule', 'duration', 'exdate'])
1410 if data.get('rrule'):
1413 recurrent_id=real_event_id,
1414 recurrent_id_date=data.get('date'),
1422 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1424 date_new = event_id.split('-')[1]
1425 date_new = time.strftime("%Y%m%dT%H%M%S", \
1426 time.strptime(date_new, "%Y%m%d%H%M%S"))
1427 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1428 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1430 context.update({'active_id': new_id, 'active_ids': [new_id]})
1433 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1434 vals['vtimezone'] = vals['vtimezone'][40:]
1436 res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1438 # set end_date for calendar searching
1439 if vals.get('recurrency', True) and vals.get('end_type', 'count') in ('count', unicode('count')) and \
1440 (vals.get('rrule_type') or vals.get('count') or vals.get('date') or vals.get('date_deadline')):
1441 for data in self.read(cr, uid, ids, ['date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
1442 end_date = self._set_recurrency_end_date(data, context=context)
1443 super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
1445 if vals.get('partner_ids', False):
1446 self.create_attendees(cr, uid, ids, context)
1448 if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1449 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1450 alarm_obj = self.pool.get('res.alarm')
1451 alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1452 return res or True and False
1454 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1458 if 'date' in groupby:
1459 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1460 virtual_id = context.get('virtual_id', True)
1461 context.update({'virtual_id': False})
1462 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1464 #remove the count, since the value is not consistent with the result of the search when expand the group
1465 for groupname in groupby:
1466 if re.get(groupname + "_count"):
1467 del re[groupname + "_count"]
1468 re.get('__context', {}).update({'virtual_id' : virtual_id})
1471 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1474 fields2 = fields and fields[:] or None
1476 EXTRAFIELDS = ('class','user_id','duration')
1477 for f in EXTRAFIELDS:
1478 if fields and (f not in fields):
1481 # FIXME This whole id mangling has to go!
1482 if isinstance(ids, (str, int, long)):
1487 select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1490 real_data = super(calendar_event, self).read(cr, uid,
1491 [real_id for base_calendar_id, real_id in select],
1492 fields=fields2, context=context, load=load)
1493 real_data = dict(zip([x['id'] for x in real_data], real_data))
1495 for base_calendar_id, real_id in select:
1496 res = real_data[real_id].copy()
1497 ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1498 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1500 res['date_deadline'] = ls[2]
1501 res['id'] = base_calendar_id
1507 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1510 if r['class']=='private':
1512 if f not in ('id','date','date_deadline','duration','user_id','state','interval','count'):
1513 if isinstance(r[f], list):
1521 for k in EXTRAFIELDS:
1522 if (k in r) and (fields and (k not in fields)):
1524 if isinstance(ids, (str, int, long)):
1525 return result and result[0] or False
1528 def copy(self, cr, uid, id, default=None, context=None):
1532 res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1533 alarm_obj = self.pool.get('res.alarm')
1534 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1537 def unlink(self, cr, uid, ids, context=None):
1538 if not isinstance(ids, list):
1541 attendee_obj=self.pool.get('calendar.attendee')
1542 for event_id in ids[:]:
1543 if len(str(event_id).split('-')) == 1:
1546 real_event_id = base_calendar_id2real_id(event_id)
1547 data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1548 date_new = event_id.split('-')[1]
1549 date_new = time.strftime("%Y%m%dT%H%M%S", \
1550 time.strptime(date_new, "%Y%m%d%H%M%S"))
1551 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1552 self.write(cr, uid, [real_event_id], {'exdate': exdate})
1553 ids.remove(event_id)
1554 for event in self.browse(cr, uid, ids, context=context):
1555 if event.attendee_ids:
1556 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1558 res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1559 self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1560 self.unlink_events(cr, uid, ids, context=context)
1563 def _set_recurrency_end_date(self, data, context=None):
1564 end_date = data.get('end_date')
1565 if data.get('recurrency') and data.get('end_type') in ('count', unicode('count')):
1566 data_date_deadline = datetime.strptime(data.get('date_deadline'), '%Y-%m-%d %H:%M:%S')
1567 if data.get('rrule_type') in ('daily', unicode('count')):
1568 rel_date = relativedelta(days=data.get('count')+1)
1569 elif data.get('rrule_type') in ('weekly', unicode('weekly')):
1570 rel_date = relativedelta(days=(data.get('count')+1)*7)
1571 elif data.get('rrule_type') in ('monthly', unicode('monthly')):
1572 rel_date = relativedelta(months=data.get('count')+1)
1573 elif data.get('rrule_type') in ('yearly', unicode('yearly')):
1574 rel_date = relativedelta(years=data.get('count')+1)
1575 end_date = data_date_deadline + rel_date
1578 def create(self, cr, uid, vals, context=None):
1582 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1583 vals['vtimezone'] = vals['vtimezone'][40:]
1585 vals['end_date'] = self._set_recurrency_end_date(vals, context=context)
1586 res = super(calendar_event, self).create(cr, uid, vals, context)
1588 alarm_obj = self.pool.get('res.alarm')
1589 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1590 self.create_attendees(cr, uid, [res], context)
1593 def do_tentative(self, cr, uid, ids, context=None, *args):
1594 """ Makes event invitation as Tentative
1595 @param self: The object pointer
1596 @param cr: the current row, from the database cursor,
1597 @param uid: the current user's ID for security checks,
1598 @param ids: List of Event IDs
1599 @param *args: Get Tupple value
1600 @param context: A standard dictionary for contextual values
1602 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1604 def do_cancel(self, cr, uid, ids, context=None, *args):
1605 """ Makes event invitation as Tentative
1606 @param self: The object pointer
1607 @param cr: the current row, from the database cursor,
1608 @param uid: the current user's ID for security checks,
1609 @param ids: List of Event IDs
1610 @param *args: Get Tupple value
1611 @param context: A standard dictionary for contextual values
1613 return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1615 def do_confirm(self, cr, uid, ids, context=None, *args):
1616 """ Makes event invitation as Tentative
1617 @param self: The object pointer
1618 @param cr: the current row, from the database cursor,
1619 @param uid: the current user's ID for security checks,
1620 @param ids: List of Event IDs
1621 @param *args: Get Tupple value
1622 @param context: A standard dictionary for contextual values
1624 return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1627 class calendar_todo(osv.osv):
1628 """ Calendar Task """
1630 _name = "calendar.todo"
1631 _inherit = "calendar.event"
1632 _description = "Calendar Task"
1634 def _get_date(self, cr, uid, ids, name, arg, context=None):
1637 @param self: The object pointer
1638 @param cr: the current row, from the database cursor,
1639 @param uid: the current user's ID for security checks,
1640 @param ids: List of calendar todo's IDs.
1641 @param args: list of tuples of form [(‘name_of_the_field', ‘operator', value), ...].
1642 @param context: A standard dictionary for contextual values
1646 for event in self.browse(cr, uid, ids, context=context):
1647 res[event.id] = event.date_start
1650 def _set_date(self, cr, uid, id, name, value, arg, context=None):
1653 @param self: The object pointer
1654 @param cr: the current row, from the database cursor,
1655 @param uid: the current user's ID for security checks,
1656 @param id: calendar's ID.
1657 @param value: Get Value
1658 @param args: list of tuples of form [('name_of_the_field', 'operator', value), ...].
1659 @param context: A standard dictionary for contextual values
1662 assert name == 'date'
1663 return self.write(cr, uid, id, { 'date_start': value }, context=context)
1666 'date': fields.function(_get_date, fnct_inv=_set_date, \
1667 string='Duration', store=True, type='datetime'),
1668 'duration': fields.integer('Duration'),
1676 class ir_values(osv.osv):
1677 _inherit = 'ir.values'
1679 def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1680 isobject=False, meta=False, preserve_user=False, company=False):
1683 @param self: The object pointer
1684 @param cr: the current row, from the database cursor,
1685 @param uid: the current user's ID for security checks,
1686 @param model: Get The Model
1691 if type(data) in (list, tuple):
1692 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1694 new_model.append(data)
1695 return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1696 value, replace, isobject, meta, preserve_user, company)
1698 def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1699 res_id_req=False, without_user=True, key2_req=True):
1702 @param self: The object pointer
1703 @param cr: the current row, from the database cursor,
1704 @param uid: the current user's ID for security checks,
1705 @param model: Get The Model
1711 if type(data) in (list, tuple):
1712 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1714 new_model.append(data)
1715 return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1716 meta, context, res_id_req, without_user, key2_req)
1719 class ir_model(osv.osv):
1721 _inherit = 'ir.model'
1723 def read(self, cr, uid, ids, fields=None, context=None,
1724 load='_classic_read'):
1726 Overrides orm read method.
1727 @param self: The object pointer
1728 @param cr: the current row, from the database cursor,
1729 @param uid: the current user's ID for security checks,
1730 @param ids: List of IR Model's IDs.
1731 @param context: A standard dictionary for contextual values
1733 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1736 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1737 context=context, load=load)
1740 val['id'] = base_calendar_id2real_id(val['id'])
1741 return isinstance(ids, (str, int, long)) and data[0] or data
1744 original_exp_report = openerp.service.report.exp_report
1746 def exp_report(db, uid, object, ids, data=None, context=None):
1749 @param db: get the current database,
1750 @param uid: the current user's ID for security checks,
1751 @param context: A standard dictionary for contextual values
1754 if object == 'printscreen.list':
1755 original_exp_report(db, uid, object, ids, data, context)
1758 new_ids.append(base_calendar_id2real_id(id))
1759 if data.get('id', False):
1760 data['id'] = base_calendar_id2real_id(data['id'])
1761 return original_exp_report(db, uid, object, new_ids, data, context)
1763 openerp.service.report.exp_report = exp_report
1765 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: