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.service import web_services
28 from openerp.tools.translate import _
33 from operator import itemgetter
34 from openerp import tools, SUPERUSER_ID
37 1: "January", 2: "February", 3: "March", 4: "April", \
38 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", \
39 10: "October", 11: "November", 12: "December"
42 def get_recurrent_dates(rrulestring, exdate, startdate=None, exrule=None):
44 Get recurrent dates based on Rule string considering exdate and start date.
45 @param rrulestring: rulestring
46 @param exdate: list of exception dates for rrule
47 @param startdate: startdate for computing recurrent dates
48 @return: list of Recurrent dates
51 val = parser.parse(''.join((re.compile('\d')).findall(date)))
55 startdate = datetime.now()
60 rset1 = rrule.rrulestr(str(rrulestring), dtstart=startdate, forceset=True)
62 datetime_obj = todate(date)
63 rset1._exdate.append(datetime_obj)
66 rset1.exrule(rrule.rrulestr(str(exrule), dtstart=startdate))
70 def base_calendar_id2real_id(base_calendar_id=None, with_date=False):
72 Convert a "virtual/recurring event id" (type string) into a real event id (type int).
73 E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
74 @param base_calendar_id: id of calendar
75 @param with_date: if a value is passed to this param it will return dates based on value of withdate + base_calendar_id
76 @return: real event id
78 if base_calendar_id and isinstance(base_calendar_id, (str, unicode)):
79 res = base_calendar_id.split('-')
84 real_date = time.strftime("%Y-%m-%d %H:%M:%S", \
85 time.strptime(res[1], "%Y%m%d%H%M%S"))
86 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
87 end = start + timedelta(hours=with_date)
88 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
91 return base_calendar_id and int(base_calendar_id) or base_calendar_id
93 def get_real_ids(ids):
94 if isinstance(ids, (str, int, long)):
95 return base_calendar_id2real_id(ids)
97 if isinstance(ids, (list, tuple)):
100 res.append(base_calendar_id2real_id(id))
103 def real_id2base_calendar_id(real_id, recurrent_date):
105 Convert a real event id (type int) into a "virtual/recurring event id" (type string).
106 E.g. real event id is 1 and recurrent_date is set to 01-12-2009 10:00:00, so
107 it will return 1-20091201100000.
108 @param real_id: real event id
109 @param recurrent_date: real event recurrent date
110 @return: string containing the real id and the recurrent date
112 if real_id and recurrent_date:
113 recurrent_date = time.strftime("%Y%m%d%H%M%S", \
114 time.strptime(recurrent_date, "%Y-%m-%d %H:%M:%S"))
115 return '%d-%s' % (real_id, recurrent_date)
118 def _links_get(self, cr, uid, context=None):
121 @param cr: the current row, from the database cursor
122 @param uid: the current user's ID for security checks
123 @param context: a standard dictionary for contextual values
124 @return: list of dictionary which contain object and name and id
126 obj = self.pool.get('res.request.link')
127 ids = obj.search(cr, uid, [])
128 res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
129 return [(r['object'], r['name']) for r in res]
131 html_invitation = """
134 <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
135 <title>%(name)s</title>
138 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
139 style="font-family: Arial, Sans-serif; font-size: 14">
141 <td width="100%%">Hello,</td>
144 <td width="100%%">You are invited for <i>%(company)s</i> Event.</td>
147 <td width="100%%">Below are the details of event. Hours and dates expressed in %(timezone)s time.</td>
151 <table cellspacing="0" cellpadding="5" border="0" summary=""
152 style="width: 90%%; font-family: Arial, Sans-serif; border: 1px Solid #ccc; background-color: #f6f6f6">
153 <tr valign="center" align="center">
154 <td bgcolor="DFDFDF">
160 <table cellpadding="8" cellspacing="0" border="0"
161 style="font-size: 14" summary="Eventdetails" bgcolor="f6f6f6"
165 <div><b>Start Date</b></div>
168 <td>%(start_date)s</td>
170 <div><b>End Date</b></div>
173 <td width="25%%">%(end_date)s</td>
176 <td><b>Description</b></td>
178 <td colspan="3">%(description)s</td>
182 <div><b>Location</b></div>
185 <td colspan="3">%(location)s</td>
189 <div><b>Event Attendees</b></div>
194 <div>%(attendees)s</div>
202 <table border="0" cellspacing="10" cellpadding="0" width="100%%"
203 style="font-family: Arial, Sans-serif; font-size: 14">
205 <td width="100%%">From:</td>
208 <td width="100%%">%(user)s</td>
211 <td width="100%%">-<font color="a7a7a7">-------------------------</font></td>
214 <td width="100%%"> <font color="a7a7a7">%(sign)s</font></td>
221 class calendar_attendee(osv.osv):
223 Calendar Attendee Information
225 _name = 'calendar.attendee'
226 _description = 'Attendee information'
231 def _get_address(self, name=None, email=None):
233 Gives email information in ical CAL-ADDRESS type format.
234 @param name: name for CAL-ADDRESS value
235 @param email: email address for CAL-ADDRESS value
239 return (name or '') + (email and ('MAILTO:' + email) or '')
241 def _compute_data(self, cr, uid, ids, name, arg, context=None):
243 Compute data on function fields for attendee values.
244 @param cr: the current row, from the database cursor
245 @param uid: the current user's ID for security checks
246 @param ids: list of calendar attendee's IDs
247 @param name: name of field
248 @param context: a standard dictionary for contextual values
249 @return: dictionary of form {id: {'field Name': value'}}
253 for attdata in self.browse(cr, uid, ids, context=context):
256 if name == 'sent_by':
257 if not attdata.sent_by_uid:
258 result[id][name] = ''
261 result[id][name] = self._get_address(attdata.sent_by_uid.name, \
262 attdata.sent_by_uid.email)
266 result[id][name] = attdata.user_id.name
267 elif attdata.partner_id:
268 result[id][name] = attdata.partner_id.name or False
270 result[id][name] = attdata.email or ''
272 if name == 'delegated_to':
274 for child in attdata.child_ids:
276 todata.append('MAILTO:' + child.email)
277 result[id][name] = ', '.join(todata)
279 if name == 'delegated_from':
281 for parent in attdata.parent_ids:
283 fromdata.append('MAILTO:' + parent.email)
284 result[id][name] = ', '.join(fromdata)
286 if name == 'event_date':
288 result[id][name] = attdata.ref.date
290 result[id][name] = False
292 if name == 'event_end_date':
294 result[id][name] = attdata.ref.date_deadline
296 result[id][name] = False
298 if name == 'sent_by_uid':
300 result[id][name] = (attdata.ref.user_id.id, attdata.ref.user_id.name)
302 result[id][name] = uid
304 if name == 'language':
305 user_obj = self.pool.get('res.users')
306 lang = user_obj.read(cr, uid, uid, ['lang'], context=context)['lang']
307 result[id][name] = lang.replace('_', '-') if lang else False
311 def _links_get(self, cr, uid, context=None):
313 Get request link for ref field in calendar attendee.
314 @param cr: the current row, from the database cursor
315 @param uid: the current user's id for security checks
316 @param context: A standard dictionary for contextual values
317 @return: list of dictionary which contain object and name and id
319 obj = self.pool.get('res.request.link')
320 ids = obj.search(cr, uid, [])
321 res = obj.read(cr, uid, ids, ['object', 'name'], context=context)
322 return [(r['object'], r['name']) for r in res]
324 def _lang_get(self, cr, uid, context=None):
326 Get language for language selection field.
327 @param cr: the current row, from the database cursor
328 @param uid: the current user's id for security checks
329 @param context: a standard dictionary for contextual values
330 @return: list of dictionary which contain code and name and id
332 obj = self.pool.get('res.lang')
333 ids = obj.search(cr, uid, [])
334 res = obj.read(cr, uid, ids, ['code', 'name'], context=context)
335 res = [((r['code']).replace('_', '-').lower(), r['name']) for r in res]
339 'cutype': fields.selection([('individual', 'Individual'), \
340 ('group', 'Group'), ('resource', 'Resource'), \
341 ('room', 'Room'), ('unknown', 'Unknown') ], \
342 'Invite Type', help="Specify the type of Invitation"),
343 'member': fields.char('Member', size=124,
344 help="Indicate the groups that the attendee belongs to"),
345 'role': fields.selection([('req-participant', 'Participation required'), \
346 ('chair', 'Chair Person'), \
347 ('opt-participant', 'Optional Participation'), \
348 ('non-participant', 'For information Purpose')], 'Role', \
349 help='Participation role for the calendar user'),
350 'state': fields.selection([('needs-action', 'Needs Action'),
351 ('tentative', 'Uncertain'),
352 ('declined', 'Declined'),
353 ('accepted', 'Accepted'),
354 ('delegated', 'Delegated')], 'Status', readonly=True, \
355 help="Status of the attendee's participation"),
356 'rsvp': fields.boolean('Required Reply?',
357 help="Indicats whether the favor of a reply is requested"),
358 'delegated_to': fields.function(_compute_data, \
359 string='Delegated To', type="char", size=124, store=True, \
360 multi='delegated_to', help="The users that the original \
361 request was delegated to"),
362 'delegated_from': fields.function(_compute_data, string=\
363 'Delegated From', type="char", store=True, size=124, multi='delegated_from'),
364 'parent_ids': fields.many2many('calendar.attendee', 'calendar_attendee_parent_rel', \
365 'attendee_id', 'parent_id', 'Delegrated From'),
366 'child_ids': fields.many2many('calendar.attendee', 'calendar_attendee_child_rel', \
367 'attendee_id', 'child_id', 'Delegrated To'),
368 'sent_by': fields.function(_compute_data, string='Sent By', \
369 type="char", multi='sent_by', store=True, size=124, \
370 help="Specify the user that is acting on behalf of the calendar user"),
371 'sent_by_uid': fields.function(_compute_data, string='Sent By User', \
372 type="many2one", relation="res.users", multi='sent_by_uid'),
373 'cn': fields.function(_compute_data, string='Common name', \
374 type="char", size=124, multi='cn', store=True),
375 'dir': fields.char('URI Reference', size=124, help="Reference to the URI\
376 that points to the directory information corresponding to the attendee."),
377 'language': fields.function(_compute_data, string='Language', \
378 type="selection", selection=_lang_get, multi='language', \
379 store=True, help="To specify the language for text values in a\
380 property or property parameter."),
381 'user_id': fields.many2one('res.users', 'User'),
382 'partner_id': fields.many2one('res.partner', 'Contact'),
383 'email': fields.char('Email', size=124, help="Email of Invited Person"),
384 'event_date': fields.function(_compute_data, string='Event Date', \
385 type="datetime", multi='event_date'),
386 'event_end_date': fields.function(_compute_data, \
387 string='Event End Date', type="datetime", \
388 multi='event_end_date'),
389 'ref': fields.reference('Event Ref', selection=_links_get, size=128),
390 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
393 'state': 'needs-action',
394 'role': 'req-participant',
396 'cutype': 'individual',
400 def copy(self, cr, uid, id, default=None, context=None):
401 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
403 def onchange_partner_id(self, cr, uid, ids, partner_id,context=None):
405 Make entry on email and availbility on change of partner_id field.
406 @param cr: the current row, from the database cursor
407 @param uid: the current user's ID for security checks
408 @param ids: list of calendar attendee's IDs
409 @param partner_id: changed value of partner id
410 @param context: a standard dictionary for contextual values
411 @return: dictionary of values which put value in email and availability fields
415 return {'value': {'email': ''}}
416 partner = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
417 return {'value': {'email': partner.email}}
419 def get_ics_file(self, cr, uid, event_obj, context=None):
421 Returns iCalendar file for the event invitation.
422 @param self: the object pointer
423 @param cr: the current row, from the database cursor
424 @param uid: the current user's id for security checks
425 @param event_obj: event object (browse record)
426 @param context: a standard dictionary for contextual values
427 @return: .ics file content
430 def ics_datetime(idate, short=False):
432 #returns the datetime as UTC, because it is stored as it in the database
433 return datetime.strptime(idate, '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.timezone('UTC'))
436 # FIXME: why isn't this in CalDAV?
440 cal = vobject.iCalendar()
441 event = cal.add('vevent')
442 if not event_obj.date_deadline or not event_obj.date:
443 raise osv.except_osv(_('Warning!'),_("First you have to specify the date of the invitation."))
444 event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S'))
445 event.add('dtstart').value = ics_datetime(event_obj.date)
446 event.add('dtend').value = ics_datetime(event_obj.date_deadline)
447 event.add('summary').value = event_obj.name
448 if event_obj.description:
449 event.add('description').value = event_obj.description
450 if event_obj.location:
451 event.add('location').value = event_obj.location
453 event.add('rrule').value = event_obj.rrule
454 if event_obj.organizer:
455 event_org = event.add('organizer')
456 event_org.params['CN'] = [event_obj.organizer]
457 event_org.value = 'MAILTO:' + (event_obj.organizer)
458 elif event_obj.user_id or event_obj.organizer_id:
459 event_org = event.add('organizer')
460 organizer = event_obj.organizer_id
462 organizer = event_obj.user_id
463 event_org.params['CN'] = [organizer.name]
464 event_org.value = 'MAILTO:' + (organizer.email or organizer.name)
466 if event_obj.alarm_id:
467 # computes alarm data
468 valarm = event.add('valarm')
469 alarm_object = self.pool.get('res.alarm')
470 alarm_data = alarm_object.read(cr, uid, event_obj.alarm_id.id, context=context)
471 # Compute trigger data
472 interval = alarm_data['trigger_interval']
473 occurs = alarm_data['trigger_occurs']
474 duration = (occurs == 'after' and alarm_data['trigger_duration']) \
475 or -(alarm_data['trigger_duration'])
476 related = alarm_data['trigger_related']
477 trigger = valarm.add('TRIGGER')
478 trigger.params['related'] = [related.upper()]
479 if interval == 'days':
480 delta = timedelta(days=duration)
481 if interval == 'hours':
482 delta = timedelta(hours=duration)
483 if interval == 'minutes':
484 delta = timedelta(minutes=duration)
485 trigger.value = delta
486 # Compute other details
487 valarm.add('DESCRIPTION').value = alarm_data['name'] or 'OpenERP'
489 for attendee in event_obj.attendee_ids:
490 attendee_add = event.add('attendee')
491 attendee_add.params['CUTYPE'] = [str(attendee.cutype)]
492 attendee_add.params['ROLE'] = [str(attendee.role)]
493 attendee_add.params['RSVP'] = [str(attendee.rsvp)]
494 attendee_add.value = 'MAILTO:' + (attendee.email or '')
495 res = cal.serialize()
498 def _send_mail(self, cr, uid, ids, mail_to, email_from=tools.config.get('email_from', False), context=None):
500 Send mail for event invitation to event attendees.
501 @param email_from: email address for user sending the mail
504 company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.name
505 for att in self.browse(cr, uid, ids, context=context):
506 sign = att.sent_by_uid and att.sent_by_uid.signature or ''
507 sign = '<br>'.join(sign and sign.split('\n') or [])
512 other_invitation_ids = self.search(cr, uid, [('ref', '=', res_obj._name + ',' + str(res_obj.id))])
514 for att2 in self.browse(cr, uid, other_invitation_ids):
515 att_infos.append(((att2.user_id and att2.user_id.name) or \
516 (att2.partner_id and att2.partner_id.name) or \
517 att2.email) + ' - Status: ' + att2.state.title())
518 #dates and times are gonna be expressed in `tz` time (local timezone of the `uid`)
519 tz = context.get('tz', pytz.timezone('UTC'))
520 #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
521 date_start = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
523 if res_obj.date_deadline:
524 date_stop = fields.datetime.context_timestamp(cr, uid, datetime.strptime(res_obj.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
525 body_vals = {'name': res_obj.name,
526 'start_date': date_start,
527 'end_date': date_stop,
529 'description': res_obj.description or '-',
530 'location': res_obj.location or '-',
531 'attendees': '<br>'.join(att_infos),
532 'user': res_obj.user_id and res_obj.user_id.name or 'OpenERP User',
536 body = html_invitation % body_vals
537 if mail_to and email_from:
538 ics_file = self.get_ics_file(cr, uid, res_obj, context=context)
539 vals = {'email_from': email_from,
546 vals['attachment_ids'] = [(0,0,{'name': 'invitation.ics',
547 'datas_fname': 'invitation.ics',
548 'datas': str(ics_file).encode('base64')})]
549 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
552 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
554 Make entry on email and availbility on change of user_id field.
555 @param cr: the current row, from the database cursor
556 @param uid: the current user's ID for security checks
557 @param ids: list of calendar attendee's IDs
558 @param user_id: changed value of User id
559 @return: dictionary of values which put value in email and availability fields
563 return {'value': {'email': ''}}
564 usr_obj = self.pool.get('res.users')
565 user = usr_obj.browse(cr, uid, user_id, *args)
566 return {'value': {'email': user.email, 'availability':user.availability}}
568 def do_tentative(self, cr, uid, ids, context=None, *args):
570 Makes event invitation as Tentative.
571 @param self: the object pointer
572 @param cr: the current row, from the database cursor
573 @param uid: the current user's ID for security checks
574 @param ids: list of calendar attendee's IDs
575 @param *args: get Tupple value
576 @param context: a standard dictionary for contextual values
578 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
580 def do_accept(self, cr, uid, ids, context=None, *args):
582 Marks event invitation as Accepted.
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
591 return self.write(cr, uid, ids, {'state': 'accepted'}, context)
593 def do_decline(self, cr, uid, ids, context=None, *args):
595 Marks event invitation as Declined.
596 @param self: the object pointer
597 @param cr: the current row, from the database cursor
598 @param uid: the current user's ID for security checks
599 @param ids: list of calendar attendee's IDs
600 @param *args: get Tupple value
601 @param context: a standard dictionary for contextual values
605 return self.write(cr, uid, ids, {'state': 'declined'}, context)
607 def create(self, cr, uid, vals, context=None):
609 Overrides orm create method.
610 @param self: The object pointer
611 @param cr: the current row, from the database cursor
612 @param uid: the current user's ID for security checks
613 @param vals: get Values
614 @param context: a standard dictionary for contextual values
618 if not vals.get("email") and vals.get("cn"):
619 cnval = vals.get("cn").split(':')
620 email = filter(lambda x:x.__contains__('@'), cnval)
621 vals['email'] = email and email[0] or ''
622 vals['cn'] = vals.get("cn")
623 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
628 class res_alarm(osv.osv):
629 """Resource Alarm """
631 _description = 'Basic Alarm Information'
634 'name':fields.char('Name', size=256, required=True),
635 'trigger_occurs': fields.selection([('before', 'Before'), \
636 ('after', 'After')], \
637 'Triggers', required=True),
638 'trigger_interval': fields.selection([('minutes', 'Minutes'), \
639 ('hours', 'Hours'), \
640 ('days', 'Days')], 'Interval', \
642 'trigger_duration': fields.integer('Duration', required=True),
643 'trigger_related': fields.selection([('start', 'The event starts'), \
644 ('end', 'The event ends')], \
645 'Related to', required=True),
646 'duration': fields.integer('Duration', help="""Duration' and 'Repeat' \
647 are both optional, but if one occurs, so MUST the other"""),
648 'repeat': fields.integer('Repeat'),
649 'active': fields.boolean('Active', help="If the active field is set to \
650 true, it will allow you to hide the event alarm information without removing it.")
653 'trigger_interval': 'minutes',
654 'trigger_duration': 5,
655 'trigger_occurs': 'before',
656 'trigger_related': 'start',
660 def do_alarm_create(self, cr, uid, ids, model, date, context=None):
662 Create Alarm for event.
663 @param cr: the current row, from the database cursor,
664 @param uid: the current user's ID for security checks,
665 @param ids: List of res alarm's IDs.
666 @param model: Model name.
667 @param date: Event date
668 @param context: A standard dictionary for contextual values
673 alarm_obj = self.pool.get('calendar.alarm')
674 res_alarm_obj = self.pool.get('res.alarm')
675 ir_obj = self.pool.get('ir.model')
676 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
678 model_obj = self.pool.get(model)
679 for data in model_obj.browse(cr, uid, ids, context=context):
681 basic_alarm = data.alarm_id
682 cal_alarm = data.base_calendar_alarm_id
683 if (not basic_alarm and cal_alarm) or (basic_alarm and cal_alarm):
685 # Find for existing res.alarm
686 duration = cal_alarm.trigger_duration
687 interval = cal_alarm.trigger_interval
688 occurs = cal_alarm.trigger_occurs
689 related = cal_alarm.trigger_related
690 domain = [('trigger_duration', '=', duration), ('trigger_interval', '=', interval), ('trigger_occurs', '=', occurs), ('trigger_related', '=', related)]
691 alarm_ids = res_alarm_obj.search(cr, uid, domain, context=context)
694 'trigger_duration': duration,
695 'trigger_interval': interval,
696 'trigger_occurs': occurs,
697 'trigger_related': related,
698 'name': str(duration) + ' ' + str(interval) + ' ' + str(occurs)
700 new_res_alarm = res_alarm_obj.create(cr, uid, val, context=context)
702 new_res_alarm = alarm_ids[0]
703 cr.execute('UPDATE %s ' % model_obj._table + \
704 ' SET base_calendar_alarm_id=%s, alarm_id=%s ' \
706 (cal_alarm.id, new_res_alarm, data.id))
708 self.do_alarm_unlink(cr, uid, [data.id], model)
712 'description': data.description,
714 'attendee_ids': [(6, 0, map(lambda x:x.id, data.attendee_ids))],
715 'trigger_related': basic_alarm.trigger_related,
716 'trigger_duration': basic_alarm.trigger_duration,
717 'trigger_occurs': basic_alarm.trigger_occurs,
718 'trigger_interval': basic_alarm.trigger_interval,
719 'duration': basic_alarm.duration,
720 'repeat': basic_alarm.repeat,
722 'event_date': data[date],
724 'model_id': model_id,
727 alarm_id = alarm_obj.create(cr, uid, vals)
728 cr.execute('UPDATE %s ' % model_obj._table + \
729 ' SET base_calendar_alarm_id=%s, alarm_id=%s '
731 ( alarm_id, basic_alarm.id, data.id) )
734 def do_alarm_unlink(self, cr, uid, ids, model, context=None):
736 Delete alarm specified in ids
737 @param cr: the current row, from the database cursor,
738 @param uid: the current user's ID for security checks,
739 @param ids: List of res alarm's IDs.
740 @param model: Model name for which alarm is to be cleared.
745 alarm_obj = self.pool.get('calendar.alarm')
746 ir_obj = self.pool.get('ir.model')
747 model_id = ir_obj.search(cr, uid, [('model', '=', model)])[0]
748 model_obj = self.pool.get(model)
749 for data in model_obj.browse(cr, uid, ids, context=context):
750 alarm_ids = alarm_obj.search(cr, uid, [('model_id', '=', model_id), ('res_id', '=', data.id)])
752 alarm_obj.unlink(cr, uid, alarm_ids)
753 cr.execute('Update %s set base_calendar_alarm_id=NULL, alarm_id=NULL\
754 where id=%%s' % model_obj._table,(data.id,))
759 class calendar_alarm(osv.osv):
760 _name = 'calendar.alarm'
761 _description = 'Event alarm information'
762 _inherit = 'res.alarm'
766 'alarm_id': fields.many2one('res.alarm', 'Basic Alarm', ondelete='cascade'),
767 'name': fields.char('Summary', size=124, help="""Contains the text to be \
768 used as the message subject for email \
769 or contains the text to be used for display"""),
770 'action': fields.selection([('audio', 'Audio'), ('display', 'Display'), \
771 ('procedure', 'Procedure'), ('email', 'Email') ], 'Action', \
772 required=True, help="Defines the action to be invoked when an alarm is triggered"),
773 'description': fields.text('Description', help='Provides a more complete \
774 description of the calendar component, than that \
775 provided by the "SUMMARY" property'),
776 'attendee_ids': fields.many2many('calendar.attendee', 'alarm_attendee_rel', \
777 'alarm_id', 'attendee_id', 'Attendees', readonly=True),
778 'attach': fields.binary('Attachment', help="""* Points to a sound resource,\
779 which is rendered when the alarm is triggered for audio,
780 * File which is intended to be sent as message attachments for email,
781 * Points to a procedure resource, which is invoked when\
782 the alarm is triggered for procedure."""),
783 'res_id': fields.integer('Resource ID'),
784 'model_id': fields.many2one('ir.model', 'Model'),
785 'user_id': fields.many2one('res.users', 'Owner'),
786 'event_date': fields.datetime('Event Date'),
787 'event_end_date': fields.datetime('Event End Date'),
788 'trigger_date': fields.datetime('Trigger Date', readonly="True"),
789 'state':fields.selection([
794 ], 'Status', select=True, readonly=True),
802 def create(self, cr, uid, vals, context=None):
804 Overrides orm create method.
805 @param self: The object pointer
806 @param cr: the current row, from the database cursor,
807 @param vals: dictionary of fields value.{'name_of_the_field': value, ...}
808 @param context: A standard dictionary for contextual values
809 @return: new record id for calendar_alarm.
813 event_date = vals.get('event_date', False)
815 dtstart = datetime.strptime(vals['event_date'], "%Y-%m-%d %H:%M:%S")
816 if vals['trigger_interval'] == 'days':
817 delta = timedelta(days=vals['trigger_duration'])
818 if vals['trigger_interval'] == 'hours':
819 delta = timedelta(hours=vals['trigger_duration'])
820 if vals['trigger_interval'] == 'minutes':
821 delta = timedelta(minutes=vals['trigger_duration'])
822 trigger_date = dtstart + (vals['trigger_occurs'] == 'after' and delta or -delta)
823 vals['trigger_date'] = trigger_date
824 res = super(calendar_alarm, self).create(cr, uid, vals, context=context)
827 def do_run_scheduler(self, cr, uid, automatic=False, use_new_cursor=False, \
829 """Scheduler for event reminder
830 @param self: The object pointer
831 @param cr: the current row, from the database cursor,
832 @param uid: the current user's ID for security checks,
833 @param ids: List of calendar alarm's IDs.
834 @param use_new_cursor: False or the dbname
835 @param context: A standard dictionary for contextual values
839 current_datetime = datetime.now()
840 alarm_ids = self.search(cr, uid, [('state', '!=', 'done')], context=context)
844 for alarm in self.browse(cr, uid, alarm_ids, context=context):
845 next_trigger_date = None
847 model_obj = self.pool.get(alarm.model_id.model)
848 res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
851 if hasattr(res_obj, 'rrule') and res_obj.rrule:
852 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
853 #exdate is a string and we need a list
854 exdate = res_obj.exdate and res_obj.exdate.split(',') or []
855 recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule)
857 trigger_interval = alarm.trigger_interval
858 if trigger_interval == 'days':
859 delta = timedelta(days=alarm.trigger_duration)
860 if trigger_interval == 'hours':
861 delta = timedelta(hours=alarm.trigger_duration)
862 if trigger_interval == 'minutes':
863 delta = timedelta(minutes=alarm.trigger_duration)
864 delta = alarm.trigger_occurs == 'after' and delta or -delta
866 for rdate in recurrent_dates:
867 if rdate + delta > current_datetime:
869 if rdate + delta <= current_datetime:
870 re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
871 rest_dates = recurrent_dates[len(re_dates):]
872 next_trigger_date = rest_dates and rest_dates[0] or None
875 re_dates = [alarm.trigger_date]
878 if alarm.action == 'email':
879 sub = '[OpenERP Reminder] %s' % (alarm.name)
891 """ % (alarm.name, alarm.trigger_date, alarm.description, \
892 alarm.user_id.name, alarm.user_id.signature)
893 mail_to = alarm.user_id.email
894 for att in alarm.attendee_ids:
895 mail_to = mail_to + " " + att.user_id.email
902 'email_from': tools.config.get('email_from', mail_to),
904 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
905 if next_trigger_date:
906 update_vals.update({'trigger_date': next_trigger_date})
908 update_vals.update({'state': 'done'})
909 self.write(cr, uid, [alarm.id], update_vals)
915 class calendar_event(osv.osv):
916 _name = "calendar.event"
917 _description = "Calendar Event"
920 def _tz_get(self, cr, uid, context=None):
921 return [(x.lower(), x) for x in pytz.all_timezones]
923 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
924 """Returns duration and/or end date based on values passed
925 @param self: The object pointer
926 @param cr: the current row, from the database cursor,
927 @param uid: the current user's ID for security checks,
928 @param ids: List of calendar event's IDs.
929 @param start_date: Starting date
930 @param duration: Duration between start date and end date
931 @param end_date: Ending Datee
932 @param context: A standard dictionary for contextual values
940 if not end_date and not duration:
942 value['duration'] = duration
944 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
945 if allday: # For all day event
947 value['duration'] = duration
948 # change start_date's time to 00:00:00 in the user's timezone
949 user = self.pool.get('res.users').browse(cr, uid, uid)
950 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
951 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
952 start = start.replace(hour=0, minute=0, second=0) # change start's time to 00:00:00
953 start = start.astimezone(pytz.utc) # convert start back to utc
954 start_date = start.strftime("%Y-%m-%d %H:%M:%S")
955 value['date'] = start_date
957 if end_date and not duration:
958 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
960 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
961 value['duration'] = round(duration, 2)
963 end = start + timedelta(hours=duration)
964 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
965 elif end_date and duration and not allday:
966 # we have both, keep them synchronized:
967 # set duration based on end_date (arbitrary decision: this avoid
968 # getting dates like 06:31:48 instead of 06:32:00)
969 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
971 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
972 value['duration'] = round(duration, 2)
974 return {'value': value}
976 def unlink_events(self, cr, uid, ids, context=None):
978 This function deletes event which are linked with the event with recurrent_id
979 (Removes the events which refers to the same UID value)
984 cr.execute("select id from %s where recurrent_id=%%s" % (self._table), (event_id,))
985 r_ids = map(lambda x: x[0], cr.fetchall())
986 self.unlink(cr, uid, r_ids, context=context)
989 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
991 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
992 @param self: The object pointer
993 @param cr: the current row, from the database cursor,
994 @param id: List of calendar event's ids.
995 @param context: A standard dictionary for contextual values
996 @return: dictionary of rrule value.
1000 if not isinstance(ids, list):
1004 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
1005 data = self.read(cr, SUPERUSER_ID, id, ['interval', 'count'], context=context)
1006 if data.get('interval', 0) < 0:
1007 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
1008 if data.get('count', 0) <= 0:
1009 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
1010 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)
1012 if data['recurrency']:
1013 result[event] = self.compute_rule_string(data)
1018 # hook method to fix the wrong signature
1019 def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
1020 return self._rrule_write(self, cr, uid, ids, field_name, field_value, args, context=context)
1022 def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1023 if not isinstance(ids, list):
1025 data = self._get_empty_rrule_data()
1027 data['recurrency'] = True
1028 for event in self.browse(cr, uid, ids, context=context):
1029 update_data = self._parse_rrule(field_value, dict(data), event.date)
1030 data.update(update_data)
1031 super(calendar_event, self).write(cr, uid, ids, data, context=context)
1035 'id': fields.integer('ID', readonly=True),
1036 'sequence': fields.integer('Sequence'),
1037 'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1038 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True,),
1039 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
1040 'create_date': fields.datetime('Created', readonly=True),
1041 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1042 'description': fields.text('Description', states={'done': [('readonly', True)]}),
1043 'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1044 ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
1045 'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1046 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1047 'Show Time as', states={'done': [('readonly', True)]}),
1048 'base_calendar_url': fields.char('Caldav URL', size=264),
1049 'state': fields.selection([
1050 ('tentative', 'Uncertain'),
1051 ('cancelled', 'Cancelled'),
1052 ('confirmed', 'Confirmed'),
1053 ], 'Status', readonly=True),
1054 'exdate': fields.text('Exception Date/Times', help="This property \
1055 defines the list of date/time exceptions for a recurring calendar component."),
1056 'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1057 rule or repeating pattern of time to exclude from the recurring rule."),
1058 'rrule': fields.function(_get_rulestring, type='char', size=124, \
1059 fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
1060 'rrule_type': fields.selection([
1061 ('daily', 'Day(s)'),
1062 ('weekly', 'Week(s)'),
1063 ('monthly', 'Month(s)'),
1064 ('yearly', 'Year(s)')
1065 ], 'Recurrency', states={'done': [('readonly', True)]},
1066 help="Let the event automatically repeat at that interval"),
1067 'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]},
1068 help="Set an alarm at this time, before the event occurs" ),
1069 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1070 'recurrent_id': fields.integer('Recurrent ID'),
1071 'recurrent_id_date': fields.datetime('Recurrent ID date'),
1072 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1073 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1074 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with organizer attribute of VEvent.
1075 'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1076 'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'),
1077 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
1078 'count': fields.integer('Repeat', help="Repeat x times"),
1079 'mo': fields.boolean('Mon'),
1080 'tu': fields.boolean('Tue'),
1081 'we': fields.boolean('Wed'),
1082 'th': fields.boolean('Thu'),
1083 'fr': fields.boolean('Fri'),
1084 'sa': fields.boolean('Sat'),
1085 'su': fields.boolean('Sun'),
1086 'select1': fields.selection([('date', 'Date of month'),
1087 ('day', 'Day of month')], 'Option'),
1088 'day': fields.integer('Date of month'),
1089 'week_list': fields.selection([
1092 ('WE', 'Wednesday'),
1096 ('SU', 'Sunday')], 'Weekday'),
1097 'byday': fields.selection([
1103 ('-1', 'Last')], 'By day'),
1104 'month_list': fields.selection(months.items(), 'Month'),
1105 'end_date': fields.date('Repeat Until'),
1106 'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1107 'event_id', 'attendee_id', 'Attendees'),
1108 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1109 'active': fields.boolean('Active', help="If the active field is set to \
1110 true, it will allow you to hide the event alarm information without removing it."),
1111 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1112 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
1115 def create_attendees(self, cr, uid, ids, context):
1116 att_obj = self.pool.get('calendar.attendee')
1117 user_obj = self.pool.get('res.users')
1118 current_user = user_obj.browse(cr, uid, uid, context=context)
1119 for event in self.browse(cr, uid, ids, context):
1121 for att in event.attendee_ids:
1122 attendees[att.partner_id.id] = True
1125 for partner in event.partner_ids:
1126 if partner.id in attendees:
1128 local_context = context.copy()
1129 local_context.pop('default_state', None)
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
1135 }, context=local_context)
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 # TODO for trunk: remove get_recurrent_ids
1181 def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1182 """Wrapper for _get_recurrent_ids to get the 'order' parameter from the context"""
1185 order = context.get('order', self._order)
1186 return self._get_recurrent_ids(cr, uid, select, domain, limit=limit, order=order, context=context)
1188 def _get_recurrent_ids(self, cr, uid, select, domain, limit=100, order=None, context=None):
1189 """Gives virtual event ids for recurring events based on value of Recurrence Rule
1190 This method gives ids of dates that comes between start date and end date of calendar views
1191 @param self: The object pointer
1192 @param cr: the current row, from the database cursor,
1193 @param uid: the current user's ID for security checks,
1194 @param limit: The Number of Results to Return
1195 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted"""
1201 fields = ['rrule', 'recurrency', 'exdate', 'exrule', 'date']
1203 order_fields = [field.split()[0] for field in order.split(',')]
1205 # fallback on self._order defined on the model
1206 order_fields = [field.split()[0] for field in self._order.split(',')]
1207 fields = list(set(fields + order_fields))
1209 for data in super(calendar_event, self).read(cr, uid, select, fields, context=context):
1210 if not data['recurrency'] or not data['rrule']:
1211 result_data.append(data)
1212 result.append(data['id'])
1214 event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1216 # TOCHECK: the start date should be replaced by event date; the event date will be changed by that of calendar code
1218 exdate = data['exdate'] and data['exdate'].split(',') or []
1219 rrule_str = data['rrule']
1221 rrule_until_date = False
1223 for rule in rrule_str.split(';'):
1224 name, value = rule.split('=')
1227 value = parser.parse(value)
1228 rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1229 value = value.strftime("%Y%m%d%H%M%S")
1230 new_rule = '%s=%s' % (name, value)
1231 new_rrule_str.append(new_rule)
1232 new_rrule_str = ';'.join(new_rrule_str)
1233 rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1234 for r_date in rdates:
1235 # fix domain evaluation
1236 # step 1: check date and replace expression by True or False, replace other expressions by True
1237 # step 2: evaluation of & and |
1238 # check if there are one False
1241 if str(arg[0]) in (str('date'), str('date_deadline')):
1243 ok = r_date.strftime('%Y-%m-%d')==arg[2]
1245 ok = r_date.strftime('%Y-%m-%d')>arg[2]
1247 ok = r_date.strftime('%Y-%m-%d')<arg[2]
1248 if (arg[1] == '>='):
1249 ok = r_date.strftime('%Y-%m-%d')>=arg[2]
1250 if (arg[1] == '<='):
1251 ok = r_date.strftime('%Y-%m-%d')<=arg[2]
1253 elif str(arg) == str('&') or str(arg) == str('|'):
1260 if not isinstance(item, basestring):
1262 elif str(item) == str('&'):
1263 first = new_pile.pop()
1264 second = new_pile.pop()
1265 res = first and second
1266 elif str(item) == str('|'):
1267 first = new_pile.pop()
1268 second = new_pile.pop()
1269 res = first or second
1270 new_pile.append(res)
1272 if [True for item in new_pile if not item]:
1274 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1275 r_data = dict(data, id=idval, date=r_date.strftime("%Y-%m-%d %H:%M:%S"))
1276 result.append(idval)
1277 result_data.append(r_data)
1278 ids = list(set(result))
1282 def comparer(left, right):
1283 for fn, mult in comparers:
1284 if type(fn(left)) == tuple and type(fn(right)) == tuple:
1285 # comparing many2one values, sorting on name_get result
1286 leftv, rightv = fn(left)[1], fn(right)[1]
1288 leftv, rightv = fn(left), fn(right)
1289 result = cmp(leftv, rightv)
1291 return mult * result
1294 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1295 comparers = [ ((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1296 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1300 def compute_rule_string(self, data):
1302 Compute rule string according to value type RECUR of iCalendar from the values given.
1303 @param self: the object pointer
1304 @param data: dictionary of freq and interval value
1305 @return: string containing recurring rule (empty if no rule)
1307 def get_week_string(freq, data):
1308 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1309 if freq == 'weekly':
1310 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1312 return ';BYDAY=' + ','.join(byday)
1315 def get_month_string(freq, data):
1316 if freq == 'monthly':
1317 if data.get('select1')=='date' and (data.get('day') < 1 or data.get('day') > 31):
1318 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1320 if data.get('select1')=='day':
1321 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1322 elif data.get('select1')=='date':
1323 return ';BYMONTHDAY=' + str(data.get('day'))
1326 def get_end_date(data):
1327 if data.get('end_date'):
1328 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1330 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1331 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1333 freq = data.get('rrule_type', False)
1336 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1337 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1341 def _get_empty_rrule_data(self):
1344 'recurrency' : False,
1346 'rrule_type' : False,
1363 def _parse_rrule(self, rule, data, date_start):
1364 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1365 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1366 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1368 if r._freq > 0 and r._freq < 4:
1369 data['rrule_type'] = rrule_type[r._freq]
1371 data['count'] = r._count
1372 data['interval'] = r._interval
1373 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1376 for i in xrange(0,7):
1377 if i in r._byweekday:
1378 data[day_list[i]] = True
1379 data['rrule_type'] = 'weekly'
1380 #repeat monthly by nweekday ((weekday, weeknumber), )
1382 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1383 data['byday'] = str(r._bynweekday[0][1])
1384 data['select1'] = 'day'
1385 data['rrule_type'] = 'monthly'
1388 data['day'] = r._bymonthday[0]
1389 data['select1'] = 'date'
1390 data['rrule_type'] = 'monthly'
1392 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1394 data['interval'] = data['interval'] * 12
1396 #FIXEME handle forever case
1398 #in case of repeat for ever that we do not support right now
1399 if not (data.get('count') or data.get('end_date')):
1401 if data.get('count'):
1402 data['end_type'] = 'count'
1404 data['end_type'] = 'end_date'
1407 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1414 if arg[0] in ('date_deadline', unicode('date_deadline')):
1415 if context.get('virtual_id', True):
1416 new_args += ['|','&',('recurrency','=',1),('end_date', arg[1], arg[2])]
1417 elif arg[0] == "id":
1418 new_id = get_real_ids(arg[2])
1419 new_arg = (arg[0], arg[1], new_id)
1420 new_args.append(new_arg)
1421 if not context.get('virtual_id', True):
1422 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count)
1424 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1425 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1426 res = self._get_recurrent_ids(cr, uid, res, args, limit, order=order, context=context)
1431 return res[offset:offset+limit]
1434 def _get_data(self, cr, uid, id, context=None):
1435 return self.read(cr, uid, id,['date', 'date_deadline'])
1437 def need_to_update(self, event_id, vals):
1438 split_id = str(event_id).split("-")
1439 if len(split_id) < 2:
1442 date_start = vals.get('date', '')
1444 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1445 return date_start == split_id[1]
1449 def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1450 def _only_changes_to_apply_on_real_ids(field_names):
1451 ''' return True if changes are only to be made on the real ids'''
1452 for field in field_names:
1453 if field not in ['message_follower_ids']:
1457 context = context or {}
1458 if isinstance(ids, (str, int, long)):
1462 # Special write of complex IDS
1463 for event_id in ids[:]:
1464 if len(str(event_id).split('-')) == 1:
1466 ids.remove(event_id)
1467 real_event_id = base_calendar_id2real_id(event_id)
1469 # if we are setting the recurrency flag to False or if we are only changing fields that
1470 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1471 # then set real ids to be updated.
1472 if not vals.get('recurrency', True) or _only_changes_to_apply_on_real_ids(vals.keys()):
1473 ids.append(real_event_id)
1476 #if edit one instance of a reccurrent id
1477 data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1478 'rrule', 'duration', 'exdate'])
1479 if data.get('rrule'):
1482 recurrent_id=real_event_id,
1483 recurrent_id_date=data.get('date'),
1491 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1493 date_new = event_id.split('-')[1]
1494 date_new = time.strftime("%Y%m%dT%H%M%S", \
1495 time.strptime(date_new, "%Y%m%d%H%M%S"))
1496 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1497 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1499 context.update({'active_id': new_id, 'active_ids': [new_id]})
1502 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1503 vals['vtimezone'] = vals['vtimezone'][40:]
1505 res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1507 # set end_date for calendar searching
1508 if vals.get('recurrency', True) and vals.get('end_type', 'count') in ('count', unicode('count')) and \
1509 (vals.get('rrule_type') or vals.get('count') or vals.get('date') or vals.get('date_deadline')):
1510 for data in self.read(cr, uid, ids, ['date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
1511 end_date = self._set_recurrency_end_date(data, context=context)
1512 super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
1514 if vals.get('partner_ids', False):
1515 self.create_attendees(cr, uid, ids, context)
1517 if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1518 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1519 alarm_obj = self.pool.get('res.alarm')
1520 alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1521 return res or True and False
1523 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1527 if 'date' in groupby:
1528 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1529 virtual_id = context.get('virtual_id', True)
1530 context.update({'virtual_id': False})
1531 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1533 #remove the count, since the value is not consistent with the result of the search when expand the group
1534 for groupname in groupby:
1535 if re.get(groupname + "_count"):
1536 del re[groupname + "_count"]
1537 re.get('__context', {}).update({'virtual_id' : virtual_id})
1540 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1543 fields2 = fields and fields[:] or None
1545 EXTRAFIELDS = ('class','user_id','duration')
1546 for f in EXTRAFIELDS:
1547 if fields and (f not in fields):
1550 # FIXME This whole id mangling has to go!
1551 if isinstance(ids, (str, int, long)):
1556 select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1559 real_data = super(calendar_event, self).read(cr, uid,
1560 [real_id for base_calendar_id, real_id in select],
1561 fields=fields2, context=context, load=load)
1562 real_data = dict(zip([x['id'] for x in real_data], real_data))
1564 for base_calendar_id, real_id in select:
1565 res = real_data[real_id].copy()
1566 ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1567 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1569 res['date_deadline'] = ls[2]
1570 res['id'] = base_calendar_id
1576 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1579 if r['class']=='private':
1581 if f not in ('id','date','date_deadline','duration','user_id','state','interval','count'):
1582 if isinstance(r[f], list):
1590 for k in EXTRAFIELDS:
1591 if (k in r) and (fields and (k not in fields)):
1593 if isinstance(ids, (str, int, long)):
1594 return result and result[0] or False
1597 def copy(self, cr, uid, id, default=None, context=None):
1601 res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1602 alarm_obj = self.pool.get('res.alarm')
1603 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1606 def unlink(self, cr, uid, ids, context=None):
1607 if not isinstance(ids, list):
1610 attendee_obj=self.pool.get('calendar.attendee')
1611 for event_id in ids[:]:
1612 if len(str(event_id).split('-')) == 1:
1615 real_event_id = base_calendar_id2real_id(event_id)
1616 data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1617 date_new = event_id.split('-')[1]
1618 date_new = time.strftime("%Y%m%dT%H%M%S", \
1619 time.strptime(date_new, "%Y%m%d%H%M%S"))
1620 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1621 self.write(cr, uid, [real_event_id], {'exdate': exdate})
1622 ids.remove(event_id)
1623 for event in self.browse(cr, uid, ids, context=context):
1624 if event.attendee_ids:
1625 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1627 res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1628 self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1629 self.unlink_events(cr, uid, ids, context=context)
1632 def _set_recurrency_end_date(self, data, context=None):
1633 end_date = data.get('end_date')
1635 if data.get('recurrency') and data.get('end_type') in ('count', unicode('count')):
1636 data_date_deadline = datetime.strptime(data.get('date_deadline'), '%Y-%m-%d %H:%M:%S')
1637 if data.get('rrule_type') in ('daily', unicode('count')):
1638 rel_date = relativedelta(days=data.get('count')+1)
1639 elif data.get('rrule_type') in ('weekly', unicode('weekly')):
1640 rel_date = relativedelta(days=(data.get('count')+1)*7)
1641 elif data.get('rrule_type') in ('monthly', unicode('monthly')):
1642 rel_date = relativedelta(months=data.get('count')+1)
1643 elif data.get('rrule_type') in ('yearly', unicode('yearly')):
1644 rel_date = relativedelta(years=data.get('count')+1)
1645 end_date = data_date_deadline
1647 end_date += rel_date
1650 def create(self, cr, uid, vals, context=None):
1654 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1655 vals['vtimezone'] = vals['vtimezone'][40:]
1657 vals['end_date'] = self._set_recurrency_end_date(vals, context=context)
1658 res = super(calendar_event, self).create(cr, uid, vals, context)
1660 alarm_obj = self.pool.get('res.alarm')
1661 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1662 self.create_attendees(cr, uid, [res], context)
1665 def do_tentative(self, cr, uid, ids, context=None, *args):
1666 """ Makes event invitation as Tentative
1667 @param self: The object pointer
1668 @param cr: the current row, from the database cursor,
1669 @param uid: the current user's ID for security checks,
1670 @param ids: List of Event IDs
1671 @param *args: Get Tupple value
1672 @param context: A standard dictionary for contextual values
1674 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1676 def do_cancel(self, cr, uid, ids, context=None, *args):
1677 """ Makes event invitation as Tentative
1678 @param self: The object pointer
1679 @param cr: the current row, from the database cursor,
1680 @param uid: the current user's ID for security checks,
1681 @param ids: List of Event IDs
1682 @param *args: Get Tupple value
1683 @param context: A standard dictionary for contextual values
1685 return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1687 def do_confirm(self, cr, uid, ids, context=None, *args):
1688 """ Makes event invitation as Tentative
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 ids: List of Event IDs
1693 @param *args: Get Tupple value
1694 @param context: A standard dictionary for contextual values
1696 return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1700 class calendar_todo(osv.osv):
1701 """ Calendar Task """
1703 _name = "calendar.todo"
1704 _inherit = "calendar.event"
1705 _description = "Calendar Task"
1707 def _get_date(self, cr, uid, ids, name, arg, context=None):
1710 @param self: The object pointer
1711 @param cr: the current row, from the database cursor,
1712 @param uid: the current user's ID for security checks,
1713 @param ids: List of calendar todo's IDs.
1714 @param args: list of tuples of form [(‘name_of_the_field', ‘operator', value), ...].
1715 @param context: A standard dictionary for contextual values
1719 for event in self.browse(cr, uid, ids, context=context):
1720 res[event.id] = event.date_start
1723 def _set_date(self, cr, uid, id, name, value, arg, context=None):
1726 @param self: The object pointer
1727 @param cr: the current row, from the database cursor,
1728 @param uid: the current user's ID for security checks,
1729 @param id: calendar's ID.
1730 @param value: Get Value
1731 @param args: list of tuples of form [('name_of_the_field', 'operator', value), ...].
1732 @param context: A standard dictionary for contextual values
1735 assert name == 'date'
1736 return self.write(cr, uid, id, { 'date_start': value }, context=context)
1739 'date': fields.function(_get_date, fnct_inv=_set_date, \
1740 string='Duration', store=True, type='datetime'),
1741 'duration': fields.integer('Duration'),
1750 class ir_values(osv.osv):
1751 _inherit = 'ir.values'
1753 def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1754 isobject=False, meta=False, preserve_user=False, company=False):
1757 @param self: The object pointer
1758 @param cr: the current row, from the database cursor,
1759 @param uid: the current user's ID for security checks,
1760 @param model: Get The Model
1765 if type(data) in (list, tuple):
1766 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1768 new_model.append(data)
1769 return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1770 value, replace, isobject, meta, preserve_user, company)
1772 def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1773 res_id_req=False, without_user=True, key2_req=True):
1776 @param self: The object pointer
1777 @param cr: the current row, from the database cursor,
1778 @param uid: the current user's ID for security checks,
1779 @param model: Get The Model
1785 if type(data) in (list, tuple):
1786 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1788 new_model.append(data)
1789 return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1790 meta, context, res_id_req, without_user, key2_req)
1794 class ir_model(osv.osv):
1796 _inherit = 'ir.model'
1798 def read(self, cr, uid, ids, fields=None, context=None,
1799 load='_classic_read'):
1801 Overrides orm read method.
1802 @param self: The object pointer
1803 @param cr: the current row, from the database cursor,
1804 @param uid: the current user's ID for security checks,
1805 @param ids: List of IR Model's IDs.
1806 @param context: A standard dictionary for contextual values
1808 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1811 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1812 context=context, load=load)
1815 val['id'] = base_calendar_id2real_id(val['id'])
1816 return isinstance(ids, (str, int, long)) and data[0] or data
1820 class virtual_report_spool(web_services.report_spool):
1822 def exp_report(self, db, uid, object, ids, data=None, context=None):
1825 @param self: The object pointer
1826 @param db: get the current database,
1827 @param uid: the current user's ID for security checks,
1828 @param context: A standard dictionary for contextual values
1831 if object == 'printscreen.list':
1832 return super(virtual_report_spool, self).exp_report(db, uid, \
1833 object, ids, data, context)
1836 new_ids.append(base_calendar_id2real_id(id))
1837 if data.get('id', False):
1838 data['id'] = base_calendar_id2real_id(data['id'])
1839 return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, data, context)
1841 virtual_report_spool()
1843 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: