With this commit we try to improve the way the events are displayed to the user. Now, an event is displayed for each attendee of the event that are selected in the filter. These events are displayed correctly based on the status of the attendee in the event. The colors displayed for the events now represent the attendees and not the organizer. If an attendee edit an event, it is edited for all others attendees. Also, when an attendee that is not the organizer try to delete the event, then the event is now declined in place of being deleted. If the organizer delete the event, it is deleted for all attendees. In case of an event where all attendees have declined it but the organizer, the organizer see now a danger icon before the name of the event and it's outlined and not filled with color, no matter of the actual status of the organizer in the event. task-2196775 COM PR: odoo/odoo#55190 ENT PR: odoo/enterprise#12196 UPG PR: odoo/upgrade#1532
247 lines
11 KiB
Python
247 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import pytz
|
|
from dateutil.parser import parse
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models, tools, _
|
|
|
|
|
|
class Meeting(models.Model):
|
|
_name = 'calendar.event'
|
|
_inherit = ['calendar.event', 'google.calendar.sync']
|
|
|
|
google_id = fields.Char(
|
|
'Google Calendar Event Id', compute='_compute_google_id', store=True, readonly=False)
|
|
|
|
@api.depends('recurrence_id.google_id')
|
|
def _compute_google_id(self):
|
|
# google ids of recurring events are built from the recurrence id and the
|
|
# original starting time in the recurrence.
|
|
# The `start` field does not appear in the dependencies on purpose!
|
|
# Event if the event is moved, the google_id remains the same.
|
|
for event in self:
|
|
google_recurrence_id = event.recurrence_id._get_event_google_id(event)
|
|
if not event.google_id and google_recurrence_id:
|
|
event.google_id = google_recurrence_id
|
|
elif not event.google_id:
|
|
event.google_id = False
|
|
|
|
@api.model
|
|
def _get_google_synced_fields(self):
|
|
return {'name', 'description', 'allday', 'start', 'date_end', 'stop',
|
|
'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active'}
|
|
|
|
@api.model
|
|
def _restart_google_sync(self):
|
|
self.env['calendar.event'].search(self._get_sync_domain()).write({
|
|
'need_sync': True,
|
|
})
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
return super().create([
|
|
dict(vals, need_sync=False) if vals.get('recurrence_id') or vals.get('recurrency') else vals
|
|
for vals in vals_list
|
|
])
|
|
|
|
def write(self, values):
|
|
recurrence_update_setting = values.get('recurrence_update')
|
|
if recurrence_update_setting in ('all_events', 'future_events') and len(self) == 1:
|
|
values = dict(values, need_sync=False)
|
|
res = super().write(values)
|
|
if recurrence_update_setting in ('all_events',) and len(self) == 1 and values.keys() & self._get_google_synced_fields():
|
|
self.recurrence_id.need_sync = True
|
|
return res
|
|
|
|
def _get_sync_domain(self):
|
|
# in case of full sync, limit to a range of 1y in past and 1y in the future by default
|
|
ICP = self.env['ir.config_parameter'].sudo()
|
|
day_range = int(ICP.get_param('google_calendar.sync.range_days', default=365))
|
|
lower_bound = fields.Datetime.subtract(fields.Datetime.now(), days=day_range)
|
|
upper_bound = fields.Datetime.add(fields.Datetime.now(), days=day_range)
|
|
return [
|
|
('partner_ids.user_ids', 'in', self.env.user.id),
|
|
('stop', '>', lower_bound),
|
|
('start', '<', upper_bound)
|
|
]
|
|
|
|
@api.model
|
|
def _odoo_values(self, google_event, default_reminders=()):
|
|
if google_event.is_cancelled():
|
|
return {'active': False}
|
|
|
|
alarm_commands = self._odoo_reminders_commands(google_event.reminders.get('overrides') or default_reminders)
|
|
attendee_commands, partner_commands = self._odoo_attendee_commands(google_event)
|
|
values = {
|
|
'name': google_event.summary or _("(No title)"),
|
|
'description': google_event.description,
|
|
'location': google_event.location,
|
|
'user_id': google_event.owner(self.env).id,
|
|
'privacy': google_event.visibility or self.default_get(['privacy'])['privacy'],
|
|
'attendee_ids': attendee_commands,
|
|
'partner_ids': partner_commands,
|
|
'alarm_ids': alarm_commands,
|
|
'recurrency': google_event.is_recurrent()
|
|
}
|
|
|
|
if not google_event.is_recurrence():
|
|
values['google_id'] = google_event.id
|
|
if google_event.start.get('dateTime'):
|
|
# starting from python3.7, use the new [datetime, date].fromisoformat method
|
|
start = parse(google_event.start.get('dateTime')).astimezone(pytz.utc).replace(tzinfo=None)
|
|
stop = parse(google_event.end.get('dateTime')).astimezone(pytz.utc).replace(tzinfo=None)
|
|
values['allday'] = False
|
|
else:
|
|
start = parse(google_event.start.get('date'))
|
|
stop = parse(google_event.end.get('date')) - relativedelta(days=1)
|
|
# Stop date should be exclusive as defined here https://developers.google.com/calendar/v3/reference/events#resource
|
|
# but it seems that's not always the case for old event
|
|
if stop < start:
|
|
stop = parse(google_event.end.get('date'))
|
|
values['allday'] = True
|
|
values['start'] = start
|
|
values['stop'] = stop
|
|
return values
|
|
|
|
@api.model
|
|
def _odoo_attendee_commands(self, google_event):
|
|
attendee_commands = []
|
|
partner_commands = []
|
|
google_attendees = google_event.attendees or []
|
|
if len(google_attendees) == 0 and google_event.organizer and google_event.organizer.get('self', False):
|
|
user = google_event.owner(self.env)
|
|
google_attendees += [{
|
|
'email': user.partner_id.email,
|
|
'responseStatus': 'accepted',
|
|
}]
|
|
emails = [a.get('email') for a in google_attendees]
|
|
existing_attendees = self.env['calendar.attendee']
|
|
if google_event.exists(self.env):
|
|
existing_attendees = self.browse(google_event.odoo_id(self.env)).attendee_ids
|
|
attendees_by_emails = {tools.email_normalize(a.email): a for a in existing_attendees}
|
|
for attendee in google_attendees:
|
|
email = attendee.get('email')
|
|
|
|
if email in attendees_by_emails:
|
|
# Update existing attendees
|
|
attendee_commands += [(1, attendees_by_emails[email].id, {'state': attendee.get('responseStatus')})]
|
|
else:
|
|
# Create new attendees
|
|
partner = self.env.user.partner_id if attendee.get('self') else self.env['res.partner'].find_or_create(attendee.get('email'))
|
|
attendee_commands += [(0, 0, {'state': attendee.get('responseStatus'), 'partner_id': partner.id})]
|
|
partner_commands += [(4, partner.id)]
|
|
if attendee.get('displayName') and not partner.name:
|
|
partner.name = attendee.get('displayName')
|
|
for odoo_attendee in attendees_by_emails.values():
|
|
# Remove old attendees
|
|
if tools.email_normalize(odoo_attendee.email) not in emails:
|
|
attendee_commands += [(2, odoo_attendee.id)]
|
|
partner_commands += [(3, odoo_attendee.partner_id.id)]
|
|
return attendee_commands, partner_commands
|
|
|
|
@api.model
|
|
def _odoo_reminders_commands(self, reminders=()):
|
|
commands = []
|
|
for reminder in reminders:
|
|
alarm_type = 'email' if reminder.get('method') == 'email' else 'notification'
|
|
alarm_type_label = _("Email") if alarm_type == 'email' else _("Notification")
|
|
|
|
minutes = reminder.get('minutes', 0)
|
|
alarm = self.env['calendar.alarm'].search([
|
|
('alarm_type', '=', alarm_type),
|
|
('duration_minutes', '=', minutes)
|
|
], limit=1)
|
|
if alarm:
|
|
commands += [(4, alarm.id)]
|
|
else:
|
|
if minutes % (60*24) == 0:
|
|
interval = 'days'
|
|
duration = minutes / 60 / 24
|
|
name = _(
|
|
"%(reminder_type)s - %(duration)s Days",
|
|
reminder_type=alarm_type_label,
|
|
duration=duration,
|
|
)
|
|
elif minutes % 60 == 0:
|
|
interval = 'hours'
|
|
duration = minutes / 60
|
|
name = _(
|
|
"%(reminder_type)s - %(duration)s Hours",
|
|
reminder_type=alarm_type_label,
|
|
duration=duration,
|
|
)
|
|
else:
|
|
interval = 'minutes'
|
|
duration = minutes
|
|
name = _(
|
|
"%(reminder_type)s - %(duration)s Minutes",
|
|
reminder_type=alarm_type_label,
|
|
duration=duration,
|
|
)
|
|
commands += [(0, 0, {'duration': duration, 'interval': interval, 'name': name, 'alarm_type': alarm_type})]
|
|
return commands
|
|
|
|
def _google_values(self):
|
|
if self.allday:
|
|
start = {'date': self.start_date.isoformat()}
|
|
end = {'date': (self.stop_date + relativedelta(days=1)).isoformat()}
|
|
else:
|
|
start = {'dateTime': pytz.utc.localize(self.start).isoformat()}
|
|
end = {'dateTime': pytz.utc.localize(self.stop).isoformat()}
|
|
|
|
reminders = [{
|
|
'method': "email" if alarm.alarm_type == "email" else "popup",
|
|
'minutes': alarm.duration_minutes
|
|
} for alarm in self.alarm_ids]
|
|
values = {
|
|
'id': self.google_id,
|
|
'start': start,
|
|
'end': end,
|
|
'summary': self.name,
|
|
'description': self.description or '',
|
|
'location': self.location or '',
|
|
'guestsCanModify': True,
|
|
'organizer': {'email': self.user_id.email, 'self': self.user_id == self.env.user},
|
|
'attendees': [{'email': attendee.email, 'responseStatus': attendee.state} for attendee in self.attendee_ids],
|
|
'extendedProperties': {
|
|
'shared': {
|
|
'%s_odoo_id' % self.env.cr.dbname: self.id,
|
|
},
|
|
},
|
|
'reminders': {
|
|
'overrides': reminders,
|
|
'useDefault': False,
|
|
}
|
|
}
|
|
if self.privacy:
|
|
values['visibility'] = self.privacy
|
|
if not self.active:
|
|
values['status'] = 'cancelled'
|
|
if self.user_id and self.user_id != self.env.user:
|
|
values['extendedProperties']['shared']['%s_owner_id' % self.env.cr.dbname] = self.user_id.id
|
|
elif not self.user_id:
|
|
# We don't store the real owner identity (mail)
|
|
# We can't store on the shared properties in that case without getting a 403
|
|
# If several odoo users are attendees but the owner is not in odoo, the event will be duplicated on odoo database
|
|
# if we are not the owner, we should change the post values to avoid errors because we don't have enough rights
|
|
# See https://developers.google.com/calendar/concepts/sharing
|
|
keep_keys = ['id', 'attendees', 'start', 'end', 'reminders']
|
|
values = {key: val for key, val in values.items() if key in keep_keys}
|
|
# values['extendedProperties']['private] should be used if the owner is not an odoo user
|
|
values['extendedProperties'] = {
|
|
'private': {
|
|
'%s_odoo_id' % self.env.cr.dbname: self.id,
|
|
},
|
|
}
|
|
return values
|
|
|
|
def _cancel(self):
|
|
# only owner can delete => others refuse the event
|
|
user = self.env.user
|
|
my_cancelled_records = self.filtered(lambda e: e.user_id == user)
|
|
super(Meeting, my_cancelled_records)._cancel()
|
|
attendees = (self - my_cancelled_records).attendee_ids.filtered(lambda a: a.partner_id == user.partner_id)
|
|
attendees.state = 'declined'
|