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.get(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.get(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.get(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.get(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 recurrent_dates = get_recurrent_dates(res_obj.rrule, res_obj.exdate, event_date, res_obj.exrule)
864 trigger_interval = alarm.trigger_interval
865 if trigger_interval == 'days':
866 delta = timedelta(days=alarm.trigger_duration)
867 if trigger_interval == 'hours':
868 delta = timedelta(hours=alarm.trigger_duration)
869 if trigger_interval == 'minutes':
870 delta = timedelta(minutes=alarm.trigger_duration)
871 delta = alarm.trigger_occurs == 'after' and delta or -delta
873 for rdate in recurrent_dates:
874 if rdate + delta > current_datetime:
876 if rdate + delta <= current_datetime:
877 re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
878 rest_dates = recurrent_dates[len(re_dates):]
879 next_trigger_date = rest_dates and rest_dates[0] or None
882 re_dates = [alarm.trigger_date]
885 if alarm.action == 'email':
886 sub = '[OpenERP Reminder] %s' % (alarm.name)
898 """ % (alarm.name, alarm.trigger_date, alarm.description, \
899 alarm.user_id.name, alarm.user_id.signature)
900 mail_to = alarm.user_id.email
901 for att in alarm.attendee_ids:
902 mail_to = mail_to + " " + att.user_id.email
909 'email_from': tools.config.get('email_from', mail_to),
911 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
912 if next_trigger_date:
913 update_vals.update({'trigger_date': next_trigger_date})
915 update_vals.update({'state': 'done'})
916 self.write(cr, uid, [alarm.id], update_vals)
922 class calendar_event(osv.osv):
923 _name = "calendar.event"
924 _description = "Calendar Event"
927 def _tz_get(self, cr, uid, context=None):
928 return [(x.lower(), x) for x in pytz.all_timezones]
930 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
931 """Returns duration and/or end date based on values passed
932 @param self: The object pointer
933 @param cr: the current row, from the database cursor,
934 @param uid: the current user's ID for security checks,
935 @param ids: List of calendar event's IDs.
936 @param start_date: Starting date
937 @param duration: Duration between start date and end date
938 @param end_date: Ending Datee
939 @param context: A standard dictionary for contextual values
947 if not end_date and not duration:
949 value['duration'] = duration
951 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
952 if allday: # For all day event
954 value['duration'] = duration
955 # change start_date's time to 00:00:00 in the user's timezone
956 user = self.pool.get('res.users').browse(cr, uid, uid)
957 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
958 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
959 start = start.replace(hour=0, minute=0, second=0) # change start's time to 00:00:00
960 start = start.astimezone(pytz.utc) # convert start back to utc
961 start_date = start.strftime("%Y-%m-%d %H:%M:%S")
962 value['date'] = start_date
964 if end_date and not duration:
965 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
967 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
968 value['duration'] = round(duration, 2)
970 end = start + timedelta(hours=duration)
971 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
972 elif end_date and duration and not allday:
973 # we have both, keep them synchronized:
974 # set duration based on end_date (arbitrary decision: this avoid
975 # getting dates like 06:31:48 instead of 06:32:00)
976 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
978 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
979 value['duration'] = round(duration, 2)
981 return {'value': value}
983 def unlink_events(self, cr, uid, ids, context=None):
985 This function deletes event which are linked with the event with recurrent_id
986 (Removes the events which refers to the same UID value)
991 cr.execute("select id from %s where recurrent_id=%%s" % (self._table), (event_id,))
992 r_ids = map(lambda x: x[0], cr.fetchall())
993 self.unlink(cr, uid, r_ids, context=context)
996 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
998 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
999 @param self: The object pointer
1000 @param cr: the current row, from the database cursor,
1001 @param id: List of calendar event's ids.
1002 @param context: A standard dictionary for contextual values
1003 @return: dictionary of rrule value.
1007 if not isinstance(ids, list):
1011 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
1012 data = self.read(cr, SUPERUSER_ID, id, ['interval', 'count'], context=context)
1013 if data.get('interval', 0) < 0:
1014 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
1015 if data.get('count', 0) <= 0:
1016 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
1017 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)
1019 if data['recurrency']:
1020 result[event] = self.compute_rule_string(data)
1025 def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1026 data = self._get_empty_rrule_data()
1028 data['recurrency'] = True
1029 for event in self.browse(cr, uid, ids, context=context):
1030 rdate = rule_date or event.date
1031 update_data = self._parse_rrule(field_value, dict(data), rdate)
1032 data.update(update_data)
1033 super(calendar_event, obj).write(cr, uid, ids, data, context=context)
1037 'id': fields.integer('ID', readonly=True),
1038 'sequence': fields.integer('Sequence'),
1039 'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1040 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True,),
1041 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
1042 'create_date': fields.datetime('Created', readonly=True),
1043 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1044 'description': fields.text('Description', states={'done': [('readonly', True)]}),
1045 'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1046 ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
1047 'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1048 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1049 'Show Time as', states={'done': [('readonly', True)]}),
1050 'base_calendar_url': fields.char('Caldav URL', size=264),
1051 'state': fields.selection([
1052 ('tentative', 'Uncertain'),
1053 ('cancelled', 'Cancelled'),
1054 ('confirmed', 'Confirmed'),
1055 ], 'Status', readonly=True),
1056 'exdate': fields.text('Exception Date/Times', help="This property \
1057 defines the list of date/time exceptions for a recurring calendar component."),
1058 'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1059 rule or repeating pattern of time to exclude from the recurring rule."),
1060 'rrule': fields.function(_get_rulestring, type='char', size=124, \
1061 fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
1062 'rrule_type': fields.selection([
1063 ('daily', 'Day(s)'),
1064 ('weekly', 'Week(s)'),
1065 ('monthly', 'Month(s)'),
1066 ('yearly', 'Year(s)')
1067 ], 'Recurrency', states={'done': [('readonly', True)]},
1068 help="Let the event automatically repeat at that interval"),
1069 'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]},
1070 help="Set an alarm at this time, before the event occurs" ),
1071 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1072 'recurrent_id': fields.integer('Recurrent ID'),
1073 'recurrent_id_date': fields.datetime('Recurrent ID date'),
1074 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1075 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1076 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with organizer attribute of VEvent.
1077 'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1078 'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'),
1079 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
1080 'count': fields.integer('Repeat', help="Repeat x times"),
1081 'mo': fields.boolean('Mon'),
1082 'tu': fields.boolean('Tue'),
1083 'we': fields.boolean('Wed'),
1084 'th': fields.boolean('Thu'),
1085 'fr': fields.boolean('Fri'),
1086 'sa': fields.boolean('Sat'),
1087 'su': fields.boolean('Sun'),
1088 'select1': fields.selection([('date', 'Date of month'),
1089 ('day', 'Day of month')], 'Option'),
1090 'day': fields.integer('Date of month'),
1091 'week_list': fields.selection([
1094 ('WE', 'Wednesday'),
1098 ('SU', 'Sunday')], 'Weekday'),
1099 'byday': fields.selection([
1105 ('-1', 'Last')], 'By day'),
1106 'month_list': fields.selection(months.items(), 'Month'),
1107 'end_date': fields.date('Repeat Until'),
1108 'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1109 'event_id', 'attendee_id', 'Attendees'),
1110 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1111 'active': fields.boolean('Active', help="If the active field is set to \
1112 true, it will allow you to hide the event alarm information without removing it."),
1113 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1114 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
1117 def create_attendees(self, cr, uid, ids, context):
1118 att_obj = self.pool.get('calendar.attendee')
1119 user_obj = self.pool.get('res.users')
1120 current_user = user_obj.browse(cr, uid, uid, context=context)
1121 for event in self.browse(cr, uid, ids, context):
1123 for att in event.attendee_ids:
1124 attendees[att.partner_id.id] = True
1127 for partner in event.partner_ids:
1128 if partner.id in attendees:
1130 att_id = self.pool.get('calendar.attendee').create(cr, uid, {
1131 'partner_id': partner.id,
1132 'user_id': partner.user_ids and partner.user_ids[0].id or False,
1133 'ref': self._name+','+str(event.id),
1134 'email': partner.email
1137 mail_to = mail_to + " " + partner.email
1138 self.write(cr, uid, [event.id], {
1139 'attendee_ids': [(4, att_id)]
1141 new_attendees.append(att_id)
1143 if mail_to and current_user.email:
1144 att_obj._send_mail(cr, uid, new_attendees, mail_to,
1145 email_from = current_user.email, context=context)
1148 def default_organizer(self, cr, uid, context=None):
1149 user_pool = self.pool.get('res.users')
1150 user = user_pool.browse(cr, uid, uid, context=context)
1153 res += " <%s>" %(user.email)
1157 'end_type': 'count',
1159 'rrule_type': False,
1160 'state': 'tentative',
1166 'user_id': lambda self, cr, uid, ctx: uid,
1167 'organizer': default_organizer,
1170 def _check_closing_date(self, cr, uid, ids, context=None):
1171 for event in self.browse(cr, uid, ids, context=context):
1172 if event.date_deadline < event.date:
1177 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
1180 def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1181 """Gives virtual event ids for recurring events based on value of Recurrence Rule
1182 This method gives ids of dates that comes between start date and end date of calendar views
1183 @param self: The object pointer
1184 @param cr: the current row, from the database cursor,
1185 @param uid: the current user's ID for security checks,
1186 @param limit: The Number of Results to Return """
1191 for data in super(calendar_event, self).read(cr, uid, select, ['rrule', 'exdate', 'exrule', 'date'], context=context):
1192 if not data['rrule']:
1193 result.append(data['id'])
1195 event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1197 # TOCHECK: the start date should be replaced by event date; the event date will be changed by that of calendar code
1199 if not data['rrule']:
1202 exdate = data['exdate'] and data['exdate'].split(',') or []
1203 rrule_str = data['rrule']
1205 rrule_until_date = False
1207 for rule in rrule_str.split(';'):
1208 name, value = rule.split('=')
1211 value = parser.parse(value)
1212 rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1213 value = value.strftime("%Y%m%d%H%M%S")
1214 new_rule = '%s=%s' % (name, value)
1215 new_rrule_str.append(new_rule)
1216 new_rrule_str = ';'.join(new_rrule_str)
1217 rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1218 for r_date in rdates:
1221 if arg[0] in ('date', 'date_deadline'):
1223 ok = ok and r_date.strftime('%Y-%m-%d')==arg[2]
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]
1234 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1235 result.append(idval)
1237 if isinstance(select, (str, int, long)):
1238 return ids and ids[0] or False
1240 ids = list(set(result))
1243 def compute_rule_string(self, data):
1245 Compute rule string according to value type RECUR of iCalendar from the values given.
1246 @param self: the object pointer
1247 @param data: dictionary of freq and interval value
1248 @return: string containing recurring rule (empty if no rule)
1250 def get_week_string(freq, data):
1251 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1252 if freq == 'weekly':
1253 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1255 return ';BYDAY=' + ','.join(byday)
1258 def get_month_string(freq, data):
1259 if freq == 'monthly':
1260 if data.get('select1')=='date' and (data.get('day') < 1 or data.get('day') > 31):
1261 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1263 if data.get('select1')=='day':
1264 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1265 elif data.get('select1')=='date':
1266 return ';BYMONTHDAY=' + str(data.get('day'))
1269 def get_end_date(data):
1270 if data.get('end_date'):
1271 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1273 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1274 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1276 freq = data.get('rrule_type', False)
1279 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1280 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1284 def _get_empty_rrule_data(self):
1287 'recurrency' : False,
1289 'rrule_type' : False,
1306 def _parse_rrule(self, rule, data, date_start):
1307 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1308 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1309 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1311 if r._freq > 0 and r._freq < 4:
1312 data['rrule_type'] = rrule_type[r._freq]
1314 data['count'] = r._count
1315 data['interval'] = r._interval
1316 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1319 for i in xrange(0,7):
1320 if i in r._byweekday:
1321 data[day_list[i]] = True
1322 data['rrule_type'] = 'weekly'
1323 #repeat monthly by nweekday ((weekday, weeknumber), )
1325 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1326 data['byday'] = r._bynweekday[0][1]
1327 data['select1'] = 'day'
1328 data['rrule_type'] = 'monthly'
1331 data['day'] = r._bymonthday[0]
1332 data['select1'] = 'date'
1333 data['rrule_type'] = 'monthly'
1335 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1337 data['interval'] = data['interval'] * 12
1339 #FIXEME handle forever case
1341 #in case of repeat for ever that we do not support right now
1342 if not (data.get('count') or data.get('end_date')):
1344 if data.get('count'):
1345 data['end_type'] = 'count'
1347 data['end_type'] = 'end_date'
1350 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1357 if arg[0] in ('date', unicode('date'), 'date_deadline', unicode('date_deadline')):
1358 if context.get('virtual_id', True):
1359 new_args += ['|','&',('recurrency','=',1),('recurrent_id_date', arg[1], arg[2])]
1360 elif arg[0] == "id":
1361 new_id = get_real_ids(arg[2])
1362 new_arg = (arg[0], arg[1], new_id)
1363 new_args.append(new_arg)
1365 #offset, limit and count must be treated separately as we may need to deal with virtual ids
1366 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=order, context=context, count=False)
1367 if context.get('virtual_id', True):
1368 res = self.get_recurrent_ids(cr, uid, res, new_args, limit, context=context)
1372 return res[offset:offset+limit]
1375 def _get_data(self, cr, uid, id, context=None):
1376 return self.read(cr, uid, id,['date', 'date_deadline'])
1378 def need_to_update(self, event_id, vals):
1379 split_id = str(event_id).split("-")
1380 if len(split_id) < 2:
1383 date_start = vals.get('date', '')
1385 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1386 return date_start == split_id[1]
1390 def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1391 def _only_changes_to_apply_on_real_ids(field_names):
1392 ''' return True if changes are only to be made on the real ids'''
1393 for field in field_names:
1394 if field not in ['message_follower_ids']:
1398 context = context or {}
1399 if isinstance(ids, (str, int, long)):
1403 # Special write of complex IDS
1404 for event_id in ids[:]:
1405 if len(str(event_id).split('-')) == 1:
1407 ids.remove(event_id)
1408 real_event_id = base_calendar_id2real_id(event_id)
1410 # if we are setting the recurrency flag to False or if we are only changing fields that
1411 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1412 # then set real ids to be updated.
1413 if not vals.get('recurrency', True) or _only_changes_to_apply_on_real_ids(vals.keys()):
1414 ids.append(real_event_id)
1417 #if edit one instance of a reccurrent id
1418 data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1419 'rrule', 'duration', 'exdate'])
1420 if data.get('rrule'):
1423 recurrent_id=real_event_id,
1424 recurrent_id_date=data.get('date'),
1432 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1434 date_new = event_id.split('-')[1]
1435 date_new = time.strftime("%Y%m%dT%H%M%S", \
1436 time.strptime(date_new, "%Y%m%d%H%M%S"))
1437 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1438 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1440 context.update({'active_id': new_id, 'active_ids': [new_id]})
1443 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1444 vals['vtimezone'] = vals['vtimezone'][40:]
1446 res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1447 if vals.get('partner_ids', False):
1448 self.create_attendees(cr, uid, ids, context)
1450 if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1451 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1452 alarm_obj = self.pool.get('res.alarm')
1453 alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1454 return res or True and False
1456 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1460 if 'date' in groupby:
1461 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1462 virtual_id = context.get('virtual_id', True)
1463 context.update({'virtual_id': False})
1464 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1466 #remove the count, since the value is not consistent with the result of the search when expand the group
1467 for groupname in groupby:
1468 if re.get(groupname + "_count"):
1469 del re[groupname + "_count"]
1470 re.get('__context', {}).update({'virtual_id' : virtual_id})
1473 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1476 fields2 = fields and fields[:] or None
1478 EXTRAFIELDS = ('class','user_id','duration')
1479 for f in EXTRAFIELDS:
1480 if fields and (f not in fields):
1483 # FIXME This whole id mangling has to go!
1484 if isinstance(ids, (str, int, long)):
1489 select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1492 real_data = super(calendar_event, self).read(cr, uid,
1493 [real_id for base_calendar_id, real_id in select],
1494 fields=fields2, context=context, load=load)
1495 real_data = dict(zip([x['id'] for x in real_data], real_data))
1497 for base_calendar_id, real_id in select:
1498 res = real_data[real_id].copy()
1499 ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1500 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1502 res['date_deadline'] = ls[2]
1503 res['id'] = base_calendar_id
1509 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1512 if r['class']=='private':
1514 if f not in ('id','date','date_deadline','duration','user_id','state'):
1515 if isinstance(r[f], list):
1523 for k in EXTRAFIELDS:
1524 if (k in r) and (fields and (k not in fields)):
1526 if isinstance(ids, (str, int, long)):
1527 return result and result[0] or False
1530 def copy(self, cr, uid, id, default=None, context=None):
1534 res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1535 alarm_obj = self.pool.get('res.alarm')
1536 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1539 def unlink(self, cr, uid, ids, context=None):
1540 if not isinstance(ids, list):
1543 attendee_obj=self.pool.get('calendar.attendee')
1544 for event_id in ids[:]:
1545 if len(str(event_id).split('-')) == 1:
1548 real_event_id = base_calendar_id2real_id(event_id)
1549 data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1550 date_new = event_id.split('-')[1]
1551 date_new = time.strftime("%Y%m%dT%H%M%S", \
1552 time.strptime(date_new, "%Y%m%d%H%M%S"))
1553 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1554 self.write(cr, uid, [real_event_id], {'exdate': exdate})
1555 ids.remove(event_id)
1556 for event in self.browse(cr, uid, ids, context=context):
1557 if event.attendee_ids:
1558 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1560 res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1561 self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1562 self.unlink_events(cr, uid, ids, context=context)
1565 def create(self, cr, uid, vals, context=None):
1569 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1570 vals['vtimezone'] = vals['vtimezone'][40:]
1572 res = super(calendar_event, self).create(cr, uid, vals, context)
1573 alarm_obj = self.pool.get('res.alarm')
1574 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1575 self.create_attendees(cr, uid, [res], context)
1578 def do_tentative(self, cr, uid, ids, context=None, *args):
1579 """ Makes event invitation as Tentative
1580 @param self: The object pointer
1581 @param cr: the current row, from the database cursor,
1582 @param uid: the current user's ID for security checks,
1583 @param ids: List of Event IDs
1584 @param *args: Get Tupple value
1585 @param context: A standard dictionary for contextual values
1587 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1589 def do_cancel(self, cr, uid, ids, context=None, *args):
1590 """ Makes event invitation as Tentative
1591 @param self: The object pointer
1592 @param cr: the current row, from the database cursor,
1593 @param uid: the current user's ID for security checks,
1594 @param ids: List of Event IDs
1595 @param *args: Get Tupple value
1596 @param context: A standard dictionary for contextual values
1598 return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1600 def do_confirm(self, cr, uid, ids, context=None, *args):
1601 """ Makes event invitation as Tentative
1602 @param self: The object pointer
1603 @param cr: the current row, from the database cursor,
1604 @param uid: the current user's ID for security checks,
1605 @param ids: List of Event IDs
1606 @param *args: Get Tupple value
1607 @param context: A standard dictionary for contextual values
1609 return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1613 class calendar_todo(osv.osv):
1614 """ Calendar Task """
1616 _name = "calendar.todo"
1617 _inherit = "calendar.event"
1618 _description = "Calendar Task"
1620 def _get_date(self, cr, uid, ids, name, arg, context=None):
1623 @param self: The object pointer
1624 @param cr: the current row, from the database cursor,
1625 @param uid: the current user's ID for security checks,
1626 @param ids: List of calendar todo's IDs.
1627 @param args: list of tuples of form [(‘name_of_the_field', ‘operator', value), ...].
1628 @param context: A standard dictionary for contextual values
1632 for event in self.browse(cr, uid, ids, context=context):
1633 res[event.id] = event.date_start
1636 def _set_date(self, cr, uid, id, name, value, arg, context=None):
1639 @param self: The object pointer
1640 @param cr: the current row, from the database cursor,
1641 @param uid: the current user's ID for security checks,
1642 @param id: calendar's ID.
1643 @param value: Get Value
1644 @param args: list of tuples of form [('name_of_the_field', 'operator', value), ...].
1645 @param context: A standard dictionary for contextual values
1648 assert name == 'date'
1649 return self.write(cr, uid, id, { 'date_start': value }, context=context)
1652 'date': fields.function(_get_date, fnct_inv=_set_date, \
1653 string='Duration', store=True, type='datetime'),
1654 'duration': fields.integer('Duration'),
1663 class ir_values(osv.osv):
1664 _inherit = 'ir.values'
1666 def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1667 isobject=False, meta=False, preserve_user=False, company=False):
1670 @param self: The object pointer
1671 @param cr: the current row, from the database cursor,
1672 @param uid: the current user's ID for security checks,
1673 @param model: Get The Model
1678 if type(data) in (list, tuple):
1679 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1681 new_model.append(data)
1682 return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1683 value, replace, isobject, meta, preserve_user, company)
1685 def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1686 res_id_req=False, without_user=True, key2_req=True):
1689 @param self: The object pointer
1690 @param cr: the current row, from the database cursor,
1691 @param uid: the current user's ID for security checks,
1692 @param model: Get The Model
1698 if type(data) in (list, tuple):
1699 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1701 new_model.append(data)
1702 return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1703 meta, context, res_id_req, without_user, key2_req)
1707 class ir_model(osv.osv):
1709 _inherit = 'ir.model'
1711 def read(self, cr, uid, ids, fields=None, context=None,
1712 load='_classic_read'):
1714 Overrides orm read method.
1715 @param self: The object pointer
1716 @param cr: the current row, from the database cursor,
1717 @param uid: the current user's ID for security checks,
1718 @param ids: List of IR Model's IDs.
1719 @param context: A standard dictionary for contextual values
1721 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1724 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1725 context=context, load=load)
1728 val['id'] = base_calendar_id2real_id(val['id'])
1729 return isinstance(ids, (str, int, long)) and data[0] or data
1733 original_exp_report = openerp.service.report.exp_report
1735 def exp_report(db, uid, object, ids, data=None, context=None):
1738 @param db: get the current database,
1739 @param uid: the current user's ID for security checks,
1740 @param context: A standard dictionary for contextual values
1743 if object == 'printscreen.list':
1744 original_exp_report(db, uid, object, ids, data, context)
1747 new_ids.append(base_calendar_id2real_id(id))
1748 if data.get('id', False):
1749 data['id'] = base_calendar_id2real_id(data['id'])
1750 return original_exp_report(db, uid, object, new_ids, data, context)
1752 openerp.service.report.exp_report = exp_report
1754 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: