mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-23 16:32:08 +02:00
19.0 vanilla
This commit is contained in:
parent
20ddc1b4a3
commit
c0efcc53f5
1162 changed files with 125577 additions and 105287 deletions
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import res_config_settings
|
||||
|
|
@ -6,6 +5,6 @@ from . import google_sync
|
|||
from . import calendar
|
||||
from . import calendar_recurrence_rule
|
||||
from . import res_users
|
||||
from . import res_users_settings
|
||||
from . import calendar_attendee
|
||||
from . import google_credentials
|
||||
from . import calendar_alarm_manager
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import pytz
|
||||
|
|
@ -7,15 +6,23 @@ from dateutil.relativedelta import relativedelta
|
|||
from uuid import uuid4
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import Domain
|
||||
|
||||
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
|
||||
|
||||
class Meeting(models.Model):
|
||||
|
||||
class CalendarEvent(models.Model):
|
||||
_name = 'calendar.event'
|
||||
_inherit = ['calendar.event', 'google.calendar.sync']
|
||||
|
||||
MEET_ROUTE = 'meet.google.com'
|
||||
|
||||
google_id = fields.Char(
|
||||
'Google Calendar Event Id', compute='_compute_google_id', store=True, readonly=False)
|
||||
guests_readonly = fields.Boolean(
|
||||
'Guests Event Modification Permission', default=False)
|
||||
videocall_source = fields.Selection(selection_add=[('google_meet', 'Google Meet')], ondelete={'google_meet': 'set discuss'})
|
||||
|
||||
@api.depends('recurrence_id.google_id')
|
||||
def _compute_google_id(self):
|
||||
|
|
@ -30,10 +37,16 @@ class Meeting(models.Model):
|
|||
elif not event.google_id:
|
||||
event.google_id = False
|
||||
|
||||
@api.depends('videocall_location')
|
||||
def _compute_videocall_source(self):
|
||||
events_with_google_url = self.filtered(lambda event: self.MEET_ROUTE in (event.videocall_location or ''))
|
||||
events_with_google_url.videocall_source = 'google_meet'
|
||||
super(CalendarEvent, self - events_with_google_url)._compute_videocall_source()
|
||||
|
||||
@api.model
|
||||
def _get_google_synced_fields(self):
|
||||
return {'name', 'description', 'allday', 'start', 'date_end', 'stop',
|
||||
'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active', 'show_as'}
|
||||
'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active', 'show_as', 'videocall_location'}
|
||||
|
||||
@api.model
|
||||
def _restart_google_sync(self):
|
||||
|
|
@ -43,8 +56,9 @@ class Meeting(models.Model):
|
|||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
description_context = self.env.context.get('skip_contact_description', False)
|
||||
notify_context = self.env.context.get('dont_notify', False)
|
||||
return super(Meeting, self.with_context(dont_notify=notify_context)).create([
|
||||
return super(CalendarEvent, self.with_context(dont_notify=notify_context, skip_contact_description=description_context)).create([
|
||||
dict(vals, need_sync=False) if vals.get('recurrence_id') or vals.get('recurrency') else vals
|
||||
for vals in vals_list
|
||||
])
|
||||
|
|
@ -74,29 +88,54 @@ class Meeting(models.Model):
|
|||
archive_values = super()._get_archive_values()
|
||||
return {**archive_values, 'need_sync': False}
|
||||
|
||||
def write(self, values):
|
||||
recurrence_update_setting = values.get('recurrence_update')
|
||||
def write(self, vals):
|
||||
recurrence_update_setting = vals.get('recurrence_update')
|
||||
if recurrence_update_setting in ('all_events', 'future_events') and len(self) == 1:
|
||||
values = dict(values, need_sync=False)
|
||||
vals = dict(vals, 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():
|
||||
if not notify_context and ([self.env.user.id != record.user_id.id for record in self]):
|
||||
self._check_modify_event_permission(vals)
|
||||
res = super(CalendarEvent, self.with_context(dont_notify=notify_context)).write(vals)
|
||||
if recurrence_update_setting == 'all_events' and len(self) == 1 and vals.keys() & self._get_google_synced_fields():
|
||||
self.recurrence_id.need_sync = True
|
||||
return res
|
||||
|
||||
def _check_modify_event_permission(self, values):
|
||||
""" Check if event modification attempt by attendee is valid to avoid duplicate events creation. """
|
||||
# Edge case: when restarting the synchronization, guests can write 'need_sync=True' on events.
|
||||
google_sync_restart = values.get('need_sync') and len(values)
|
||||
# Edge case 2: when resetting an account, we must be able to erase the event's google_id.
|
||||
skip_event_permission = self.env.context.get('skip_event_permission', False)
|
||||
# Edge case 3: check if event is synchronizable in order to make sure the error is worth it.
|
||||
is_synchronizable = self._check_values_to_sync(values)
|
||||
if google_sync_restart or skip_event_permission or not is_synchronizable:
|
||||
return
|
||||
if any(event.guests_readonly and self.env.user.id != event.user_id.id for event in self):
|
||||
raise ValidationError(
|
||||
_("The following event can only be updated by the organizer "
|
||||
"according to the event permissions set on Google Calendar.")
|
||||
)
|
||||
|
||||
def _skip_send_mail_status_update(self):
|
||||
"""If a google calendar is not syncing with the user, don't send a mail."""
|
||||
user_id = self._get_event_user()
|
||||
if user_id.is_google_calendar_synced() and user_id.res_users_settings_id._is_google_calendar_valid():
|
||||
return True
|
||||
return super()._skip_send_mail_status_update()
|
||||
|
||||
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 [
|
||||
return Domain([
|
||||
('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=()):
|
||||
|
|
@ -118,12 +157,13 @@ class Meeting(models.Model):
|
|||
'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'],
|
||||
'privacy': google_event.visibility or False,
|
||||
'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'
|
||||
'show_as': 'free' if google_event.is_available() else 'busy',
|
||||
'guests_readonly': not bool(google_event.guestsCanModify)
|
||||
}
|
||||
# Remove 'videocall_location' when not sent by Google, otherwise the local videocall will be discarded.
|
||||
if not values.get('videocall_location'):
|
||||
|
|
@ -251,7 +291,7 @@ class Meeting(models.Model):
|
|||
# Increase performance handling 'future_events' edge case as it was an 'all_events' update.
|
||||
if archive_future_events:
|
||||
recurrence_update_setting = 'all_events'
|
||||
super(Meeting, self).action_mass_archive(recurrence_update_setting)
|
||||
super().action_mass_archive(recurrence_update_setting)
|
||||
|
||||
def _google_values(self):
|
||||
# In Google API, all-day events must have their 'dateTime' information set
|
||||
|
|
@ -260,12 +300,15 @@ class Meeting(models.Model):
|
|||
start = {'date': None, 'dateTime': None}
|
||||
end = {'date': None, 'dateTime': None}
|
||||
if self.allday:
|
||||
# For all-day events, 'dateTime' must be set to None to indicate that it's an all-day event.
|
||||
# Otherwise, if both 'date' and 'dateTime' are set, Google may not recognize it as an all-day event.
|
||||
start['date'] = self.start_date.isoformat()
|
||||
end['date'] = (self.stop_date + relativedelta(days=1)).isoformat()
|
||||
else:
|
||||
# For timed events, 'date' must be set to None to indicate that it's not an all-day event.
|
||||
# Otherwise, if both 'date' and 'dateTime' are set, Google may not recognize it as a timed event
|
||||
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
|
||||
|
|
@ -273,9 +316,9 @@ class Meeting(models.Model):
|
|||
|
||||
attendees = self.attendee_ids
|
||||
attendee_values = [{
|
||||
'email': attendee.partner_id.sudo().email_normalized,
|
||||
'email': attendee.partner_id.email_normalized,
|
||||
'responseStatus': attendee.state or 'needsAction',
|
||||
} for attendee in attendees if attendee.partner_id.sudo().email_normalized]
|
||||
} 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 = {
|
||||
|
|
@ -283,9 +326,9 @@ class Meeting(models.Model):
|
|||
'start': start,
|
||||
'end': end,
|
||||
'summary': self.name,
|
||||
'description': tools.html_sanitize(self.description) if not tools.is_html_empty(self.description) else '',
|
||||
'description': self._get_customer_description(),
|
||||
'location': self.location or '',
|
||||
'guestsCanModify': True,
|
||||
'guestsCanModify': not self.guests_readonly,
|
||||
'organizer': {'email': self.user_id.email, 'self': self.user_id == self.env.user},
|
||||
'attendees': attendee_values,
|
||||
'extendedProperties': {
|
||||
|
|
@ -300,6 +343,8 @@ class Meeting(models.Model):
|
|||
}
|
||||
if not self.google_id and not self.videocall_location and not self.location:
|
||||
values['conferenceData'] = {'createRequest': {'requestId': uuid4().hex}}
|
||||
if self.google_id and not self.videocall_location:
|
||||
values['conferenceData'] = None
|
||||
if self.privacy:
|
||||
values['visibility'] = self.privacy
|
||||
if self.show_as:
|
||||
|
|
@ -329,7 +374,10 @@ class Meeting(models.Model):
|
|||
# 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()
|
||||
for event in self:
|
||||
# remove the tracking data to avoid calling _track_template in the pre-commit phase
|
||||
self.env.cr.precommit.data.pop(f'mail.tracking.create.{event._name}.{event.id}', None)
|
||||
super(CalendarEvent, my_cancelled_records)._cancel()
|
||||
attendees = (self - my_cancelled_records).attendee_ids.filtered(lambda a: a.partner_id == user.partner_id)
|
||||
attendees.state = 'declined'
|
||||
|
||||
|
|
@ -338,3 +386,8 @@ class Meeting(models.Model):
|
|||
if self.user_id and self.user_id.sudo().google_calendar_token:
|
||||
return self.user_id
|
||||
return self.env.user
|
||||
|
||||
def _is_google_insertion_blocked(self, sender_user):
|
||||
self.ensure_one()
|
||||
has_different_owner = self.user_id and self.user_id != sender_user
|
||||
return has_different_owner
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools import SQL
|
||||
|
||||
|
||||
class AlarmManager(models.AbstractModel):
|
||||
|
|
@ -8,5 +9,7 @@ class AlarmManager(models.AbstractModel):
|
|||
|
||||
@api.model
|
||||
def _get_notify_alert_extra_conditions(self):
|
||||
res = super()._get_notify_alert_extra_conditions()
|
||||
return f'{res} AND "event"."google_id" IS NULL'
|
||||
base = super()._get_notify_alert_extra_conditions()
|
||||
if self.env.context.get('alarm_type') == 'email':
|
||||
return SQL("%s AND event.google_id IS NULL", base)
|
||||
return base
|
||||
|
|
|
|||
|
|
@ -1,23 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
from odoo.addons.google_calendar.models.google_sync import google_calendar_token
|
||||
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
|
||||
|
||||
class Attendee(models.Model):
|
||||
_name = 'calendar.attendee'
|
||||
_inherit = 'calendar.attendee'
|
||||
|
||||
def _send_mail_to_attendees(self, mail_template, force_send=False):
|
||||
""" Override
|
||||
If not synced with Google, let Odoo in charge of sending emails
|
||||
Otherwise, nothing to do: Google will send them
|
||||
"""
|
||||
with google_calendar_token(self.env.user.sudo()) as token:
|
||||
if not token:
|
||||
super()._send_mail_to_attendees(mail_template, force_send)
|
||||
class CalendarAttendee(models.Model):
|
||||
_inherit = 'calendar.attendee'
|
||||
|
||||
def do_tentative(self):
|
||||
# Synchronize event after state change
|
||||
|
|
@ -42,14 +31,10 @@ class Attendee(models.Model):
|
|||
# For weird reasons, we can't sync status when we are not the responsible
|
||||
# We can't adapt google_value to only keep ['id', 'summary', 'attendees', 'start', 'end', 'reminders']
|
||||
# and send that. We get a Forbidden for non-organizer error even if we only send start, end that are mandatory !
|
||||
if self._context.get('all_events'):
|
||||
service = GoogleCalendarService(self.env['google.service'].with_user(self.recurrence_id.base_event_id.user_id))
|
||||
self.recurrence_id.with_user(self.recurrence_id.base_event_id.user_id)._sync_odoo2google(service)
|
||||
else:
|
||||
all_events = self.mapped('event_id').filtered(lambda e: e.google_id)
|
||||
other_events = all_events.filtered(lambda e: e.user_id and e.user_id.id != self.env.user.id)
|
||||
for user in other_events.mapped('user_id'):
|
||||
service = GoogleCalendarService(self.env['google.service'].with_user(user))
|
||||
other_events.filtered(lambda ev: ev.user_id.id == user.id).with_user(user)._sync_odoo2google(service)
|
||||
google_service = GoogleCalendarService(self.env['google.service'])
|
||||
(all_events - other_events)._sync_odoo2google(google_service)
|
||||
all_events = self.mapped('event_id').filtered(lambda e: e.google_id)
|
||||
other_events = all_events.filtered(lambda e: e.user_id and e.user_id.id != self.env.user.id)
|
||||
for user in other_events.mapped('user_id'):
|
||||
service = GoogleCalendarService(self.env['google.service'].with_user(user))
|
||||
other_events.filtered(lambda ev: ev.user_id.id == user.id).with_user(user)._sync_odoo2google(service)
|
||||
google_service = GoogleCalendarService(self.env['google.service'])
|
||||
(all_events - other_events)._sync_odoo2google(google_service)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
import logging
|
||||
|
||||
from odoo import api, models, Command
|
||||
from odoo import api, models
|
||||
from odoo.fields import Command, Domain
|
||||
from odoo.tools import email_normalize
|
||||
|
||||
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class RecurrenceRule(models.Model):
|
||||
|
||||
class CalendarRecurrence(models.Model):
|
||||
_name = 'calendar.recurrence'
|
||||
_inherit = ['calendar.recurrence', 'google.calendar.sync']
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ class RecurrenceRule(models.Model):
|
|||
}]
|
||||
event.with_user(event._get_event_user())._google_delete(google_service, event.google_id)
|
||||
event.google_id = False
|
||||
self.env['calendar.event'].create(vals)
|
||||
self.env['calendar.event'].with_context(skip_contact_description=True).create(vals)
|
||||
|
||||
self.calendar_event_ids.need_sync = False
|
||||
return detached_events
|
||||
|
|
@ -121,7 +122,7 @@ class RecurrenceRule(models.Model):
|
|||
self.calendar_event_ids.write({'need_sync': False, 'partner_ids': [Command.unlink(att.partner_id.id) for att in attendees]})
|
||||
|
||||
old_event_values = self.base_event_id and self.base_event_id.read(base_event_time_fields)[0]
|
||||
if old_event_values and any(new_event_values[key] != old_event_values[key] for key in base_event_time_fields):
|
||||
if old_event_values and any(new_event_values.get(key) and new_event_values[key] != old_event_values[key] for key in base_event_time_fields):
|
||||
# we need to recreate the recurrence, time_fields were modified.
|
||||
base_event_id = self.base_event_id
|
||||
non_equal_values = [
|
||||
|
|
@ -173,7 +174,7 @@ class RecurrenceRule(models.Model):
|
|||
# Google reuse the event google_id to identify the recurrence in that case
|
||||
base_event = self.env['calendar.event'].search([('google_id', '=', vals['google_id'])])
|
||||
if not base_event:
|
||||
base_event = self.env['calendar.event'].create(base_values)
|
||||
base_event = self.env['calendar.event'].with_context(skip_contact_description=True).create(base_values)
|
||||
else:
|
||||
# We override the base_event values because they could have been changed in Google interface
|
||||
# The event google_id will be recalculated once the recurrence is created
|
||||
|
|
@ -184,7 +185,7 @@ class RecurrenceRule(models.Model):
|
|||
vals['event_tz'] = gevent.start.get('timeZone')
|
||||
attendee_values[base_event.id] = {'attendee_ids': base_values.get('attendee_ids')}
|
||||
|
||||
recurrence = super(RecurrenceRule, self.with_context(dont_notify=True))._create_from_google(gevents, vals_list)
|
||||
recurrence = super(CalendarRecurrence, self.with_context(dont_notify=True))._create_from_google(gevents, vals_list)
|
||||
generic_values_creation = {
|
||||
rec.id: attendee_values[rec.base_event_id.id]
|
||||
for rec in recurrence if attendee_values.get(rec.base_event_id.id)
|
||||
|
|
@ -197,7 +198,7 @@ class RecurrenceRule(models.Model):
|
|||
# older versions of the module. When synced, these recurrency may come back from Google after database cleaning
|
||||
# and trigger errors as the records are not properly populated.
|
||||
# We also prevent sync of other user recurrent events.
|
||||
return [('calendar_event_ids.user_id', '=', self.env.user.id), ('rrule', '!=', False)]
|
||||
return Domain('calendar_event_ids.user_id', '=', self.env.user.id) & Domain('rrule', '!=', False)
|
||||
|
||||
@api.model
|
||||
def _odoo_values(self, google_recurrence, default_reminders=()):
|
||||
|
|
@ -239,3 +240,9 @@ class RecurrenceRule(models.Model):
|
|||
if event:
|
||||
return event._get_event_user()
|
||||
return self.env.user
|
||||
|
||||
def _is_google_insertion_blocked(self, sender_user):
|
||||
self.ensure_one()
|
||||
has_base_event = self.base_event_id
|
||||
has_different_owner = self.base_event_id.user_id and self.base_event_id.user_id != sender_user
|
||||
return has_base_event and has_different_owner
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import requests
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.google_account.models.google_service import GOOGLE_TOKEN_ENDPOINT
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class GoogleCredentials(models.Model):
|
||||
""""Google Account of res_users"""
|
||||
|
||||
_name = 'google.calendar.credentials'
|
||||
_description = 'Google Calendar Account Data'
|
||||
|
||||
user_ids = fields.One2many('res.users', 'google_calendar_account_id', required=True)
|
||||
calendar_rtoken = fields.Char('Refresh Token', copy=False)
|
||||
calendar_token = fields.Char('User token', copy=False)
|
||||
calendar_token_validity = fields.Datetime('Token Validity', copy=False)
|
||||
calendar_sync_token = fields.Char('Next Sync Token', copy=False)
|
||||
|
||||
calendar_cal_id = fields.Char('Calendar ID', copy=False, help='Last Calendar ID who has been synchronized. If it is changed, we remove all links between GoogleID and Odoo Google Internal ID')
|
||||
synchronization_stopped = fields.Boolean('Google Synchronization stopped', copy=False)
|
||||
|
||||
def _set_auth_tokens(self, access_token, refresh_token, ttl):
|
||||
self.write({
|
||||
'calendar_rtoken': refresh_token,
|
||||
'calendar_token': access_token,
|
||||
'calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl) if ttl else False,
|
||||
})
|
||||
|
||||
def _google_calendar_authenticated(self):
|
||||
self.ensure_one()
|
||||
return bool(self.sudo().calendar_rtoken)
|
||||
|
||||
def _is_google_calendar_valid(self):
|
||||
self.ensure_one()
|
||||
return self.calendar_token_validity and self.calendar_token_validity >= (fields.Datetime.now() + timedelta(minutes=1))
|
||||
|
||||
def _refresh_google_calendar_token(self):
|
||||
# LUL TODO similar code exists in google_drive. Should be factorized in google_account
|
||||
self.ensure_one()
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
client_id = get_param('google_calendar_client_id')
|
||||
client_secret = get_param('google_calendar_client_secret')
|
||||
|
||||
if not client_id or not client_secret:
|
||||
raise UserError(_("The account for the Google Calendar service is not configured."))
|
||||
|
||||
headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
data = {
|
||||
'refresh_token': self.calendar_rtoken,
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'grant_type': 'refresh_token',
|
||||
}
|
||||
|
||||
try:
|
||||
_dummy, response, _dummy = self.env['google.service']._do_request(GOOGLE_TOKEN_ENDPOINT, params=data, headers=headers, method='POST', preuri='')
|
||||
ttl = response.get('expires_in')
|
||||
self.write({
|
||||
'calendar_token': response.get('access_token'),
|
||||
'calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl),
|
||||
})
|
||||
except requests.HTTPError as error:
|
||||
if error.response.status_code in (400, 401): # invalid grant or invalid client
|
||||
# Delete refresh token and make sure it's commited
|
||||
self.env.cr.rollback()
|
||||
self._set_auth_tokens(False, False, 0)
|
||||
self.env.cr.commit()
|
||||
error_key = error.response.json().get("error", "nc")
|
||||
error_msg = _("An error occurred while generating the token. Your authorization code may be invalid or has already expired [%s]. "
|
||||
"You should check your Client ID and secret on the Google APIs plateform or try to stop and restart your calendar synchronisation.",
|
||||
error_key)
|
||||
raise UserError(error_msg)
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
|
@ -7,10 +6,12 @@ from functools import wraps
|
|||
from requests import HTTPError
|
||||
import pytz
|
||||
from dateutil.parser import parse
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import api, fields, models, registry, _
|
||||
from odoo.tools import ormcache_context, email_normalize
|
||||
from odoo.osv import expression
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.fields import Domain
|
||||
from odoo.modules.registry import Registry
|
||||
from odoo.tools import email_normalize
|
||||
from odoo.sql_db import BaseCursor
|
||||
|
||||
from odoo.addons.google_calendar.utils.google_event import GoogleEvent
|
||||
|
|
@ -37,7 +38,7 @@ def after_commit(func):
|
|||
|
||||
@self.env.cr.postcommit.add
|
||||
def called_after():
|
||||
db_registry = registry(dbname)
|
||||
db_registry = Registry(dbname)
|
||||
with db_registry.cursor() as cr:
|
||||
env = api.Environment(cr, uid, context)
|
||||
try:
|
||||
|
|
@ -53,43 +54,45 @@ def google_calendar_token(user):
|
|||
yield user._get_google_calendar_token()
|
||||
|
||||
|
||||
class GoogleSync(models.AbstractModel):
|
||||
class GoogleCalendarSync(models.AbstractModel):
|
||||
_name = 'google.calendar.sync'
|
||||
_description = "Synchronize a record with Google Calendar"
|
||||
|
||||
google_id = fields.Char('Google Calendar Id', copy=False)
|
||||
google_id = fields.Char('Google Calendar Id', index='btree_not_null', copy=False)
|
||||
need_sync = fields.Boolean(default=True, copy=False)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
def write(self, vals):
|
||||
google_service = GoogleCalendarService(self.env['google.service'])
|
||||
if 'google_id' in vals:
|
||||
self._event_ids_from_google_ids.clear_cache(self)
|
||||
synced_fields = self._get_google_synced_fields()
|
||||
if 'need_sync' not in vals and vals.keys() & synced_fields and not self.env.user.google_synchronization_stopped:
|
||||
vals['need_sync'] = True
|
||||
|
||||
result = super().write(vals)
|
||||
for record in self.filtered('need_sync'):
|
||||
if record.google_id:
|
||||
record.with_user(record._get_event_user())._google_patch(google_service, record.google_id, record._google_values(), timeout=3)
|
||||
if self.env.user._get_google_sync_status() != "sync_paused":
|
||||
for record in self:
|
||||
if record.need_sync and record.google_id:
|
||||
record.with_user(record._get_event_user())._google_patch(google_service, record.google_id, record._google_values(), timeout=3)
|
||||
|
||||
return result
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if any(vals.get('google_id') for vals in vals_list):
|
||||
self._event_ids_from_google_ids.clear_cache(self)
|
||||
if self.env.user.google_synchronization_stopped:
|
||||
for vals in vals_list:
|
||||
user_ids = {v['user_id'] for v in vals_list if v.get('user_id')}
|
||||
users_with_sync = self.env['res.users'].browse(user_ids).filtered(lambda u: not u.sudo().google_synchronization_stopped)
|
||||
users_with_sync_set = set(users_with_sync.ids)
|
||||
|
||||
for vals in vals_list:
|
||||
if vals.get('user_id', False) and vals['user_id'] not in users_with_sync_set:
|
||||
vals.update({'need_sync': False})
|
||||
records = super().create(vals_list)
|
||||
self._handle_allday_recurrences_edge_case(records, vals_list)
|
||||
|
||||
google_service = GoogleCalendarService(self.env['google.service'])
|
||||
records_to_sync = records.filtered(lambda r: r.need_sync and r.active)
|
||||
for record in records_to_sync:
|
||||
record.with_user(record._get_event_user())._google_insert(google_service, record._google_values(), timeout=3)
|
||||
if self.env.user._get_google_sync_status() != "sync_paused":
|
||||
for record in records:
|
||||
if record.need_sync and record.active:
|
||||
record.with_user(record._get_event_user())._google_insert(google_service, record._google_values(), timeout=3)
|
||||
return records
|
||||
|
||||
def _handle_allday_recurrences_edge_case(self, records, vals_list):
|
||||
|
|
@ -124,12 +127,7 @@ class GoogleSync(models.AbstractModel):
|
|||
def _from_google_ids(self, google_ids):
|
||||
if not google_ids:
|
||||
return self.browse()
|
||||
return self.browse(self._event_ids_from_google_ids(google_ids))
|
||||
|
||||
@api.model
|
||||
@ormcache_context('google_ids', keys=('active_test',))
|
||||
def _event_ids_from_google_ids(self, google_ids):
|
||||
return self.search([('google_id', 'in', google_ids)]).ids
|
||||
return self.search([('google_id', 'in', google_ids)])
|
||||
|
||||
def _sync_odoo2google(self, google_service: GoogleCalendarService):
|
||||
if not self:
|
||||
|
|
@ -142,36 +140,42 @@ class GoogleSync(models.AbstractModel):
|
|||
|
||||
updated_records = records_to_sync.filtered('google_id')
|
||||
new_records = records_to_sync - updated_records
|
||||
for record in cancelled_records.filtered(lambda e: e.google_id and e.need_sync):
|
||||
record.with_user(record._get_event_user())._google_delete(google_service, record.google_id)
|
||||
for record in new_records:
|
||||
record.with_user(record._get_event_user())._google_insert(google_service, record._google_values())
|
||||
for record in updated_records:
|
||||
record.with_user(record._get_event_user())._google_patch(google_service, record.google_id, record._google_values())
|
||||
if self.env.user._get_google_sync_status() != "sync_paused":
|
||||
for record in cancelled_records:
|
||||
if record.google_id and record.need_sync:
|
||||
record.with_user(record._get_event_user())._google_delete(google_service, record.google_id)
|
||||
for record in new_records:
|
||||
if record._is_google_insertion_blocked(sender_user=self.env.user):
|
||||
continue
|
||||
record.with_user(record._get_event_user())._google_insert(google_service, record._google_values())
|
||||
for record in updated_records:
|
||||
record.with_user(record._get_event_user())._google_patch(google_service, record.google_id, record._google_values())
|
||||
|
||||
def _cancel(self):
|
||||
self.with_context(dont_notify=True).write({'google_id': False})
|
||||
self.unlink()
|
||||
|
||||
@api.model
|
||||
def _sync_google2odoo(self, google_events: GoogleEvent, default_reminders=()):
|
||||
def _sync_google2odoo(self, google_events: GoogleEvent, write_dates=None, default_reminders=()):
|
||||
"""Synchronize Google recurrences in Odoo. Creates new recurrences, updates
|
||||
existing ones.
|
||||
|
||||
:param google_recurrences: Google recurrences to synchronize in Odoo
|
||||
:param google_events: Google recurrences to synchronize in Odoo
|
||||
:param write_dates: A dictionary mapping Odoo record IDs to their write dates.
|
||||
:param default_reminders:
|
||||
:return: synchronized odoo recurrences
|
||||
"""
|
||||
write_dates = dict(write_dates or {})
|
||||
existing = google_events.exists(self.env)
|
||||
new = google_events - existing - google_events.cancelled()
|
||||
write_dates = self._context.get('write_dates', {})
|
||||
|
||||
odoo_values = [
|
||||
dict(self._odoo_values(e, default_reminders), need_sync=False)
|
||||
for e in new
|
||||
]
|
||||
new_odoo = self.with_context(dont_notify=True)._create_from_google(new, odoo_values)
|
||||
new_odoo = self.with_context(dont_notify=True, skip_contact_description=True)._create_from_google(new, odoo_values)
|
||||
cancelled = existing.cancelled()
|
||||
cancelled_odoo = self.browse(cancelled.odoo_ids(self.env)).exists()
|
||||
cancelled_odoo = self.browse(cancelled.odoo_ids(self.env))
|
||||
|
||||
# Check if it is a recurring event that has been rescheduled.
|
||||
# We have to check if an event already exists in Odoo.
|
||||
|
|
@ -184,7 +188,7 @@ class GoogleSync(models.AbstractModel):
|
|||
google_ids_to_remove = [event.full_recurring_event_id() for event in rescheduled_events]
|
||||
cancelled_odoo += self.env['calendar.event'].search([('google_id', 'in', google_ids_to_remove)])
|
||||
|
||||
cancelled_odoo._cancel()
|
||||
cancelled_odoo.exists()._cancel()
|
||||
synced_records = new_odoo + cancelled_odoo
|
||||
pending = existing - cancelled
|
||||
pending_odoo = self.browse(pending.odoo_ids(self.env)).exists()
|
||||
|
|
@ -244,10 +248,9 @@ class GoogleSync(models.AbstractModel):
|
|||
'reason': reason}
|
||||
_logger.warning(error_log)
|
||||
|
||||
body = _(
|
||||
"The following event could not be synced with Google Calendar. </br>"
|
||||
"It will not be synced as long at it is not updated.</br>"
|
||||
"%(reason)s", reason=reason)
|
||||
body = _("The following event could not be synced with Google Calendar.") + Markup("<br/>") + \
|
||||
_("It will not be synced as long at it is not updated.") + Markup("<br/>") + \
|
||||
reason
|
||||
|
||||
if event:
|
||||
event.message_post(
|
||||
|
|
@ -260,7 +263,7 @@ class GoogleSync(models.AbstractModel):
|
|||
def _google_delete(self, google_service: GoogleCalendarService, google_id, timeout=TIMEOUT):
|
||||
with google_calendar_token(self.env.user.sudo()) as token:
|
||||
if token:
|
||||
is_recurrence = self._context.get('is_recurrence', False)
|
||||
is_recurrence = self.env.context.get('is_recurrence', False)
|
||||
google_service.google_service = google_service.google_service.with_context(is_recurrence=is_recurrence)
|
||||
google_service.delete(google_id, token=token, timeout=timeout)
|
||||
# When the record has been deleted on our side, we need to delete it on google but we don't want
|
||||
|
|
@ -272,6 +275,8 @@ class GoogleSync(models.AbstractModel):
|
|||
with google_calendar_token(self.env.user.sudo()) as token:
|
||||
if token:
|
||||
try:
|
||||
send_updates = not self._is_event_over()
|
||||
google_service.google_service = google_service.google_service.with_context(send_updates=send_updates)
|
||||
google_service.patch(google_id, values, token=token, timeout=timeout)
|
||||
except HTTPError as e:
|
||||
if e.response.status_code in (400, 403):
|
||||
|
|
@ -279,6 +284,21 @@ class GoogleSync(models.AbstractModel):
|
|||
if values:
|
||||
self.exists().with_context(dont_notify=True).need_sync = False
|
||||
|
||||
def _get_post_sync_values(self, request_values, google_values):
|
||||
""" Return the values to be written in the event right after its insertion in Google side. """
|
||||
writeable_values = {
|
||||
'google_id': request_values['id'],
|
||||
'need_sync': False,
|
||||
}
|
||||
return writeable_values
|
||||
|
||||
def _need_video_call(self):
|
||||
""" Implement this method to return True if the event needs a video call
|
||||
:return: bool
|
||||
"""
|
||||
self.ensure_one()
|
||||
return True
|
||||
|
||||
@after_commit
|
||||
def _google_insert(self, google_service: GoogleCalendarService, values, timeout=TIMEOUT):
|
||||
if not values:
|
||||
|
|
@ -286,14 +306,10 @@ class GoogleSync(models.AbstractModel):
|
|||
with google_calendar_token(self.env.user.sudo()) as token:
|
||||
if token:
|
||||
try:
|
||||
send_updates = self._context.get('send_updates', True)
|
||||
send_updates = self.env.context.get('send_updates', True) and not self._is_event_over()
|
||||
google_service.google_service = google_service.google_service.with_context(send_updates=send_updates)
|
||||
google_id = google_service.insert(values, token=token, timeout=timeout)
|
||||
# Everything went smoothly
|
||||
self.with_context(dont_notify=True).write({
|
||||
'google_id': google_id,
|
||||
'need_sync': False,
|
||||
})
|
||||
google_values = google_service.insert(values, token=token, timeout=timeout, need_video_call=self._need_video_call())
|
||||
self.with_context(dont_notify=True).write(self._get_post_sync_values(values, google_values))
|
||||
except HTTPError as e:
|
||||
if e.response.status_code in (400, 403):
|
||||
self._google_error_handling(e)
|
||||
|
|
@ -307,18 +323,21 @@ class GoogleSync(models.AbstractModel):
|
|||
"""
|
||||
domain = self._get_sync_domain()
|
||||
if not full_sync:
|
||||
is_active_clause = (self._active_name, '=', True) if self._active_name else expression.TRUE_LEAF
|
||||
domain = expression.AND([domain, [
|
||||
'|',
|
||||
'&', ('google_id', '=', False), is_active_clause,
|
||||
('need_sync', '=', True),
|
||||
]])
|
||||
is_active_clause = Domain(self._active_name, '=', True) if self._active_name else Domain.TRUE
|
||||
domain &= (Domain('google_id', '=', False) & is_active_clause) | Domain('need_sync', '=', True)
|
||||
# We want to limit to 200 event sync per transaction, it shouldn't be a problem for the day to day
|
||||
# but it allows to run the first synchro within an acceptable time without timeout.
|
||||
# If there is a lot of event to synchronize to google the first time,
|
||||
# they will be synchronized eventually with the cron running few times a day
|
||||
return self.with_context(active_test=False).search(domain, limit=200)
|
||||
|
||||
def _check_any_records_to_sync(self):
|
||||
""" Returns True if there are pending records to be synchronized from Odoo to Google, False otherwise. """
|
||||
is_active_clause = Domain(self._active_name, '=', True) if self._active_name else Domain.TRUE
|
||||
domain = self._get_sync_domain()
|
||||
domain &= (Domain('google_id', '=', False) & is_active_clause) | Domain('need_sync', '=', True)
|
||||
return self.search_count(domain, limit=1) > 0
|
||||
|
||||
def _write_from_google(self, gevent, vals):
|
||||
self.write(vals)
|
||||
|
||||
|
|
@ -329,17 +348,10 @@ class GoogleSync(models.AbstractModel):
|
|||
@api.model
|
||||
def _get_sync_partner(self, emails):
|
||||
normalized_emails = [email_normalize(contact) for contact in emails if email_normalize(contact)]
|
||||
user_partners = self.env['mail.thread']._mail_search_on_user(normalized_emails, extra_domain=[('share', '=', False)])
|
||||
partners = [user_partner for user_partner in user_partners if user_partner.type != 'private']
|
||||
remaining = [email for email in normalized_emails if
|
||||
email not in [partner.email_normalized for partner in partners]]
|
||||
if remaining:
|
||||
partners += self.env['mail.thread']._mail_find_partner_from_emails(remaining, records=self, force_create=True, extra_domain=[('type', '!=', 'private')])
|
||||
unsorted_partners = self.env['res.partner'].browse([p.id for p in partners if p.id])
|
||||
partners = self.env['mail.thread']._partner_find_from_emails_single(normalized_emails)
|
||||
# partners needs to be sorted according to the emails order provided by google
|
||||
k = {value: idx for idx, value in enumerate(emails)}
|
||||
result = unsorted_partners.sorted(key=lambda p: k.get(p.email_normalized, -1))
|
||||
return result
|
||||
return partners.sorted(key=lambda p: k.get(p.email_normalized, -1))
|
||||
|
||||
@api.model
|
||||
def _odoo_values(self, google_event: GoogleEvent, default_reminders=()):
|
||||
|
|
@ -382,3 +394,12 @@ class GoogleSync(models.AbstractModel):
|
|||
the appropriate user accordingly.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _is_google_insertion_blocked(self, sender_user):
|
||||
"""
|
||||
Returns True if the record insertion to Google should be blocked.
|
||||
This is a necessary step for ensuring data match between Odoo and Google,
|
||||
as it avoids that events have permanently the wrong organizer in Google
|
||||
by not synchronizing records through owner and not through the attendees.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
|
@ -9,3 +8,5 @@ class ResConfigSettings(models.TransientModel):
|
|||
|
||||
cal_client_id = fields.Char("Client_id", config_parameter='google_calendar_client_id', default='')
|
||||
cal_client_secret = fields.Char("Client_key", config_parameter='google_calendar_client_secret', default='')
|
||||
cal_sync_paused = fields.Boolean("Google Synchronization Paused", config_parameter='google_calendar_sync_paused',
|
||||
help="Indicates if synchronization with Google Calendar is paused or not.")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
|
@ -7,61 +6,55 @@ import logging
|
|||
from odoo import api, fields, models, Command
|
||||
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService, InvalidSyncToken
|
||||
from odoo.addons.google_calendar.models.google_sync import google_calendar_token
|
||||
from odoo.addons.google_account.models import google_service
|
||||
from odoo.exceptions import LockError
|
||||
from odoo.loglevels import exception_to_unicode
|
||||
from odoo.tools import str2bool
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class User(models.Model):
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
google_calendar_account_id = fields.Many2one('google.calendar.credentials')
|
||||
google_calendar_rtoken = fields.Char(related='google_calendar_account_id.calendar_rtoken', groups="base.group_system")
|
||||
google_calendar_token = fields.Char(related='google_calendar_account_id.calendar_token')
|
||||
google_calendar_token_validity = fields.Datetime(related='google_calendar_account_id.calendar_token_validity')
|
||||
google_calendar_sync_token = fields.Char(related='google_calendar_account_id.calendar_sync_token')
|
||||
google_calendar_cal_id = fields.Char(related='google_calendar_account_id.calendar_cal_id')
|
||||
google_synchronization_stopped = fields.Boolean(related='google_calendar_account_id.synchronization_stopped', readonly=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('google_token_uniq', 'unique (google_calendar_account_id)', "The user has already a google account"),
|
||||
]
|
||||
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + ['google_synchronization_stopped', 'google_calendar_account_id']
|
||||
|
||||
@property
|
||||
def SELF_WRITEABLE_FIELDS(self):
|
||||
return super().SELF_WRITEABLE_FIELDS + ['google_synchronization_stopped', 'google_calendar_account_id']
|
||||
google_calendar_rtoken = fields.Char(related='res_users_settings_id.google_calendar_rtoken', groups="base.group_system")
|
||||
google_calendar_token = fields.Char(related='res_users_settings_id.google_calendar_token', groups="base.group_system")
|
||||
google_calendar_token_validity = fields.Datetime(related='res_users_settings_id.google_calendar_token_validity', groups="base.group_system")
|
||||
google_calendar_sync_token = fields.Char(related='res_users_settings_id.google_calendar_sync_token', groups="base.group_system")
|
||||
google_calendar_cal_id = fields.Char(related='res_users_settings_id.google_calendar_cal_id', groups="base.group_system")
|
||||
google_synchronization_stopped = fields.Boolean(related='res_users_settings_id.google_synchronization_stopped', readonly=False, groups="base.group_system")
|
||||
|
||||
def _get_google_calendar_token(self):
|
||||
self.ensure_one()
|
||||
if self.google_calendar_account_id.calendar_rtoken and not self.google_calendar_account_id._is_google_calendar_valid():
|
||||
self.sudo().google_calendar_account_id._refresh_google_calendar_token()
|
||||
return self.google_calendar_account_id.calendar_token
|
||||
if self.res_users_settings_id.sudo().google_calendar_rtoken and not self.res_users_settings_id._is_google_calendar_valid():
|
||||
self.sudo().res_users_settings_id._refresh_google_calendar_token()
|
||||
return self.res_users_settings_id.sudo().google_calendar_token
|
||||
|
||||
def _get_google_sync_status(self):
|
||||
""" Returns the calendar synchronization status (active, paused or stopped). """
|
||||
status = "sync_active"
|
||||
if str2bool(self.env['ir.config_parameter'].sudo().get_param("google_calendar_sync_paused"), default=False):
|
||||
status = "sync_paused"
|
||||
elif self.sudo().google_calendar_rtoken and not self.sudo().google_synchronization_stopped:
|
||||
status = "sync_active"
|
||||
elif self.sudo().google_synchronization_stopped:
|
||||
status = "sync_stopped"
|
||||
return status
|
||||
|
||||
def _check_pending_odoo_records(self):
|
||||
""" Returns True if sync is active and there are records to be synchronized to Google. """
|
||||
if self._get_google_sync_status() != "sync_active":
|
||||
return False
|
||||
pending_events = self.env['calendar.event']._check_any_records_to_sync()
|
||||
pending_recurrences = self.env['calendar.recurrence']._check_any_records_to_sync()
|
||||
return pending_events or pending_recurrences
|
||||
|
||||
def _sync_google_calendar(self, calendar_service: GoogleCalendarService):
|
||||
self.ensure_one()
|
||||
if self.google_synchronization_stopped:
|
||||
results = self._sync_request(calendar_service)
|
||||
if not results or (not results.get('events') and not self._check_pending_odoo_records()):
|
||||
return False
|
||||
|
||||
# don't attempt to sync when another sync is already in progress, as we wouldn't be
|
||||
# able to commit the transaction anyway (row is locked)
|
||||
self.env.cr.execute("""SELECT id FROM res_users WHERE id = %s FOR NO KEY UPDATE SKIP LOCKED""", [self.id])
|
||||
if not self.env.cr.rowcount:
|
||||
_logger.info("skipping calendar sync, locked user %s", self.login)
|
||||
return False
|
||||
|
||||
full_sync = not bool(self.google_calendar_sync_token)
|
||||
with google_calendar_token(self) as token:
|
||||
try:
|
||||
events, next_sync_token, default_reminders = calendar_service.get_events(self.google_calendar_account_id.calendar_sync_token, token=token)
|
||||
except InvalidSyncToken:
|
||||
events, next_sync_token, default_reminders = calendar_service.get_events(token=token)
|
||||
full_sync = True
|
||||
self.google_calendar_account_id.calendar_sync_token = next_sync_token
|
||||
|
||||
events, default_reminders, full_sync = results.values()
|
||||
# Google -> Odoo
|
||||
send_updates = not full_sync
|
||||
events.clear_type_ambiguity(self.env)
|
||||
|
|
@ -75,8 +68,8 @@ class User(models.Model):
|
|||
odoo_recurrences = self.env['calendar.recurrence'].browse(recurrences.odoo_ids(self.env))
|
||||
recurrences_write_dates = {r.id: r.write_date for r in odoo_recurrences}
|
||||
events_write_dates = {e.id: e.write_date for e in odoo_events}
|
||||
synced_recurrences = self.env['calendar.recurrence'].with_context(write_dates=recurrences_write_dates)._sync_google2odoo(recurrences)
|
||||
synced_events = self.env['calendar.event'].with_context(write_dates=events_write_dates)._sync_google2odoo(events - recurrences, default_reminders=default_reminders)
|
||||
synced_recurrences = self.env['calendar.recurrence']._sync_google2odoo(recurrences, recurrences_write_dates)
|
||||
synced_events = self.env['calendar.event']._sync_google2odoo(events - recurrences, events_write_dates, default_reminders=default_reminders)
|
||||
|
||||
# Odoo -> Google
|
||||
recurrences = self.env['calendar.recurrence']._get_records_to_sync(full_sync=full_sync)
|
||||
|
|
@ -87,12 +80,63 @@ class User(models.Model):
|
|||
events = self.env['calendar.event']._get_records_to_sync(full_sync=full_sync)
|
||||
(events - synced_events).with_context(send_updates=send_updates)._sync_odoo2google(calendar_service)
|
||||
|
||||
return bool(events | synced_events) or bool(recurrences | synced_recurrences)
|
||||
return bool(results) and (bool(events | synced_events) or bool(recurrences | synced_recurrences))
|
||||
|
||||
def _sync_single_event(self, calendar_service: GoogleCalendarService, odoo_event, event_id):
|
||||
self.ensure_one()
|
||||
results = self._sync_request(calendar_service, event_id)
|
||||
if not results or not results.get('events'):
|
||||
return False
|
||||
event, default_reminders, full_sync = results.values()
|
||||
# Google -> Odoo
|
||||
send_updates = not full_sync
|
||||
event.clear_type_ambiguity(self.env)
|
||||
synced_events = self.env['calendar.event']._sync_google2odoo(event, default_reminders=default_reminders)
|
||||
# Odoo -> Google
|
||||
odoo_event.with_context(send_updates=send_updates)._sync_odoo2google(calendar_service)
|
||||
return bool(odoo_event | synced_events)
|
||||
|
||||
def _sync_request(self, calendar_service, event_id=None):
|
||||
if self._get_google_sync_status() != "sync_active":
|
||||
return False
|
||||
# don't attempt to sync when another sync is already in progress, as we wouldn't be
|
||||
# able to commit the transaction anyway (row is locked)
|
||||
self.ensure_one()
|
||||
try:
|
||||
self.lock_for_update(allow_referencing=True)
|
||||
except LockError:
|
||||
_logger.info("skipping calendar sync, locked user %s", self.login)
|
||||
return False
|
||||
|
||||
full_sync = not bool(self.sudo().google_calendar_sync_token)
|
||||
with google_calendar_token(self) as token:
|
||||
try:
|
||||
if not event_id:
|
||||
events, next_sync_token, default_reminders = calendar_service.get_events(self.res_users_settings_id.sudo().google_calendar_sync_token, token=token)
|
||||
else:
|
||||
# We force the sync_token parameter to avoid doing a full sync.
|
||||
# Other events are fetched when the calendar view is displayed.
|
||||
events, next_sync_token, default_reminders = calendar_service.get_events(sync_token=token, token=token, event_id=event_id)
|
||||
except InvalidSyncToken:
|
||||
events, next_sync_token, default_reminders = calendar_service.get_events(token=token)
|
||||
full_sync = True
|
||||
if next_sync_token:
|
||||
self.res_users_settings_id.sudo().google_calendar_sync_token = next_sync_token
|
||||
return {
|
||||
'events': events,
|
||||
'default_reminders': default_reminders,
|
||||
'full_sync': full_sync,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _sync_all_google_calendar(self):
|
||||
""" Cron job """
|
||||
users = self.env['res.users'].search([('google_calendar_rtoken', '!=', False), ('google_synchronization_stopped', '=', False)])
|
||||
domain = [('google_calendar_rtoken', '!=', False), ('google_synchronization_stopped', '=', False)]
|
||||
# google_calendar_token_validity is not stored on res.users
|
||||
if not self:
|
||||
users = self.env['res.users'].sudo().search(domain).sorted('google_calendar_token_validity')
|
||||
else:
|
||||
users = self.filtered_domain(domain).sorted('google_calendar_token_validity')
|
||||
google = GoogleCalendarService(self.env['google.service'])
|
||||
for user in users:
|
||||
_logger.info("Calendar Synchro - Starting synchronization for %s", user)
|
||||
|
|
@ -100,17 +144,66 @@ class User(models.Model):
|
|||
user.with_user(user).sudo()._sync_google_calendar(google)
|
||||
self.env.cr.commit()
|
||||
except Exception as e:
|
||||
_logger.exception("[%s] Calendar Synchro - Exception : %s !", user, exception_to_unicode(e))
|
||||
_logger.exception("[%s] Calendar Synchro - Exception : %s!", user, exception_to_unicode(e))
|
||||
self.env.cr.rollback()
|
||||
|
||||
def is_google_calendar_synced(self):
|
||||
""" True if Google Calendar settings are filled (Client ID / Secret) and user calendar is synced
|
||||
meaning we can make API calls, false otherwise."""
|
||||
self.ensure_one()
|
||||
return self.sudo().google_calendar_token and self._get_google_sync_status() == 'sync_active'
|
||||
|
||||
def stop_google_synchronization(self):
|
||||
self.ensure_one()
|
||||
self.google_synchronization_stopped = True
|
||||
self.sudo().google_synchronization_stopped = True
|
||||
|
||||
def restart_google_synchronization(self):
|
||||
self.ensure_one()
|
||||
if not self.google_calendar_account_id:
|
||||
self.google_calendar_account_id = self.env['google.calendar.credentials'].sudo().create([{'user_ids': [Command.set(self.ids)]}])
|
||||
self.google_synchronization_stopped = False
|
||||
self.sudo().google_synchronization_stopped = False
|
||||
self.env['calendar.recurrence']._restart_google_sync()
|
||||
self.env['calendar.event']._restart_google_sync()
|
||||
|
||||
def unpause_google_synchronization(self):
|
||||
self.env['ir.config_parameter'].sudo().set_param("google_calendar_sync_paused", False)
|
||||
|
||||
def pause_google_synchronization(self):
|
||||
self.env['ir.config_parameter'].sudo().set_param("google_calendar_sync_paused", True)
|
||||
|
||||
@api.model
|
||||
def _has_setup_credentials(self):
|
||||
""" Checks if both Client ID and Client Secret are defined in the database. """
|
||||
ICP_sudo = self.env['ir.config_parameter'].sudo()
|
||||
client_id = self.env['google.service']._get_client_id('calendar')
|
||||
client_secret = google_service._get_client_secret(ICP_sudo, 'calendar')
|
||||
return bool(client_id and client_secret)
|
||||
|
||||
@api.model
|
||||
def check_calendar_credentials(self):
|
||||
res = super().check_calendar_credentials()
|
||||
res['google_calendar'] = self._has_setup_credentials()
|
||||
return res
|
||||
|
||||
def check_synchronization_status(self):
|
||||
res = super().check_synchronization_status()
|
||||
credentials_status = self.check_calendar_credentials()
|
||||
sync_status = 'missing_credentials'
|
||||
if credentials_status.get('google_calendar'):
|
||||
sync_status = self._get_google_sync_status()
|
||||
if sync_status == 'sync_active' and not self.sudo().google_calendar_rtoken:
|
||||
sync_status = 'sync_stopped'
|
||||
res['google_calendar'] = sync_status
|
||||
return res
|
||||
|
||||
def _has_any_active_synchronization(self):
|
||||
"""
|
||||
Check if synchronization is active for Google Calendar.
|
||||
This function retrieves the synchronization status from the user's environment
|
||||
and checks if the Google Calendar synchronization is active.
|
||||
|
||||
:return: Action to delete the event
|
||||
"""
|
||||
sync_status = self.check_synchronization_status()
|
||||
res = super()._has_any_active_synchronization()
|
||||
if sync_status.get('google_calendar') == 'sync_active':
|
||||
return True
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
_inherit = "res.users.settings"
|
||||
|
||||
# Google Calendar tokens and synchronization information.
|
||||
google_calendar_rtoken = fields.Char('Refresh Token', copy=False, groups='base.group_system')
|
||||
google_calendar_token = fields.Char('User token', copy=False, groups='base.group_system')
|
||||
google_calendar_token_validity = fields.Datetime('Token Validity', copy=False, groups='base.group_system')
|
||||
google_calendar_sync_token = fields.Char('Next Sync Token', copy=False, groups='base.group_system')
|
||||
google_calendar_cal_id = fields.Char('Calendar ID', copy=False, groups='base.group_system',
|
||||
help='Last Calendar ID who has been synchronized. If it is changed, we remove all links between GoogleID and Odoo Google Internal ID')
|
||||
google_synchronization_stopped = fields.Boolean('Google Synchronization stopped', copy=False, groups='base.group_system')
|
||||
|
||||
@api.model
|
||||
def _get_fields_blacklist(self):
|
||||
""" Get list of google fields that won't be formatted in session_info. """
|
||||
google_fields_blacklist = [
|
||||
'google_calendar_rtoken',
|
||||
'google_calendar_token',
|
||||
'google_calendar_token_validity',
|
||||
'google_calendar_sync_token',
|
||||
'google_calendar_cal_id',
|
||||
'google_synchronization_stopped'
|
||||
]
|
||||
return super()._get_fields_blacklist() + google_fields_blacklist
|
||||
|
||||
def _set_google_auth_tokens(self, access_token, refresh_token, ttl):
|
||||
self.sudo().write({
|
||||
'google_calendar_rtoken': refresh_token,
|
||||
'google_calendar_token': access_token,
|
||||
'google_calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl) if ttl else False,
|
||||
})
|
||||
|
||||
def _google_calendar_authenticated(self):
|
||||
self.ensure_one()
|
||||
return bool(self.sudo().google_calendar_rtoken)
|
||||
|
||||
def _is_google_calendar_valid(self):
|
||||
self.ensure_one()
|
||||
return self.sudo().google_calendar_token_validity and self.sudo().google_calendar_token_validity >= (fields.Datetime.now() + timedelta(minutes=1))
|
||||
|
||||
def _refresh_google_calendar_token(self):
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
access_token, ttl = self.env['google.service']._refresh_google_token('calendar', self.sudo().google_calendar_rtoken)
|
||||
self.sudo().write({
|
||||
'google_calendar_token': access_token,
|
||||
'google_calendar_token_validity': fields.Datetime.now() + timedelta(seconds=ttl),
|
||||
})
|
||||
except requests.HTTPError as error:
|
||||
if error.response.status_code in (400, 401): # invalid grant or invalid client
|
||||
# Delete refresh token and make sure it's commited
|
||||
self.env.cr.rollback()
|
||||
self.sudo()._set_google_auth_tokens(False, False, 0)
|
||||
self.env.cr.commit()
|
||||
error_key = error.response.json().get("error", "nc")
|
||||
error_msg = _("An error occurred while generating the token. Your authorization code may be invalid or has already expired [%s]. "
|
||||
"You should check your Client ID and secret on the Google APIs plateform or try to stop and restart your calendar synchronization.",
|
||||
error_key)
|
||||
raise UserError(error_msg)
|
||||
Loading…
Add table
Add a link
Reference in a new issue