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
40 from werkzeug.exceptions import BadRequest
43 _logger = logging.getLogger(__name__)
46 def calendar_id2real_id(calendar_id=None, with_date=False):
48 Convert a "virtual/recurring event id" (type string) into a real event id (type int).
49 E.g. virtual/recurring event id is 4-20091201100000, so it will return 4.
50 @param calendar_id: id of calendar
51 @param with_date: if a value is passed to this param it will return dates based on value of withdate + calendar_id
52 @return: real event id
54 if calendar_id and isinstance(calendar_id, (str, unicode)):
55 res = calendar_id.split('-')
59 real_date = time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(res[1], "%Y%m%d%H%M%S"))
60 start = datetime.strptime(real_date, "%Y-%m-%d %H:%M:%S")
61 end = start + timedelta(hours=with_date)
62 return (int(real_id), real_date, end.strftime("%Y-%m-%d %H:%M:%S"))
64 return calendar_id and int(calendar_id) or calendar_id
66 def get_real_ids(ids):
67 if isinstance(ids, (str, int, long)):
68 return calendar_id2real_id(ids)
70 if isinstance(ids, (list, tuple)):
71 return [calendar_id2real_id(id) for id in ids]
74 class calendar_attendee(osv.Model):
76 Calendar Attendee Information
78 _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 data_pool = self.pool['ir.model.data']
202 mailmess_pool = self.pool['mail.message']
203 mail_pool = self.pool['mail.mail']
204 template_pool = self.pool['email.template']
205 local_context = context.copy()
207 'needsAction': 'grey',
209 'tentative': '#FFFF00',
213 if not isinstance(ids, (tuple, list)):
216 dummy, template_id = data_pool.get_object_reference(cr, uid, 'calendar', template_xmlid)
217 dummy, act_id = data_pool.get_object_reference(cr, uid, 'calendar', "view_calendar_event_calendar")
218 local_context.update({
220 'action_id': self.pool['ir.actions.act_window'].search(cr, uid, [('view_id', '=', act_id)], context=context)[0],
222 'base_url': self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url', default='http://localhost:8069', context=context)
225 for attendee in self.browse(cr, uid, ids, context=context):
226 if attendee.email and email_from:
227 ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
228 mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
232 vals['attachment_ids'] = [(0, 0, {'name': 'invitation.ics',
233 'datas_fname': 'invitation.ics',
234 'datas': str(ics_file).encode('base64')})]
235 vals['model'] = None # We don't want to have the mail in the tchatter while in queue!
236 the_mailmess = mail_pool.browse(cr, uid, mail_id, context=context).mail_message_id
237 mailmess_pool.write(cr, uid, [the_mailmess.id], vals, context=context)
238 mail_ids.append(mail_id)
241 res = mail_pool.send(cr, uid, mail_ids, context=context)
245 def onchange_user_id(self, cr, uid, ids, user_id, *args, **argv):
247 Make entry on email and availability on change of user_id field.
248 @param ids: list of attendee's IDs
249 @param user_id: changed value of User id
250 @return: dictionary of values which put value in email and availability fields
253 return {'value': {'email': ''}}
255 user = self.pool['res.users'].browse(cr, uid, user_id, *args)
256 return {'value': {'email': user.email, 'availability': user.availability}}
258 def do_tentative(self, cr, uid, ids, context=None, *args):
260 Makes event invitation as Tentative.
261 @param ids: list of attendee's IDs
263 return self.write(cr, uid, ids, {'state': 'tentative'}, context)
265 def do_accept(self, cr, uid, ids, context=None, *args):
267 Marks event invitation as Accepted.
268 @param ids: list of attendee's IDs
272 meeting_obj = self.pool['calendar.event']
273 res = self.write(cr, uid, ids, {'state': 'accepted'}, context)
274 for attendee in self.browse(cr, uid, ids, context=context):
275 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has accepted invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
279 def do_decline(self, cr, uid, ids, context=None, *args):
281 Marks event invitation as Declined.
282 @param ids: list of calendar attendee's IDs
286 meeting_obj = self.pool['calendar.event']
287 res = self.write(cr, uid, ids, {'state': 'declined'}, context)
288 for attendee in self.browse(cr, uid, ids, context=context):
289 meeting_obj.message_post(cr, uid, attendee.event_id.id, body=_(("%s has declined invitation") % (attendee.cn)), subtype="calendar.subtype_invitation", context=context)
292 def create(self, cr, uid, vals, context=None):
295 if not vals.get("email") and vals.get("cn"):
296 cnval = vals.get("cn").split(':')
297 email = filter(lambda x: x.__contains__('@'), cnval)
298 vals['email'] = email and email[0] or ''
299 vals['cn'] = vals.get("cn")
300 res = super(calendar_attendee, self).create(cr, uid, vals, context=context)
304 class res_partner(osv.Model):
305 _inherit = 'res.partner'
307 'calendar_last_notif_ack': fields.datetime('Last notification marked as read from base Calendar'),
310 def get_attendee_detail(self, cr, uid, ids, meeting_id, context=None):
314 meeting = self.pool['calendar.event'].browse(cr, uid, get_real_ids(meeting_id), context=context)
315 for partner in self.browse(cr, uid, ids, context=context):
316 data = self.name_get(cr, uid, [partner.id], context)[0]
318 for attendee in meeting.attendee_ids:
319 if attendee.partner_id.id == partner.id:
320 data = (data[0], data[1], attendee.state)
324 def calendar_last_notif_ack(self, cr, uid, context=None):
325 partner = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id
326 self.write(cr, uid, partner.id, {'calendar_last_notif_ack': datetime.now()}, context=context)
330 class calendar_alarm_manager(osv.AbstractModel):
331 _name = 'calendar.alarm_manager'
333 def get_next_potential_limit_alarm(self, cr, uid, seconds, notif=True, mail=True, partner_id=None, context=None):
338 crm.date - interval '1' minute * calcul_delta.max_delta AS first_alarm,
340 WHEN crm.recurrency THEN crm.end_date - interval '1' minute * calcul_delta.min_delta
341 ELSE crm.date_deadline - interval '1' minute * calcul_delta.min_delta
343 crm.date as first_event_date,
345 WHEN crm.recurrency THEN crm.end_date
346 ELSE crm.date_deadline
347 END as last_event_date,
348 calcul_delta.min_delta,
349 calcul_delta.max_delta,
352 calendar_event AS crm
356 rel.calendar_event_id, max(alarm.duration_minutes) AS max_delta,min(alarm.duration_minutes) AS min_delta
358 calendar_alarm_calendar_event_rel AS rel
359 LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
360 WHERE alarm.type in %s
361 GROUP BY rel.calendar_event_id
362 ) AS calcul_delta ON calcul_delta.calendar_event_id = crm.id
366 LEFT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = crm.id
367 AND part_rel.res_partner_id = %s
373 type_to_read += ('notification',)
375 type_to_read += ('email',)
377 tuple_params = (type_to_read,)
379 #ADD FILTER ON PARTNER_ID
381 base_request += filter_user
382 tuple_params += (partner_id, )
385 tuple_params += (seconds, seconds,)
396 ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%s' second )
397 AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%s' second )
400 for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
401 res[event_id].update({
402 'event_id': event_id,
403 'first_alarm': first_alarm,
404 'last_alarm': last_alarm,
405 'first_meeting': first_meeting,
406 'last_meeting': last_meeting,
407 'min_duration': min_duration,
408 'max_duration': max_duration,
414 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):
419 alarm_type.append('notification')
421 alarm_type.append('email')
423 if one_date - timedelta(minutes=event_maxdelta) < datetime.now() + timedelta(seconds=in_the_next_X_seconds): # if an alarm is possible for this date
424 for alarm in event.alarm_ids:
425 if alarm.type in alarm_type and \
426 one_date - timedelta(minutes=alarm.duration_minutes) < datetime.now() + timedelta(seconds=in_the_next_X_seconds) and \
427 (not after or one_date - timedelta(minutes=alarm.duration_minutes) > datetime.strptime(after.split('.')[0], "%Y-%m-%d %H:%M:%S")):
429 'alarm_id': alarm.id,
430 'event_id': event.id,
431 'notify_at': one_date - timedelta(minutes=alarm.duration_minutes),
437 def get_next_mail(self,cr,uid,context=None):
438 cron = self.pool.get('ir.cron').search(cr,uid,[('model','ilike',self._name)],context=context)
439 if cron and len(cron) == 1:
440 cron = self.pool.get('ir.cron').browse(cr,uid,cron[0],context=context)
442 raise ("Cron for " + self._name + " not identified :( !")
444 if cron.interval_type=="weeks":
445 cron_interval = cron.interval_number * 7 * 24 * 60 * 60
446 elif cron.interval_type=="days":
447 cron_interval = cron.interval_number * 24 * 60 * 60
448 elif cron.interval_type=="hours":
449 cron_interval = cron.interval_number * 60 * 60
450 elif cron.interval_type=="minutes":
451 cron_interval = cron.interval_number * 60
452 elif cron.interval_type=="seconds":
453 cron_interval = cron.interval_number
455 if not cron_interval:
456 raise ("Cron delay for " + self._name + " can not be calculated :( !")
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 = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S');
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;
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 = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S');
498 LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,ajax_check_every_seconds,after=partner.cal_last_notif,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.cal_last_notif,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, 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'].do_run_scheduler(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 data.get('recurrency') and data.get('end_type') in ('count', unicode('count')):
688 data_date_deadline = datetime.strptime(data.get('date_deadline'), '%Y-%m-%d %H:%M:%S')
689 if data.get('rrule_type') in ('daily', unicode('count')):
690 rel_date = relativedelta(days=data.get('count') + 1)
691 elif data.get('rrule_type') in ('weekly', unicode('weekly')):
692 rel_date = relativedelta(days=(data.get('count') + 1) * 7)
693 elif data.get('rrule_type') in ('monthly', unicode('monthly')):
694 rel_date = relativedelta(months=data.get('count') + 1)
695 elif data.get('rrule_type') in ('yearly', unicode('yearly')):
696 rel_date = relativedelta(years=data.get('count') + 1)
697 end_date = data_date_deadline + rel_date
699 end_date = data.get('end_date')
702 def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
704 Return the first attendee where the user connected has been invited from all the meeting_ids in parameters
706 user = self.pool['res.users'].browse(cr, uid, uid, context=context)
707 for meeting_id in meeting_ids:
708 for attendee in self.browse(cr, uid, meeting_id, context).attendee_ids:
709 if user.partner_id.id == attendee.partner_id.id:
713 def _get_display_time(self, cr, uid, meeting_id, context=None):
715 Return date and time (from to from) based on duration with timezone in string :
717 1) if user add duration for 2 hours, return : August-23-2013 at ( 04-30 To 06-30) (Europe/Brussels)
718 2) if event all day ,return : AllDay, July-31-2013
723 tz = context.get('tz', False)
724 if not tz: # tz can have a value False, so dont do it in the default value of get !
725 tz = pytz.timezone('UTC')
727 meeting = self.browse(cr, uid, meeting_id, context=context)
728 date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
729 date_deadline = fields.datetime.context_timestamp(cr, uid, datetime.strptime(meeting.date_deadline, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context)
730 event_date = date.strftime('%B-%d-%Y')
731 display_time = date.strftime('%H-%M')
733 time = _("AllDay , %s") % (event_date)
734 elif meeting.duration < 24:
735 duration = date + timedelta(hours=meeting.duration)
736 time = ("%s at ( %s To %s) (%s)") % (event_date, display_time, duration.strftime('%H-%M'), tz)
738 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)
741 def _compute(self, cr, uid, ids, fields, arg, context=None):
743 for meeting_id in ids:
745 attendee = self._find_my_attendee(cr, uid, [meeting_id], context)
747 if field == 'is_attendee':
748 res[meeting_id][field] = True if attendee else False
749 elif field == 'attendee_status':
750 res[meeting_id][field] = attendee.state if attendee else 'needsAction'
751 elif field == 'display_time':
752 res[meeting_id][field] = self._get_display_time(cr, uid, meeting_id, context=context)
755 def _get_rulestring(self, cr, uid, ids, name, arg, context=None):
757 Gets Recurrence rule string according to value type RECUR of iCalendar from the values given.
758 @return: dictionary of rrule value.
761 if not isinstance(ids, list):
765 #read these fields as SUPERUSER because if the record is private a normal search could return False and raise an error
766 data = self.browse(cr, SUPERUSER_ID, id, context=context)
768 if data.interval and data.interval < 0:
769 raise osv.except_osv(_('Warning!'), _('Interval cannot be negative.'))
770 if data.count and data.count <= 0:
771 raise osv.except_osv(_('Warning!'), _('Count cannot be negative or 0.'))
773 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)
775 if data['recurrency']:
776 result[event] = self.compute_rule_string(data)
781 def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
782 if not isinstance(ids, list):
784 data = self._get_empty_rrule_data()
786 data['recurrency'] = True
787 for event in self.browse(cr, uid, ids, context=context):
789 update_data = self._parse_rrule(field_value, dict(data), rdate)
790 data.update(update_data)
791 self.write(cr, uid, ids, data, context=context)
794 def _tz_get(self, cr, uid, context=None):
795 return [(x.lower(), x) for x in pytz.all_timezones]
799 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
802 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
806 'id': fields.integer('ID', readonly=True),
807 'state': fields.selection([('draft', 'Unconfirmed'), ('open', 'Confirmed')], string='Status', readonly=True, track_visibility='onchange'),
808 'name': fields.char('Meeting Subject', required=True, states={'done': [('readonly', True)]}),
809 'is_attendee': fields.function(_compute, string='Attendee', type="boolean", multi='attendee'),
810 'attendee_status': fields.function(_compute, string='Attendee Status', type="selection", selection=calendar_attendee.STATE_SELECTION, multi='attendee'),
811 'display_time': fields.function(_compute, string='Event Time', type="char", multi='attendee'),
812 'date': fields.datetime('Date', states={'done': [('readonly', True)]}, required=True, track_visibility='onchange'),
813 'date_deadline': fields.datetime('End Date', states={'done': [('readonly', True)]}, required=True,),
814 'duration': fields.float('Duration', states={'done': [('readonly', True)]}),
815 'description': fields.text('Description', states={'done': [('readonly', True)]}),
816 'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
817 'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
818 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
819 'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_rrule_write, store=True, string='Recurrent Rule'),
820 '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"),
821 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
822 'recurrent_id': fields.integer('Recurrent ID'),
823 'recurrent_id_date': fields.datetime('Recurrent ID date'),
824 'vtimezone': fields.selection(_tz_get, string='Timezone'),
825 'end_type': fields.selection([('count', 'Number of repetitions'), ('end_date', 'End date')], 'Recurrence Termination'),
826 'interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
827 'count': fields.integer('Repeat', help="Repeat x times"),
828 'mo': fields.boolean('Mon'),
829 'tu': fields.boolean('Tue'),
830 'we': fields.boolean('Wed'),
831 'th': fields.boolean('Thu'),
832 'fr': fields.boolean('Fri'),
833 'sa': fields.boolean('Sat'),
834 'su': fields.boolean('Sun'),
835 'month_by': fields.selection([('date', 'Date of month'), ('day', 'Day of month')], 'Option', oldname='select1'),
836 'day': fields.integer('Date of month'),
837 'week_list': fields.selection([('MO', 'Monday'), ('TU', 'Tuesday'), ('WE', 'Wednesday'), ('TH', 'Thursday'), ('FR', 'Friday'), ('SA', 'Saturday'), ('SU', 'Sunday')], 'Weekday'),
838 'byday': fields.selection([('1', 'First'), ('2', 'Second'), ('3', 'Third'), ('4', 'Fourth'), ('5', 'Fifth'), ('-1', 'Last')], 'By day'),
839 'end_date': fields.date('Repeat Until'),
840 'allday': fields.boolean('All Day', states={'done': [('readonly', True)]}),
841 'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
842 'color_partner_id': fields.related('user_id', 'partner_id', 'id', type="integer", string="colorize", store=False), # Color of creator
843 '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."),
844 'categ_ids': fields.many2many('calendar.event.type', 'meeting_category_rel', 'event_id', 'type_id', 'Tags'),
845 'attendee_ids': fields.one2many('calendar.attendee', 'event_id', 'Attendees', ondelete='cascade'),
846 'partner_ids': fields.many2many('res.partner', string='Attendees', states={'done': [('readonly', True)]}),
847 'alarm_ids': fields.many2many('calendar.alarm', string='Reminders', ondelete="restrict"),
860 'user_id': lambda self, cr, uid, ctx: uid,
861 'partner_ids': lambda self, cr, uid, ctx: [self.pool['res.users'].browse(cr, uid, [uid], context=ctx)[0].partner_id.id]
864 def _check_closing_date(self, cr, uid, ids, context=None):
865 for event in self.browse(cr, uid, ids, context=context):
866 if event.date_deadline < event.date:
871 (_check_closing_date, 'Error ! End date cannot be set before start date.', ['date_deadline']),
874 def onchange_dates(self, cr, uid, ids, start_date, duration=False, end_date=False, allday=False, context=None):
876 """Returns duration and/or end date based on values passed
877 @param ids: List of calendar event's IDs.
886 if not end_date and not duration:
888 value['duration'] = duration
890 if allday: # For all day event
891 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
892 user = self.pool['res.users'].browse(cr, uid, uid)
893 tz = pytz.timezone(user.tz) if user.tz else pytz.utc
894 start = pytz.utc.localize(start).astimezone(tz) # convert start in user's timezone
895 start = start.astimezone(pytz.utc) # convert start back to utc
897 value['duration'] = 24.0
898 value['date'] = datetime.strftime(start, "%Y-%m-%d %H:%M:%S")
900 start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
902 if end_date and not duration:
903 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
905 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
906 value['duration'] = round(duration, 2)
908 end = start + timedelta(hours=duration)
909 value['date_deadline'] = end.strftime("%Y-%m-%d %H:%M:%S")
910 elif end_date and duration and not allday:
911 # we have both, keep them synchronized:
912 # set duration based on end_date (arbitrary decision: this avoid
913 # getting dates like 06:31:48 instead of 06:32:00)
914 end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
916 duration = float(diff.days) * 24 + (float(diff.seconds) / 3600)
917 value['duration'] = round(duration, 2)
919 return {'value': value}
921 def new_invitation_token(self, cr, uid, record, partner_id):
922 return uuid.uuid4().hex
924 def create_attendees(self, cr, uid, ids, context):
925 user_obj = self.pool['res.users']
926 current_user = user_obj.browse(cr, uid, uid, context=context)
928 for event in self.browse(cr, uid, ids, context):
930 for att in event.attendee_ids:
931 attendees[att.partner_id.id] = True
933 new_att_partner_ids = []
934 for partner in event.partner_ids:
935 if partner.id in attendees:
937 access_token = self.new_invitation_token(cr, uid, event, partner.id)
939 'partner_id': partner.id,
940 'event_id': event.id,
941 'access_token': access_token,
942 'email': partner.email,
945 if partner.id == current_user.partner_id.id:
946 values['state'] = 'accepted'
948 att_id = self.pool['calendar.attendee'].create(cr, uid, values, context=context)
949 new_attendees.append(att_id)
950 new_att_partner_ids.append(partner.id)
952 if not current_user.email or current_user.email != partner.email:
953 mail_from = current_user.email or tools.config.get('email_from', False)
954 if self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, att_id, email_from=mail_from, context=context):
955 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)
958 self.write(cr, uid, [event.id], {'attendee_ids': [(4, att) for att in new_attendees]}, context=context)
959 if new_att_partner_ids:
960 self.message_subscribe(cr, uid, [event.id], new_att_partner_ids, context=context)
962 # We remove old attendees who are not in partner_ids now.
963 all_partner_ids = [part.id for part in event.partner_ids]
964 all_part_attendee_ids = [att.partner_id.id for att in event.attendee_ids]
965 all_attendee_ids = [att.id for att in event.attendee_ids]
966 partner_ids_to_remove = map(lambda x: x, set(all_part_attendee_ids + new_att_partner_ids) - set(all_partner_ids))
968 attendee_ids_to_remove = []
970 if partner_ids_to_remove:
971 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)
972 if attendee_ids_to_remove:
973 self.pool['calendar.attendee'].unlink(cr, uid, attendee_ids_to_remove, context)
976 'new_attendee_ids': new_attendees,
977 'old_attendee_ids': all_attendee_ids,
978 'removed_attendee_ids': attendee_ids_to_remove
982 def get_search_fields(self,browse_event,order_fields,r_date=None):
984 for ord in order_fields:
985 if ord == 'id' and r_date:
986 sort_fields[ord] = '%s-%s' % (browse_event[ord], r_date.strftime("%Y%m%d%H%M%S"))
988 sort_fields[ord] = browse_event[ord]
989 'If we sort on FK, we obtain a browse_record, so we need to sort on name_get'
990 if type(browse_event[ord]) is openerp.osv.orm.browse_record:
991 name_get = browse_event[ord].name_get()
992 if len(name_get) and len(name_get[0])>=2:
993 sort_fields[ord] = name_get[0][1]
997 def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
999 """Gives virtual event ids for recurring events
1000 This method gives ids of dates that comes between start date and end date of calendar views
1002 @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
1008 if isinstance(event_id, (str, int, long)):
1009 ids_to_browse = [event_id] # keep select for return
1011 ids_to_browse = event_id
1014 order_fields = [field.split()[0] for field in order.split(',')]
1016 # fallback on self._order defined on the model
1017 order_fields = [field.split()[0] for field in self._order.split(',')]
1019 if 'id' not in order_fields:
1020 order_fields.append('id')
1024 for ev in self.browse(cr, uid, ids_to_browse, context=context):
1025 if not ev.recurrency or not ev.rrule:
1026 result.append(ev.id)
1027 result_data.append(self.get_search_fields(ev,order_fields))
1030 rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
1032 for r_date in rdates:
1033 # fix domain evaluation
1034 # step 1: check date and replace expression by True or False, replace other expressions by True
1035 # step 2: evaluation of & and |
1036 # check if there are one False
1040 if str(arg[0]) in (str('date'), str('date_deadline'), str('end_date')):
1042 ok = r_date.strftime('%Y-%m-%d') == arg[2]
1044 ok = r_date.strftime('%Y-%m-%d') > arg[2]
1046 ok = r_date.strftime('%Y-%m-%d') < arg[2]
1047 if (arg[1] == '>='):
1048 ok = r_date.strftime('%Y-%m-%d') >= arg[2]
1049 if (arg[1] == '<='):
1050 ok = r_date.strftime('%Y-%m-%d') <= arg[2]
1052 elif str(arg) == str('&') or str(arg) == str('|'):
1059 if not isinstance(item, basestring):
1061 elif str(item) == str('&'):
1062 first = new_pile.pop()
1063 second = new_pile.pop()
1064 res = first and second
1065 elif str(item) == str('|'):
1066 first = new_pile.pop()
1067 second = new_pile.pop()
1068 res = first or second
1069 new_pile.append(res)
1071 if [True for item in new_pile if not item]:
1073 result_data.append(self.get_search_fields(ev,order_fields,r_date=r_date))
1076 def comparer(left, right):
1077 for fn, mult in comparers:
1078 result = cmp(fn(left), fn(right))
1080 return mult * result
1083 sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
1084 comparers = [ ((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
1085 ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
1087 if isinstance(event_id, (str, int, long)):
1088 return ids and ids[0] or False
1093 def compute_rule_string(self, data):
1095 Compute rule string according to value type RECUR of iCalendar from the values given.
1096 @param self: the object pointer
1097 @param data: dictionary of freq and interval value
1098 @return: string containing recurring rule (empty if no rule)
1100 def get_week_string(freq, data):
1101 weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1102 if freq == 'weekly':
1103 byday = map(lambda x: x.upper(), filter(lambda x: data.get(x) and x in weekdays, data))
1104 # byday = map(lambda x: x.upper(),[data[day] for day in weekdays if data[day]])
1107 return ';BYDAY=' + ','.join(byday)
1110 def get_month_string(freq, data):
1111 if freq == 'monthly':
1112 if data.get('month_by') == 'date' and (data.get('day') < 1 or data.get('day') > 31):
1113 raise osv.except_osv(_('Error!'), ("Please select a proper day of the month."))
1115 if data.get('month_by') == 'day': # Eg : Second Monday of the month
1116 return ';BYDAY=' + data.get('byday') + data.get('week_list')
1117 elif data.get('month_by') == 'date': # Eg : 16th of the month
1118 return ';BYMONTHDAY=' + str(data.get('day'))
1121 def get_end_date(data):
1122 if data.get('end_date'):
1123 data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
1125 return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
1126 ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
1128 freq = data.get('rrule_type', False) # day/week/month/year
1131 interval_srting = data.get('interval') and (';INTERVAL=' + str(data.get('interval'))) or ''
1132 res = 'FREQ=' + freq.upper() + get_week_string(freq, data) + interval_srting + get_end_date(data) + get_month_string(freq, data)
1136 def _get_empty_rrule_data(self):
1139 'recurrency': False,
1141 'rrule_type': False,
1157 def _parse_rrule(self, rule, data, date_start):
1158 day_list = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
1159 rrule_type = ['yearly', 'monthly', 'weekly', 'daily']
1160 r = rrule.rrulestr(rule, dtstart=datetime.strptime(date_start, "%Y-%m-%d %H:%M:%S"))
1162 if r._freq > 0 and r._freq < 4:
1163 data['rrule_type'] = rrule_type[r._freq]
1165 data['count'] = r._count
1166 data['interval'] = r._interval
1167 data['end_date'] = r._until and r._until.strftime("%Y-%m-%d %H:%M:%S")
1170 for i in xrange(0, 7):
1171 if i in r._byweekday:
1172 data[day_list[i]] = True
1173 data['rrule_type'] = 'weekly'
1174 #repeat monthly by nweekday ((weekday, weeknumber), )
1176 data['week_list'] = day_list[r._bynweekday[0][0]].upper()
1177 data['byday'] = r._bynweekday[0][1]
1178 data['month_by'] = 'day'
1179 data['rrule_type'] = 'monthly'
1182 data['day'] = r._bymonthday[0]
1183 data['month_by'] = 'date'
1184 data['rrule_type'] = 'monthly'
1186 #repeat yearly but for openerp it's monthly, take same information as monthly but interval is 12 times
1188 data['interval'] = data['interval'] * 12
1190 #FIXEME handle forever case
1192 #in case of repeat for ever that we do not support right now
1193 if not (data.get('count') or data.get('end_date')):
1195 if data.get('count'):
1196 data['end_type'] = 'count'
1198 data['end_type'] = 'end_date'
1201 def message_get_subscription_data(self, cr, uid, ids, user_pid=None, context=None):
1203 for virtual_id in ids:
1204 real_id = calendar_id2real_id(virtual_id)
1205 result = super(calendar_event, self).message_get_subscription_data(cr, uid, [real_id], user_pid=None, context=context)
1206 res[virtual_id] = result[real_id]
1209 def onchange_partner_ids(self, cr, uid, ids, value, context=None):
1210 """ The basic purpose of this method is to check that destination partners
1211 effectively have email addresses. Otherwise a warning is thrown.
1212 :param value: value format: [[6, 0, [3, 4]]]
1216 if not value or not value[0] or not value[0][0] == 6:
1219 res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
1222 def onchange_rec_day(self,cr,uid,id,date,mo,tu,we,th,fr,sa,su):
1223 """ set the start date according to the first occurence of rrule"""
1224 rrule_obj = self._get_empty_rrule_data()
1227 'rrule_type':'weekly',
1237 str_rrule = self.compute_rule_string(rrule_obj)
1238 first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
1239 return {'value': { 'date' : first_occurence.strftime("%Y-%m-%d") + ' 00:00:00' } }
1242 def check_partners_email(self, cr, uid, partner_ids, context=None):
1243 """ Verify that selected partner_ids have an email_address defined.
1244 Otherwise throw a warning. """
1245 partner_wo_email_lst = []
1246 for partner in self.pool['res.partner'].browse(cr, uid, partner_ids, context=context):
1247 if not partner.email:
1248 partner_wo_email_lst.append(partner)
1249 if not partner_wo_email_lst:
1251 warning_msg = _('The following contacts have no email address :')
1252 for partner in partner_wo_email_lst:
1253 warning_msg += '\n- %s' % (partner.name)
1256 'title': _('Email addresses not found'),
1257 'message': warning_msg,
1261 # ----------------------------------------
1263 # ----------------------------------------
1265 # shows events of the day for this user
1267 def _needaction_domain_get(self, cr, uid, context=None):
1268 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)]
1270 def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
1271 if isinstance(thread_id, str):
1272 thread_id = get_real_ids(thread_id)
1273 if context.get('default_date'):
1274 del context['default_date']
1275 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)
1277 def do_sendmail(self, cr, uid, ids, context=None):
1278 for event in self.browse(cr, uid, ids, context):
1279 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1281 if current_user.email:
1282 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):
1283 self.message_post(cr, uid, event.id, body=_("An invitation email has been sent to attendee(s)"), subtype="calendar.subtype_invitation", context=context)
1286 def get_attendee(self, cr, uid, meeting_id, context=None):
1287 # Used for view in controller
1288 invitation = {'meeting': {}, 'attendee': []}
1290 meeting = self.browse(cr, uid, int(meeting_id), context)
1291 invitation['meeting'] = {
1292 'event': meeting.name,
1293 'where': meeting.location,
1294 'when': meeting.display_time
1297 for attendee in meeting.attendee_ids:
1298 invitation['attendee'].append({'name': attendee.cn, 'status': attendee.state})
1301 def get_interval(self, cr, uid, ids, date, interval, context=None):
1302 #Function used only in calendar_event_data.xml for email template
1303 date = datetime.strptime(date.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT)
1304 if interval == 'day':
1306 elif interval == 'month':
1307 res = date.strftime('%B') + " " + str(date.year)
1308 elif interval == 'dayname':
1309 res = date.strftime('%A')
1310 elif interval == 'time':
1311 res = date.strftime('%I:%M %p')
1314 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1318 if context.get('mymeetings', False):
1319 partner_id = self.pool['res.users'].browse(cr, uid, uid, context).partner_id.id
1320 args += ['|', ('partner_ids', 'in', [partner_id]), ('user_id', '=', uid)]
1326 if arg[0] in ('date', unicode('date')) and arg[1] == ">=":
1327 if context.get('virtual_id', True):
1328 new_args += ['|', '&', ('recurrency', '=', 1), ('end_date', arg[1], arg[2])]
1329 elif arg[0] == "id":
1330 new_id = get_real_ids(arg[2])
1331 new_arg = (arg[0], arg[1], new_id)
1332 new_args.append(new_arg)
1334 if not context.get('virtual_id', True):
1335 return super(calendar_event, self).search(cr, uid, new_args, offset=offset, limit=limit, order=order, context=context, count=count)
1337 # offset, limit, order and count must be treated separately as we may need to deal with virtual ids
1338 res = super(calendar_event, self).search(cr, uid, new_args, offset=0, limit=0, order=None, context=context, count=False)
1339 res = self.get_recurrent_ids(cr, uid, res, args, order=order, context=context)
1343 return res[offset: offset + limit]
1346 def copy(self, cr, uid, id, default=None, context=None):
1350 default = default or {}
1351 default['attendee_ids'] = False
1353 res = super(calendar_event, self).copy(cr, uid, calendar_id2real_id(id), default, context)
1356 def _detach_one_event(self, cr, uid, id, values=dict(), context=None):
1357 real_event_id = calendar_id2real_id(id)
1358 data = self.read(cr, uid, id, ['date', 'date_deadline', 'rrule', 'duration'])
1360 if data.get('rrule'):
1363 recurrent_id=real_event_id,
1364 recurrent_id_date=data.get('date'),
1368 end_date = datetime.strptime(values.get('date', False) or data.get('date'),"%Y-%m-%d %H:%M:%S")
1369 + timedelta(hours=values.get('duration', False) or data.get('duration'))
1375 new_id = self.copy(cr, uid, real_event_id, default=data, context=context)
1378 def open_after_detach_event(self, cr, uid, ids, context=None):
1382 new_id = self._detach_one_event(cr, uid, ids[0], context=context)
1384 'type': 'ir.actions.act_window',
1385 'res_model': 'calendar.event',
1386 'view_mode': 'form',
1388 'target': 'current',
1389 'flags': {'form': {'action_buttons': True, 'options' : { 'mode' : 'edit' } } }
1395 def write(self, cr, uid, ids, values, context=None):
1396 def _only_changes_to_apply_on_real_ids(field_names):
1397 ''' return True if changes are only to be made on the real ids'''
1398 for field in field_names:
1399 if field in ['date','active']:
1403 context = context or {}
1406 if isinstance(ids, (str,int, long)):
1407 if len(str(ids).split('-')) == 1:
1415 # Special write of complex IDS
1416 for event_id in ids:
1417 if len(str(event_id).split('-')) == 1:
1420 ids.remove(event_id)
1421 real_event_id = calendar_id2real_id(event_id)
1423 # if we are setting the recurrency flag to False or if we are only changing fields that
1424 # should be only updated on the real ID and not on the virtual (like message_follower_ids):
1425 # then set real ids to be updated.
1426 if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()):
1427 ids.append(real_event_id)
1430 data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration'])
1431 if data.get('rrule'):
1432 new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
1434 res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
1436 # set end_date for calendar searching
1437 if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
1438 (values.get('rrule_type') or values.get('count') or values.get('date') or values.get('date_deadline')):
1439 for data in self.read(cr, uid, ids, ['date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
1440 end_date = self._get_recurrency_end_date(data, context=context)
1441 super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
1443 attendees_create = False
1444 if values.get('partner_ids', False):
1445 attendees_create = self.create_attendees(cr, uid, ids, context)
1447 if values.get('date', False) and values.get('active', True):
1448 the_id = new_id or (ids and int(ids[0]))
1450 if attendees_create:
1451 attendees_create = attendees_create[the_id]
1452 mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids']))
1454 mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids]
1457 current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
1458 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):
1459 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)
1461 return res or True and False
1463 def create(self, cr, uid, vals, context=None):
1467 if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user
1468 vals['user_id'] = uid
1470 if vals.get('recurrency', True) and vals.get('end_type', 'count') in ('count', unicode('count')) and \
1471 (vals.get('rrule_type') or vals.get('count') or vals.get('date') or vals.get('date_deadline')):
1472 vals['end_date'] = self._get_recurrency_end_date(vals, context=context)
1474 res = super(calendar_event, self).create(cr, uid, vals, context=context)
1475 self.create_attendees(cr, uid, [res], context=context)
1478 def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False):
1482 if 'date' in groupby:
1483 raise osv.except_osv(_('Warning!'), _('Group by date is not supported, use the calendar view instead.'))
1484 virtual_id = context.get('virtual_id', True)
1485 context.update({'virtual_id': False})
1486 res = super(calendar_event, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
1488 #remove the count, since the value is not consistent with the result of the search when expand the group
1489 for groupname in groupby:
1490 if result.get(groupname + "_count"):
1491 del result[groupname + "_count"]
1492 result.get('__context', {}).update({'virtual_id': virtual_id})
1495 def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
1498 fields2 = fields and fields[:] or None
1499 EXTRAFIELDS = ('class', 'user_id', 'duration', 'date', 'rrule', 'vtimezone')
1500 for f in EXTRAFIELDS:
1501 if fields and (f not in fields):
1504 if isinstance(ids, (str, int, long)):
1509 select = map(lambda x: (x, calendar_id2real_id(x)), select)
1512 real_data = super(calendar_event, self).read(cr, uid, [real_id for calendar_id, real_id in select], fields=fields2, context=context, load=load)
1513 real_data = dict(zip([x['id'] for x in real_data], real_data))
1515 for calendar_id, real_id in select:
1516 res = real_data[real_id].copy()
1517 ls = calendar_id2real_id(calendar_id, with_date=res and res.get('duration', 0) or 0)
1518 if not isinstance(ls, (str, int, long)) and len(ls) >= 2:
1520 res['date_deadline'] = ls[2]
1521 res['id'] = calendar_id
1526 user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id']
1529 if r['class'] == 'private':
1531 if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count'):
1532 if isinstance(r[f], list):
1540 for k in EXTRAFIELDS:
1541 if (k in r) and (fields and (k not in fields)):
1544 if isinstance(ids, (str, int, long)):
1545 return result and result[0] or False
1548 def unlink(self, cr, uid, ids, unlink_level=0, context=None):
1549 if not isinstance(ids, list):
1556 # One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it
1557 for event_id in ids:
1558 if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL
1559 if self.browse(cr, uid, event_id).recurrent_id:
1560 ids_to_exclure.append(event_id)
1562 ids_to_unlink.append(event_id)
1564 ids_to_exclure.append(event_id)
1567 res = super(calendar_event, self).unlink(cr, uid, ids_to_unlink, context=context)
1570 for id_to_exclure in ids_to_exclure:
1571 res = self.write(cr, uid, id_to_exclure, {'active': False}, context=context)
1576 class mail_message(osv.Model):
1577 _inherit = "mail.message"
1579 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1581 convert the search on real ids in the case it was asked on virtual ids, then call super()
1583 for index in range(len(args)):
1584 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1585 args[index][2] = get_real_ids(args[index][2])
1586 return super(mail_message, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1588 def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
1589 if doc_model == 'calendar.event':
1590 order = context.get('order', self._order)
1591 for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
1592 doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
1593 return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
1596 class ir_attachment(osv.Model):
1597 _inherit = "ir.attachment"
1599 def search(self, cr, uid, args, offset=0, limit=0, order=None, context=None, count=False):
1601 convert the search on real ids in the case it was asked on virtual ids, then call super()
1603 for index in range(len(args)):
1604 if args[index][0] == "res_id" and isinstance(args[index][2], str):
1605 args[index][2] = get_real_ids(args[index][2])
1606 return super(ir_attachment, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count)
1608 def write(self, cr, uid, ids, vals, context=None):
1610 when posting an attachment (new or not), convert the virtual ids in real ids.
1612 if isinstance(vals.get('res_id'), str):
1613 vals['res_id'] = get_real_ids(vals.get('res_id'))
1614 return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
1617 class ir_http(osv.AbstractModel):
1618 _inherit = 'ir.http'
1620 def _auth_method_calendar(self):
1621 token = request.params['token']
1622 db = request.params['db']
1624 registry = openerp.modules.registry.RegistryManager.get(db)
1625 attendee_pool = registry.get('calendar.attendee')
1626 error_message = False
1627 with registry.cursor() as cr:
1628 attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token)])
1630 error_message = """Invalid Invitation Token."""
1631 elif request.session.uid and request.session.login != 'anonymous':
1632 # if valid session but user is not match
1633 attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
1634 user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
1635 if attendee.partner_id.id != user.partner_id.id:
1636 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)
1639 raise BadRequest(error_message)
1642 class invite_wizard(osv.osv_memory):
1643 _inherit = 'mail.wizard.invite'
1645 def default_get(self, cr, uid, fields, context=None):
1647 in case someone clicked on 'invite others' wizard in the followers widget, transform virtual ids in real ids
1649 result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
1650 if 'res_id' in result:
1651 result['res_id'] = get_real_ids(result['res_id'])