Files
odoo_source/addons/google_calendar/models/calendar.py
T
thcl-odoo b6e08d662d [FIX] google_calendar: add show_as on sync
Current behavior :
When a Google Agenda event is created with "Show as" set to Available,
Show as (`show_as`) isn't set accordingly

Steps :
- Install Calendar
- setup and activate Google Calendar sync
- Create an event and set "Show as" on Available
- Refresh Odoo calendar to sync it
-> Created event "show as" is set to Busy

OPW-2805096

closes odoo/odoo#90261

X-original-commit: ff56f8126e105f2196843fed1646a3081f25aff3
Signed-off-by: Arnaud Joset <arj@odoo.com>
Signed-off-by: Claude Thibault (thcl) <thcl@odoo.com>
2022-05-04 10:55:47 +02:00

280 lines
14 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):
notify_context = self.env.context.get('dont_notify', False)
return super(Meeting, self.with_context(dont_notify=notify_context)).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)
notify_context = self.env.context.get('dont_notify', False)
res = super(Meeting, self.with_context(dont_notify=notify_context)).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),
# Do not sync events that follow the recurrence, they are already synced at recurrence creation
'!', '&', '&', ('recurrency', '=', True), ('recurrence_id', '!=', False), ('follow_recurrence', '=', True)
]
@api.model
def _odoo_values(self, google_event, default_reminders=()):
if google_event.is_cancelled():
return {'active': False}
# default_reminders is never () it is set to google's default reminder (30 min before)
# we need to check 'useDefault' for the event to determine if we have to use google's
# default reminder or not
reminder_command = google_event.reminders.get('overrides')
if not reminder_command:
reminder_command = google_event.reminders.get('useDefault') and default_reminders or ()
alarm_commands = self._odoo_reminders_commands(reminder_command)
attendee_commands, partner_commands = self._odoo_attendee_commands(google_event)
related_event = self.search([('google_id', '=', google_event.id)], limit=1)
name = google_event.summary or related_event and related_event.name or _("(No title)")
values = {
'name': name,
'description': google_event.description and tools.html_sanitize(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,
'alarm_ids': alarm_commands,
'recurrency': google_event.is_recurrent(),
'videocall_location': google_event.get_meeting_url(),
'show_as': 'free' if google_event.is_available() else 'busy'
}
if partner_commands:
# Add partner_commands only if set from Google. The write method on calendar_events will
# override attendee commands if the partner_ids command is set but empty.
values['partner_ids'] = partner_commands
if not google_event.is_recurrence():
values['google_id'] = google_event.id
if google_event.is_recurrent() and not google_event.is_recurrence():
# Propagate the follow_recurrence according to the google result
values['follow_recurrence'] = google_event.is_recurrence_follower()
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': 'needsAction',
}]
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}
partners = self._get_sync_partner(emails)
for attendee in zip(emails, partners, google_attendees):
email = attendee[0]
if email in attendees_by_emails:
# Update existing attendees
attendee_commands += [(1, attendees_by_emails[email].id, {'state': attendee[2].get('responseStatus')})]
else:
# Create new attendees
if attendee[2].get('self'):
partner = self.env.user.partner_id
elif attendee[1]:
partner = attendee[1]
else:
continue
attendee_commands += [(0, 0, {'state': attendee[2].get('responseStatus'), 'partner_id': partner.id})]
partner_commands += [(4, partner.id)]
if attendee[2].get('displayName') and not partner.name:
partner.name = attendee[2].get('displayName')
for odoo_attendee in attendees_by_emails.values():
# Remove old attendees but only if it does not correspond to the current user.
email = tools.email_normalize(odoo_attendee.email)
if email not in emails and email != self.env.user.email:
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]
attendees = self.attendee_ids
if self.user_id and self.user_id != self.env.user and bool(self.user_id.sudo().google_calendar_token):
# We avoid updating the other attendee status if we are not the organizer
attendees = self.attendee_ids.filtered(lambda att: att.partner_id == self.env.user.partner_id)
attendee_values = [{'email': attendee.partner_id.email_normalized, 'responseStatus': attendee.state} for attendee in attendees if attendee.partner_id.email_normalized]
# We sort the attendees to avoid undeterministic test fails. It's not mandatory for Google.
attendee_values.sort(key=lambda k: k['email'])
values = {
'id': self.google_id,
'start': start,
'end': end,
'summary': self.name,
'description': tools.html2plaintext(self.description) if not tools.is_html_empty(self.description) else '',
'location': self.location or '',
'guestsCanModify': True,
'organizer': {'email': self.user_id.email, 'self': self.user_id == self.env.user},
'attendees': attendee_values,
'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 and not bool(self.user_id.sudo().google_calendar_token):
# The organizer is an Odoo user that do not sync his calendar
values['extendedProperties']['shared']['%s_owner_id' % self.env.cr.dbname] = self.user_id.id
elif not self.user_id:
# We can't store on the shared properties in that case without getting a 403. It can happen when
# the owner is not an Odoo user: We don't store the real owner identity (mail)
# If we are not the owner, we should change the post values to avoid errors because we don't have
# write permissions
# See https://developers.google.com/calendar/concepts/sharing
keep_keys = ['id', 'summary', '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'