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 def _links_get(self, cr, uid, context=None):
120 @param cr: the current row, from the database cursor
121 @param uid: the current user's ID for security checks
122 @param context: a standard dictionary for contextual values
123 @return: list of dictionary which contain object and name and id
125 obj = self.pool.get('res.request.link')
126 ids = obj.search(cr, uid, [])
127 res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
128 return [(r['object'], r['name']) for r in res]
130 html_invitation = """
133 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
134 <title>%(name)s</title>
137 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
138 style="font-family: Arial, Sans-serif; font-size: 14">
140 <td width="100%%">Hello,</td>
143 <td width="100%%">You are invited for <i>%(company)s</i> Event.</td>
146 <td width="100%%">Below are the details of event. Hours and dates expressed in %(timezone)s time.</td>
150 <table cellspacing="0" cellpadding="5" border="0" summary=""
151 style="width: 90%%; font-family: Arial, Sans-serif; border: 1px Solid #ccc; background-color: #f6f6f6">
152 <tr valign="center" align="center">
153 <td bgcolor="DFDFDF">
159 <table cellpadding="8" cellspacing="0" border="0"
160 style="font-size: 14" summary="Eventdetails" bgcolor="f6f6f6"
164 <div><b>Start Date</b></div>
167 <td>%(start_date)s</td>
169 <div><b>End Date</b></div>
172 <td width="25%%">%(end_date)s</td>
175 <td><b>Description</b></td>
177 <td colspan="3">%(description)s</td>
181 <div><b>Location</b></div>
184 <td colspan="3">%(location)s</td>
188 <div><b>Event Attendees</b></div>
193 <div>%(attendees)s</div>
201 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
202 style="font-family: Arial, Sans-serif; font-size: 14">
204 <td width="100%%">From:</td>
207 <td width="100%%">%(user)s</td>
210 <td width="100%%">-<font color="a7a7a7">-------------------------</font></td>
213 <td width="100%%"> <font color="a7a7a7">%(sign)s</font></td>
220 class calendar_attendee(osv.osv):
222 Calendar Attendee Information
224 _name = 'calendar.attendee'
225 _description = 'Attendee information'
230 def _get_address(self, name=None, email=None):
232 Gives email information in ical CAL-ADDRESS type format.
233 @param name: name for CAL-ADDRESS value
234 @param email: email address for CAL-ADDRESS value
238 return (name or '') + (email and ('MAILTO:' + email) or '')
240 def _compute_data(self, cr, uid, ids, name, arg, context=None):
242 Compute data on function fields for attendee values.
243 @param cr: the current row, from the database cursor
244 @param uid: the current user's ID for security checks
245 @param ids: list of calendar attendee's IDs
246 @param name: name of field
247 @param context: a standard dictionary for contextual values
248 @return: dictionary of form {id: {'field Name': value'}}
252 for attdata in self.browse(cr, uid, ids, context=context):
255 if name == 'sent_by':
256 if not attdata.sent_by_uid:
257 result[id][name] = ''
260 result[id][name] = self._get_address(attdata.sent_by_uid.name, \
261 attdata.sent_by_uid.email)
265 result[id][name] = attdata.user_id.name
266 elif attdata.partner_id:
267 result[id][name] = attdata.partner_id.name or False
269 result[id][name] = attdata.email or ''
271 if name == 'delegated_to':
273 for child in attdata.child_ids:
275 todata.append('MAILTO:' + child.email)
276 result[id][name] = ', '.join(todata)
278 if name == 'delegated_from':
280 for parent in attdata.parent_ids:
282 fromdata.append('MAILTO:' + parent.email)
283 result[id][name] = ', '.join(fromdata)
285 if name == 'event_date':
287 result[id][name] = attdata.ref.date
289 result[id][name] = False
291 if name == 'event_end_date':
293 result[id][name] = attdata.ref.date_deadline
295 result[id][name] = False
297 if name == 'sent_by_uid':
299 result[id][name] = (attdata.ref.user_id.id, attdata.ref.user_id.name)
301 result[id][name] = uid
303 if name == 'language':
304 user_obj = self.pool.get('res.users')
305 lang = user_obj.read(cr, uid, uid, ['lang'], context=context)['lang']
306 result[id][name] = lang.replace('_', '-') if lang else False
310 def _links_get(self, cr, uid, context=None):
312 Get request link for ref field in calendar attendee.
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 object and name and id
318 obj = self.pool.get('res.request.link')
319 ids = obj.search(cr, uid, [])
320 res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
321 return [(r['object'], r['name']) for r in res]
323 def _lang_get(self, cr, uid, context=None):
325 Get language for language selection field.
326 @param cr: the current row, from the database cursor
327 @param uid: the current user's id for security checks
328 @param context: a standard dictionary for contextual values
329 @return: list of dictionary which contain code and name and id
331 obj = self.pool.get('res.lang')
332 ids = obj.search(cr, uid, [])
333 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
334 res = [((r['code']).replace('_', '-').lower(), r['name']) for r in res]
338 'cutype': fields.selection([('individual', 'Individual'), \
339 ('group', 'Group'), ('resource', 'Resource'), \
340 ('room', 'Room'), ('unknown', 'Unknown') ], \
341 'Invite Type', help="Specify the type of Invitation"),
342 'member': fields.char('Member', size=124,
343 help="Indicate the groups that the attendee belongs to"),
344 'role': fields.selection([('req-participant', 'Participation required'), \
345 ('chair', 'Chair Person'), \
346 ('opt-participant', 'Optional Participation'), \
347 ('non-participant', 'For information Purpose')], 'Role', \
348 help='Participation role for the calendar user'),
349 'state': fields.selection([('needs-action', 'Needs Action'),
350 ('tentative', 'Uncertain'),
351 ('declined', 'Declined'),
352 ('accepted', 'Accepted'),
353 ('delegated', 'Delegated')], 'Status', readonly=True, \
354 help="Status of the attendee's participation"),
355 'rsvp': fields.boolean('Required Reply?',
356 help="Indicats whether the favor of a reply is requested"),
357 'delegated_to': fields.function(_compute_data, \
358 string='Delegated To', type="char", size=124, store=True, \
359 multi='delegated_to', help="The users that the original \
360 request was delegated to"),
361 'delegated_from': fields.function(_compute_data, string=\
362 'Delegated From', type="char", store=True, size=124, multi='delegated_from'),
363 'parent_ids': fields.many2many('calendar.attendee', 'calendar_attendee_parent_rel', \
364 'attendee_id', 'parent_id', 'Delegrated From'),
365 'child_ids': fields.many2many('calendar.attendee', 'calendar_attendee_child_rel', \
366 'attendee_id', 'child_id', 'Delegrated To'),
367 'sent_by': fields.function(_compute_data, string='Sent By', \
368 type="char", multi='sent_by', store=True, size=124, \
369 help="Specify the user that is acting on behalf of the calendar user"),
370 'sent_by_uid': fields.function(_compute_data, string='Sent By User', \
371 type="many2one", relation="res.users", multi='sent_by_uid'),
372 'cn': fields.function(_compute_data, string='Common name', \
373 type="char", size=124, multi='cn', store=True),
374 'dir': fields.char('URI Reference', size=124, help="Reference to the URI\
375 that points to the directory information corresponding to the attendee."),
376 'language': fields.function(_compute_data, string='Language', \
377 type="selection", selection=_lang_get, multi='language', \
378 store=True, help="To specify the language for text values in a\
379 property or property parameter."),
380 'user_id': fields.many2one('res.users', 'User'),
381 'partner_id': fields.many2one('res.partner', 'Contact'),
382 'email': fields.char('Email', size=124, help="Email of Invited Person"),
383 'event_date': fields.function(_compute_data, string='Event Date', \
384 type="datetime", multi='event_date'),
385 'event_end_date': fields.function(_compute_data, \
386 string='Event End Date', type="datetime", \
387 multi='event_end_date'),
388 'ref': fields.reference('Event Ref', selection=_links_get, size=128),
389 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
392 'state': 'needs-action',
393 'role': 'req-participant',
395 'cutype': 'individual',
399 def copy(self, cr, uid, id, default=None, context=None):
400 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
402 def onchange_partner_id(self, cr, uid, ids, partner_id,context=None):
404 Make entry on email and availbility on change of partner_id field.
405 @param cr: the current row, from the database cursor
406 @param uid: the current user's ID for security checks
407 @param ids: list of calendar attendee's IDs
408 @param partner_id: changed value of partner id
409 @param context: a standard dictionary for contextual values
410 @return: dictionary of values which put value in email and availability fields
414 return {'value': {'email': ''}}
415 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
416 return {'value': {'email': partner.email}}
418 def get_ics_file(self, cr, uid, event_obj, context=None):
420 Returns iCalendar file for the event invitation.
421 @param self: the object pointer
422 @param cr: the current row, from the database cursor
423 @param uid: the current user's id for security checks
424 @param event_obj: event object (browse record)
425 @param context: a standard dictionary for contextual values
426 @return: .ics file content
429 def ics_datetime(idate, short=False):
431 #returns the datetime as UTC, because it is stored as it in the database
432 return datetime.strptime(idate, '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.timezone('UTC'))
435 # FIXME: why isn't this in CalDAV?
439 cal = vobject.iCalendar()
440 event = cal.add('vevent')
441 if not event_obj.date_deadline or not event_obj.date:
442 raise osv.except_osv(_('Warning!'),_("First you have to specify the date of the invitation."))
443 event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S'))
444 event.add('dtstart').value = ics_datetime(event_obj.date)
445 event.add('dtend').value = ics_datetime(event_obj.date_deadline)
446 event.add('summary').value = event_obj.name
447 if event_obj.description:
448 event.add('description').value = event_obj.description
449 if event_obj.location:
450 event.add('location').value = event_obj.location
452 event.add('rrule').value = event_obj.rrule
453 if event_obj.organizer:
454 event_org = event.add('organizer')
455 event_org.params['CN'] = [event_obj.organizer]
456 event_org.value = 'MAILTO:' + (event_obj.organizer)
457 elif event_obj.user_id or event_obj.organizer_id:
458 event_org = event.add('organizer')
459 organizer = event_obj.organizer_id
461 organizer = event_obj.user_id
462 event_org.params['CN'] = [organizer.name]
463 event_org.value = 'MAILTO:' + (organizer.email or organizer.name)
465 if event_obj.alarm_id:
466 # computes alarm data
467 valarm = event.add('valarm')
468 alarm_object = self.pool.get('res.alarm')
469 alarm_data = alarm_object.read(cr, uid, event_obj.alarm_id.id, context=context)
470 # Compute trigger data
471 interval = alarm_data['trigger_interval']
472 occurs = alarm_data['trigger_occurs']
473 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
474 or -(alarm_data['trigger_duration'])
475 related = alarm_data['trigger_related']
476 trigger = valarm.add('TRIGGER')
477 trigger.params['related'] = [related.upper()]
478 if interval == 'days':
479 delta = timedelta(days=duration)
480 if interval == 'hours':
481 delta = timedelta(hours=duration)
482 if interval == 'minutes':
483 delta = timedelta(minutes=duration)
484 trigger.value = delta
485 # Compute other details
486 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
488 for attendee in event_obj.attendee_ids:
489 attendee_add = event.add('attendee')
490 attendee_add.params['CUTYPE'] = [str(attendee.cutype)]
491 attendee_add.params['ROLE'] = [str(attendee.role)]
492 attendee_add.params['RSVP'] = [str(attendee.rsvp)]
493 attendee_add.value = 'MAILTO:' + (attendee.email or '')
494 res = cal.serialize()
497 def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
499 Send mail for event invitation to event attendees.
500 @param email_from: email address for user sending the mail
503 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
504 for att in self.browse(cr, uid, ids, context=context):
505 sign = att.sent_by_uid and att.sent_by_uid.signature or ''
506 sign = '<br>'.join(sign and sign.split('\n') or [])
511 other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
513 for att2 in self.browse(cr, uid, other_invitation_ids):
514 att_infos.append(((att2.user_id and att2.user_id.name) or \
515 (att2.partner_id and att2.partner_id.name) or \
516 att2.email) + ' - Status: ' + att2.state.title())
517 #dates and times are gonna be expressed in `tz` time (local timezone of the `uid`)
518 tz = context.get('tz', pytz.timezone('UTC'))
519 #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
520 date_start = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
522 if res_obj.date_deadline:
523 date_stop = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
524 body_vals = {'name': res_obj.name,
525 'start_date': date_start,
526 'end_date': date_stop,
528 'description': res_obj.description or '-',
529 'location': res_obj.location or '-',
530 'attendees': '<br>'.join(att_infos),
531 'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
535 body = html_invitation % body_vals
536 if mail_to and email_from:
537 ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
538 vals = {'email_from': email_from,
545 vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
546 'datas_fname': 'invitation.ics',
547 'datas': str(ics_file).encode('base64')})]
548 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
551 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
553 Make entry on email and availbility on change of user_id field.
554 @param cr: the current row, from the database cursor
555 @param uid: the current user's ID for security checks
556 @param ids: list of calendar attendee's IDs
557 @param user_id: changed value of User id
558 @return: dictionary of values which put value in email and availability fields
562 return {'value': {'email': ''}}
563 usr_obj = self.pool.get('res.users')
564 user = usr_obj.browse(cr, uid, user_id, *args)
565 return {'value': {'email': user.email, 'availability':user.availability}}
567 def do_tentative(self, cr, uid, ids, context=None, *args):
569 Makes event invitation as Tentative.
570 @param self: the object pointer
571 @param cr: the current row, from the database cursor
572 @param uid: the current user's ID for security checks
573 @param ids: list of calendar attendee's IDs
574 @param *args: get Tupple value
575 @param context: a standard dictionary for contextual values
577 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
579 def do_accept(self, cr, uid, ids, context=None, *args):
581 Update state of invitation as Accepted and if the invited user is other
582 then event user it will make a copy of this event for invited user.
583 @param cr: the current row, from the database cursor
584 @param uid: the current user's ID for security checks
585 @param ids: list of calendar attendee's IDs
586 @param context: a standard dictionary for contextual values
592 for vals in self.browse(cr, uid, ids, context=context):
593 if vals.ref and vals.ref.user_id:
594 mod_obj = self.pool[vals.ref._name]
595 res=mod_obj.read(cr,uid,[vals.ref.id],['duration','class'],context)
596 defaults = {'user_id': vals.user_id.id, 'organizer_id': vals.ref.user_id.id,'duration':res[0]['duration'],'class':res[0]['class']}
597 mod_obj.copy(cr, uid, vals.ref.id, default=defaults, context=context)
598 self.write(cr, uid, vals.id, {'state': 'accepted'}, context)
602 def do_decline(self, cr, uid, ids, context=None, *args):
604 Marks event invitation as Declined.
605 @param self: the object pointer
606 @param cr: the current row, from the database cursor
607 @param uid: the current user's ID for security checks
608 @param ids: list of calendar attendee's IDs
609 @param *args: get Tupple value
610 @param context: a standard dictionary for contextual values
614 return self.write(cr, uid, ids, {'state': 'declined'}, context)
616 def create(self, cr, uid, vals, context=None):
618 Overrides orm create method.
619 @param self: The object pointer
620 @param cr: the current row, from the database cursor
621 @param uid: the current user's ID for security checks
622 @param vals: get Values
623 @param context: a standard dictionary for contextual values
627 if not vals.get("email") and vals.get("cn"):
628 cnval = vals.get("cn").split(':')
629 email = filter(lambda x:x.__contains__('@'), cnval)
630 vals['email'] = email and email[0] or ''
631 vals['cn'] = vals.get("cn")
632 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
637 class res_alarm(osv.osv):
638 """Resource Alarm """
640 _description = 'Basic Alarm Information'
643 'name':fields.char('Name', size=256, required=True),
644 'trigger_occurs': fields.selection([('before', 'Before'), \
645 ('after', 'After')], \
646 'Triggers', required=True),
647 'trigger_interval': fields.selection([('minutes', 'Minutes'), \
648 ('hours', 'Hours'), \
649 ('days', 'Days')], 'Interval', \
651 'trigger_duration': fields.integer('Duration', required=True),
652 'trigger_related': fields.selection([('start', 'The event starts'), \
653 ('end', 'The event ends')], \
654 'Related to', required=True),
655 'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \
656 are both optional, but if one occurs, so MUST the other"""),
657 'repeat': fields.integer('Repeat'),
658 'active': fields.boolean('Active', help="If the active field is set to \
659 true, it will allow you to hide the event alarm information without removing it.")
662 'trigger_interval': 'minutes',
663 'trigger_duration': 5,
664 'trigger_occurs': 'before',
665 'trigger_related': 'start',
669 def do_alarm_create(self, cr, uid, ids, model, date, context=None):
671 Create Alarm for event.
672 @param cr: the current row, from the database cursor,
673 @param uid: the current user's ID for security checks,
674 @param ids: List of res alarm's IDs.
675 @param model: Model name.
676 @param date: Event date
677 @param context: A standard dictionary for contextual values
682 alarm_obj = self.pool.get('calendar.alarm')
683 res_alarm_obj = self.pool.get('res.alarm')
684 ir_obj = self.pool.get('ir.model')
685 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
687 model_obj = self.pool[model]
688 for data in model_obj.browse(cr, uid, ids, context=context):
690 basic_alarm = data.alarm_id
691 cal_alarm = data.base_calendar_alarm_id
692 if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm):
694 # Find for existing res.alarm
695 duration = cal_alarm.trigger_duration
696 interval = cal_alarm.trigger_interval
697 occurs = cal_alarm.trigger_occurs
698 related = cal_alarm.trigger_related
699 domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)]
700 alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context)
703 'trigger_duration': duration,
704 'trigger_interval': interval,
705 'trigger_occurs': occurs,
706 'trigger_related': related,
707 'name': str(duration) + ' ' + str(interval) + ' ' + str(occurs)
709 new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context)
711 new_res_alarm = alarm_ids[0]
712 cr.execute('UPDATE %s ' % model_obj._table + \
713 ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \
715 (cal_alarm.id, new_res_alarm, data.id))
717 self.do_alarm_unlink(cr, uid, [data.id], model)
721 'description': data.description,
723 'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))],
724 'trigger_related': basic_alarm.trigger_related,
725 'trigger_duration': basic_alarm.trigger_duration,
726 'trigger_occurs': basic_alarm.trigger_occurs,
727 'trigger_interval': basic_alarm.trigger_interval,
728 'duration': basic_alarm.duration,
729 'repeat': basic_alarm.repeat,
731 'event_date': data[date],
733 'model_id': model_id,
736 alarm_id = alarm_obj.create(cr, uid, vals)
737 cr.execute('UPDATE %s ' % model_obj._table + \
738 ' SET base_calendar_alarm_id=%s, alarm_id=%s '
740 ( alarm_id, basic_alarm.id, data.id) )
743 def do_alarm_unlink(self, cr, uid, ids, model, context=None):
745 Delete alarm specified in ids
746 @param cr: the current row, from the database cursor,
747 @param uid: the current user's ID for security checks,
748 @param ids: List of res alarm's IDs.
749 @param model: Model name for which alarm is to be cleared.
754 alarm_obj = self.pool.get('calendar.alarm')
755 ir_obj = self.pool.get('ir.model')
756 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
757 model_obj = self.pool[model]
758 for data in model_obj.browse(cr, uid, ids, context=context):
759 alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', data.id)])
761 alarm_obj.unlink(cr, uid, alarm_ids)
762 cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\
763 where id=%%s' % model_obj._table,(data.id,))
768 class calendar_alarm(osv.osv):
769 _name = 'calendar.alarm'
770 _description = 'Event alarm information'
771 _inherit = 'res.alarm'
775 'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'),
776 'name': fields.char('Summary', size=124, help="""Contains the text to be \
777 used as the message subject for email \
778 or contains the text to be used for display"""),
779 'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \
780 ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \
781 required=True, help="Defines the action to be invoked when an alarm is triggered"),
782 'description': fields.text('Description', help='Provides a more complete \
783 description of the calendar component, than that \
784 provided by the "SUMMARY" property'),
785 'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \
786 'alarm_id', 'attendee_id', 'Attendees', readonly=True),
787 'attach': fields.binary('Attachment', help="""* Points to a sound resource,\
788 which is rendered when the alarm is triggered for audio,
789 * File which is intended to be sent as message attachments for email,
790 * Points to a procedure resource, which is invoked when\
791 the alarm is triggered for procedure."""),
792 'res_id': fields.integer('Resource ID'),
793 'model_id': fields.many2one('ir.model', 'Model'),
794 'user_id': fields.many2one('res.users', 'Owner'),
795 'event_date': fields.datetime('Event Date'),
796 'event_end_date': fields.datetime('Event End Date'),
797 'trigger_date': fields.datetime('Trigger Date', readonly="True"),
798 'state':fields.selection([
803 ], 'Status', select=True, readonly=True),
811 def create(self, cr, uid, vals, context=None):
813 Overrides orm create method.
814 @param self: The object pointer
815 @param cr: the current row, from the database cursor,
816 @param vals: dictionary of fields value.{'name_of_the_field': value, ...}
817 @param context: A standard dictionary for contextual values
818 @return: new record id for calendar_alarm.
822 event_date = vals.get('event_date', False)
824 dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S")
825 if vals['trigger_interval'] == 'days':
826 delta = timedelta(days=vals['trigger_duration'])
827 if vals['trigger_interval'] == 'hours':
828 delta = timedelta(hours=vals['trigger_duration'])
829 if vals['trigger_interval'] == 'minutes':
830 delta = timedelta(minutes=vals['trigger_duration'])
831 trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta)
832 vals['trigger_date'] = trigger_date
833 res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
836 def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
838 """Scheduler for event reminder
839 @param self: The object pointer
840 @param cr: the current row, from the database cursor,
841 @param uid: the current user's ID for security checks,
842 @param ids: List of calendar alarm's IDs.
843 @param use_new_cursor: False or the dbname
844 @param context: A standard dictionary for contextual values
848 current_datetime = datetime.now()
849 alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
853 for alarm in self.browse(cr, uid, alarm_ids, context=context):
854 next_trigger_date = None
856 model_obj = self.pool[alarm.model_id.model]
857 res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
860 if hasattr(res_obj, 'rrule') and res_obj.rrule:
861 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
862 #exdate is a string and we need a list
863 exdate = res_obj.exdate and res_obj.exdate.split(',') or []
864 recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule)
866 trigger_interval = alarm.trigger_interval
867 if trigger_interval == 'days':
868 delta = timedelta(days=alarm.trigger_duration)
869 if trigger_interval == 'hours':
870 delta = timedelta(hours=alarm.trigger_duration)
871 if trigger_interval == 'minutes':
872 delta = timedelta(minutes=alarm.trigger_duration)
873 delta = alarm.trigger_occurs == 'after' and delta or -delta
875 for rdate in recurrent_dates:
876 if rdate + delta > current_datetime:
878 if rdate + delta <= current_datetime:
879 re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
880 rest_dates = recurrent_dates[len(re_dates):]
881 next_trigger_date = rest_dates and rest_dates[0] or None
884 re_dates = [alarm.trigger_date]
887 if alarm.action == 'email':
888 sub = '[OpenERP Reminder] %s' % (alarm.name)
900 """ % (alarm.name, alarm.trigger_date, alarm.description, \
901 alarm.user_id.name, alarm.user_id.signature)
902 mail_to = alarm.user_id.email
903 for att in alarm.attendee_ids:
904 mail_to = mail_to + " " + att.user_id.email
911 'email_from': tools.config.get('email_from', mail_to),
913 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
914 if next_trigger_date:
915 update_vals.update({'trigger_date': next_trigger_date})
917 update_vals.update({'state': 'done'})
918 self.write(cr, uid, [alarm.id], update_vals)
924 class calendar_event(osv.osv):
925 _name = "calendar.event"
926 _description = "Calendar Event"
929 def _tz_get(self, cr, uid, context=None):
930 return [(x.lower(), x) for x in pytz.all_timezones]
932 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
933 """Returns duration and/or end date based on values passed
934 @param self: The object pointer
935 @param cr: the current row, from the database cursor,
936 @param uid: the current user's ID for security checks,
937 @param ids: List of calendar event's IDs.
938 @param start_date: Starting date
939 @param duration: Duration between start date and end date
940 @param end_date: Ending Datee
941 @param context: A standard dictionary for contextual values
949 if not end_date and not duration:
951 value['duration'] = duration
953 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
954 if allday: # For all day event
956 value['duration'] = duration
957 # change start_date's time to 00:00:00 in the user's timezone
958 user = self.pool.get('res.users').browse(cr, uid, uid)
959 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
960 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
961 start = start.replace(hour=0, minute=0, second=0) # change start's time to 00:00:00
962 start = start.astimezone(pytz.utc) # convert start back to utc
963 start_date = start.strftime("%Y-%m-%d %H:%M:%S")
964 value['date'] = start_date
966 if end_date and not duration:
967 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
969 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
970 value['duration'] = round(duration, 2)
972 end = start + timedelta(hours=duration)
973 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
974 elif end_date and duration and not allday:
975 # we have both, keep them synchronized:
976 # set duration based on end_date (arbitrary decision: this avoid
977 # getting dates like 06:31:48 instead of 06:32:00)
978 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
980 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
981 value['duration'] = round(duration, 2)
983 return {'value': value}
985 def unlink_events(self, cr, uid, ids, context=None):
987 This function deletes event which are linked with the event with recurrent_id
988 (Removes the events which refers to the same UID value)
993 cr.execute("select id from %s where recurrent_id=%%s" % (self._table), (event_id,))
994 r_ids = map(lambda x: x[0], cr.fetchall())
995 self.unlink(cr, uid, r_ids, context=context)
998 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
1000 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
1001 @param self: The object pointer
1002 @param cr: the current row, from the database cursor,
1003 @param id: List of calendar event's ids.
1004 @param context: A standard dictionary for contextual values
1005 @return: dictionary of rrule value.
1009 if not isinstance(ids, list):
1013 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
1014 data = self.read(cr, SUPERUSER_ID, id, ['interval', 'count'], context=context)
1015 if data.get('interval', 0) < 0:
1016 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
1017 if data.get('count', 0) <= 0:
1018 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
1019 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)
1021 if data['recurrency']:
1022 result[event] = self.compute_rule_string(data)
1027 def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1028 data = self._get_empty_rrule_data()
1030 data['recurrency'] = True
1031 for event in self.browse(cr, uid, ids, context=context):
1032 rdate = rule_date or event.date
1033 update_data = self._parse_rrule(field_value, dict(data), rdate)
1034 data.update(update_data)
1035 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
1039 'id': fields.integer('ID', readonly=True),
1040 'sequence': fields.integer('Sequence'),
1041 'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1042 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True,),
1043 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
1044 'create_date': fields.datetime('Created', readonly=True),
1045 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1046 'description': fields.text('Description', states={'done': [('readonly', True)]}),
1047 'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1048 ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
1049 'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1050 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1051 'Show Time as', states={'done': [('readonly', True)]}),
1052 'base_calendar_url': fields.char('Caldav URL', size=264),
1053 'state': fields.selection([
1054 ('tentative', 'Uncertain'),
1055 ('cancelled', 'Cancelled'),
1056 ('confirmed', 'Confirmed'),
1057 ], 'Status', readonly=True),
1058 'exdate': fields.text('Exception Date/Times', help="This property \
1059 defines the list of date/time exceptions for a recurring calendar component."),
1060 'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1061 rule or repeating pattern of time to exclude from the recurring rule."),
1062 'rrule': fields.function(_get_rulestring, type='char', size=124, \
1063 fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1064 'rrule_type': fields.selection([
1065 ('daily', 'Day(s)'),
1066 ('weekly', 'Week(s)'),
1067 ('monthly', 'Month(s)'),
1068 ('yearly', 'Year(s)')
1069 ], 'Recurrency', states={'done': [('readonly', True)]},
1070 help="Let the event automatically repeat at that interval"),
1071 'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]},
1072 help="Set an alarm at this time, before the event occurs" ),
1073 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1074 'recurrent_id': fields.integer('Recurrent ID'),
1075 'recurrent_id_date': fields.datetime('Recurrent ID date'),
1076 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1077 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1078 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with organizer attribute of VEvent.
1079 'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1080 'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'),
1081 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
1082 'count': fields.integer('Repeat', help="Repeat x times"),
1083 'mo': fields.boolean('Mon'),
1084 'tu': fields.boolean('Tue'),
1085 'we': fields.boolean('Wed'),
1086 'th': fields.boolean('Thu'),
1087 'fr': fields.boolean('Fri'),
1088 'sa': fields.boolean('Sat'),
1089 'su': fields.boolean('Sun'),
1090 'select1': fields.selection([('date', 'Date of month'),
1091 ('day', 'Day of month')], 'Option'),
1092 'day': fields.integer('Date of month'),
1093 'week_list': fields.selection([
1096 ('WE', 'Wednesday'),
1100 ('SU', 'Sunday')], 'Weekday'),
1101 'byday': fields.selection([
1107 ('-1', 'Last')], 'By day'),
1108 'month_list': fields.selection(months.items(), 'Month'),
1109 'end_date': fields.date('Repeat Until'),
1110 'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1111 'event_id', 'attendee_id', 'Attendees'),
1112 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1113 'active': fields.boolean('Active', help="If the active field is set to \
1114 true, it will allow you to hide the event alarm information without removing it."),
1115 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1116 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
1119 def create_attendees(self, cr, uid, ids, context):
1120 att_obj = self.pool.get('calendar.attendee')
1121 user_obj = self.pool.get('res.users')
1122 current_user = user_obj.browse(cr, uid, uid, context=context)
1123 for event in self.browse(cr, uid, ids, context):
1125 for att in event.attendee_ids:
1126 attendees[att.partner_id.id] = True
1129 for partner in event.partner_ids:
1130 if partner.id in attendees:
1132 att_id = self.pool.get('calendar.attendee').create(cr, uid, {
1133 'partner_id': partner.id,
1134 'user_id': partner.user_ids and partner.user_ids[0].id or False,
1135 'ref': self._name+','+str(event.id),
1136 'email': partner.email
1139 mail_to = mail_to + " " + partner.email
1140 self.write(cr, uid, [event.id], {
1141 'attendee_ids': [(4, att_id)]
1143 new_attendees.append(att_id)
1145 if mail_to and current_user.email:
1146 att_obj._send_mail(cr, uid, new_attendees, mail_to,
1147 email_from = current_user.email, context=context)
1150 def default_organizer(self, cr, uid, context=None):
1151 user_pool = self.pool.get('res.users')
1152 user = user_pool.browse(cr, uid, uid, context=context)
1155 res += " <%s>" %(user.email)
1159 'end_type': 'count',
1161 'rrule_type': False,
1162 'state': 'tentative',
1168 'user_id': lambda self, cr, uid, ctx: uid,
1169 'organizer': default_organizer,
1172 def _check_closing_date(self, cr, uid, ids, context=None):
1173 for event in self.browse(cr, uid, ids, context=context):
1174 if event.date_deadline < event.date:
1179 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
1182 def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1183 """Gives virtual event ids for recurring events based on value of Recurrence Rule
1184 This method gives ids of dates that comes between start date and end date of calendar views
1185 @param self: The object pointer
1186 @param cr: the current row, from the database cursor,
1187 @param uid: the current user's ID for security checks,
1188 @param limit: The Number of Results to Return """
1193 for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'exdate', 'exrule', 'date'], context=context):
1194 if not data['rrule']:
1195 result.append(data['id'])
1197 event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1199 # TOCHECK: the start date should be replaced by event date; the event date will be changed by that of calendar code
1201 if not data['rrule']:
1204 exdate = data['exdate'] and data['exdate'].split(',') or []
1205 rrule_str = data['rrule']
1207 rrule_until_date = False
1209 for rule in rrule_str.split(';'):
1210 name, value = rule.split('=')
1213 value = parser.parse(value)
1214 rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1215 value = value.strftime("%Y%m%d%H%M%S")
1216 new_rule = '%s=%s' % (name, value)
1217 new_rrule_str.append(new_rule)
1218 new_rrule_str = ';'.join(new_rrule_str)
1219 rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1220 for r_date in rdates:
1223 if arg[0] in ('date', 'date_deadline'):
1225 ok = ok and r_date.strftime('%Y-%m-%d')==arg[2]
1227 ok = ok and r_date.strftime('%Y-%m-%d')>arg[2]
1229 ok = ok and r_date.strftime('%Y-%m-%d')<arg[2]
1231 ok = ok and r_date.strftime('%Y-%m-%d')>=arg[2]
1233 ok = ok and r_date.strftime('%Y-%m-%d')<=arg[2]
1236 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1237 result.append(idval)
1239 if isinstance(select, (str, int, long)):
1240 return ids and ids[0] or False
1242 ids = list(set(result))
1245 def compute_rule_string(self, data):
1247 Compute rule string according to value type RECUR of iCalendar from the values given.
1248 @param self: the object pointer
1249 @param data: dictionary of freq and interval value
1250 @return: string containing recurring rule (empty if no rule)
1252 def get_week_string(freq, data):
1253 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1254 if freq == 'weekly':
1255 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1257 return ';BYDAY=' + ','.join(byday)
1260 def get_month_string(freq, data):
1261 if freq == 'monthly':
1262 if data.get('select1')=='date' and (data.get('day') < 1 or data.get('day') > 31):
1263 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1265 if data.get('select1')=='day':
1266 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1267 elif data.get('select1')=='date':
1268 return ';BYMONTHDAY=' + str(data.get('day'))
1271 def get_end_date(data):
1272 if data.get('end_date'):
1273 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1275 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1276 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1278 freq = data.get('rrule_type', False)
1281 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1282 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1286 def _get_empty_rrule_data(self):
1289 'recurrency' : False,
1291 'rrule_type' : False,
1308 def _parse_rrule(self, rule, data, date_start):
1309 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1310 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1311 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1313 if r._freq > 0 and r._freq < 4:
1314 data['rrule_type'] = rrule_type[r._freq]
1316 data['count'] = r._count
1317 data['interval'] = r._interval
1318 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1321 for i in xrange(0,7):
1322 if i in r._byweekday:
1323 data[day_list[i]] = True
1324 data['rrule_type'] = 'weekly'
1325 #repeat monthly by nweekday ((weekday, weeknumber), )
1327 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1328 data['byday'] = r._bynweekday[0][1]
1329 data['select1'] = 'day'
1330 data['rrule_type'] = 'monthly'
1333 data['day'] = r._bymonthday[0]
1334 data['select1'] = 'date'
1335 data['rrule_type'] = 'monthly'
1337 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1339 data['interval'] = data['interval'] * 12
1341 #FIXEME handle forever case
1343 #in case of repeat for ever that we do not support right now
1344 if not (data.get('count') or data.get('end_date')):
1346 if data.get('count'):
1347 data['end_type'] = 'count'
1349 data['end_type'] = 'end_date'
1352 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1359 if arg[0] in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1360 if context.get('virtual_id', True):
1361 new_args += ['|','&',('recurrency','=',1),('recurrent_id_date', arg[1], arg[2])]
1362 elif arg[0] == "id":
1363 new_id = get_real_ids(arg[2])
1364 new_arg = (arg[0], arg[1], new_id)
1365 new_args.append(new_arg)
1367 #offset, limit and count must be treated separately as we may need to deal with virtual ids
1368 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=order, context=context, count=False)
1369 if context.get('virtual_id', True):
1370 res = self.get_recurrent_ids(cr, uid, res, new_args, limit, context=context)
1374 return res[offset:offset+limit]
1377 def _get_data(self, cr, uid, id, context=None):
1378 return self.read(cr, uid, id,['date', 'date_deadline'])
1380 def need_to_update(self, event_id, vals):
1381 split_id = str(event_id).split("-")
1382 if len(split_id) < 2:
1385 date_start = vals.get('date', '')
1387 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1388 return date_start == split_id[1]
1392 def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1393 def _only_changes_to_apply_on_real_ids(field_names):
1394 ''' return True if changes are only to be made on the real ids'''
1395 for field in field_names:
1396 if field not in ['message_follower_ids']:
1400 context = context or {}
1401 if isinstance(ids, (str, int, long)):
1405 # Special write of complex IDS
1406 for event_id in ids[:]:
1407 if len(str(event_id).split('-')) == 1:
1409 ids.remove(event_id)
1410 real_event_id = base_calendar_id2real_id(event_id)
1412 # if we are setting the recurrency flag to False or if we are only changing fields that
1413 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1414 # then set real ids to be updated.
1415 if not vals.get('recurrency', True) or _only_changes_to_apply_on_real_ids(vals.keys()):
1416 ids.append(real_event_id)
1419 #if edit one instance of a reccurrent id
1420 data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1421 'rrule', 'duration', 'exdate'])
1422 if data.get('rrule'):
1425 recurrent_id=real_event_id,
1426 recurrent_id_date=data.get('date'),
1434 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1436 date_new = event_id.split('-')[1]
1437 date_new = time.strftime("%Y%m%dT%H%M%S", \
1438 time.strptime(date_new, "%Y%m%d%H%M%S"))
1439 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1440 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1442 context.update({'active_id': new_id, 'active_ids': [new_id]})
1445 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1446 vals['vtimezone'] = vals['vtimezone'][40:]
1448 res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1449 if vals.get('partner_ids', False):
1450 self.create_attendees(cr, uid, ids, context)
1452 if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1453 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1454 alarm_obj = self.pool.get('res.alarm')
1455 alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1456 return res or True and False
1458 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1462 if 'date' in groupby:
1463 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1464 virtual_id = context.get('virtual_id', True)
1465 context.update({'virtual_id': False})
1466 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1468 #remove the count, since the value is not consistent with the result of the search when expand the group
1469 for groupname in groupby:
1470 if re.get(groupname + "_count"):
1471 del re[groupname + "_count"]
1472 re.get('__context', {}).update({'virtual_id' : virtual_id})
1475 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1478 fields2 = fields and fields[:] or None
1480 EXTRAFIELDS = ('class','user_id','duration')
1481 for f in EXTRAFIELDS:
1482 if fields and (f not in fields):
1485 # FIXME This whole id mangling has to go!
1486 if isinstance(ids, (str, int, long)):
1491 select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1494 real_data = super(calendar_event, self).read(cr, uid,
1495 [real_id for base_calendar_id, real_id in select],
1496 fields=fields2, context=context, load=load)
1497 real_data = dict(zip([x['id'] for x in real_data], real_data))
1499 for base_calendar_id, real_id in select:
1500 res = real_data[real_id].copy()
1501 ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1502 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1504 res['date_deadline'] = ls[2]
1505 res['id'] = base_calendar_id
1511 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1514 if r['class']=='private':
1516 if f not in ('id','date','date_deadline','duration','user_id','state'):
1517 if isinstance(r[f], list):
1525 for k in EXTRAFIELDS:
1526 if (k in r) and (fields and (k not in fields)):
1528 if isinstance(ids, (str, int, long)):
1529 return result and result[0] or False
1532 def copy(self, cr, uid, id, default=None, context=None):
1536 res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1537 alarm_obj = self.pool.get('res.alarm')
1538 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1541 def unlink(self, cr, uid, ids, context=None):
1542 if not isinstance(ids, list):
1545 attendee_obj=self.pool.get('calendar.attendee')
1546 for event_id in ids[:]:
1547 if len(str(event_id).split('-')) == 1:
1550 real_event_id = base_calendar_id2real_id(event_id)
1551 data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1552 date_new = event_id.split('-')[1]
1553 date_new = time.strftime("%Y%m%dT%H%M%S", \
1554 time.strptime(date_new, "%Y%m%d%H%M%S"))
1555 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1556 self.write(cr, uid, [real_event_id], {'exdate': exdate})
1557 ids.remove(event_id)
1558 for event in self.browse(cr, uid, ids, context=context):
1559 if event.attendee_ids:
1560 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1562 res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1563 self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1564 self.unlink_events(cr, uid, ids, context=context)
1567 def create(self, cr, uid, vals, context=None):
1571 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1572 vals['vtimezone'] = vals['vtimezone'][40:]
1574 res = super(calendar_event, self).create(cr, uid, vals, context)
1575 alarm_obj = self.pool.get('res.alarm')
1576 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1577 self.create_attendees(cr, uid, [res], context)
1580 def do_tentative(self, cr, uid, ids, context=None, *args):
1581 """ Makes event invitation as Tentative
1582 @param self: The object pointer
1583 @param cr: the current row, from the database cursor,
1584 @param uid: the current user's ID for security checks,
1585 @param ids: List of Event IDs
1586 @param *args: Get Tupple value
1587 @param context: A standard dictionary for contextual values
1589 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1591 def do_cancel(self, cr, uid, ids, context=None, *args):
1592 """ Makes event invitation as Tentative
1593 @param self: The object pointer
1594 @param cr: the current row, from the database cursor,
1595 @param uid: the current user's ID for security checks,
1596 @param ids: List of Event IDs
1597 @param *args: Get Tupple value
1598 @param context: A standard dictionary for contextual values
1600 return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1602 def do_confirm(self, cr, uid, ids, context=None, *args):
1603 """ Makes event invitation as Tentative
1604 @param self: The object pointer
1605 @param cr: the current row, from the database cursor,
1606 @param uid: the current user's ID for security checks,
1607 @param ids: List of Event IDs
1608 @param *args: Get Tupple value
1609 @param context: A standard dictionary for contextual values
1611 return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1615 class calendar_todo(osv.osv):
1616 """ Calendar Task """
1618 _name = "calendar.todo"
1619 _inherit = "calendar.event"
1620 _description = "Calendar Task"
1622 def _get_date(self, cr, uid, ids, name, arg, context=None):
1625 @param self: The object pointer
1626 @param cr: the current row, from the database cursor,
1627 @param uid: the current user's ID for security checks,
1628 @param ids: List of calendar todo's IDs.
1629 @param args: list of tuples of form [(‘name_of_the_field', ‘operator', value), ...].
1630 @param context: A standard dictionary for contextual values
1634 for event in self.browse(cr, uid, ids, context=context):
1635 res[event.id] = event.date_start
1638 def _set_date(self, cr, uid, id, name, value, arg, context=None):
1641 @param self: The object pointer
1642 @param cr: the current row, from the database cursor,
1643 @param uid: the current user's ID for security checks,
1644 @param id: calendar's ID.
1645 @param value: Get Value
1646 @param args: list of tuples of form [('name_of_the_field', 'operator', value), ...].
1647 @param context: A standard dictionary for contextual values
1650 assert name == 'date'
1651 return self.write(cr, uid, id, { 'date_start': value }, context=context)
1654 'date': fields.function(_get_date, fnct_inv=_set_date, \
1655 string='Duration', store=True, type='datetime'),
1656 'duration': fields.integer('Duration'),
1665 class ir_values(osv.osv):
1666 _inherit = 'ir.values'
1668 def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1669 isobject=False, meta=False, preserve_user=False, company=False):
1672 @param self: The object pointer
1673 @param cr: the current row, from the database cursor,
1674 @param uid: the current user's ID for security checks,
1675 @param model: Get The Model
1680 if type(data) in (list, tuple):
1681 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1683 new_model.append(data)
1684 return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1685 value, replace, isobject, meta, preserve_user, company)
1687 def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1688 res_id_req=False, without_user=True, key2_req=True):
1691 @param self: The object pointer
1692 @param cr: the current row, from the database cursor,
1693 @param uid: the current user's ID for security checks,
1694 @param model: Get The Model
1700 if type(data) in (list, tuple):
1701 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1703 new_model.append(data)
1704 return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1705 meta, context, res_id_req, without_user, key2_req)
1709 class ir_model(osv.osv):
1711 _inherit = 'ir.model'
1713 def read(self, cr, uid, ids, fields=None, context=None,
1714 load='_classic_read'):
1716 Overrides orm read method.
1717 @param self: The object pointer
1718 @param cr: the current row, from the database cursor,
1719 @param uid: the current user's ID for security checks,
1720 @param ids: List of IR Model's IDs.
1721 @param context: A standard dictionary for contextual values
1723 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1726 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1727 context=context, load=load)
1730 val['id'] = base_calendar_id2real_id(val['id'])
1731 return isinstance(ids, (str, int, long)) and data[0] or data
1735 original_exp_report = openerp.service.report.exp_report
1737 def exp_report(db, uid, object, ids, data=None, context=None):
1740 @param db: get the current database,
1741 @param uid: the current user's ID for security checks,
1742 @param context: A standard dictionary for contextual values
1745 if object == 'printscreen.list':
1746 original_exp_report(db, uid, object, ids, data, context)
1749 new_ids.append(base_calendar_id2real_id(id))
1750 if data.get('id', False):
1751 data['id'] = base_calendar_id2real_id(data['id'])
1752 return original_exp_report(db, uid, object, new_ids, data, context)
1754 openerp.service.report.exp_report = exp_report
1756 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: