Several imrovements have been made in google_calendar that affets the efficiency of the sync and the validity of the synched data This commit apply the more relevant modifications to this module. Taskid: 2484335 Part-of: odoo/odoo#68700
385 lines
18 KiB
Python
385 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
from contextlib import contextmanager
|
|
from functools import wraps
|
|
import requests
|
|
import pytz
|
|
from dateutil.parser import parse
|
|
|
|
from odoo import api, fields, models, registry, _
|
|
from odoo.tools import ormcache_context
|
|
from odoo.exceptions import UserError
|
|
from odoo.osv import expression
|
|
|
|
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
|
|
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
|
from odoo.addons.microsoft_account.models.microsoft_service import TIMEOUT
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
MAX_RECURRENT_EVENT = 720
|
|
|
|
|
|
# API requests are sent to Microsoft Calendar after the current transaction ends.
|
|
# This ensures changes are sent to Microsoft only if they really happened in the Odoo database.
|
|
# It is particularly important for event creation , otherwise the event might be created
|
|
# twice in Microsoft if the first creation crashed in Odoo.
|
|
def after_commit(func):
|
|
@wraps(func)
|
|
def wrapped(self, *args, **kwargs):
|
|
dbname = self.env.cr.dbname
|
|
context = self.env.context
|
|
uid = self.env.uid
|
|
|
|
@self.env.cr.postcommit.add
|
|
def called_after():
|
|
db_registry = registry(dbname)
|
|
with api.Environment.manage(), db_registry.cursor() as cr:
|
|
env = api.Environment(cr, uid, context)
|
|
try:
|
|
func(self.with_env(env), *args, **kwargs)
|
|
except Exception as e:
|
|
_logger.warning("Could not sync record now: %s" % self)
|
|
_logger.exception(e)
|
|
|
|
return wrapped
|
|
|
|
@contextmanager
|
|
def microsoft_calendar_token(user):
|
|
yield user._get_microsoft_calendar_token()
|
|
|
|
|
|
class MicrosoftSync(models.AbstractModel):
|
|
_name = 'microsoft.calendar.sync'
|
|
_description = "Synchronize a record with Microsoft Calendar"
|
|
|
|
microsoft_id = fields.Char('Microsoft Calendar Id', copy=False)
|
|
# This field helps to known when a microsoft event need to be resynced
|
|
need_sync_m = fields.Boolean(default=True, copy=False)
|
|
active = fields.Boolean(default=True)
|
|
|
|
def write(self, vals):
|
|
microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
|
|
if 'microsoft_id' in vals:
|
|
self._from_microsoft_ids.clear_cache(self)
|
|
synced_fields = self._get_microsoft_synced_fields()
|
|
if 'need_sync_m' not in vals and vals.keys() & synced_fields and not self.env.user.microsoft_synchronization_stopped:
|
|
fields_to_sync = [x for x in vals.keys() if x in synced_fields]
|
|
if fields_to_sync:
|
|
vals['need_sync_m'] = True
|
|
else:
|
|
fields_to_sync = [x for x in vals.keys() if x in synced_fields]
|
|
|
|
result = super().write(vals)
|
|
need_delete = 'active' in vals.keys() and not vals.get('active')
|
|
for record in self.filtered('need_sync_m'):
|
|
if need_delete and record.microsoft_id:
|
|
# We need to delete the event. Cancel is not sufficant. Errors may occurs
|
|
record._microsoft_delete(microsoft_service, record.microsoft_id, timeout=3)
|
|
elif record.microsoft_id and fields_to_sync:
|
|
values = record._microsoft_values(fields_to_sync)
|
|
if not values:
|
|
continue
|
|
record._microsoft_patch(microsoft_service, record.microsoft_id, values, timeout=3)
|
|
return result
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
if any(vals.get('microsoft_id') for vals in vals_list):
|
|
self._from_microsoft_ids.clear_cache(self)
|
|
if self.env.user.microsoft_synchronization_stopped:
|
|
for vals in vals_list:
|
|
vals.update({'need_sync_m': False})
|
|
records = super().create(vals_list)
|
|
|
|
microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
|
|
records_to_sync = records.filtered(lambda r: r.need_sync_m and r.active)
|
|
for record in records_to_sync:
|
|
record._microsoft_insert(microsoft_service, record._microsoft_values(self._get_microsoft_synced_fields()), timeout=3)
|
|
return records
|
|
|
|
def unlink(self):
|
|
synced = self.filtered('microsoft_id')
|
|
microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
|
|
for ev in synced:
|
|
ev._microsoft_delete(microsoft_service, ev.microsoft_id)
|
|
return super().unlink()
|
|
|
|
def _write_from_microsoft(self, microsoft_event, vals):
|
|
self.write(vals)
|
|
|
|
@api.model
|
|
def _create_from_microsoft(self, microsoft_event, vals_list):
|
|
return self.create(vals_list)
|
|
|
|
@api.model
|
|
@ormcache_context('microsoft_ids', keys=('active_test',))
|
|
def _from_microsoft_ids(self, microsoft_ids):
|
|
if not microsoft_ids:
|
|
return self.browse()
|
|
return self.search([('microsoft_id', 'in', microsoft_ids)])
|
|
|
|
def _sync_odoo2microsoft(self, microsoft_service: MicrosoftCalendarService):
|
|
if not self:
|
|
return
|
|
if self._active_name:
|
|
records_to_sync = self.filtered(self._active_name)
|
|
else:
|
|
records_to_sync = self
|
|
cancelled_records = self - records_to_sync
|
|
|
|
records_to_sync._ensure_attendees_have_email()
|
|
updated_records = records_to_sync.filtered('microsoft_id')
|
|
new_records = records_to_sync - updated_records
|
|
for record in cancelled_records.filtered('microsoft_id'):
|
|
record._microsoft_delete(microsoft_service, record.microsoft_id)
|
|
for record in new_records:
|
|
values = record._microsoft_values(self._get_microsoft_synced_fields())
|
|
if isinstance(values, dict):
|
|
record._microsoft_insert(microsoft_service, values)
|
|
else:
|
|
for value in values:
|
|
record._microsoft_insert(microsoft_service, value)
|
|
for record in updated_records.filtered('need_sync_m'):
|
|
values = record._microsoft_values(self._get_microsoft_synced_fields())
|
|
if not values:
|
|
continue
|
|
record._microsoft_patch(microsoft_service, record.microsoft_id, values)
|
|
|
|
def _cancel_microsoft(self):
|
|
self.microsoft_id = False
|
|
self.unlink()
|
|
|
|
def _sync_recurrence_microsoft2odoo(self, microsoft_events: MicrosoftEvent):
|
|
recurrent_masters = microsoft_events.filter(lambda e: e.is_recurrence())
|
|
recurrents = microsoft_events.filter(lambda e: e.is_recurrent_not_master())
|
|
default_values = {'need_sync_m': False}
|
|
|
|
new_recurrence = self.env['calendar.recurrence']
|
|
|
|
for recurrent_master in recurrent_masters:
|
|
new_calendar_recurrence = dict(self.env['calendar.recurrence']._microsoft_to_odoo_values(recurrent_master, (), default_values), need_sync_m=False)
|
|
to_create = recurrents.filter(lambda e: e.seriesMasterId == new_calendar_recurrence['microsoft_id'])
|
|
recurrents -= to_create
|
|
base_values = dict(self.env['calendar.event']._microsoft_to_odoo_values(recurrent_master, (), default_values), need_sync_m=False)
|
|
to_create_values = []
|
|
if new_calendar_recurrence.get('end_type', False) in ['count', 'forever']:
|
|
to_create = list(to_create)[:MAX_RECURRENT_EVENT]
|
|
for recurrent_event in to_create:
|
|
if recurrent_event.type == 'occurrence':
|
|
value = self.env['calendar.event']._microsoft_to_odoo_recurrence_values(recurrent_event, (), base_values)
|
|
else:
|
|
value = self.env['calendar.event']._microsoft_to_odoo_values(recurrent_event, (), default_values)
|
|
|
|
to_create_values += [dict(value, need_sync_m=False)]
|
|
|
|
new_calendar_recurrence['calendar_event_ids'] = [(0, 0, to_create_value) for to_create_value in to_create_values]
|
|
new_recurrence_odoo = self.env['calendar.recurrence'].create(new_calendar_recurrence)
|
|
new_recurrence_odoo.base_event_id = new_recurrence_odoo.calendar_event_ids[0] if new_recurrence_odoo.calendar_event_ids else False
|
|
new_recurrence |= new_recurrence_odoo
|
|
|
|
microsoft_ids = [x.seriesMasterId for x in recurrents]
|
|
recurrences = self.env['calendar.recurrence'].search([('microsoft_id', 'in', microsoft_ids)])
|
|
for recurrent_master_id in set([x.seriesMasterId for x in recurrents]):
|
|
recurrence_id = recurrences.filtered(lambda ev: ev.microsoft_id == recurrent_master_id)
|
|
to_update = recurrents.filter(lambda e: e.seriesMasterId == recurrent_master_id)
|
|
for recurrent_event in to_update:
|
|
if recurrent_event.type == 'occurrence':
|
|
value = self.env['calendar.event']._microsoft_to_odoo_recurrence_values(recurrent_event, (), {'need_sync_m': False})
|
|
else:
|
|
value = self.env['calendar.event']._microsoft_to_odoo_values(recurrent_event, (), default_values)
|
|
existing_event = recurrence_id.calendar_event_ids.filtered(lambda e: e._range() == (value['start'], value['stop']))
|
|
if not existing_event:
|
|
continue
|
|
value.pop('start')
|
|
value.pop('stop')
|
|
existing_event._write_from_microsoft(recurrent_event, value)
|
|
new_recurrence |= recurrence_id
|
|
return new_recurrence
|
|
|
|
def _update_microsoft_recurrence(self, recurrence_event, events):
|
|
vals = dict(self.base_event_id._microsoft_to_odoo_values(recurrence_event, ()), need_sync_m=False)
|
|
vals['microsoft_recurrence_master_id'] = vals.pop('microsoft_id')
|
|
self.base_event_id.write(vals)
|
|
values = {}
|
|
default_values = {}
|
|
|
|
normal_events = []
|
|
events_to_update = events.filter(lambda e: e.seriesMasterId == self.microsoft_id)
|
|
if self.end_type in ['count', 'forever']:
|
|
events_to_update = list(events_to_update)[:MAX_RECURRENT_EVENT]
|
|
|
|
for recurrent_event in events_to_update:
|
|
if recurrent_event.type == 'occurrence':
|
|
value = self.env['calendar.event']._microsoft_to_odoo_recurrence_values(recurrent_event, (), default_values)
|
|
normal_events += [recurrent_event.odoo_id(self.env)]
|
|
else:
|
|
value = self.env['calendar.event']._microsoft_to_odoo_values(recurrent_event, (), default_values)
|
|
self.env['calendar.event'].browse(recurrent_event.odoo_id(self.env)).with_context(no_mail_to_attendees=True, mail_create_nolog=True).write(dict(value, need_sync_m=False))
|
|
if value.get('start') and value.get('stop'):
|
|
values[(self.id, value.get('start'), value.get('stop'))] = dict(value, need_sync_m=False)
|
|
|
|
if (self.id, vals.get('start'), vals.get('stop')) in values:
|
|
base_event_vals = dict(vals)
|
|
base_event_vals.update(values[(self.id, vals.get('start'), vals.get('stop'))])
|
|
self.base_event_id.write(base_event_vals)
|
|
|
|
old_record = self._apply_recurrence(specific_values_creation=values, no_send_edit=True)
|
|
|
|
vals.pop('microsoft_id', None)
|
|
vals.pop('start', None)
|
|
vals.pop('stop', None)
|
|
normal_events = [e for e in normal_events if e in self.calendar_event_ids.ids]
|
|
normal_event_ids = self.env['calendar.event'].browse(normal_events) - old_record
|
|
if normal_event_ids:
|
|
vals['follow_recurrence'] = True
|
|
(self.env['calendar.event'].browse(normal_events) - old_record).write(vals)
|
|
|
|
old_record._cancel_microsoft()
|
|
if not self.base_event_id:
|
|
self.base_event_id = self._get_first_event(include_outliers=False)
|
|
|
|
@api.model
|
|
def _sync_microsoft2odoo(self, microsoft_events: MicrosoftEvent, default_reminders=()):
|
|
"""Synchronize Microsoft recurrences in Odoo. Creates new recurrences, updates
|
|
existing ones.
|
|
|
|
:return: synchronized odoo
|
|
"""
|
|
existing = microsoft_events.exists(self.env)
|
|
new = microsoft_events - existing - microsoft_events.cancelled()
|
|
new_recurrent = new.filter(lambda e: e.is_recurrent())
|
|
|
|
default_values = {}
|
|
|
|
odoo_values = [
|
|
dict(self._microsoft_to_odoo_values(e, default_reminders, default_values), need_sync_m=False)
|
|
for e in (new - new_recurrent)
|
|
]
|
|
new_odoo = self.with_context(dont_notify=True)._create_from_microsoft(new, odoo_values)
|
|
|
|
synced_recurrent_records = self._sync_recurrence_microsoft2odoo(new_recurrent)
|
|
|
|
cancelled = existing.cancelled()
|
|
cancelled_odoo = self.browse(cancelled.odoo_ids(self.env))
|
|
cancelled_odoo._cancel_microsoft()
|
|
|
|
recurrent_cancelled = self.env['calendar.recurrence'].search([
|
|
('microsoft_id', 'in', (microsoft_events.cancelled() - cancelled).microsoft_ids())])
|
|
recurrent_cancelled._cancel_microsoft()
|
|
|
|
synced_records = new_odoo + cancelled_odoo + synced_recurrent_records.calendar_event_ids
|
|
|
|
for mevent in (existing - cancelled).filter(lambda e: e.lastModifiedDateTime and not e.seriesMasterId):
|
|
# Last updated wins.
|
|
# This could be dangerous if microsoft server time and odoo server time are different
|
|
if mevent.is_recurrence():
|
|
odoo_record = self.env['calendar.recurrence'].browse(mevent.odoo_id(self.env))
|
|
else:
|
|
odoo_record = self.browse(mevent.odoo_id(self.env))
|
|
odoo_record_updated = pytz.utc.localize(odoo_record.write_date)
|
|
updated = parse(mevent.lastModifiedDateTime or str(odoo_record_updated))
|
|
if updated >= odoo_record_updated:
|
|
vals = dict(odoo_record._microsoft_to_odoo_values(mevent, default_reminders), need_sync_m=False)
|
|
odoo_record._write_from_microsoft(mevent, vals)
|
|
if odoo_record._name == 'calendar.recurrence':
|
|
odoo_record._update_microsoft_recurrence(mevent, microsoft_events)
|
|
synced_recurrent_records |= odoo_record
|
|
else:
|
|
synced_records |= odoo_record
|
|
|
|
return synced_records, synced_recurrent_records
|
|
|
|
@after_commit
|
|
def _microsoft_delete(self, microsoft_service: MicrosoftCalendarService, microsoft_id, timeout=TIMEOUT):
|
|
with microsoft_calendar_token(self.env.user.sudo()) as token:
|
|
if token:
|
|
microsoft_service.delete(microsoft_id, token=token, timeout=timeout)
|
|
|
|
@after_commit
|
|
def _microsoft_patch(self, microsoft_service: MicrosoftCalendarService, microsoft_id, values, timeout=TIMEOUT):
|
|
with microsoft_calendar_token(self.env.user.sudo()) as token:
|
|
if token:
|
|
self._ensure_attendees_have_email()
|
|
microsoft_service.patch(microsoft_id, values, token=token, timeout=timeout)
|
|
self.need_sync_m = False
|
|
|
|
@after_commit
|
|
def _microsoft_insert(self, microsoft_service: MicrosoftCalendarService, values, timeout=TIMEOUT):
|
|
if not values:
|
|
return
|
|
with microsoft_calendar_token(self.env.user.sudo()) as token:
|
|
if token:
|
|
self._ensure_attendees_have_email()
|
|
microsoft_id = microsoft_service.insert(values, token=token, timeout=timeout)
|
|
self.write({
|
|
'microsoft_id': microsoft_id,
|
|
'need_sync_m': False,
|
|
})
|
|
|
|
def _microsoft_attendee_answer(self, microsoft_service: MicrosoftCalendarService, microsoft_id, answer, params, timeout=TIMEOUT):
|
|
if not answer:
|
|
return
|
|
with microsoft_calendar_token(self.env.user.sudo()) as token:
|
|
if token:
|
|
self._ensure_attendees_have_email()
|
|
microsoft_service.answer(microsoft_id, answer, params, token=token, timeout=timeout)
|
|
self.write({
|
|
'need_sync_m': False,
|
|
})
|
|
|
|
def _get_microsoft_records_to_sync(self, full_sync=False):
|
|
"""Return records that should be synced from Odoo to Microsoft
|
|
|
|
:param full_sync: If True, all events attended by the user are returned
|
|
:return: events
|
|
"""
|
|
domain = self._get_microsoft_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, [
|
|
'|',
|
|
'&', ('microsoft_id', '=', False), is_active_clause,
|
|
('need_sync_m', '=', True),
|
|
]])
|
|
return self.with_context(active_test=False).search(domain)
|
|
|
|
@api.model
|
|
def _microsoft_to_odoo_values(self, microsoft_event: MicrosoftEvent, default_reminders=()):
|
|
"""Implements this method to return a dict of Odoo values corresponding
|
|
to the Microsoft event given as parameter
|
|
:return: dict of Odoo formatted values
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _microsoft_values(self, fields_to_sync):
|
|
"""Implements this method to return a dict with values formatted
|
|
according to the Microsoft Calendar API
|
|
:return: dict of Microsoft formatted values
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _ensure_attendees_have_email(self):
|
|
raise NotImplementedError()
|
|
|
|
def _get_microsoft_sync_domain(self):
|
|
"""Return a domain used to search records to synchronize.
|
|
e.g. return a domain to synchronize records owned by the current user.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def _get_microsoft_synced_fields(self):
|
|
"""Return a set of field names. Changing one of these fields
|
|
marks the record to be re-synchronized.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@api.model
|
|
def _restart_microsoft_sync(self):
|
|
""" Turns on the microsoft synchronization for all the events of
|
|
a given user.
|
|
"""
|
|
raise NotImplementedError()
|