[IMP] microsoft_calendar: apply the google_calendar improvements into this module

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
This commit is contained in:
Arnaud Joset
2021-08-26 10:21:02 +00:00
parent 40fd44f618
commit f7d16b4599
8 changed files with 136 additions and 75 deletions
+32 -7
View File
@@ -45,8 +45,9 @@ class Meeting(models.Model):
@api.model_create_multi
def create(self, vals_list):
return super().create([
dict(vals, need_sync_m=False) if vals.get('recurrency') else vals
notify_context = self.env.context.get('dont_notify', False)
return super(Meeting, self.with_context(dont_notify=notify_context)).create([
dict(vals, need_sync_m=False) if vals.get('recurrence_id') or vals.get('recurrency') else vals
for vals in vals_list
])
@@ -61,10 +62,26 @@ class Meeting(models.Model):
if previous_event_before_write != previous_event_after_write:
# Outlook returns a 400 error if you try to synchronize an occurrence of this type.
raise UserError(_("Modified occurrence is crossing or overlapping adjacent occurrence."))
return super().write(values)
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_microsoft_synced_fields():
self.recurrence_id.need_sync_m = True
return res
def _get_microsoft_sync_domain(self):
return [('partner_ids.user_ids', 'in', self.env.user.id)]
# 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 _microsoft_to_odoo_values(self, microsoft_event, default_reminders=(), default_values={}):
@@ -93,13 +110,20 @@ class Meeting(models.Model):
'user_id': microsoft_event.owner(self.env).id,
'privacy': sensitivity_o2m.get(microsoft_event.sensitivity, self.default_get(['privacy'])['privacy']),
'attendee_ids': commands_attendee,
'partner_ids': commands_partner,
'allday': microsoft_event.isAllDay,
'start': start,
'stop': stop,
'show_as': 'free' if microsoft_event.showAs == 'free' else 'busy',
'recurrency': microsoft_event.is_recurrent()
}
if commands_partner:
# Add partner_commands only if set from Microsoft. The write method on calendar_events will
# override attendee commands if the partner_ids command is set but empty.
values['partner_ids'] = commands_partner
if microsoft_event.is_recurrent() and not microsoft_event.is_recurrence():
# Propagate the follow_recurrence according to the google result
values['follow_recurrence'] = not microsoft_event.is_recurrence_outlier()
values['microsoft_id'] = microsoft_event.id
if microsoft_event.is_recurrent():
@@ -306,7 +330,7 @@ class Meeting(models.Model):
pattern['type'] = recurrence.rrule_type
else:
prefix = 'absolute' if recurrence.month_by == 'date' else 'relative'
pattern['type'] = prefix + recurrence.rrule_type.capitalize()
pattern['type'] = recurrence.rrule_type and prefix + recurrence.rrule_type.capitalize()
if recurrence.month_by == 'date':
pattern['dayOfMonth'] = recurrence.day
@@ -334,8 +358,9 @@ class Meeting(models.Model):
}
pattern['index'] = byday_selection[recurrence.byday]
dtstart = recurrence.dtstart or fields.Datetime.now()
rule_range = {
'startDate': (recurrence.dtstart.date()).isoformat()
'startDate': (dtstart.date()).isoformat()
}
if recurrence.end_type == 'count': # e.g. stop after X occurence
@@ -11,17 +11,6 @@ class Attendee(models.Model):
_name = 'calendar.attendee'
_inherit = 'calendar.attendee'
def write(self, vals):
"""
Tell to resync the link microsoft/odoo of the event when the status of an attendee is modified
"""
res = super().write(vals)
if vals.get('state'):
# When the state is changed, the corresponding event must be sync with microsoft
microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
self.event_id.filtered('microsoft_id')._sync_odoo2microsoft(microsoft_service)
return res
def _send_mail_to_attendees(self, mail_template, force_send=False):
""" Override the super method
If not synced with Microsoft Outlook, let Odoo in charge of sending emails
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from odoo import api, fields, models
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
@@ -18,8 +16,20 @@ class RecurrenceRule(models.Model):
microsoft_id = fields.Char('Microsoft Calendar Recurrence Id')
def _compute_rrule(self):
for recurrence in self:
if recurrence.rrule != recurrence._rrule_serialize():
recurrence.write({'rrule': recurrence._rrule_serialize()})
def _inverse_rrule(self):
for recurrence in self.filtered('rrule'):
values = self._rrule_parse(recurrence.rrule, recurrence.dtstart)
# We avoid to trigger patch in the inverse
values.update({'need_sync_m': False})
recurrence.write(values)
def _apply_recurrence(self, specific_values_creation=None, no_send_edit=False):
events = self.calendar_event_ids
events = self.filtered('need_sync_m').calendar_event_ids
detached_events = super()._apply_recurrence(specific_values_creation, no_send_edit)
microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
@@ -44,18 +54,6 @@ class RecurrenceRule(models.Model):
event._microsoft_delete(microsoft_service, event.microsoft_id)
event.microsoft_id = False
self.env['calendar.event'].create(vals)
if not no_send_edit:
for recurrence in self:
values = recurrence._microsoft_values(self._get_microsoft_synced_fields())
if not values:
continue
values = values[0]
if not recurrence.microsoft_id:
recurrence._microsoft_insert(microsoft_service, values)
else:
recurrence._microsoft_patch(microsoft_service, recurrence.microsoft_id, values)
self.calendar_event_ids.need_sync_m = False
return detached_events
@@ -63,10 +61,13 @@ class RecurrenceRule(models.Model):
# If only some events are updated, sync those events.
# If all events are updated, sync the recurrence instead.
values['need_sync_m'] = bool(dtstart)
if not dtstart:
self.need_sync_m = True
return super()._write_events(values, dtstart=dtstart)
def _get_rrule(self, dtstart=None):
if not dtstart and self.dtstart:
dtstart = self.dtstart
return super()._get_rrule(dtstart)
def _get_microsoft_synced_fields(self):
return {'rrule'} | self.env['calendar.event']._get_microsoft_synced_fields()
@@ -76,13 +77,56 @@ class RecurrenceRule(models.Model):
'need_sync_m': True,
})
def _write_from_microsoft(self, microsoft_event, vals):
current_rrule = self.rrule
# event_tz is written on event in Microsoft but on recurrence in Odoo
vals['event_tz'] = microsoft_event.start.get('timeZone')
super()._write_from_microsoft(microsoft_event, vals)
base_event_time_fields = ['start', 'stop', 'allday']
new_event_values = self.env["calendar.event"]._microsoft_to_odoo_values(microsoft_event)
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):
# we need to recreate the recurrence, time_fields were modified.
base_event_id = self.base_event_id
# We archive the old events to recompute the recurrence. These events are already deleted on Microsoft side.
# We can't call _cancel because events without user_id would not be deleted
(self.calendar_event_ids - base_event_id).microsoft_id = False
(self.calendar_event_ids - base_event_id).unlink()
base_event_id.with_context(dont_notify=True).write(dict(new_event_values, microsoft_id=False, need_sync_m=False))
if self.rrule == current_rrule:
# if the rrule has changed, it will be recalculated below
# There is no detached event now
self.with_context(dont_notify=True)._apply_recurrence()
else:
time_fields = (
self.env["calendar.event"]._get_time_fields()
| self.env["calendar.event"]._get_recurrent_fields()
)
# We avoid to write time_fields because they are not shared between events.
self._write_events(dict({
field: value
for field, value in new_event_values.items()
if field not in time_fields
}, need_sync_m=False)
)
# We apply the rrule check after the time_field check because the microsoft_id are generated according
# to base_event start datetime.
if self.rrule != current_rrule:
detached_events = self._apply_recurrence()
detached_events.microsoft_id = False
detached_events.unlink()
def _get_microsoft_sync_domain(self):
return [('calendar_event_ids.user_id', '=', self.env.user.id)]
# Empty rrule may exists in historical data. It is not a desired behavior but it could have been created with
# older versions of the module. When synced, these recurrence may come back from Microsoft 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)]
def _cancel_microsoft(self):
self.calendar_event_ids._cancel_microsoft()
self.microsoft_id = False # Set to False to avoid error with unlink from microsoft and avoid to synchronize.
self.unlink()
super()._cancel_microsoft()
@api.model
def _microsoft_to_odoo_values(self, microsoft_recurrence, default_reminders=(), default_values={}):
@@ -94,19 +138,21 @@ class RecurrenceRule(models.Model):
}
def _microsoft_values(self, fields_to_sync):
events = self.calendar_event_ids.sorted('start')
events_outliers = self.calendar_event_ids.filtered(lambda e: not e.follow_recurrence)
events = self.calendar_event_ids.sorted('start')
normal_event = (events - events_outliers)[:1] or events[:1]
if not normal_event:
return {}
values = [normal_event._microsoft_values(fields_to_sync, initial_values={'type': 'seriesMaster'})]
values = normal_event._microsoft_values(fields_to_sync, initial_values={'type': 'seriesMaster'})
if self.microsoft_id:
values[0]['id'] = self.microsoft_id
for event in events_outliers:
event_value = event._microsoft_values(fields_to_sync)
values += [event_value]
values['id'] = self.microsoft_id
if events_outliers:
# We send the data as a list. If we directly send a list of values, we have issues...
values = [values]
for event in events_outliers:
event_value = event._microsoft_values(fields_to_sync)
values += [event_value]
return values
def _ensure_attendees_have_email(self):
@@ -73,13 +73,16 @@ class MicrosoftSync(models.AbstractModel):
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 record.microsoft_id and fields_to_sync:
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
@@ -97,23 +100,20 @@ class MicrosoftSync(models.AbstractModel):
record._microsoft_insert(microsoft_service, record._microsoft_values(self._get_microsoft_synced_fields()), timeout=3)
return records
@api.ondelete(at_uninstall=False)
def _unlink_except_synchronized(self):
if self.env.context.get('archive_on_error') and self._active_name:
return
if self.filtered('microsoft_id'):
raise UserError(_("You cannot delete a record synchronized with Microsoft Calendar, archive it instead."))
def unlink(self):
"""We can't delete an event that is also in Microsoft Calendar. Otherwise we would
have no clue that the event must must deleted from Microsoft Calendar at the next sync.
"""
synced = self.filtered('microsoft_id')
if self.env.context.get('archive_on_error') and self._active_name:
synced.write({self._active_name: False})
self = self - synced
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):
@@ -142,7 +142,7 @@ class MicrosoftSync(models.AbstractModel):
else:
for value in values:
record._microsoft_insert(microsoft_service, value)
for record in updated_records:
for record in updated_records.filtered('need_sync_m'):
values = record._microsoft_values(self._get_microsoft_synced_fields())
if not values:
continue
@@ -180,8 +180,10 @@ class MicrosoftSync(models.AbstractModel):
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 = self.env['calendar.recurrence'].search([('microsoft_id', '=', recurrent_master_id)])
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':
@@ -193,7 +195,7 @@ class MicrosoftSync(models.AbstractModel):
continue
value.pop('start')
value.pop('stop')
existing_event.write(value)
existing_event._write_from_microsoft(recurrent_event, value)
new_recurrence |= recurrence_id
return new_recurrence
@@ -256,7 +258,7 @@ class MicrosoftSync(models.AbstractModel):
dict(self._microsoft_to_odoo_values(e, default_reminders, default_values), need_sync_m=False)
for e in (new - new_recurrent)
]
new_odoo = self.create(odoo_values)
new_odoo = self.with_context(dont_notify=True)._create_from_microsoft(new, odoo_values)
synced_recurrent_records = self._sync_recurrence_microsoft2odoo(new_recurrent)
@@ -281,7 +283,7 @@ class MicrosoftSync(models.AbstractModel):
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(vals)
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
@@ -265,8 +265,6 @@ const MicrosoftCalendarRenderer = CalendarRenderer.include({
var self = this;
var context = this.getSession().user_context;
this.$microsoftButton.prop('disabled', true);
this._getMicrosoftStopButton();
this.$microsoftButton.hide();
this.trigger_up('syncMicrosoftCalendar', {
on_always: function () {
self.$microsoftButton.prop('disabled', false);
@@ -69,11 +69,12 @@ class TestSyncOdoo2Microsoft(TransactionCase):
'partner_id': partner.id,
})
user.stop_microsoft_synchronization()
# In case of full sync, limit to a range of 1y in past and 1y in the future by default
event = self.env['calendar.event'].with_user(user).create({
'microsoft_id': microsoft_id,
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'start': datetime(2021, 1, 15, 8, 0),
'stop': datetime(2021, 1, 15, 18, 0),
'partner_ids': [(4, partner.id)],
})
@@ -82,8 +83,8 @@ class TestSyncOdoo2Microsoft(TransactionCase):
microsoft_guid = self.env['ir.config_parameter'].sudo().get_param('microsoft_calendar.microsoft_guid', False)
self.assertMicrosoftEventPatched(event.microsoft_id, {
'id': event.microsoft_id,
'start': {'dateTime': '2020-01-15T08:00:00+00:00', 'timeZone': 'Europe/London'},
'end': {'dateTime': '2020-01-15T18:00:00+00:00', 'timeZone': 'Europe/London'},
'start': {'dateTime': '2021-01-15T08:00:00+00:00', 'timeZone': 'Europe/London'},
'end': {'dateTime': '2021-01-15T18:00:00+00:00', 'timeZone': 'Europe/London'},
'subject': 'Event',
'body': {'content': '', 'contentType': 'text'},
'attendees': [],
@@ -7,7 +7,6 @@ import logging
from werkzeug import urls
from odoo import api, _
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_account.models.microsoft_service import TIMEOUT
@@ -66,14 +65,14 @@ class MicrosoftCalendarService():
headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
if not values.get('id'):
values.pop('id', None)
status, data, time = self.microsoft_service._do_request(url, json.dumps(values), headers, method='POST', timeout=timeout)
dummy, data, dummy = self.microsoft_service._do_request(url, json.dumps(values, separators=(',', ':')), headers, method='POST', timeout=timeout)
return data['id']
@requires_auth_token
def patch(self, event_id, values, token=None, timeout=TIMEOUT):
url = "/v1.0/me/calendar/events/%s" % event_id
headers = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % token}
self.microsoft_service._do_request(url, json.dumps(values), headers, method='PATCH', timeout=timeout)
self.microsoft_service._do_request(url, json.dumps(values, separators=(',', ':')), headers, method='PATCH', timeout=timeout)
@requires_auth_token
def delete(self, event_id, token=None, timeout=TIMEOUT):
@@ -191,7 +191,8 @@ class MicrosoftEvent(abc.Set):
if month_by:
result['month_by'] = month_by
week_days = [x[:2] for x in pattern.get('daysOfWeek', [])]
# daysOfWeek contains the full name of the day, the fields contain the first 3 letters (mon, tue, etc)
week_days = [x[:3] for x in pattern.get('daysOfWeek', [])]
for week_day in ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']:
result[week_day] = week_day in week_days
if week_days: