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)
843 for alarm in self.browse(cr, uid, alarm_ids, context=context):
844 next_trigger_date = None
846 model_obj = self.pool.get(alarm.model_id.model)
847 res_obj = model_obj.browse(cr, uid, alarm.res_id, context=context)
850 if hasattr(res_obj, 'rrule') and res_obj.rrule:
851 event_date = datetime.strptime(res_obj.date, '%Y-%m-%d %H:%M:%S')
852 #exdate is a string and we need a list
853 exdate = res_obj.exdate and res_obj.exdate.split(',') or []
854 recurrent_dates = get_recurrent_dates(res_obj.rrule, exdate, event_date, res_obj.exrule)
856 trigger_interval = alarm.trigger_interval
857 if trigger_interval == 'days':
858 delta = timedelta(days=alarm.trigger_duration)
859 if trigger_interval == 'hours':
860 delta = timedelta(hours=alarm.trigger_duration)
861 if trigger_interval == 'minutes':
862 delta = timedelta(minutes=alarm.trigger_duration)
863 delta = alarm.trigger_occurs == 'after' and delta or -delta
865 for rdate in recurrent_dates:
866 if rdate + delta > current_datetime:
868 if rdate + delta <= current_datetime:
869 re_dates.append(rdate.strftime("%Y-%m-%d %H:%M:%S"))
870 rest_dates = recurrent_dates[len(re_dates):]
871 next_trigger_date = rest_dates and rest_dates[0] or None
874 re_dates = [alarm.trigger_date]
877 if alarm.action == 'email':
878 sub = '[OpenERP Reminder] %s' % (alarm.name)
890 """ % (alarm.name, alarm.trigger_date, alarm.description, \
891 alarm.user_id.name, alarm.user_id.signature)
892 mail_to.add(alarm.user_id.email)
893 for att in alarm.attendee_ids:
894 if att.user_id.email:
895 mail_to.add(att.user_id.email)
897 mail_to = ','.join(mail_to)
903 'email_from': tools.config.get('email_from', mail_to),
905 self.pool.get('mail.mail').create(cr, uid, vals, context=context)
906 if next_trigger_date:
907 update_vals.update({'trigger_date': next_trigger_date})
909 update_vals.update({'state': 'done'})
910 self.write(cr, uid, [alarm.id], update_vals)
916 class calendar_event(osv.osv):
917 _name = "calendar.event"
918 _description = "Calendar Event"
921 def _tz_get(self, cr, uid, context=None):
922 return [(x.lower(), x) for x in pytz.all_timezones]
924 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
925 """Returns duration and/or end date based on values passed
926 @param self: The object pointer
927 @param cr: the current row, from the database cursor,
928 @param uid: the current user's ID for security checks,
929 @param ids: List of calendar event's IDs.
930 @param start_date: Starting date
931 @param duration: Duration between start date and end date
932 @param end_date: Ending Datee
933 @param context: A standard dictionary for contextual values
941 if not end_date and not duration:
943 value['duration'] = duration
945 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
946 if allday: # For all day event
948 value['duration'] = duration
949 # change start_date's time to 00:00:00 in the user's timezone
950 user = self.pool.get('res.users').browse(cr, uid, uid)
951 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
952 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
953 start = start.replace(hour=0, minute=0, second=0) # change start's time to 00:00:00
954 start = start.astimezone(pytz.utc) # convert start back to utc
955 start_date = start.strftime("%Y-%m-%d %H:%M:%S")
956 value['date'] = start_date
958 if end_date and not duration:
959 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
961 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
962 value['duration'] = round(duration, 2)
964 end = start + timedelta(hours=duration)
965 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
966 elif end_date and duration and not allday:
967 # we have both, keep them synchronized:
968 # set duration based on end_date (arbitrary decision: this avoid
969 # getting dates like 06:31:48 instead of 06:32:00)
970 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
972 duration = float(diff.days)* 24 + (float(diff.seconds) / 3600)
973 value['duration'] = round(duration, 2)
975 return {'value': value}
977 def unlink_events(self, cr, uid, ids, context=None):
979 This function deletes event which are linked with the event with recurrent_id
980 (Removes the events which refers to the same UID value)
985 cr.execute("select id from %s where recurrent_id=%%s" % (self._table), (event_id,))
986 r_ids = map(lambda x: x[0], cr.fetchall())
987 self.unlink(cr, uid, r_ids, context=context)
990 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
992 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
993 @param self: The object pointer
994 @param cr: the current row, from the database cursor,
995 @param id: List of calendar event's ids.
996 @param context: A standard dictionary for contextual values
997 @return: dictionary of rrule value.
1001 if not isinstance(ids, list):
1005 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
1006 data = self.read(cr, SUPERUSER_ID, id, ['interval', 'count'], context=context)
1007 if data.get('interval', 0) < 0:
1008 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
1009 if data.get('count', 0) <= 0:
1010 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
1011 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)
1013 if data['recurrency']:
1014 result[event] = self.compute_rule_string(data)
1019 # hook method to fix the wrong signature
1020 def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
1021 return self._rrule_write(self, cr, uid, ids, field_name, field_value, args, context=context)
1023 def _rrule_write(self, obj, cr, uid, ids, field_name, field_value, args, context=None):
1024 if not isinstance(ids, list):
1026 data = self._get_empty_rrule_data()
1028 data['recurrency'] = True
1029 for event in self.browse(cr, uid, ids, context=context):
1030 update_data = self._parse_rrule(field_value, dict(data), event.date)
1031 data.update(update_data)
1032 super(calendar_event, self).write(cr, uid, ids, data, context=context)
1036 'id': fields.integer('ID', readonly=True),
1037 'sequence': fields.integer('Sequence'),
1038 'name': fields.char('Description', size=64, required=False, states={'done': [('readonly', True)]}),
1039 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True,),
1040 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
1041 'create_date': fields.datetime('Created', readonly=True),
1042 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
1043 'description': fields.text('Description', states={'done': [('readonly', True)]}),
1044 'class': fields.selection([('public', 'Public'), ('private', 'Private'), \
1045 ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
1046 'location': fields.char('Location', size=264, help="Location of Event", states={'done': [('readonly', True)]}),
1047 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], \
1048 'Show Time as', states={'done': [('readonly', True)]}),
1049 'base_calendar_url': fields.char('Caldav URL', size=264),
1050 'state': fields.selection([
1051 ('tentative', 'Uncertain'),
1052 ('cancelled', 'Cancelled'),
1053 ('confirmed', 'Confirmed'),
1054 ], 'Status', readonly=True),
1055 'exdate': fields.text('Exception Date/Times', help="This property \
1056 defines the list of date/time exceptions for a recurring calendar component."),
1057 'exrule': fields.char('Exception Rule', size=352, help="Defines a \
1058 rule or repeating pattern of time to exclude from the recurring rule."),
1059 'rrule': fields.function(_get_rulestring, type='char', size=124, \
1060 fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
1061 'rrule_type': fields.selection([
1062 ('daily', 'Day(s)'),
1063 ('weekly', 'Week(s)'),
1064 ('monthly', 'Month(s)'),
1065 ('yearly', 'Year(s)')
1066 ], 'Recurrency', states={'done': [('readonly', True)]},
1067 help="Let the event automatically repeat at that interval"),
1068 'alarm_id': fields.many2one('res.alarm', 'Reminder', states={'done': [('readonly', True)]},
1069 help="Set an alarm at this time, before the event occurs" ),
1070 'base_calendar_alarm_id': fields.many2one('calendar.alarm', 'Alarm'),
1071 'recurrent_id': fields.integer('Recurrent ID'),
1072 'recurrent_id_date': fields.datetime('Recurrent ID date'),
1073 'vtimezone': fields.selection(_tz_get, size=64, string='Timezone'),
1074 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
1075 'organizer': fields.char("Organizer", size=256, states={'done': [('readonly', True)]}), # Map with organizer attribute of VEvent.
1076 'organizer_id': fields.many2one('res.users', 'Organizer', states={'done': [('readonly', True)]}),
1077 'end_type' : fields.selection([('count', 'Number of repetitions'), ('end_date','End date')], 'Recurrence Termination'),
1078 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
1079 'count': fields.integer('Repeat', help="Repeat x times"),
1080 'mo': fields.boolean('Mon'),
1081 'tu': fields.boolean('Tue'),
1082 'we': fields.boolean('Wed'),
1083 'th': fields.boolean('Thu'),
1084 'fr': fields.boolean('Fri'),
1085 'sa': fields.boolean('Sat'),
1086 'su': fields.boolean('Sun'),
1087 'select1': fields.selection([('date', 'Date of month'),
1088 ('day', 'Day of month')], 'Option'),
1089 'day': fields.integer('Date of month'),
1090 'week_list': fields.selection([
1093 ('WE', 'Wednesday'),
1097 ('SU', 'Sunday')], 'Weekday'),
1098 'byday': fields.selection([
1104 ('-1', 'Last')], 'By day'),
1105 'month_list': fields.selection(months.items(), 'Month'),
1106 'end_date': fields.date('Repeat Until'),
1107 'attendee_ids': fields.many2many('calendar.attendee', 'event_attendee_rel', \
1108 'event_id', 'attendee_id', 'Attendees'),
1109 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
1110 'active': fields.boolean('Active', help="If the active field is set to \
1111 true, it will allow you to hide the event alarm information without removing it."),
1112 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
1113 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
1116 def create_attendees(self, cr, uid, ids, context):
1117 att_obj = self.pool.get('calendar.attendee')
1118 user_obj = self.pool.get('res.users')
1119 current_user = user_obj.browse(cr, uid, uid, context=context)
1120 for event in self.browse(cr, uid, ids, context):
1122 for att in event.attendee_ids:
1123 attendees[att.partner_id.id] = True
1126 for partner in event.partner_ids:
1127 if partner.id in attendees:
1129 local_context = context.copy()
1130 local_context.pop('default_state', None)
1131 att_id = self.pool.get('calendar.attendee').create(cr, uid, {
1132 'partner_id': partner.id,
1133 'user_id': partner.user_ids and partner.user_ids[0].id or False,
1134 'ref': self._name+','+str(event.id),
1135 'email': partner.email
1136 }, context=local_context)
1138 mail_to.add(partner.email)
1139 self.write(cr, uid, [event.id], {
1140 'attendee_ids': [(4, att_id)]
1142 new_attendees.append(att_id)
1144 if mail_to and current_user.email:
1145 mail_to = ','.join(mail_to)
1146 att_obj._send_mail(cr, uid, new_attendees, mail_to,
1147 email_from = current_user.email, context=context)
1150 def default_organizer(self, cr, uid, context=None):
1151 user_pool = self.pool.get('res.users')
1152 user = user_pool.browse(cr, uid, uid, context=context)
1155 res += " <%s>" %(user.email)
1159 'end_type': 'count',
1161 'rrule_type': False,
1162 'state': 'tentative',
1168 'user_id': lambda self, cr, uid, ctx: uid,
1169 'organizer': default_organizer,
1172 def _check_closing_date(self, cr, uid, ids, context=None):
1173 for event in self.browse(cr, uid, ids, context=context):
1174 if event.date_deadline < event.date:
1179 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
1182 # TODO for trunk: remove get_recurrent_ids
1183 def get_recurrent_ids(self, cr, uid, select, domain, limit=100, context=None):
1184 """Wrapper for _get_recurrent_ids to get the 'order' parameter from the context"""
1187 order = context.get('order', self._order)
1188 return self._get_recurrent_ids(cr, uid, select, domain, limit=limit, order=order, context=context)
1190 def _get_recurrent_ids(self, cr, uid, select, domain, limit=100, order=None, context=None):
1191 """Gives virtual event ids for recurring events based on value of Recurrence Rule
1192 This method gives ids of dates that comes between start date and end date of calendar views
1193 @param self: The object pointer
1194 @param cr: the current row, from the database cursor,
1195 @param uid: the current user's ID for security checks,
1196 @param limit: The Number of Results to Return
1197 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted"""
1203 fields = ['rrule', 'recurrency', 'exdate', 'exrule', 'date']
1205 order_fields = [field.split()[0] for field in order.split(',')]
1207 # fallback on self._order defined on the model
1208 order_fields = [field.split()[0] for field in self._order.split(',')]
1209 fields = list(set(fields + order_fields))
1211 for data in super(calendar_event, self).read(cr, uid, select, fields, context=context):
1212 if not data['recurrency'] or not data['rrule']:
1213 result_data.append(data)
1214 result.append(data['id'])
1216 event_date = datetime.strptime(data['date'], "%Y-%m-%d %H:%M:%S")
1218 # TOCHECK: the start date should be replaced by event date; the event date will be changed by that of calendar code
1220 exdate = data['exdate'] and data['exdate'].split(',') or []
1221 rrule_str = data['rrule']
1223 rrule_until_date = False
1225 for rule in rrule_str.split(';'):
1226 name, value = rule.split('=')
1229 value = parser.parse(value)
1230 rrule_until_date = parser.parse(value.strftime("%Y-%m-%d %H:%M:%S"))
1231 value = value.strftime("%Y%m%d%H%M%S")
1232 new_rule = '%s=%s' % (name, value)
1233 new_rrule_str.append(new_rule)
1234 new_rrule_str = ';'.join(new_rrule_str)
1235 rdates = get_recurrent_dates(str(new_rrule_str), exdate, event_date, data['exrule'])
1236 for r_date in rdates:
1237 # fix domain evaluation
1238 # step 1: check date and replace expression by True or False, replace other expressions by True
1239 # step 2: evaluation of & and |
1240 # check if there are one False
1243 if str(arg[0]) in (str('date'), str('date_deadline')):
1245 ok = r_date.strftime('%Y-%m-%d')==arg[2]
1247 ok = r_date.strftime('%Y-%m-%d')>arg[2]
1249 ok = r_date.strftime('%Y-%m-%d')<arg[2]
1250 if (arg[1] == '>='):
1251 ok = r_date.strftime('%Y-%m-%d')>=arg[2]
1252 if (arg[1] == '<='):
1253 ok = r_date.strftime('%Y-%m-%d')<=arg[2]
1255 elif str(arg) == str('&') or str(arg) == str('|'):
1262 if not isinstance(item, basestring):
1264 elif str(item) == str('&'):
1265 first = new_pile.pop()
1266 second = new_pile.pop()
1267 res = first and second
1268 elif str(item) == str('|'):
1269 first = new_pile.pop()
1270 second = new_pile.pop()
1271 res = first or second
1272 new_pile.append(res)
1274 if [True for item in new_pile if not item]:
1276 idval = real_id2base_calendar_id(data['id'], r_date.strftime("%Y-%m-%d %H:%M:%S"))
1277 r_data = dict(data, id=idval, date=r_date.strftime("%Y-%m-%d %H:%M:%S"))
1278 result.append(idval)
1279 result_data.append(r_data)
1280 ids = list(set(result))
1284 def comparer(left, right):
1285 for fn, mult in comparers:
1286 if type(fn(left)) == tuple and type(fn(right)) == tuple:
1287 # comparing many2one values, sorting on name_get result
1288 leftv, rightv = fn(left)[1], fn(right)[1]
1290 leftv, rightv = fn(left), fn(right)
1291 result = cmp(leftv, rightv)
1293 return mult * result
1296 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1297 comparers = [ ((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1298 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1302 def compute_rule_string(self, data):
1304 Compute rule string according to value type RECUR of iCalendar from the values given.
1305 @param self: the object pointer
1306 @param data: dictionary of freq and interval value
1307 @return: string containing recurring rule (empty if no rule)
1309 def get_week_string(freq, data):
1310 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1311 if freq == 'weekly':
1312 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1314 return ';BYDAY=' + ','.join(byday)
1317 def get_month_string(freq, data):
1318 if freq == 'monthly':
1319 if data.get('select1')=='date' and (data.get('day') < 1 or data.get('day') > 31):
1320 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1322 if data.get('select1')=='day':
1323 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1324 elif data.get('select1')=='date':
1325 return ';BYMONTHDAY=' + str(data.get('day'))
1328 def get_end_date(data):
1329 if data.get('end_date'):
1330 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959'
1332 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1333 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1335 freq = data.get('rrule_type', False)
1338 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1339 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1343 def _get_empty_rrule_data(self):
1346 'recurrency' : False,
1348 'rrule_type' : False,
1365 def _parse_rrule(self, rule, data, date_start):
1366 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1367 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1368 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1370 if r._freq > 0 and r._freq < 4:
1371 data['rrule_type'] = rrule_type[r._freq]
1373 data['count'] = r._count
1374 data['interval'] = r._interval
1375 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1378 for i in xrange(0,7):
1379 if i in r._byweekday:
1380 data[day_list[i]] = True
1381 data['rrule_type'] = 'weekly'
1382 #repeat monthly by nweekday ((weekday, weeknumber), )
1384 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1385 data['byday'] = str(r._bynweekday[0][1])
1386 data['select1'] = 'day'
1387 data['rrule_type'] = 'monthly'
1390 data['day'] = r._bymonthday[0]
1391 data['select1'] = 'date'
1392 data['rrule_type'] = 'monthly'
1394 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1396 data['interval'] = data['interval'] * 12
1398 #FIXEME handle forever case
1400 #in case of repeat for ever that we do not support right now
1401 if not (data.get('count') or data.get('end_date')):
1403 if data.get('count'):
1404 data['end_type'] = 'count'
1406 data['end_type'] = 'end_date'
1409 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1416 if arg[0] in ('date_deadline', unicode('date_deadline')):
1417 if context.get('virtual_id', True):
1418 new_args += ['|','&',('recurrency','=',1),('end_date', arg[1], arg[2])]
1419 elif arg[0] == "id":
1420 new_id = get_real_ids(arg[2])
1421 new_arg = (arg[0], arg[1], new_id)
1422 new_args.append(new_arg)
1423 if not context.get('virtual_id', True):
1424 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count)
1426 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1427 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1428 res = self._get_recurrent_ids(cr, uid, res, args, limit, order=order, context=context)
1433 return res[offset:offset+limit]
1436 def _get_data(self, cr, uid, id, context=None):
1437 return self.read(cr, uid, id,['date', 'date_deadline'])
1439 def need_to_update(self, event_id, vals):
1440 split_id = str(event_id).split("-")
1441 if len(split_id) < 2:
1444 date_start = vals.get('date', '')
1446 date_start = datetime.strptime(date_start, '%Y-%m-%d %H:%M:%S').strftime("%Y%m%d%H%M%S")
1447 return date_start == split_id[1]
1451 def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
1452 def _only_changes_to_apply_on_real_ids(field_names):
1453 ''' return True if changes are only to be made on the real ids'''
1454 for field in field_names:
1455 if field not in ['message_follower_ids']:
1459 context = context or {}
1460 if isinstance(ids, (str, int, long)):
1464 # Special write of complex IDS
1465 for event_id in ids[:]:
1466 if len(str(event_id).split('-')) == 1:
1468 ids.remove(event_id)
1469 real_event_id = base_calendar_id2real_id(event_id)
1471 # if we are setting the recurrency flag to False or if we are only changing fields that
1472 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1473 # then set real ids to be updated.
1474 if not vals.get('recurrency', True) or _only_changes_to_apply_on_real_ids(vals.keys()):
1475 ids.append(real_event_id)
1478 #if edit one instance of a reccurrent id
1479 data = self.read(cr, uid, event_id, ['date', 'date_deadline', \
1480 'rrule', 'duration', 'exdate'])
1481 if data.get('rrule'):
1484 recurrent_id=real_event_id,
1485 recurrent_id_date=data.get('date'),
1493 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1495 date_new = event_id.split('-')[1]
1496 date_new = time.strftime("%Y%m%dT%H%M%S", \
1497 time.strptime(date_new, "%Y%m%d%H%M%S"))
1498 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1499 res = self.write(cr, uid, [real_event_id], {'exdate': exdate})
1501 context.update({'active_id': new_id, 'active_ids': [new_id]})
1504 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1505 vals['vtimezone'] = vals['vtimezone'][40:]
1507 res = super(calendar_event, self).write(cr, uid, ids, vals, context=context)
1509 # set end_date for calendar searching
1510 if vals.get('recurrency', True) and vals.get('end_type', 'count') in ('count', unicode('count')) and \
1511 (vals.get('rrule_type') or vals.get('count') or vals.get('date') or vals.get('date_deadline')):
1512 for data in self.read(cr, uid, ids, ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
1513 end_date = self._set_recurrency_end_date(data, context=context)
1514 super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
1516 if vals.get('partner_ids', False):
1517 self.create_attendees(cr, uid, ids, context)
1519 if ('alarm_id' in vals or 'base_calendar_alarm_id' in vals)\
1520 or ('date' in vals or 'duration' in vals or 'date_deadline' in vals):
1521 alarm_obj = self.pool.get('res.alarm')
1522 alarm_obj.do_alarm_create(cr, uid, ids, self._name, 'date', context=context)
1523 return res or True and False
1525 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1529 if 'date' in groupby:
1530 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1531 virtual_id = context.get('virtual_id', True)
1532 context.update({'virtual_id': False})
1533 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1535 #remove the count, since the value is not consistent with the result of the search when expand the group
1536 for groupname in groupby:
1537 if re.get(groupname + "_count"):
1538 del re[groupname + "_count"]
1539 re.get('__context', {}).update({'virtual_id' : virtual_id})
1542 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1545 fields2 = fields and fields[:] or None
1547 EXTRAFIELDS = ('class','user_id','duration')
1548 for f in EXTRAFIELDS:
1549 if fields and (f not in fields):
1552 # FIXME This whole id mangling has to go!
1553 if isinstance(ids, (str, int, long)):
1558 select = map(lambda x: (x, base_calendar_id2real_id(x)), select)
1561 real_data = super(calendar_event, self).read(cr, uid,
1562 [real_id for base_calendar_id, real_id in select],
1563 fields=fields2, context=context, load=load)
1564 real_data = dict(zip([x['id'] for x in real_data], real_data))
1566 for base_calendar_id, real_id in select:
1567 res = real_data[real_id].copy()
1568 ls = base_calendar_id2real_id(base_calendar_id, with_date=res and res.get('duration', 0) or 0)
1569 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1571 res['date_deadline'] = ls[2]
1572 res['id'] = base_calendar_id
1578 user_id = type(r['user_id']) in (tuple,list) and r['user_id'][0] or r['user_id']
1581 if r['class']=='private':
1583 if f not in ('id','date','date_deadline','duration','user_id','state','interval','count'):
1584 if isinstance(r[f], list):
1592 for k in EXTRAFIELDS:
1593 if (k in r) and (fields and (k not in fields)):
1595 if isinstance(ids, (str, int, long)):
1596 return result and result[0] or False
1599 def copy(self, cr, uid, id, default=None, context=None):
1603 res = super(calendar_event, self).copy(cr, uid, base_calendar_id2real_id(id), default, context)
1604 alarm_obj = self.pool.get('res.alarm')
1605 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1608 def unlink(self, cr, uid, ids, context=None):
1609 if not isinstance(ids, list):
1612 attendee_obj=self.pool.get('calendar.attendee')
1613 for event_id in ids[:]:
1614 if len(str(event_id).split('-')) == 1:
1617 real_event_id = base_calendar_id2real_id(event_id)
1618 data = self.read(cr, uid, real_event_id, ['exdate'], context=context)
1619 date_new = event_id.split('-')[1]
1620 date_new = time.strftime("%Y%m%dT%H%M%S", \
1621 time.strptime(date_new, "%Y%m%d%H%M%S"))
1622 exdate = (data['exdate'] and (data['exdate'] + ',') or '') + date_new
1623 self.write(cr, uid, [real_event_id], {'exdate': exdate})
1624 ids.remove(event_id)
1625 for event in self.browse(cr, uid, ids, context=context):
1626 if event.attendee_ids:
1627 attendee_obj.unlink(cr, uid, [x.id for x in event.attendee_ids], context=context)
1629 res = super(calendar_event, self).unlink(cr, uid, ids, context=context)
1630 self.pool.get('res.alarm').do_alarm_unlink(cr, uid, ids, self._name)
1631 self.unlink_events(cr, uid, ids, context=context)
1634 def _set_recurrency_end_date(self, data, context=None):
1635 if not data.get('recurrency'):
1638 end_type = data.get('end_type')
1639 end_date = data.get('end_date')
1641 if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'date_deadline']):
1642 count = data['count'] + 1
1644 'daily': ('days', 1),
1645 'weekly': ('days', 7),
1646 'monthly': ('months', 1),
1647 'yearly': ('years', 1),
1648 }[data['rrule_type']]
1650 deadline = datetime.strptime(data['date_deadline'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
1651 return deadline + relativedelta(**{delay: count * mult})
1654 def create(self, cr, uid, vals, context=None):
1658 if vals.get('vtimezone', '') and vals.get('vtimezone', '').startswith('/freeassociation.sourceforge.net/tzfile/'):
1659 vals['vtimezone'] = vals['vtimezone'][40:]
1661 res = super(calendar_event, self).create(cr, uid, vals, context)
1663 data = self.read(cr, uid, [res], ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context)[0]
1664 end_date = self._set_recurrency_end_date(data, context=context)
1665 self.write(cr, uid, [res], {'end_date': end_date}, context=context)
1667 alarm_obj = self.pool.get('res.alarm')
1668 alarm_obj.do_alarm_create(cr, uid, [res], self._name, 'date', context=context)
1669 self.create_attendees(cr, uid, [res], context)
1672 def do_tentative(self, cr, uid, ids, context=None, *args):
1673 """ Makes event invitation as Tentative
1674 @param self: The object pointer
1675 @param cr: the current row, from the database cursor,
1676 @param uid: the current user's ID for security checks,
1677 @param ids: List of Event IDs
1678 @param *args: Get Tupple value
1679 @param context: A standard dictionary for contextual values
1681 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
1683 def do_cancel(self, cr, uid, ids, context=None, *args):
1684 """ Makes event invitation as Tentative
1685 @param self: The object pointer
1686 @param cr: the current row, from the database cursor,
1687 @param uid: the current user's ID for security checks,
1688 @param ids: List of Event IDs
1689 @param *args: Get Tupple value
1690 @param context: A standard dictionary for contextual values
1692 return self.write(cr, uid, ids, {'state': 'cancelled'}, context)
1694 def do_confirm(self, cr, uid, ids, context=None, *args):
1695 """ Makes event invitation as Tentative
1696 @param self: The object pointer
1697 @param cr: the current row, from the database cursor,
1698 @param uid: the current user's ID for security checks,
1699 @param ids: List of Event IDs
1700 @param *args: Get Tupple value
1701 @param context: A standard dictionary for contextual values
1703 return self.write(cr, uid, ids, {'state': 'confirmed'}, context)
1707 class calendar_todo(osv.osv):
1708 """ Calendar Task """
1710 _name = "calendar.todo"
1711 _inherit = "calendar.event"
1712 _description = "Calendar Task"
1714 def _get_date(self, cr, uid, ids, name, arg, context=None):
1717 @param self: The object pointer
1718 @param cr: the current row, from the database cursor,
1719 @param uid: the current user's ID for security checks,
1720 @param ids: List of calendar todo's IDs.
1721 @param args: list of tuples of form [(‘name_of_the_field', ‘operator', value), ...].
1722 @param context: A standard dictionary for contextual values
1726 for event in self.browse(cr, uid, ids, context=context):
1727 res[event.id] = event.date_start
1730 def _set_date(self, cr, uid, id, name, value, arg, context=None):
1733 @param self: The object pointer
1734 @param cr: the current row, from the database cursor,
1735 @param uid: the current user's ID for security checks,
1736 @param id: calendar's ID.
1737 @param value: Get Value
1738 @param args: list of tuples of form [('name_of_the_field', 'operator', value), ...].
1739 @param context: A standard dictionary for contextual values
1742 assert name == 'date'
1743 return self.write(cr, uid, id, { 'date_start': value }, context=context)
1746 'date': fields.function(_get_date, fnct_inv=_set_date, \
1747 string='Duration', store=True, type='datetime'),
1748 'duration': fields.integer('Duration'),
1757 class ir_values(osv.osv):
1758 _inherit = 'ir.values'
1760 def set(self, cr, uid, key, key2, name, models, value, replace=True, \
1761 isobject=False, meta=False, preserve_user=False, company=False):
1764 @param self: The object pointer
1765 @param cr: the current row, from the database cursor,
1766 @param uid: the current user's ID for security checks,
1767 @param model: Get The Model
1772 if type(data) in (list, tuple):
1773 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1775 new_model.append(data)
1776 return super(ir_values, self).set(cr, uid, key, key2, name, new_model, \
1777 value, replace, isobject, meta, preserve_user, company)
1779 def get(self, cr, uid, key, key2, models, meta=False, context=None, \
1780 res_id_req=False, without_user=True, key2_req=True):
1783 @param self: The object pointer
1784 @param cr: the current row, from the database cursor,
1785 @param uid: the current user's ID for security checks,
1786 @param model: Get The Model
1792 if type(data) in (list, tuple):
1793 new_model.append((data[0], base_calendar_id2real_id(data[1])))
1795 new_model.append(data)
1796 return super(ir_values, self).get(cr, uid, key, key2, new_model, \
1797 meta, context, res_id_req, without_user, key2_req)
1801 class ir_model(osv.osv):
1803 _inherit = 'ir.model'
1805 def read(self, cr, uid, ids, fields=None, context=None,
1806 load='_classic_read'):
1808 Overrides orm read method.
1809 @param self: The object pointer
1810 @param cr: the current row, from the database cursor,
1811 @param uid: the current user's ID for security checks,
1812 @param ids: List of IR Model's IDs.
1813 @param context: A standard dictionary for contextual values
1815 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
1818 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, \
1819 context=context, load=load)
1822 val['id'] = base_calendar_id2real_id(val['id'])
1823 return isinstance(ids, (str, int, long)) and data[0] or data
1827 class virtual_report_spool(web_services.report_spool):
1829 def exp_report(self, db, uid, object, ids, data=None, context=None):
1832 @param self: The object pointer
1833 @param db: get the current database,
1834 @param uid: the current user's ID for security checks,
1835 @param context: A standard dictionary for contextual values
1838 if object == 'printscreen.list':
1839 return super(virtual_report_spool, self).exp_report(db, uid, \
1840 object, ids, data, context)
1843 new_ids.append(base_calendar_id2real_id(id))
1844 if data.get('id', False):
1845 data['id'] = base_calendar_id2real_id(data['id'])
1846 return super(virtual_report_spool, self).exp_report(db, uid, object, new_ids, data, context)
1848 virtual_report_spool()
1850 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: