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):
435 cron = self.pool['ir.model.data'].get_object(
436 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
438 _logger.error("Cron for " + self._name + " can not be identified !")
441 if cron.interval_type == "weeks":
442 cron_interval = cron.interval_number * 7 * 24 * 60 * 60
443 elif cron.interval_type == "days":
444 cron_interval = cron.interval_number * 24 * 60 * 60
445 elif cron.interval_type == "hours":
446 cron_interval = cron.interval_number * 60 * 60
447 elif cron.interval_type == "minutes":
448 cron_interval = cron.interval_number * 60
449 elif cron.interval_type == "seconds":
450 cron_interval = cron.interval_number
452 cron_interval = False
454 if not cron_interval:
455 _logger.error("Cron delay can not be computed !")
458 all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
460 for event in all_events: # .values()
461 max_delta = all_events[event]['max_duration']
462 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
463 if curEvent.recurrency:
466 for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
467 in_date_format = one_date.replace(tzinfo=None)
468 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
470 for alert in LastFound:
471 self.do_mail_reminder(cr, uid, alert, context=context)
473 if not bFound: # if it's the first alarm for this recurrent event
475 if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event
478 in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
479 LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
481 for alert in LastFound:
482 self.do_mail_reminder(cr, uid, alert, context=context)
484 def get_next_notif(self, cr, uid, context=None):
485 ajax_check_every_seconds = 300
486 partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
492 all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
494 for event in all_events: # .values()
495 max_delta = all_events[event]['max_duration']
496 curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
497 if curEvent.recurrency:
500 for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
501 in_date_format = one_date.replace(tzinfo=None)
502 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)
504 for alert in LastFound:
505 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
506 if not bFound: # if it's the first alarm for this recurrent event
508 if bFound and not LastFound: # if the precedent event had alarm but not this one, we can stop the search fot this event
511 in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
512 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)
514 for alert in LastFound:
515 all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
518 def do_mail_reminder(self, cr, uid, alert, context=None):
523 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
524 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
526 if alarm.type == 'email':
527 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)
531 def do_notif_reminder(self, cr, uid, alert, context=None):
532 alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
533 event = self.pool['calendar.event'].browse(cr, uid, alert['event_id'], context=context)
535 if alarm.type == 'notification':
536 message = event.display_time
538 delta = alert['notify_at'] - datetime.now()
539 delta = delta.seconds + delta.days * 3600 * 24
542 'event_id': event.id,
546 'notify_at': alert['notify_at'].strftime("%Y-%m-%d %H:%M:%S"),
550 class calendar_alarm(osv.Model):
551 _name = 'calendar.alarm'
552 _description = 'Event alarm'
554 def _get_duration(self, cr, uid, ids, field_name, arg, context=None):
556 for alarm in self.browse(cr, uid, ids, context=context):
557 if alarm.interval == "minutes":
558 res[alarm.id] = alarm.duration
559 elif alarm.interval == "hours":
560 res[alarm.id] = alarm.duration * 60
561 elif alarm.interval == "days":
562 res[alarm.id] = alarm.duration * 60 * 24
568 'name': fields.char('Name', required=True), # fields function
569 'type': fields.selection([('notification', 'Notification'), ('email', 'Email')], 'Type', required=True),
570 'duration': fields.integer('Amount', required=True),
571 'interval': fields.selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days')], 'Unit', required=True),
572 'duration_minutes': fields.function(_get_duration, type='integer', string='duration_minutes', store=True),
576 'type': 'notification',
581 def _update_cron(self, cr, uid, context=None):
583 cron = self.pool['ir.model.data'].get_object(
584 cr, uid, 'calendar', 'ir_cron_scheduler_alarm', context=context)
587 return cron.toggle(model=self._name, domain=[('type', '=', 'email')])
589 def create(self, cr, uid, values, context=None):
590 res = super(calendar_alarm, self).create(cr, uid, values, context=context)
592 self._update_cron(cr, uid, context=context)
596 def write(self, cr, uid, ids, values, context=None):
597 res = super(calendar_alarm, self).write(cr, uid, ids, values, context=context)
599 self._update_cron(cr, uid, context=context)
603 def unlink(self, cr, uid, ids, context=None):
604 res = super(calendar_alarm, self).unlink(cr, uid, ids, context=context)
606 self._update_cron(cr, uid, context=context)
611 class ir_values(osv.Model):
612 _inherit = 'ir.values'
614 def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False):
618 if type(data) in (list, tuple):
619 new_model.append((data[0], calendar_id2real_id(data[1])))
621 new_model.append(data)
622 return super(ir_values, self).set(cr, uid, key, key2, name, new_model,
623 value, replace, isobject, meta, preserve_user, company)
625 def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True):
630 if type(data) in (list, tuple):
631 new_model.append((data[0], calendar_id2real_id(data[1])))
633 new_model.append(data)
634 return super(ir_values, self).get(cr, uid, key, key2, new_model,
635 meta, context, res_id_req, without_user, key2_req)
638 class ir_model(osv.Model):
640 _inherit = 'ir.model'
642 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
643 new_ids = isinstance(ids, (str, int, long)) and [ids] or ids
646 data = super(ir_model, self).read(cr, uid, new_ids, fields=fields, context=context, load=load)
649 val['id'] = calendar_id2real_id(val['id'])
650 return isinstance(ids, (str, int, long)) and data[0] or data
653 original_exp_report = openerp.service.report.exp_report
656 def exp_report(db, uid, object, ids, data=None, context=None):
660 if object == 'printscreen.list':
661 original_exp_report(db, uid, object, ids, data, context)
664 new_ids.append(calendar_id2real_id(id))
665 if data.get('id', False):
666 data['id'] = calendar_id2real_id(data['id'])
667 return original_exp_report(db, uid, object, new_ids, data, context)
670 openerp.service.report.exp_report = exp_report
673 class calendar_event_type(osv.Model):
674 _name = 'calendar.event.type'
675 _description = 'Meeting Type'
677 'name': fields.char('Name', required=True, translate=True),
681 class calendar_event(osv.Model):
682 """ Model for Calendar Event """
683 _name = 'calendar.event'
684 _description = "Meeting"
686 _inherit = ["mail.thread", "ir.needaction_mixin"]
688 def do_run_scheduler(self, cr, uid, id, context=None):
689 self.pool['calendar.alarm_manager'].get_next_mail(cr, uid, context=context)
691 def get_recurrent_date_by_event(self, cr, uid, event, context=None):
692 """Get recurrent dates based on Rule string and all event where recurrent_id is child
696 val = parser.parse(''.join((re.compile('\d')).findall(date)))
697 ## Dates are localized to saved timezone if any, else current timezone.
699 val = pytz.UTC.localize(val)
700 return val.astimezone(timezone)
702 timezone = pytz.timezone(event.vtimezone or context.get('tz') or 'UTC')
703 startdate = pytz.UTC.localize(datetime.strptime(event.date, "%Y-%m-%d %H:%M:%S")) # Add "+hh:mm" timezone
705 startdate = datetime.now()
707 ## Convert the start date to saved timezone (or context tz) as it'll
708 ## define the correct hour/day asked by the user to repeat for recurrence.
709 startdate = startdate.astimezone(timezone) # transform "+hh:mm" timezone
710 rset1 = rrule.rrulestr(str(event.rrule), dtstart=startdate, forceset=True)
711 ids_depending = self.search(cr, uid, [('recurrent_id', '=', event.id), '|', ('active', '=', False), ('active', '=', True)], context=context)
712 all_events = self.browse(cr, uid, ids_depending, context=context)
714 for ev in all_events:
715 rset1._exdate.append(todate(ev.recurrent_id_date))
717 return [d.astimezone(pytz.UTC) for d in rset1]
719 def _get_recurrency_end_date(self, data, context=None):
720 if not data.get('recurrency'):
723 end_type = data.get('end_type')
724 end_date = data.get('end_date')
726 if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'date_deadline']):
727 count = data['count'] + 1
729 'daily': ('days', 1),
730 'weekly': ('days', 7),
731 'monthly': ('months', 1),
732 'yearly': ('years', 1),
733 }[data['rrule_type']]
735 deadline = datetime.strptime(data['date_deadline'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
736 return deadline + relativedelta(**{delay: count * mult})
739 def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
741 Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
743 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
744 for meeting_id in meeting_ids:
745 for attendee in self.browse(cr, uid, meeting_id, context).attendee_ids:
746 if user.partner_id.id == attendee.partner_id.id:
750 def _get_display_time(self, cr, uid, meeting_id, context=None):
752 Return date and time (from to from) based on duration with timezone in string :
754 1) if user add duration for 2 hours, return : August-23-2013 at (04-30 To 06-30) (Europe/Brussels)
755 2) if event all day ,return : AllDay, July-31-2013
760 tz = context.get('tz', False)
761 if not tz: # tz can have a value False, so dont do it in the default value of get !
762 tz = pytz.timezone('UTC')
764 meeting = self.browse(cr, uid, meeting_id, context=context)
765 date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
766 date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
767 event_date = date.strftime('%B-%d-%Y')
768 display_time = date.strftime('%H-%M')
770 time = _("AllDay , %s") % (event_date)
771 elif meeting.duration < 24:
772 duration = date + timedelta(hours=meeting.duration)
773 time = ("%s at (%s To %s) (%s)") % (event_date, display_time, duration.strftime('%H-%M'), tz)
775 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)
778 def _compute(self, cr, uid, ids, fields, arg, context=None):
780 for meeting_id in ids:
782 attendee = self._find_my_attendee(cr, uid, [meeting_id], context)
784 if field == 'is_attendee':
785 res[meeting_id][field] = True if attendee else False
786 elif field == 'attendee_status':
787 res[meeting_id][field] = attendee.state if attendee else 'needsAction'
788 elif field == 'display_time':
789 res[meeting_id][field] = self._get_display_time(cr, uid, meeting_id, context=context)
792 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
794 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
795 @return: dictionary of rrule value.
798 if not isinstance(ids, list):
802 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
803 data = self.browse(cr, SUPERUSER_ID, id, context=context)
805 if data.interval and data.interval < 0:
806 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
807 if data.count and data.count <= 0:
808 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
810 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)
812 if data['recurrency']:
813 result[event] = self.compute_rule_string(data)
818 # retro compatibility function
819 def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
820 return self._set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=context)
822 def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
823 if not isinstance(ids, list):
825 data = self._get_empty_rrule_data()
827 data['recurrency'] = True
828 for event in self.browse(cr, uid, ids, context=context):
830 update_data = self._parse_rrule(field_value, dict(data), rdate)
831 data.update(update_data)
832 self.write(cr, uid, ids, data, context=context)
835 def _tz_get(self, cr, uid, context=None):
836 return [(x.lower(), x) for x in pytz.all_timezones]
840 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
843 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
847 'id': fields.integer('ID', readonly=True),
848 'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
849 'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
850 'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
851 'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
852 'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
853 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True, track_visibility='onchange'),
854 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
855 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
856 'description': fields.text('Description', states={'done': [('readonly', True)]}),
857 'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
858 'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
859 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
860 'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
861 '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"),
862 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
863 'recurrent_id': fields.integer('Recurrent ID'),
864 'recurrent_id_date': fields.datetime('Recurrent ID date'),
865 'vtimezone': fields.selection(_tz_get, string='Timezone'),
866 'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
867 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
868 'count': fields.integer('Repeat', help="Repeat x times"),
869 'mo': fields.boolean('Mon'),
870 'tu': fields.boolean('Tue'),
871 'we': fields.boolean('Wed'),
872 'th': fields.boolean('Thu'),
873 'fr': fields.boolean('Fri'),
874 'sa': fields.boolean('Sat'),
875 'su': fields.boolean('Sun'),
876 'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
877 'day': fields.integer('Date of month'),
878 'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
879 'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
880 'end_date': fields.date('Repeat Until'),
881 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
882 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
883 'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False), # Color of creator
884 '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."),
885 'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
886 'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
887 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
888 'alarm_ids': fields.many2many('calendar.alarm', string='Reminders', ondelete="restrict"),
892 def _get_default_partners(self, cr, uid, ctx=None):
893 ret = [self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id]
894 active_id = ctx.get('active_id')
895 if ctx.get('active_model') == 'res.partner' and active_id:
896 if active_id not in ret:
897 ret.append(active_id)
910 'user_id': lambda self, cr, uid, ctx: uid,
911 'partner_ids': _get_default_partners,
914 def _check_closing_date(self, cr, uid, ids, context=None):
915 for event in self.browse(cr, uid, ids, context=context):
916 if event.date_deadline < event.date:
921 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
924 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
926 """Returns duration and/or end date based on values passed
927 @param ids: List of calendar event's IDs.
936 if not end_date and not duration:
938 value['duration'] = duration
940 if allday: # For all day event
941 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
942 user = self.pool['res.users'].browse(cr, uid, uid)
943 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
944 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
945 start = start.astimezone(pytz.utc) # convert start back to utc
947 value['duration'] = 24.0
948 value['date'] = datetime.strftime(start, "%Y-%m-%d %H:%M:%S")
950 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
952 if end_date and not duration:
953 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
955 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
956 value['duration'] = round(duration, 2)
958 end = start + timedelta(hours=duration)
959 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
960 elif end_date and duration and not allday:
961 # we have both, keep them synchronized:
962 # set duration based on end_date (arbitrary decision: this avoid
963 # getting dates like 06:31:48 instead of 06:32:00)
964 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
966 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
967 value['duration'] = round(duration, 2)
969 return {'value': value}
971 def new_invitation_token(self, cr, uid, record, partner_id):
972 return uuid.uuid4().hex
974 def create_attendees(self, cr, uid, ids, context):
975 user_obj = self.pool['res.users']
976 current_user = user_obj.browse(cr, uid, uid, context=context)
978 for event in self.browse(cr, uid, ids, context):
980 for att in event.attendee_ids:
981 attendees[att.partner_id.id] = True
983 new_att_partner_ids = []
984 for partner in event.partner_ids:
985 if partner.id in attendees:
987 access_token = self.new_invitation_token(cr, uid, event, partner.id)
989 'partner_id': partner.id,
990 'event_id': event.id,
991 'access_token': access_token,
992 'email': partner.email,
995 if partner.id == current_user.partner_id.id:
996 values['state'] = 'accepted'
998 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
999 new_attendees.append(att_id)
1000 new_att_partner_ids.append(partner.id)
1002 if not current_user.email or current_user.email != partner.email:
1003 mail_from = current_user.email or tools.config.get('email_from', False)
1004 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
1005 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)
1008 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
1009 if new_att_partner_ids:
1010 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
1012 # We remove old attendees who are not in partner_ids now.
1013 all_partner_ids = [part.id for part in event.partner_ids]
1014 all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
1015 all_attendee_ids = [att.id for att in event.attendee_ids]
1016 partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
1018 attendee_ids_to_remove = []
1020 if partner_ids_to_remove:
1021 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)
1022 if attendee_ids_to_remove:
1023 self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
1026 'new_attendee_ids': new_attendees,
1027 'old_attendee_ids': all_attendee_ids,
1028 'removed_attendee_ids': attendee_ids_to_remove
1032 def get_search_fields(self, browse_event, order_fields, r_date=None):
1034 for ord in order_fields:
1035 if ord == 'id' and r_date:
1036 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
1038 sort_fields[ord] = browse_event[ord]
1039 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
1040 name_get = browse_event[ord].name_get()
1041 if len(name_get) and len(name_get[0]) >= 2:
1042 sort_fields[ord] = name_get[0][1]
1046 def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
1048 """Gives virtual event ids for recurring events
1049 This method gives ids of dates that comes between start date and end date of calendar views
1051 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1057 if isinstance(event_id, (str, int, long)):
1058 ids_to_browse = [event_id] # keep select for return
1060 ids_to_browse = event_id
1063 order_fields = [field.split()[0] for field in order.split(',')]
1065 # fallback on self._order defined on the model
1066 order_fields = [field.split()[0] for field in self._order.split(',')]
1068 if 'id' not in order_fields:
1069 order_fields.append('id')
1073 for ev in self.browse(cr, uid, ids_to_browse, context=context):
1074 if not ev.recurrency or not ev.rrule:
1075 result.append(ev.id)
1076 result_data.append(self.get_search_fields(ev, order_fields))
1079 rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1081 for r_date in rdates:
1082 # fix domain evaluation
1083 # step 1: check date and replace expression by True or False, replace other expressions by True
1084 # step 2: evaluation of & and |
1085 # check if there are one False
1089 if str(arg[0]) in (str('date'), str('date_deadline'), str('end_date')):
1091 ok = r_date.strftime('%Y-%m-%d') == arg[2]
1093 ok = r_date.strftime('%Y-%m-%d') > arg[2]
1095 ok = r_date.strftime('%Y-%m-%d') < arg[2]
1096 if (arg[1] == '>='):
1097 ok = r_date.strftime('%Y-%m-%d') >= arg[2]
1098 if (arg[1] == '<='):
1099 ok = r_date.strftime('%Y-%m-%d') <= arg[2]
1101 elif str(arg) == str('&') or str(arg) == str('|'):
1108 if not isinstance(item, basestring):
1110 elif str(item) == str('&'):
1111 first = new_pile.pop()
1112 second = new_pile.pop()
1113 res = first and second
1114 elif str(item) == str('|'):
1115 first = new_pile.pop()
1116 second = new_pile.pop()
1117 res = first or second
1118 new_pile.append(res)
1120 if [True for item in new_pile if not item]:
1122 result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
1125 def comparer(left, right):
1126 for fn, mult in comparers:
1127 result = cmp(fn(left), fn(right))
1129 return mult * result
1132 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1133 comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1134 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1136 if isinstance(event_id, (str, int, long)):
1137 return ids and ids[0] or False
1141 def compute_rule_string(self, data):
1143 Compute rule string according to value type RECUR of iCalendar from the values given.
1144 @param self: the object pointer
1145 @param data: dictionary of freq and interval value
1146 @return: string containing recurring rule (empty if no rule)
1148 def get_week_string(freq, data):
1149 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1150 if freq == 'weekly':
1151 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1152 # byday = map(lambda x: x.upper(),[data[day] for day in weekdays if data[day]])
1155 return ';BYDAY=' + ','.join(byday)
1158 def get_month_string(freq, data):
1159 if freq == 'monthly':
1160 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1161 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1163 if data.get('month_by') == 'day': # Eg : Second Monday of the month
1164 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1165 elif data.get('month_by') == 'date': # Eg : 16th of the month
1166 return ';BYMONTHDAY=' + str(data.get('day'))
1169 def get_end_date(data):
1170 if data.get('end_date'):
1171 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1173 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1174 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1176 freq = data.get('rrule_type', False) # day/week/month/year
1179 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1180 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1184 def _get_empty_rrule_data(self):
1187 'recurrency': False,
1189 'rrule_type': False,
1205 def _parse_rrule(self, rule, data, date_start):
1206 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1207 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1208 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1210 if r._freq > 0 and r._freq < 4:
1211 data['rrule_type'] = rrule_type[r._freq]
1213 data['count'] = r._count
1214 data['interval'] = r._interval
1215 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1218 for i in xrange(0, 7):
1219 if i in r._byweekday:
1220 data[day_list[i]] = True
1221 data['rrule_type'] = 'weekly'
1222 #repeat monthly by nweekday ((weekday, weeknumber), )
1224 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1225 data['byday'] = str(r._bynweekday[0][1])
1226 data['month_by'] = 'day'
1227 data['rrule_type'] = 'monthly'
1230 data['day'] = r._bymonthday[0]
1231 data['month_by'] = 'date'
1232 data['rrule_type'] = 'monthly'
1234 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1236 data['interval'] = data['interval'] * 12
1238 #FIXEME handle forever case
1240 #in case of repeat for ever that we do not support right now
1241 if not (data.get('count') or data.get('end_date')):
1243 if data.get('count'):
1244 data['end_type'] = 'count'
1246 data['end_type'] = 'end_date'
1249 def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1251 for virtual_id in ids:
1252 real_id = calendar_id2real_id(virtual_id)
1253 result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1254 res[virtual_id] = result[real_id]
1257 def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1258 """ The basic purpose of this method is to check that destination partners
1259 effectively have email addresses. Otherwise a warning is thrown.
1260 :param value: value format: [[6, 0, [3, 4]]]
1264 if not value or not value[0] or not value[0][0] == 6:
1267 res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1270 def onchange_rec_day(self, cr, uid, id, date, mo, tu, we, th, fr, sa, su):
1271 """ set the start date according to the first occurence of rrule"""
1272 rrule_obj = self._get_empty_rrule_data()
1275 'rrule_type': 'weekly',
1285 str_rrule = self.compute_rule_string(rrule_obj)
1286 first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
1287 return {'value': {'date': first_occurence.strftime("%Y-%m-%d") + ' 00:00:00'}}
1289 def check_partners_email(self, cr, uid, partner_ids, context=None):
1290 """ Verify that selected partner_ids have an email_address defined.
1291 Otherwise throw a warning. """
1292 partner_wo_email_lst = []
1293 for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1294 if not partner.email:
1295 partner_wo_email_lst.append(partner)
1296 if not partner_wo_email_lst:
1298 warning_msg = _('The following contacts have no email address :')
1299 for partner in partner_wo_email_lst:
1300 warning_msg += '\n- %s' % (partner.name)
1301 return {'warning': {
1302 'title': _('Email addresses not found'),
1303 'message': warning_msg,
1306 # ----------------------------------------
1308 # ----------------------------------------
1310 # shows events of the day for this user
1312 def _needaction_domain_get(self, cr, uid, context=None):
1313 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)]
1315 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
1316 if isinstance(thread_id, str):
1317 thread_id = get_real_ids(thread_id)
1318 if context.get('default_date'):
1319 del context['default_date']
1320 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)
1322 def do_sendmail(self, cr, uid, ids, context=None):
1323 for event in self.browse(cr, uid, ids, context):
1324 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1326 if current_user.email:
1327 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):
1328 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="calendar.subtype_invitation", context=context)
1331 def get_attendee(self, cr, uid, meeting_id, context=None):
1332 # Used for view in controller
1333 invitation = {'meeting': {}, 'attendee': []}
1335 meeting = self.browse(cr, uid, int(meeting_id), context)
1336 invitation['meeting'] = {
1337 'event': meeting.name,
1338 'where': meeting.location,
1339 'when': meeting.display_time
1342 for attendee in meeting.attendee_ids:
1343 invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1346 def get_interval(self, cr, uid, ids, date, interval, tz=None, context=None):
1347 #Function used only in calendar_event_data.xml for email template
1348 date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
1351 timezone = pytz.timezone(tz or 'UTC')
1352 date = date.replace(tzinfo=pytz.timezone('UTC')).astimezone(timezone)
1354 if interval == 'day':
1356 elif interval == 'month':
1357 res = date.strftime('%B') + " " + str(date.year)
1358 elif interval == 'dayname':
1359 res = date.strftime('%A')
1360 elif interval == 'time':
1361 res = date.strftime('%I:%M %p')
1364 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1368 if context.get('mymeetings', False):
1369 partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1370 args += ['|', ('partner_ids', 'in', [partner_id]), ('user_id', '=', uid)]
1376 if arg[0] in ('date', unicode('date')) and arg[1] == ">=":
1377 if context.get('virtual_id', True):
1378 new_args += ['|', '&', ('recurrency', '=', 1), ('end_date', arg[1], arg[2])]
1379 elif arg[0] == "id":
1380 new_id = get_real_ids(arg[2])
1381 new_arg = (arg[0], arg[1], new_id)
1382 new_args.append(new_arg)
1384 if not context.get('virtual_id', True):
1385 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count)
1387 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1388 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1389 res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1393 return res[offset: offset + limit]
1396 def copy(self, cr, uid, id, default=None, context=None):
1400 default = default or {}
1401 default['attendee_ids'] = False
1403 res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1406 def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1407 real_event_id = calendar_id2real_id(id)
1408 data = self.read(cr, uid, id, ['date', 'date_deadline', 'rrule', 'duration'])
1410 if data.get('rrule'):
1413 recurrent_id=real_event_id,
1414 recurrent_id_date=data.get('date'),
1418 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'))
1424 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1427 def open_after_detach_event(self, cr, uid, ids, context=None):
1431 new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1433 'type': 'ir.actions.act_window',
1434 'res_model': 'calendar.event',
1435 'view_mode': 'form',
1437 'target': 'current',
1438 'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
1441 def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
1444 for n, calendar_id in enumerate(arg[2]):
1445 if isinstance(calendar_id, str):
1446 arg[2][n] = calendar_id.split('-')[0]
1447 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)
1449 def write(self, cr, uid, ids, values, context=None):
1450 def _only_changes_to_apply_on_real_ids(field_names):
1451 ''' return True if changes are only to be made on the real ids'''
1452 for field in field_names:
1453 if field in ['date', 'active']:
1457 context = context or {}
1459 if isinstance(ids, (str, int, long)):
1460 if len(str(ids).split('-')) == 1:
1468 # Special write of complex IDS
1469 for event_id in ids:
1470 if len(str(event_id).split('-')) == 1:
1473 ids.remove(event_id)
1474 real_event_id = calendar_id2real_id(event_id)
1476 # if we are setting the recurrency flag to False or if we are only changing fields that
1477 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1478 # then set real ids to be updated.
1479 if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()):
1480 ids.append(real_event_id)
1483 data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration'])
1484 if data.get('rrule'):
1485 new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
1487 res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
1489 # set end_date for calendar searching
1490 if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
1491 (values.get('rrule_type') or values.get('count') or values.get('date') or values.get('date_deadline')):
1492 for data in self.read(cr, uid, ids, ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
1493 end_date = self._get_recurrency_end_date(data, context=context)
1494 super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
1496 attendees_create = False
1497 if values.get('partner_ids', False):
1498 attendees_create = self.create_attendees(cr, uid, ids, context)
1500 if values.get('date', False) and values.get('active', True):
1501 the_id = new_id or (ids and int(ids[0]))
1503 if attendees_create:
1504 attendees_create = attendees_create[the_id]
1505 mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
1507 mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1510 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1511 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):
1512 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)
1514 return res or True and False
1516 def create(self, cr, uid, vals, context=None):
1520 if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user
1521 vals['user_id'] = uid
1523 res = super(calendar_event, self).create(cr, uid, vals, context=context)
1525 data = self.read(cr, uid, [res], ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context)[0]
1526 end_date = self._get_recurrency_end_date(data, context=context)
1527 self.write(cr, uid, [res], {'end_date': end_date}, context=context)
1529 self.create_attendees(cr, uid, [res], context=context)
1532 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1536 if 'date' in groupby:
1537 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1538 virtual_id = context.get('virtual_id', True)
1539 context.update({'virtual_id': False})
1540 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1542 #remove the count, since the value is not consistent with the result of the search when expand the group
1543 for groupname in groupby:
1544 if result.get(groupname + "_count"):
1545 del result[groupname + "_count"]
1546 result.get('__context', {}).update({'virtual_id': virtual_id})
1549 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1552 fields2 = fields and fields[:] or None
1553 EXTRAFIELDS = ('class', 'user_id', 'duration', 'date', 'rrule', 'vtimezone')
1554 for f in EXTRAFIELDS:
1555 if fields and (f not in fields):
1558 if isinstance(ids, (str, int, long)):
1563 select = map(lambda x: (x, calendar_id2real_id(x)), select)
1566 real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1567 real_data = dict(zip([x['id'] for x in real_data], real_data))
1569 for calendar_id, real_id in select:
1570 res = real_data[real_id].copy()
1571 ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) or 0)
1572 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1574 res['date_deadline'] = ls[2]
1575 res['id'] = calendar_id
1580 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1583 if r['class'] == 'private':
1585 if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date'):
1586 if isinstance(r[f], list):
1594 for k in EXTRAFIELDS:
1595 if (k in r) and (fields and (k not in fields)):
1598 if isinstance(ids, (str, int, long)):
1599 return result and result[0] or False
1602 def unlink(self, cr, uid, ids, unlink_level=0, context=None):
1603 if not isinstance(ids, list):
1610 # One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it
1611 for event_id in ids:
1612 if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL
1613 if self.browse(cr, uid, event_id).recurrent_id:
1614 ids_to_exclure.append(event_id)
1616 ids_to_unlink.append(event_id)
1618 ids_to_exclure.append(event_id)
1621 res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1624 for id_to_exclure in ids_to_exclure:
1625 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1630 class mail_message(osv.Model):
1631 _inherit = "mail.message"
1633 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1635 convert the search on real ids in the case it was asked on virtual ids, then call super()
1637 for index in range(len(args)):
1638 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1639 args[index][2] = get_real_ids(args[index][2])
1640 return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1642 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1645 if doc_model == 'calendar.event':
1646 order = context.get('order', self._order)
1647 for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1648 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1649 return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1652 class ir_attachment(osv.Model):
1653 _inherit = "ir.attachment"
1655 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1657 convert the search on real ids in the case it was asked on virtual ids, then call super()
1659 for index in range(len(args)):
1660 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1661 args[index][2] = get_real_ids(args[index][2])
1662 return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1664 def write(self, cr, uid, ids, vals, context=None):
1666 when posting an attachment (new or not), convert the virtual ids in real ids.
1668 if isinstance(vals.get('res_id'), str):
1669 vals['res_id'] = get_real_ids(vals.get('res_id'))
1670 return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1673 class ir_http(osv.AbstractModel):
1674 _inherit = 'ir.http'
1676 def _auth_method_calendar(self):
1677 token = request.params['token']
1678 db = request.params['db']
1680 registry = openerp.modules.registry.RegistryManager.get(db)
1681 attendee_pool = registry.get('calendar.attendee')
1682 error_message = False
1683 with registry.cursor() as cr:
1684 attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
1686 error_message = """Invalid Invitation Token."""
1687 elif request.session.uid and request.session.login != 'anonymous':
1688 # if valid session but user is not match
1689 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1690 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1691 if attendee.partner_id.id != user.partner_id.id:
1692 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)
1695 raise BadRequest(error_message)
1699 class invite_wizard(osv.osv_memory):
1700 _inherit = 'mail.wizard.invite'
1702 def default_get(self, cr, uid, fields, context=None):
1704 in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1706 result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1707 if 'res_id' in result:
1708 result['res_id'] = get_real_ids(result['res_id'])