[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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user