1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Business Applications
5 # Copyright (c) 2011-2014 OpenERP S.A. <http://openerp.com>
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 ##############################################################################
26 import openerp.service.report
28 from werkzeug.exceptions import BadRequest
29 from datetime import datetime, timedelta
30 from dateutil import parser
31 from dateutil import rrule
32 from dateutil.relativedelta import relativedelta
33 from openerp import tools, SUPERUSER_ID
34 from openerp.osv import fields, osv
35 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
36 from openerp.tools.translate import _
37 from openerp.http import request
38 from operator import itemgetter
41 _logger = logging.getLogger(__name__)
44 def calendar_id2real_id(calendar_id=None, with_date=False):
46 Convert a "virtual/recurring event id" (type string) into a real event id (type int).
47 E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
48 @param calendar_id: id of calendar
49 @param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id
50 @return: real event id
52 if calendar_id and isinstance(calendar_id, (str, unicode)):
53 res = calendar_id.split('-')
57 real_date = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(res[1], "%Y%m%d%H%M%S"))
58 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
59 end = start + timedelta(hours=with_date)
60 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
62 return calendar_id and int(calendar_id) or calendar_id
65 def get_real_ids(ids):
66 if isinstance(ids, (str, int, long)):
67 return calendar_id2real_id(ids)
69 if isinstance(ids, (list, tuple)):
70 return [calendar_id2real_id(id) for id in ids]
73 class calendar_attendee(osv.Model):
75 Calendar Attendee Information
77 _name = 'calendar.attendee'
79 _description = 'Attendee information'
81 def _compute_data(self, cr, uid, ids, name, arg, context=None):
83 Compute data on function fields for attendee values.
84 @param ids: list of calendar attendee's IDs
85 @param name: name of field
86 @return: dictionary of form {id: {'field Name': value'}}
90 for attdata in self.browse(cr, uid, ids, context=context):
94 if attdata.partner_id:
95 result[id][name] = attdata.partner_id.name or False
97 result[id][name] = attdata.email or ''
98 if name == 'event_date':
99 result[id][name] = attdata.event_id.date
100 if name == 'event_end_date':
101 result[id][name] = attdata.event_id.date_deadline
105 ('needsAction', 'Needs Action'),
106 ('tentative', 'Uncertain'),
107 ('declined', 'Declined'),
108 ('accepted', 'Accepted'),
112 'state': fields.selection(STATE_SELECTION, 'Status', readonly=True, help="Status of the attendee's participation"),
113 'cn': fields.function(_compute_data, string='Common name', type="char", multi='cn', store=True),
114 'partner_id': fields.many2one('res.partner', 'Contact', readonly="True"),
115 'email': fields.char('Email', help="Email of Invited Person"),
116 'event_date': fields.function(_compute_data, string='Event Date', type="datetime", multi='event_date'),
117 'event_end_date': fields.function(_compute_data, string='Event End Date', type="datetime", multi='event_end_date'),
118 'availability': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Free/Busy', readonly="True"),
119 'access_token': fields.char('Invitation Token'),
120 'event_id': fields.many2one('calendar.event', 'Meeting linked'),
123 'state': 'needsAction',
126 def copy(self, cr, uid, id, default=None, context=None):
127 raise osv.except_osv(_('Warning!'), _('You cannot duplicate a calendar attendee.'))
129 def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
131 Make entry on email and availability on change of partner_id field.
132 @param partner_id: changed value of partner id
135 return {'value': {'email': ''}}
136 partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
137 return {'value': {'email': partner.email}}
139 def get_ics_file(self, cr, uid, event_obj, context=None):
141 Returns iCalendar file for the event invitation.
142 @param event_obj: event object (browse record)
143 @return: .ics file content
147 def ics_datetime(idate, short=False):
149 return datetime.strptime(idate.split('.')[0], '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.timezone('UTC'))
153 # FIXME: why isn't this in CalDAV?
158 cal = vobject.iCalendar()
159 event = cal.add('vevent')
160 if not event_obj.date_deadline or not event_obj.date:
161 raise osv.except_osv(_('Warning!'), _("First you have to specify the date of the invitation."))
162 event.add('created').value = ics_datetime(time.strftime('%Y-%m-%d %H:%M:%S'))
163 event.add('dtstart').value = ics_datetime(event_obj.date)
164 event.add('dtend').value = ics_datetime(event_obj.date_deadline)
165 event.add('summary').value = event_obj.name
166 if event_obj.description:
167 event.add('description').value = event_obj.description
168 if event_obj.location:
169 event.add('location').value = event_obj.location
171 event.add('rrule').value = event_obj.rrule
173 if event_obj.alarm_ids:
174 for alarm in event_obj.alarm_ids:
175 valarm = event.add('valarm')
176 interval = alarm.interval
177 duration = alarm.duration
178 trigger = valarm.add('TRIGGER')
179 trigger.params['related'] = ["START"]
180 if interval == 'days':
181 delta = timedelta(days=duration)
182 elif interval == 'hours':
183 delta = timedelta(hours=duration)
184 elif interval == 'minutes':
185 delta = timedelta(minutes=duration)
186 trigger.value = delta
187 valarm.add('DESCRIPTION').value = alarm.name or 'OpenERP'
188 for attendee in event_obj.attendee_ids:
189 attendee_add = event.add('attendee')
190 attendee_add.value = 'MAILTO:' + (attendee.email or '')
191 res = cal.serialize()
194 def _send_mail_to_attendees(self, cr, uid, ids, email_from=tools.config.get('email_from', False), template_xmlid='calendar_template_meeting_invitation', context=None):
196 Send mail for event invitation to event attendees.
197 @param email_from: email address for user sending the mail
201 if self.pool['ir.config_parameter'].get_param(cr, uid, 'calendar.block_mail', default=False):
205 data_pool = self.pool['ir.model.data']
206 mailmess_pool = self.pool['mail.message']
207 mail_pool = self.pool['mail.mail']
208 template_pool = self.pool['email.template']
209 local_context = context.copy()
211 'needsAction': 'grey',
213 'tentative': '#FFFF00',
217 if not isinstance(ids, (tuple, list)):
220 dummy, template_id = data_pool.get_object_reference(cr, uid, 'calendar', template_xmlid)
221 dummy, act_id = data_pool.get_object_reference(cr, uid, 'calendar', "view_calendar_event_calendar")
222 local_context.update({
224 'action_id': self.pool['ir.actions.act_window'].search(cr, uid, [('view_id', '=', act_id)], context=context)[0],
226 'base_url': self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
229 for attendee in self.browse(cr, uid, ids, context=context):
230 if attendee.email and email_from and attendee.email != email_from:
231 ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
232 mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
236 vals['attachment_ids'] = [(0, 0, {'name': 'invitation.ics',
237 'datas_fname': 'invitation.ics',
238 'datas': str(ics_file).encode('base64')})]
239 vals['model'] = None # We don't want to have the mail in the tchatter while in queue!
240 the_mailmess = mail_pool.browse(cr, uid, mail_id, context=context).mail_message_id
241 mailmess_pool.write(cr, uid, [the_mailmess.id], vals, context=context)
242 mail_ids.append(mail_id)
245 res = mail_pool.send(cr, uid, mail_ids, context=context)
249 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
251 Make entry on email and availability on change of user_id field.
252 @param ids: list of attendee's IDs
253 @param user_id: changed value of User id
254 @return: dictionary of values which put value in email and availability fields
257 return {'value': {'email': ''}}
259 user = self.pool['res.users'].browse(cr, uid, user_id, *args)
260 return {'value': {'email': user.email, 'availability': user.availability}}
262 def do_tentative(self, cr, uid, ids, context=None, *args):
264 Makes event invitation as Tentative.
265 @param ids: list of attendee's IDs
267 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
269 def do_accept(self, cr, uid, ids, context=None, *args):
271 Marks event invitation as Accepted.
272 @param ids: list of attendee's IDs
276 meeting_obj = self.pool['calendar.event']
277 res = self.write(cr, uid, ids, {'state': 'accepted'}, context)
278 for attendee in self.browse(cr, uid, ids, context=context):
279 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
283 def do_decline(self, cr, uid, ids, context=None, *args):
285 Marks event invitation as Declined.
286 @param ids: list of calendar attendee's IDs
290 meeting_obj = self.pool['calendar.event']
291 res = self.write(cr, uid, ids, {'state': 'declined'}, context)
292 for attendee in self.browse(cr, uid, ids, context=context):
293 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has declined invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
296 def create(self, cr, uid, vals, context=None):
299 if not vals.get("email") and vals.get("cn"):
300 cnval = vals.get("cn").split(':')
301 email = filter(lambda x: x.__contains__('@'), cnval)
302 vals['email'] = email and email[0] or ''
303 vals['cn'] = vals.get("cn")
304 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
308 class res_partner(osv.Model):
309 _inherit = 'res.partner'
311 'calendar_last_notif_ack': fields.datetime('Last notification marked as read from base Calendar'),
314 def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
318 meeting = self.pool['calendar.event'].browse(cr, uid, get_real_ids(meeting_id), context=context)
319 for partner in self.browse(cr, uid, ids, context=context):
320 data = self.name_get(cr, uid, [partner.id], context)[0]
322 for attendee in meeting.attendee_ids:
323 if attendee.partner_id.id == partner.id:
324 data = (data[0], data[1], attendee.state)
328 def calendar_last_notif_ack(self, cr, uid, context=None):
329 partner = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id
330 self.write(cr, uid, partner.id, {'calendar_last_notif_ack': datetime.now()}, context=context)
334 class calendar_alarm_manager(osv.AbstractModel):
335 _name = 'calendar.alarm_manager'
337 def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None):
342 cal.date - interval '1' minute * calcul_delta.max_delta AS first_alarm,
344 WHEN cal.recurrency THEN cal.end_date - interval '1' minute * calcul_delta.min_delta
345 ELSE cal.date_deadline - interval '1' minute * calcul_delta.min_delta
347 cal.date as first_event_date,
349 WHEN cal.recurrency THEN cal.end_date
350 ELSE cal.date_deadline
351 END as last_event_date,
352 calcul_delta.min_delta,
353 calcul_delta.max_delta,
356 calendar_event AS cal
360 rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
362 calendar_alarm_calendar_event_rel AS rel
363 LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
364 WHERE alarm.type in %s
365 GROUP BY rel.calendar_event_id
366 ) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id
370 RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
371 AND part_rel.res_partner_id = %s
377 type_to_read += ('notification',)
379 type_to_read += ('email',)
381 tuple_params = (type_to_read,)
383 #ADD FILTER ON PARTNER_ID
385 base_request += filter_user
386 tuple_params += (partner_id, )
389 tuple_params += (seconds, seconds,)
391 cr.execute("""SELECT *
392 FROM ( %s ) AS ALL_EVENTS
393 WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second )
394 AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%%s' second )
395 """ % base_request, tuple_params)
397 for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
399 'event_id': event_id,
400 'first_alarm': first_alarm,
401 'last_alarm': last_alarm,
402 'first_meeting': first_meeting,
403 'last_meeting': last_meeting,
404 'min_duration': min_duration,
405 'max_duration': max_duration,
411 def do_check_alarm_for_one_date(self, cr, uid, one_date, event, event_maxdelta, in_the_next_X_seconds, after=False, notif=True, mail=True, context=None):
416 alarm_type.append('notification')
418 alarm_type.append('email')
420 if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
421 for alarm in event.alarm_ids:
422 if alarm.type in alarm_type and \
423 one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
424 (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], "%Y-%m-%d %H:%M:%S")):
426 'alarm_id': alarm.id,
427 'event_id': event.id,
428 'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
433 def get_next_mail(self, cr, uid, context=None):
434 cron = self.pool.get('ir.cron').search(cr, uid, [('model', 'ilike', self._name)], context=context)
435 if cron and len(cron) == 1:
436 cron = self.pool.get('ir.cron').browse(cr, uid, cron[0], context=context)
438 _logger.exception("Cron for " + self._name + " can not be identified !")
440 if cron.interval_type == "weeks":
441 cron_interval = cron.interval_number * 7 * 24 * 60 * 60
442 elif cron.interval_type == "days":
443 cron_interval = cron.interval_number * 24 * 60 * 60
444 elif cron.interval_type == "hours":
445 cron_interval = cron.interval_number * 60 * 60
446 elif cron.interval_type == "minutes":
447 cron_interval = cron.interval_number * 60
448 elif cron.interval_type == "seconds":
449 cron_interval = cron.interval_number
451 if not cron_interval:
452 _logger.exception("Cron delay can not be computed !")
454 all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
456 for event in all_events: # .values()
457 max_delta = all_events[event]['max_duration']
458 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
459 if curEvent.recurrency:
462 for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
463 in_date_format = one_date.replace(tzinfo=None)
464 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
466 for alert in LastFound:
467 self.do_mail_reminder(cr, uid, alert, context=context)
469 if not bFound: # if it's the first alarm for this recurrent event
471 if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event
474 in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
475 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
477 for alert in LastFound:
478 self.do_mail_reminder(cr, uid, alert, context=context)
480 def get_next_notif(self, cr, uid, context=None):
481 ajax_check_every_seconds = 300
482 partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
488 all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
490 for event in all_events: # .values()
491 max_delta = all_events[event]['max_duration']
492 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
493 if curEvent.recurrency:
496 for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
497 in_date_format = one_date.replace(tzinfo=None)
498 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner.calendar_last_notif_ack, mail=False, context=context)
500 for alert in LastFound:
501 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
502 if not bFound: # if it's the first alarm for this recurrent event
504 if bFound and not LastFound: # if the precedent event had alarm but not this one, we can stop the search fot this event
507 in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
508 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, partner.calendar_last_notif_ack, mail=False, context=context)
510 for alert in LastFound:
511 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
514 def do_mail_reminder(self, cr, uid, alert, context=None):
519 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
520 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
522 if alarm.type == 'email':
523 res = self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], template_xmlid='calendar_template_meeting_reminder', context=context)
527 def do_notif_reminder(self, cr, uid, alert, context=None):
528 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
529 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
531 if alarm.type == 'notification':
532 message = event.display_time
534 delta = alert['notify_at'] - datetime.now()
535 delta = delta.seconds + delta.days * 3600 * 24
538 'event_id': event.id,
542 'notify_at': alert['notify_at'].strftime("%Y-%m-%d %H:%M:%S"),
546 class calendar_alarm(osv.Model):
547 _name = 'calendar.alarm'
548 _description = 'Event alarm'
550 def _get_duration(self, cr, uid, ids, field_name, arg, context=None):
552 for alarm in self.browse(cr, uid, ids, context=context):
553 if alarm.interval == "minutes":
554 res[alarm.id] = alarm.duration
555 elif alarm.interval == "hours":
556 res[alarm.id] = alarm.duration * 60
557 elif alarm.interval == "days":
558 res[alarm.id] = alarm.duration * 60 * 24
564 'name': fields.char('Name', required=True), # fields function
565 'type': fields.selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True),
566 'duration': fields.integer('Amount', required=True),
567 'interval': fields.selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], 'Unit', required=True),
568 'duration_minutes': fields.function(_get_duration, type='integer', string='duration_minutes', store=True),
572 'type': 'notification',
578 class ir_values(osv.Model):
579 _inherit = 'ir.values'
581 def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
585 if type(data) in (list, tuple):
586 new_model.append((data[0], calendar_id2real_id(data[1])))
588 new_model.append(data)
589 return super(ir_values, self).set(cr, uid, key, key2, name, new_model,
590 value, replace, isobject, meta, preserve_user, company)
592 def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
597 if type(data) in (list, tuple):
598 new_model.append((data[0], calendar_id2real_id(data[1])))
600 new_model.append(data)
601 return super(ir_values, self).get(cr, uid, key, key2, new_model,
602 meta, context, res_id_req, without_user, key2_req)
605 class ir_model(osv.Model):
607 _inherit = 'ir.model'
609 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
610 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
613 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, context=context, load=load)
616 val['id'] = calendar_id2real_id(val['id'])
617 return isinstance(ids, (str, int, long)) and data[0] or data
620 original_exp_report = openerp.service.report.exp_report
623 def exp_report(db, uid, object, ids, data=None, context=None):
627 if object == 'printscreen.list':
628 original_exp_report(db, uid, object, ids, data, context)
631 new_ids.append(calendar_id2real_id(id))
632 if data.get('id', False):
633 data['id'] = calendar_id2real_id(data['id'])
634 return original_exp_report(db, uid, object, new_ids, data, context)
637 openerp.service.report.exp_report = exp_report
640 class calendar_event_type(osv.Model):
641 _name = 'calendar.event.type'
642 _description = 'Meeting Type'
644 'name': fields.char('Name', required=True, translate=True),
648 class calendar_event(osv.Model):
649 """ Model for Calendar Event """
650 _name = 'calendar.event'
651 _description = "Meeting"
653 _inherit = ["mail.thread", "ir.needaction_mixin"]
655 def do_run_scheduler(self, cr, uid, id, context=None):
656 self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context)
658 def get_recurrent_date_by_event(self, cr, uid, event, context=None):
659 """Get recurrent dates based on Rule string and all event where recurrent_id is child
663 val = parser.parse(''.join((re.compile('\d')).findall(date)))
664 ## Dates are localized to saved timezone if any, else current timezone.
666 val = pytz.UTC.localize(val)
667 return val.astimezone(timezone)
669 timezone = pytz.timezone(event.vtimezone or context.get('tz') or 'UTC')
670 startdate = pytz.UTC.localize(datetime.strptime(event.date, "%Y-%m-%d %H:%M:%S")) # Add "+hh:mm" timezone
672 startdate = datetime.now()
674 ## Convert the start date to saved timezone (or context tz) as it'll
675 ## define the correct hour/day asked by the user to repeat for recurrence.
676 startdate = startdate.astimezone(timezone) # transform "+hh:mm" timezone
677 rset1 = rrule.rrulestr(str(event.rrule), dtstart=startdate, forceset=True)
678 ids_depending = self.search(cr, uid, [('recurrent_id', '=', event.id), '|', ('active', '=', False), ('active', '=', True)], context=context)
679 all_events = self.browse(cr, uid, ids_depending, context=context)
681 for ev in all_events:
682 rset1._exdate.append(todate(ev.recurrent_id_date))
684 return [d.astimezone(pytz.UTC) for d in rset1]
686 def _get_recurrency_end_date(self, data, context=None):
687 if not data.get('recurrency'):
690 end_type = data.get('end_type')
691 end_date = data.get('end_date')
693 if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'date_deadline']):
694 count = data['count'] + 1
696 'daily': ('days', 1),
697 'weekly': ('days', 7),
698 'monthly': ('months', 1),
699 'yearly': ('years', 1),
700 }[data['rrule_type']]
702 deadline = datetime.strptime(data['date_deadline'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
703 return deadline + relativedelta(**{delay: count * mult})
706 def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
708 Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
710 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
711 for meeting_id in meeting_ids:
712 for attendee in self.browse(cr, uid, meeting_id, context).attendee_ids:
713 if user.partner_id.id == attendee.partner_id.id:
717 def _get_display_time(self, cr, uid, meeting_id, context=None):
719 Return date and time (from to from) based on duration with timezone in string :
721 1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
722 2) if event all day ,return : AllDay, July-31-2013
727 tz = context.get('tz', False)
728 if not tz: # tz can have a value False, so dont do it in the default value of get !
729 tz = pytz.timezone('UTC')
731 meeting = self.browse(cr, uid, meeting_id, context=context)
732 date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
733 date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
734 event_date = date.strftime('%B-%d-%Y')
735 display_time = date.strftime('%H-%M')
737 time = _("AllDay , %s") % (event_date)
738 elif meeting.duration < 24:
739 duration = date + timedelta(hours=meeting.duration)
740 time = ("%s at (%s To %s) (%s)") % (event_date, display_time, duration.strftime('%H-%M'), tz)
742 time = ("%s at %s To\n %s at %s (%s)") % (event_date, display_time, date_deadline.strftime('%B-%d-%Y'), date_deadline.strftime('%H-%M'), tz)
745 def _compute(self, cr, uid, ids, fields, arg, context=None):
747 for meeting_id in ids:
749 attendee = self._find_my_attendee(cr, uid, [meeting_id], context)
751 if field == 'is_attendee':
752 res[meeting_id][field] = True if attendee else False
753 elif field == 'attendee_status':
754 res[meeting_id][field] = attendee.state if attendee else 'needsAction'
755 elif field == 'display_time':
756 res[meeting_id][field] = self._get_display_time(cr, uid, meeting_id, context=context)
759 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
761 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
762 @return: dictionary of rrule value.
765 if not isinstance(ids, list):
769 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
770 data = self.browse(cr, SUPERUSER_ID, id, context=context)
772 if data.interval and data.interval < 0:
773 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
774 if data.count and data.count <= 0:
775 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
777 data = self.read(cr, uid, id, ['id', 'byday', 'recurrency', 'month_list', 'end_date', 'rrule_type', 'month_by', 'interval', 'count', 'end_type', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su', 'day', 'week_list'], context=context)
779 if data['recurrency']:
780 result[event] = self.compute_rule_string(data)
785 # retro compatibility function
786 def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
787 return self._set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=context)
789 def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
790 if not isinstance(ids, list):
792 data = self._get_empty_rrule_data()
794 data['recurrency'] = True
795 for event in self.browse(cr, uid, ids, context=context):
797 update_data = self._parse_rrule(field_value, dict(data), rdate)
798 data.update(update_data)
799 self.write(cr, uid, ids, data, context=context)
802 def _tz_get(self, cr, uid, context=None):
803 return [(x.lower(), x) for x in pytz.all_timezones]
807 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
810 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
814 'id': fields.integer('ID', readonly=True),
815 'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
816 'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
817 'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
818 'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
819 'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
820 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True, track_visibility='onchange'),
821 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
822 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
823 'description': fields.text('Description', states={'done': [('readonly', True)]}),
824 'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
825 'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
826 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
827 'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
828 'rrule_type': fields.selection([('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)')], 'Recurrency', states={'done': [('readonly', True)]}, help="Let the event automatically repeat at that interval"),
829 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
830 'recurrent_id': fields.integer('Recurrent ID'),
831 'recurrent_id_date': fields.datetime('Recurrent ID date'),
832 'vtimezone': fields.selection(_tz_get, string='Timezone'),
833 'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
834 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
835 'count': fields.integer('Repeat', help="Repeat x times"),
836 'mo': fields.boolean('Mon'),
837 'tu': fields.boolean('Tue'),
838 'we': fields.boolean('Wed'),
839 'th': fields.boolean('Thu'),
840 'fr': fields.boolean('Fri'),
841 'sa': fields.boolean('Sat'),
842 'su': fields.boolean('Sun'),
843 'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
844 'day': fields.integer('Date of month'),
845 'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
846 'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
847 'end_date': fields.date('Repeat Until'),
848 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
849 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
850 'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False), # Color of creator
851 'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the event alarm information without removing it."),
852 'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
853 'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
854 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
855 'alarm_ids': fields.many2many('calendar.alarm', string='Reminders', ondelete="restrict"),
868 'user_id': lambda self, cr, uid, ctx: uid,
869 'partner_ids': lambda self, cr, uid, ctx: [self.pool['res.users'].browse(cr, uid, [uid], context=ctx)[0].partner_id.id]
872 def _check_closing_date(self, cr, uid, ids, context=None):
873 for event in self.browse(cr, uid, ids, context=context):
874 if event.date_deadline < event.date:
879 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
882 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
884 """Returns duration and/or end date based on values passed
885 @param ids: List of calendar event's IDs.
894 if not end_date and not duration:
896 value['duration'] = duration
898 if allday: # For all day event
899 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
900 user = self.pool['res.users'].browse(cr, uid, uid)
901 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
902 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
903 start = start.astimezone(pytz.utc) # convert start back to utc
905 value['duration'] = 24.0
906 value['date'] = datetime.strftime(start, "%Y-%m-%d %H:%M:%S")
908 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
910 if end_date and not duration:
911 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
913 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
914 value['duration'] = round(duration, 2)
916 end = start + timedelta(hours=duration)
917 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
918 elif end_date and duration and not allday:
919 # we have both, keep them synchronized:
920 # set duration based on end_date (arbitrary decision: this avoid
921 # getting dates like 06:31:48 instead of 06:32:00)
922 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
924 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
925 value['duration'] = round(duration, 2)
927 return {'value': value}
929 def new_invitation_token(self, cr, uid, record, partner_id):
930 return uuid.uuid4().hex
932 def create_attendees(self, cr, uid, ids, context):
933 user_obj = self.pool['res.users']
934 current_user = user_obj.browse(cr, uid, uid, context=context)
936 for event in self.browse(cr, uid, ids, context):
938 for att in event.attendee_ids:
939 attendees[att.partner_id.id] = True
941 new_att_partner_ids = []
942 for partner in event.partner_ids:
943 if partner.id in attendees:
945 access_token = self.new_invitation_token(cr, uid, event, partner.id)
947 'partner_id': partner.id,
948 'event_id': event.id,
949 'access_token': access_token,
950 'email': partner.email,
953 if partner.id == current_user.partner_id.id:
954 values['state'] = 'accepted'
956 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
957 new_attendees.append(att_id)
958 new_att_partner_ids.append(partner.id)
960 if not current_user.email or current_user.email != partner.email:
961 mail_from = current_user.email or tools.config.get('email_from', False)
962 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
963 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee %s") % (partner.name,), subtype="calendar.subtype_invitation", context=context)
966 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
967 if new_att_partner_ids:
968 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
970 # We remove old attendees who are not in partner_ids now.
971 all_partner_ids = [part.id for part in event.partner_ids]
972 all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
973 all_attendee_ids = [att.id for att in event.attendee_ids]
974 partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
976 attendee_ids_to_remove = []
978 if partner_ids_to_remove:
979 attendee_ids_to_remove = self.pool["calendar.attendee"].search(cr, uid, [('partner_id.id', 'in', partner_ids_to_remove), ('event_id.id', '=', event.id)], context=context)
980 if attendee_ids_to_remove:
981 self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
984 'new_attendee_ids': new_attendees,
985 'old_attendee_ids': all_attendee_ids,
986 'removed_attendee_ids': attendee_ids_to_remove
990 def get_search_fields(self, browse_event, order_fields, r_date=None):
992 for ord in order_fields:
993 if ord == 'id' and r_date:
994 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
996 sort_fields[ord] = browse_event[ord]
997 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
998 name_get = browse_event[ord].name_get()
999 if len(name_get) and len(name_get[0]) >= 2:
1000 sort_fields[ord] = name_get[0][1]
1004 def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
1006 """Gives virtual event ids for recurring events
1007 This method gives ids of dates that comes between start date and end date of calendar views
1009 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1015 if isinstance(event_id, (str, int, long)):
1016 ids_to_browse = [event_id] # keep select for return
1018 ids_to_browse = event_id
1021 order_fields = [field.split()[0] for field in order.split(',')]
1023 # fallback on self._order defined on the model
1024 order_fields = [field.split()[0] for field in self._order.split(',')]
1026 if 'id' not in order_fields:
1027 order_fields.append('id')
1031 for ev in self.browse(cr, uid, ids_to_browse, context=context):
1032 if not ev.recurrency or not ev.rrule:
1033 result.append(ev.id)
1034 result_data.append(self.get_search_fields(ev, order_fields))
1037 rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1039 for r_date in rdates:
1040 # fix domain evaluation
1041 # step 1: check date and replace expression by True or False, replace other expressions by True
1042 # step 2: evaluation of & and |
1043 # check if there are one False
1047 if str(arg[0]) in (str('date'), str('date_deadline'), str('end_date')):
1049 ok = r_date.strftime('%Y-%m-%d') == arg[2]
1051 ok = r_date.strftime('%Y-%m-%d') > arg[2]
1053 ok = r_date.strftime('%Y-%m-%d') < arg[2]
1054 if (arg[1] == '>='):
1055 ok = r_date.strftime('%Y-%m-%d') >= arg[2]
1056 if (arg[1] == '<='):
1057 ok = r_date.strftime('%Y-%m-%d') <= arg[2]
1059 elif str(arg) == str('&') or str(arg) == str('|'):
1066 if not isinstance(item, basestring):
1068 elif str(item) == str('&'):
1069 first = new_pile.pop()
1070 second = new_pile.pop()
1071 res = first and second
1072 elif str(item) == str('|'):
1073 first = new_pile.pop()
1074 second = new_pile.pop()
1075 res = first or second
1076 new_pile.append(res)
1078 if [True for item in new_pile if not item]:
1080 result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
1083 def comparer(left, right):
1084 for fn, mult in comparers:
1085 result = cmp(fn(left), fn(right))
1087 return mult * result
1090 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1091 comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1092 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1094 if isinstance(event_id, (str, int, long)):
1095 return ids and ids[0] or False
1099 def compute_rule_string(self, data):
1101 Compute rule string according to value type RECUR of iCalendar from the values given.
1102 @param self: the object pointer
1103 @param data: dictionary of freq and interval value
1104 @return: string containing recurring rule (empty if no rule)
1106 def get_week_string(freq, data):
1107 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1108 if freq == 'weekly':
1109 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1110 # byday = map(lambda x: x.upper(),[data[day] for day in weekdays if data[day]])
1113 return ';BYDAY=' + ','.join(byday)
1116 def get_month_string(freq, data):
1117 if freq == 'monthly':
1118 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1119 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1121 if data.get('month_by') == 'day': # Eg : Second Monday of the month
1122 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1123 elif data.get('month_by') == 'date': # Eg : 16th of the month
1124 return ';BYMONTHDAY=' + str(data.get('day'))
1127 def get_end_date(data):
1128 if data.get('end_date'):
1129 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1131 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1132 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1134 freq = data.get('rrule_type', False) # day/week/month/year
1137 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1138 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1142 def _get_empty_rrule_data(self):
1145 'recurrency': False,
1147 'rrule_type': False,
1163 def _parse_rrule(self, rule, data, date_start):
1164 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1165 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1166 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1168 if r._freq > 0 and r._freq < 4:
1169 data['rrule_type'] = rrule_type[r._freq]
1171 data['count'] = r._count
1172 data['interval'] = r._interval
1173 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1176 for i in xrange(0, 7):
1177 if i in r._byweekday:
1178 data[day_list[i]] = True
1179 data['rrule_type'] = 'weekly'
1180 #repeat monthly by nweekday ((weekday, weeknumber), )
1182 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1183 data['byday'] = str(r._bynweekday[0][1])
1184 data['month_by'] = 'day'
1185 data['rrule_type'] = 'monthly'
1188 data['day'] = r._bymonthday[0]
1189 data['month_by'] = 'date'
1190 data['rrule_type'] = 'monthly'
1192 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1194 data['interval'] = data['interval'] * 12
1196 #FIXEME handle forever case
1198 #in case of repeat for ever that we do not support right now
1199 if not (data.get('count') or data.get('end_date')):
1201 if data.get('count'):
1202 data['end_type'] = 'count'
1204 data['end_type'] = 'end_date'
1207 def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1209 for virtual_id in ids:
1210 real_id = calendar_id2real_id(virtual_id)
1211 result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1212 res[virtual_id] = result[real_id]
1215 def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1216 """ The basic purpose of this method is to check that destination partners
1217 effectively have email addresses. Otherwise a warning is thrown.
1218 :param value: value format: [[6, 0, [3, 4]]]
1222 if not value or not value[0] or not value[0][0] == 6:
1225 res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1228 def onchange_rec_day(self, cr, uid, id, date, mo, tu, we, th, fr, sa, su):
1229 """ set the start date according to the first occurence of rrule"""
1230 rrule_obj = self._get_empty_rrule_data()
1233 'rrule_type': 'weekly',
1243 str_rrule = self.compute_rule_string(rrule_obj)
1244 first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
1245 return {'value': {'date': first_occurence.strftime("%Y-%m-%d") + ' 00:00:00'}}
1247 def check_partners_email(self, cr, uid, partner_ids, context=None):
1248 """ Verify that selected partner_ids have an email_address defined.
1249 Otherwise throw a warning. """
1250 partner_wo_email_lst = []
1251 for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1252 if not partner.email:
1253 partner_wo_email_lst.append(partner)
1254 if not partner_wo_email_lst:
1256 warning_msg = _('The following contacts have no email address :')
1257 for partner in partner_wo_email_lst:
1258 warning_msg += '\n- %s' % (partner.name)
1259 return {'warning': {
1260 'title': _('Email addresses not found'),
1261 'message': warning_msg,
1264 # ----------------------------------------
1266 # ----------------------------------------
1268 # shows events of the day for this user
1270 def _needaction_domain_get(self, cr, uid, context=None):
1271 return [('end_date', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 23:59:59')), ('date', '>=', time.strftime(DEFAULT_SERVER_DATE_FORMAT + ' 23:59:59')), ('user_id', '=', uid)]
1273 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
1274 if isinstance(thread_id, str):
1275 thread_id = get_real_ids(thread_id)
1276 if context.get('default_date'):
1277 del context['default_date']
1278 return super(calendar_event, self).message_post(cr, uid, thread_id, body=body, subject=subject, type=type, subtype=subtype, parent_id=parent_id, attachments=attachments, context=context, **kwargs)
1280 def do_sendmail(self, cr, uid, ids, context=None):
1281 for event in self.browse(cr, uid, ids, context):
1282 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1284 if current_user.email:
1285 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], email_from=current_user.email, context=context):
1286 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="calendar.subtype_invitation", context=context)
1289 def get_attendee(self, cr, uid, meeting_id, context=None):
1290 # Used for view in controller
1291 invitation = {'meeting': {}, 'attendee': []}
1293 meeting = self.browse(cr, uid, int(meeting_id), context)
1294 invitation['meeting'] = {
1295 'event': meeting.name,
1296 'where': meeting.location,
1297 'when': meeting.display_time
1300 for attendee in meeting.attendee_ids:
1301 invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1304 def get_interval(self, cr, uid, ids, date, interval, tz=None, context=None):
1305 #Function used only in calendar_event_data.xml for email template
1306 date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
1309 timezone = pytz.timezone(tz or 'UTC')
1310 date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
1312 if interval == 'day':
1314 elif interval == 'month':
1315 res = date.strftime('%B') + " " + str(date.year)
1316 elif interval == 'dayname':
1317 res = date.strftime('%A')
1318 elif interval == 'time':
1319 res = date.strftime('%I:%M %p')
1322 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1326 if context.get('mymeetings', False):
1327 partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1328 args += ['|', ('partner_ids', 'in', [partner_id]), ('user_id', '=', uid)]
1334 if arg[0] in ('date', unicode('date')) and arg[1] == ">=":
1335 if context.get('virtual_id', True):
1336 new_args += ['|', '&', ('recurrency', '=', 1), ('end_date', arg[1], arg[2])]
1337 elif arg[0] == "id":
1338 new_id = get_real_ids(arg[2])
1339 new_arg = (arg[0], arg[1], new_id)
1340 new_args.append(new_arg)
1342 if not context.get('virtual_id', True):
1343 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count)
1345 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1346 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1347 res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1351 return res[offset: offset + limit]
1354 def copy(self, cr, uid, id, default=None, context=None):
1358 default = default or {}
1359 default['attendee_ids'] = False
1361 res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1364 def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1365 real_event_id = calendar_id2real_id(id)
1366 data = self.read(cr, uid, id, ['date', 'date_deadline', 'rrule', 'duration'])
1368 if data.get('rrule'):
1371 recurrent_id=real_event_id,
1372 recurrent_id_date=data.get('date'),
1376 end_date=datetime.strptime(values.get('date', False) or data.get('date'), "%Y-%m-%d %H:%M:%S") + timedelta(hours=values.get('duration', False) or data.get('duration'))
1382 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1385 def open_after_detach_event(self, cr, uid, ids, context=None):
1389 new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1391 'type': 'ir.actions.act_window',
1392 'res_model': 'calendar.event',
1393 'view_mode': 'form',
1395 'target': 'current',
1396 'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
1399 def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
1402 for n, calendar_id in enumerate(arg[2]):
1403 if isinstance(calendar_id, str):
1404 arg[2][n] = calendar_id.split('-')[0]
1405 return super(calendar_event, self)._name_search(cr, user, name=name, args=args, operator=operator, context=context, limit=limit, name_get_uid=name_get_uid)
1407 def write(self, cr, uid, ids, values, context=None):
1408 def _only_changes_to_apply_on_real_ids(field_names):
1409 ''' return True if changes are only to be made on the real ids'''
1410 for field in field_names:
1411 if field in ['date', 'active']:
1415 context = context or {}
1417 if isinstance(ids, (str, int, long)):
1418 if len(str(ids).split('-')) == 1:
1426 # Special write of complex IDS
1427 for event_id in ids:
1428 if len(str(event_id).split('-')) == 1:
1431 ids.remove(event_id)
1432 real_event_id = calendar_id2real_id(event_id)
1434 # if we are setting the recurrency flag to False or if we are only changing fields that
1435 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1436 # then set real ids to be updated.
1437 if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()):
1438 ids.append(real_event_id)
1441 data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration'])
1442 if data.get('rrule'):
1443 new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
1445 res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
1447 # set end_date for calendar searching
1448 if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
1449 (values.get('rrule_type') or values.get('count') or values.get('date') or values.get('date_deadline')):
1450 for data in self.read(cr, uid, ids, ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
1451 end_date = self._get_recurrency_end_date(data, context=context)
1452 super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
1454 attendees_create = False
1455 if values.get('partner_ids', False):
1456 attendees_create = self.create_attendees(cr, uid, ids, context)
1458 if values.get('date', False) and values.get('active', True):
1459 the_id = new_id or (ids and int(ids[0]))
1461 if attendees_create:
1462 attendees_create = attendees_create[the_id]
1463 mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
1465 mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1468 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1469 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context):
1470 self.message_post(cr, uid, the_id, body=_("A email has been send to specify that the date has been changed !"), subtype="calendar.subtype_invitation", context=context)
1472 return res or True and False
1474 def create(self, cr, uid, vals, context=None):
1478 if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user
1479 vals['user_id'] = uid
1481 res = super(calendar_event, self).create(cr, uid, vals, context=context)
1483 data = self.read(cr, uid, [res], ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context)[0]
1484 end_date = self._get_recurrency_end_date(data, context=context)
1485 self.write(cr, uid, [res], {'end_date': end_date}, context=context)
1487 self.create_attendees(cr, uid, [res], context=context)
1490 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
1494 if 'date' in groupby:
1495 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1496 virtual_id = context.get('virtual_id', True)
1497 context.update({'virtual_id': False})
1498 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
1500 #remove the count, since the value is not consistent with the result of the search when expand the group
1501 for groupname in groupby:
1502 if result.get(groupname + "_count"):
1503 del result[groupname + "_count"]
1504 result.get('__context', {}).update({'virtual_id': virtual_id})
1507 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1510 fields2 = fields and fields[:] or None
1511 EXTRAFIELDS = ('class', 'user_id', 'duration', 'date', 'rrule', 'vtimezone')
1512 for f in EXTRAFIELDS:
1513 if fields and (f not in fields):
1516 if isinstance(ids, (str, int, long)):
1521 select = map(lambda x: (x, calendar_id2real_id(x)), select)
1524 real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1525 real_data = dict(zip([x['id'] for x in real_data], real_data))
1527 for calendar_id, real_id in select:
1528 res = real_data[real_id].copy()
1529 ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) or 0)
1530 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1532 res['date_deadline'] = ls[2]
1533 res['id'] = calendar_id
1538 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1541 if r['class'] == 'private':
1543 if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date'):
1544 if isinstance(r[f], list):
1552 for k in EXTRAFIELDS:
1553 if (k in r) and (fields and (k not in fields)):
1556 if isinstance(ids, (str, int, long)):
1557 return result and result[0] or False
1560 def unlink(self, cr, uid, ids, unlink_level=0, context=None):
1561 if not isinstance(ids, list):
1568 # One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it
1569 for event_id in ids:
1570 if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL
1571 if self.browse(cr, uid, event_id).recurrent_id:
1572 ids_to_exclure.append(event_id)
1574 ids_to_unlink.append(event_id)
1576 ids_to_exclure.append(event_id)
1579 res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1582 for id_to_exclure in ids_to_exclure:
1583 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1588 class mail_message(osv.Model):
1589 _inherit = "mail.message"
1591 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1593 convert the search on real ids in the case it was asked on virtual ids, then call super()
1595 for index in range(len(args)):
1596 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1597 args[index][2] = get_real_ids(args[index][2])
1598 return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1600 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1603 if doc_model == 'calendar.event':
1604 order = context.get('order', self._order)
1605 for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1606 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1607 return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1610 class ir_attachment(osv.Model):
1611 _inherit = "ir.attachment"
1613 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1615 convert the search on real ids in the case it was asked on virtual ids, then call super()
1617 for index in range(len(args)):
1618 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1619 args[index][2] = get_real_ids(args[index][2])
1620 return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1622 def write(self, cr, uid, ids, vals, context=None):
1624 when posting an attachment (new or not), convert the virtual ids in real ids.
1626 if isinstance(vals.get('res_id'), str):
1627 vals['res_id'] = get_real_ids(vals.get('res_id'))
1628 return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1631 class ir_http(osv.AbstractModel):
1632 _inherit = 'ir.http'
1634 def _auth_method_calendar(self):
1635 token = request.params['token']
1636 db = request.params['db']
1638 registry = openerp.modules.registry.RegistryManager.get(db)
1639 attendee_pool = registry.get('calendar.attendee')
1640 error_message = False
1641 with registry.cursor() as cr:
1642 attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
1644 error_message = """Invalid Invitation Token."""
1645 elif request.session.uid and request.session.login != 'anonymous':
1646 # if valid session but user is not match
1647 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1648 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1649 if attendee.partner_id.id != user.partner_id.id:
1650 error_message = """Invitation cannot be forwarded via email. This event/meeting belongs to %s and you are logged in as %s. Please ask organizer to add you.""" % (attendee.email, user.email)
1653 raise BadRequest(error_message)
1657 class invite_wizard(osv.osv_memory):
1658 _inherit = 'mail.wizard.invite'
1660 def default_get(self, cr, uid, fields, context=None):
1662 in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1664 result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1665 if 'res_id' in result:
1666 result['res_id'] = get_real_ids(result['res_id'])