mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-23 01:12:06 +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,5 +5,6 @@ from . import microsoft_sync
|
|||
from . import calendar
|
||||
from . import calendar_recurrence_rule
|
||||
from . import res_users
|
||||
from . import res_users_settings
|
||||
from . import calendar_attendee
|
||||
from . import calendar_alarm_manager
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import pytz
|
||||
import re
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil.parser import parse
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import is_html_empty, email_normalize
|
||||
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools import email_normalize
|
||||
|
||||
ATTENDEE_CONVERTER_O2M = {
|
||||
'needsAction': 'notresponded',
|
||||
|
|
@ -36,12 +33,11 @@ MAX_RECURRENT_EVENT = 720
|
|||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Meeting(models.Model):
|
||||
|
||||
class CalendarEvent(models.Model):
|
||||
_name = 'calendar.event'
|
||||
_inherit = ['calendar.event', 'microsoft.calendar.sync']
|
||||
|
||||
# contains organizer event id and universal event id separated by a ':'
|
||||
microsoft_id = fields.Char('Microsoft Calendar Event Id')
|
||||
microsoft_recurrence_master_id = fields.Char('Microsoft Recurrence Master Id')
|
||||
|
||||
def _get_organizer(self):
|
||||
|
|
@ -51,11 +47,13 @@ class Meeting(models.Model):
|
|||
def _get_microsoft_synced_fields(self):
|
||||
return {'name', 'description', 'allday', 'start', 'date_end', 'stop',
|
||||
'user_id', 'privacy',
|
||||
'attendee_ids', 'alarm_ids', 'location', 'show_as', 'active'}
|
||||
'attendee_ids', 'alarm_ids', 'location', 'show_as', 'active', 'videocall_location'}
|
||||
|
||||
@api.model
|
||||
def _restart_microsoft_sync(self):
|
||||
self.env['calendar.event'].with_context(dont_notify=True).search(self._get_microsoft_sync_domain()).write({
|
||||
domain = self._get_microsoft_sync_domain()
|
||||
|
||||
self.env['calendar.event'].with_context(dont_notify=True).search(domain).write({
|
||||
'need_sync_m': True,
|
||||
})
|
||||
|
||||
|
|
@ -65,7 +63,14 @@ class Meeting(models.Model):
|
|||
The 'microsoft_synchronization_stopped' variable needs to be 'False' and Outlook account must be connected.
|
||||
"""
|
||||
outlook_connected = self.env.user._get_microsoft_calendar_token()
|
||||
return outlook_connected and self.env.user.microsoft_synchronization_stopped is False
|
||||
return outlook_connected and self.env.user.sudo().microsoft_synchronization_stopped is False
|
||||
|
||||
def _skip_send_mail_status_update(self):
|
||||
"""If microsoft calendar is not syncing, don't send a mail."""
|
||||
user_id = self._get_event_user_m()
|
||||
if self.with_user(user_id)._check_microsoft_sync_status() and user_id._get_microsoft_sync_status() == "sync_active":
|
||||
return self.microsoft_id or self.need_sync_m
|
||||
return super()._skip_send_mail_status_update()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
|
|
@ -76,7 +81,8 @@ class Meeting(models.Model):
|
|||
if self._check_microsoft_sync_status() and not notify_context and recurrency_in_batch:
|
||||
self._forbid_recurrence_creation()
|
||||
|
||||
for vals in vals_list:
|
||||
vals_check_organizer = self._check_organizer_validation_conditions(vals_list)
|
||||
for vals in [vals for vals, check_organizer in zip(vals_list, vals_check_organizer) if check_organizer]:
|
||||
# If event has a different organizer, check its sync status and verify if the user is listed as attendee.
|
||||
sender_user, partner_ids = self._get_organizer_user_change_info(vals)
|
||||
partner_included = partner_ids and len(partner_ids) > 0 and sender_user.partner_id.id in partner_ids
|
||||
|
|
@ -85,7 +91,7 @@ class Meeting(models.Model):
|
|||
# for a recurrent event, we do not create events separately but we directly
|
||||
# create the recurrency from the corresponding calendar.recurrence.
|
||||
# That's why, events from a recurrency have their `need_sync_m` attribute set to False.
|
||||
return super(Meeting, self.with_context(dont_notify=notify_context)).create([
|
||||
return super(CalendarEvent, 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
|
||||
])
|
||||
|
|
@ -144,7 +150,7 @@ class Meeting(models.Model):
|
|||
Suggest user to update recurrences in Outlook due to the Outlook Calendar spam limitation.
|
||||
"""
|
||||
error_msg = _("Due to an Outlook Calendar limitation, recurrence updates must be done directly in Outlook Calendar.")
|
||||
if any(not record.microsoft_id for record in self):
|
||||
if any(not record.ms_universal_event_id for record in self):
|
||||
# If any event is not synced, suggest deleting it in Odoo and recreating it in Outlook.
|
||||
error_msg = _(
|
||||
"Due to an Outlook Calendar limitation, recurrence updates must be done directly in Outlook Calendar.\n"
|
||||
|
|
@ -158,39 +164,51 @@ class Meeting(models.Model):
|
|||
"""
|
||||
raise UserError(_("Due to an Outlook Calendar limitation, recurrent events must be created directly in Outlook Calendar."))
|
||||
|
||||
def write(self, values):
|
||||
def write(self, vals):
|
||||
values = vals
|
||||
recurrence_update_setting = values.get('recurrence_update')
|
||||
notify_context = self.env.context.get('dont_notify', False)
|
||||
|
||||
# Forbid recurrence updates through Odoo and suggest user to update it in Outlook.
|
||||
if self._check_microsoft_sync_status():
|
||||
if not notify_context:
|
||||
recurrency_in_batch = self.filtered(lambda ev: ev.recurrency)
|
||||
recurrence_update_attempt = recurrence_update_setting or 'recurrency' in values or recurrency_in_batch and len(recurrency_in_batch) > 0
|
||||
if not notify_context and recurrence_update_attempt and not 'active' in values:
|
||||
self._forbid_recurrence_update()
|
||||
# Check if this is an Outlook recurring event with active sync
|
||||
if recurrence_update_attempt and 'active' not in values:
|
||||
recurring_events = self.filtered('microsoft_recurrence_master_id')
|
||||
if recurring_events and any(
|
||||
event.with_user(organizer)._check_microsoft_sync_status()
|
||||
for event in recurring_events
|
||||
if (organizer := event._get_organizer())
|
||||
):
|
||||
self._forbid_recurrence_update()
|
||||
|
||||
# When changing the organizer, check its sync status and verify if the user is listed as attendee.
|
||||
# Updates from Microsoft must skip this check since changing the organizer on their side is not possible.
|
||||
change_from_microsoft = self.env.context.get('dont_notify', False)
|
||||
deactivated_events_ids = []
|
||||
new_user_id = values.get('user_id')
|
||||
for event in self:
|
||||
if values.get('user_id') and event.user_id.id != values['user_id'] and not change_from_microsoft:
|
||||
if new_user_id and event.user_id.id != new_user_id and not change_from_microsoft and event.microsoft_id:
|
||||
sender_user, partner_ids = event._get_organizer_user_change_info(values)
|
||||
partner_included = sender_user.partner_id in event.attendee_ids.partner_id or sender_user.partner_id.id in partner_ids
|
||||
event._check_organizer_validation(sender_user, partner_included)
|
||||
event._recreate_event_different_organizer(values, sender_user)
|
||||
deactivated_events_ids.append(event.id)
|
||||
if event.microsoft_id:
|
||||
event._recreate_event_different_organizer(values, sender_user)
|
||||
deactivated_events_ids.append(event.id)
|
||||
|
||||
# check a Outlook limitation in overlapping the actual recurrence
|
||||
if recurrence_update_setting == 'self_only' and 'start' in values:
|
||||
self._check_recurrence_overlapping(values['start'])
|
||||
|
||||
# if a single event becomes the base event of a recurrency, it should be first
|
||||
# removed from the Outlook calendar.
|
||||
if 'recurrency' in values and values['recurrency']:
|
||||
for e in self.filtered(lambda e: not e.recurrency and not e.recurrence_id):
|
||||
e._microsoft_delete(e._get_organizer(), e.ms_organizer_event_id, timeout=3)
|
||||
e.microsoft_id = False
|
||||
# removed from the Outlook calendar. Additionaly, checks if synchronization is not paused.
|
||||
if self.env.user._get_microsoft_sync_status() != "sync_paused" and values.get('recurrency'):
|
||||
for event in self:
|
||||
if not event.recurrency and not event.recurrence_id:
|
||||
event._microsoft_delete(event._get_organizer(), event.microsoft_id, timeout=3)
|
||||
event.microsoft_id = False
|
||||
event.ms_universal_event_id = False
|
||||
|
||||
deactivated_events = self.browse(deactivated_events_ids)
|
||||
# Update attendee status before 'values' variable is overridden in super.
|
||||
|
|
@ -198,11 +216,11 @@ class Meeting(models.Model):
|
|||
if attendee_ids and values.get('partner_ids'):
|
||||
(self - deactivated_events)._update_attendee_status(attendee_ids)
|
||||
|
||||
res = super(Meeting, (self - deactivated_events).with_context(dont_notify=notify_context)).write(values)
|
||||
res = super(CalendarEvent, (self - deactivated_events).with_context(dont_notify=notify_context)).write(values)
|
||||
|
||||
# Deactivate events that were recreated after changing organizer.
|
||||
if deactivated_events:
|
||||
res |= super(Meeting, deactivated_events.with_context(dont_notify=notify_context)).write({**values, 'active': False})
|
||||
res |= super(CalendarEvent, deactivated_events.with_context(dont_notify=notify_context)).write({**values, 'active': False})
|
||||
|
||||
if recurrence_update_setting in ('all_events',) and len(self) == 1 \
|
||||
and values.keys() & self._get_microsoft_synced_fields():
|
||||
|
|
@ -223,14 +241,18 @@ class Meeting(models.Model):
|
|||
""" Copy current event values, delete it and recreate it with the new organizer user. """
|
||||
self.ensure_one()
|
||||
event_copy = {**self.copy_data()[0], 'microsoft_id': False}
|
||||
self.env['calendar.event'].with_user(sender_user).create({**event_copy, **values})
|
||||
self.env['calendar.event'].with_user(sender_user).with_context(skip_contact_description=True).create(
|
||||
{**event_copy, **values},
|
||||
)
|
||||
if self.ms_universal_event_id:
|
||||
self._microsoft_delete(self._get_organizer(), self.ms_organizer_event_id)
|
||||
self._microsoft_delete(self._get_organizer(), self.microsoft_id)
|
||||
|
||||
@api.model
|
||||
def _get_organizer_user_change_info(self, values):
|
||||
""" Return the sender user of the event and the partner ids listed on the event values. """
|
||||
sender_user_id = values.get('user_id', self.env.user.id)
|
||||
sender_user_id = values.get('user_id')
|
||||
if not sender_user_id:
|
||||
sender_user_id = self.env.user.id
|
||||
sender_user = self.env['res.users'].browse(sender_user_id)
|
||||
attendee_values = self._attendees_values(values['partner_ids']) if 'partner_ids' in values else []
|
||||
partner_ids = []
|
||||
|
|
@ -272,17 +294,17 @@ class Meeting(models.Model):
|
|||
custom_lower_bound_range = ICP.get_param('microsoft_calendar.sync.lower_bound_range')
|
||||
if custom_lower_bound_range:
|
||||
lower_bound = fields.Datetime.subtract(fields.Datetime.now(), days=int(custom_lower_bound_range))
|
||||
domain = [
|
||||
('partner_ids.user_ids', 'in', self.env.user.id),
|
||||
domain = Domain([
|
||||
('partner_ids.user_ids', 'in', [self.env.user.id]),
|
||||
('stop', '>', lower_bound),
|
||||
('start', '<', upper_bound),
|
||||
'!', '&', '&', ('recurrency', '=', True), ('recurrence_id', '!=', False), ('follow_recurrence', '=', True)
|
||||
]
|
||||
])
|
||||
|
||||
# Synchronize events that were created after the first synchronization date, when applicable.
|
||||
first_synchronization_date = ICP.get_param('microsoft_calendar.sync.first_synchronization_date')
|
||||
if first_synchronization_date:
|
||||
domain = expression.AND([domain, [('create_date', '>=', first_synchronization_date)]])
|
||||
domain &= Domain('create_date', '>=', first_synchronization_date)
|
||||
|
||||
return self._extend_microsoft_domain(domain)
|
||||
|
||||
|
|
@ -312,7 +334,7 @@ class Meeting(models.Model):
|
|||
'description': microsoft_event.body and microsoft_event.body['content'],
|
||||
'location': microsoft_event.location and microsoft_event.location.get('displayName') or False,
|
||||
'user_id': microsoft_event.owner_id(self.env),
|
||||
'privacy': sensitivity_o2m.get(microsoft_event.sensitivity, self.default_get(['privacy'])['privacy']),
|
||||
'privacy': sensitivity_o2m.get(microsoft_event.sensitivity, False),
|
||||
'attendee_ids': commands_attendee,
|
||||
'allday': microsoft_event.isAllDay,
|
||||
'start': start,
|
||||
|
|
@ -340,7 +362,9 @@ class Meeting(models.Model):
|
|||
values['location'] = False
|
||||
|
||||
if with_ids:
|
||||
values['microsoft_id'] = combine_ids(microsoft_event.id, microsoft_event.iCalUId)
|
||||
values['microsoft_id'] = microsoft_event.id
|
||||
values['ms_universal_event_id'] = microsoft_event.iCalUId
|
||||
|
||||
|
||||
if microsoft_event.is_recurrent():
|
||||
values['microsoft_recurrence_master_id'] = microsoft_event.seriesMasterId
|
||||
|
|
@ -362,7 +386,8 @@ class Meeting(models.Model):
|
|||
stop = parse(microsoft_event.end.get('dateTime')).astimezone(timeZone_stop).replace(tzinfo=None)
|
||||
values = default_values or {}
|
||||
values.update({
|
||||
'microsoft_id': combine_ids(microsoft_event.id, microsoft_event.iCalUId),
|
||||
'microsoft_id': microsoft_event.id,
|
||||
'ms_universal_event_id': microsoft_event.iCalUId,
|
||||
'microsoft_recurrence_master_id': microsoft_event.seriesMasterId,
|
||||
'start': start,
|
||||
'stop': stop,
|
||||
|
|
@ -388,9 +413,11 @@ class Meeting(models.Model):
|
|||
elif self.env.user.partner_id.email not in emails:
|
||||
commands_attendee += [(0, 0, {'state': 'accepted', 'partner_id': self.env.user.partner_id.id})]
|
||||
commands_partner += [(4, self.env.user.partner_id.id)]
|
||||
partners = self.env['mail.thread']._mail_find_partner_from_emails(emails, records=self, force_create=True)
|
||||
partners = self.env['mail.thread']._partner_find_from_emails_single(emails, no_create=False)
|
||||
attendees_by_emails = {a.email: a for a in existing_attendees}
|
||||
for email, partner, attendee_info in zip(emails, partners, microsoft_attendees):
|
||||
partners_by_emails = {p.email_normalized: p for p in partners}
|
||||
for email, attendee_info in zip(emails, microsoft_attendees):
|
||||
partner = partners_by_emails.get(email_normalize(email) or email, self.env['res.partner'])
|
||||
# Responses from external invitations are stored in the 'responseStatus' field.
|
||||
# This field only carries the current user's event status because Microsoft hides other user's status.
|
||||
if self.env.user.email == email and microsoft_event.responseStatus:
|
||||
|
|
@ -476,7 +503,7 @@ class Meeting(models.Model):
|
|||
return 'organizer'
|
||||
return ATTENDEE_CONVERTER_O2M.get(attendee.state, 'None')
|
||||
|
||||
def _microsoft_values(self, fields_to_sync, initial_values={}):
|
||||
def _microsoft_values(self, fields_to_sync, initial_values=()):
|
||||
values = dict(initial_values)
|
||||
if not fields_to_sync:
|
||||
return values
|
||||
|
|
@ -492,7 +519,7 @@ class Meeting(models.Model):
|
|||
|
||||
if 'description' in fields_to_sync:
|
||||
values['body'] = {
|
||||
'content': self.description if not is_html_empty(self.description) else '',
|
||||
'content': self._get_customer_description(),
|
||||
'contentType': "html",
|
||||
}
|
||||
|
||||
|
|
@ -511,6 +538,12 @@ class Meeting(models.Model):
|
|||
if 'location' in fields_to_sync:
|
||||
values['location'] = {'displayName': self.location or ''}
|
||||
|
||||
if not self.location and 'videocall_location' in fields_to_sync and self._need_video_call():
|
||||
values['isOnlineMeeting'] = True
|
||||
values['onlineMeetingProvider'] = 'teamsForBusiness'
|
||||
else:
|
||||
values['isOnlineMeeting'] = False
|
||||
|
||||
if 'alarm_ids' in fields_to_sync:
|
||||
alarm_id = self.alarm_ids.filtered(lambda a: a.alarm_type == 'notification')[:1]
|
||||
values['isReminderOn'] = bool(alarm_id)
|
||||
|
|
@ -535,6 +568,11 @@ class Meeting(models.Model):
|
|||
'private': 'private',
|
||||
'confidential': 'confidential',
|
||||
}
|
||||
# Set default privacy in event according to the organizer's calendar default privacy if defined.
|
||||
if self.user_id:
|
||||
sensitivity_o2m[False] = sensitivity_o2m.get(self.user_id.calendar_default_privacy)
|
||||
else:
|
||||
sensitivity_o2m[False] = 'normal'
|
||||
values['sensitivity'] = sensitivity_o2m.get(self.privacy)
|
||||
|
||||
if 'active' in fields_to_sync and not self.active:
|
||||
|
|
@ -617,11 +655,11 @@ class Meeting(models.Model):
|
|||
"all attendees must have an email address. However, some events do "
|
||||
"not respect this condition. As long as the events are incorrect, "
|
||||
"the calendars will not be synchronized."
|
||||
"\nEither update the events/attendees or archive these events %s:"
|
||||
"\n%s", details, invalid_events))
|
||||
"\nEither update the events/attendees or archive these events %(details)s:"
|
||||
"\n%(invalid_events)s", details=details, invalid_events=invalid_events))
|
||||
|
||||
def _microsoft_values_occurence(self, initial_values={}):
|
||||
values = initial_values
|
||||
def _microsoft_values_occurence(self, initial_values=()):
|
||||
values = dict(initial_values)
|
||||
values['type'] = 'occurrence'
|
||||
|
||||
if self.allday:
|
||||
|
|
@ -646,7 +684,10 @@ class Meeting(models.Model):
|
|||
"""
|
||||
user = self.env.user
|
||||
records = self.filtered(lambda e: not e.user_id or e.user_id == user or user.partner_id in e.partner_ids)
|
||||
super(Meeting, records)._cancel_microsoft()
|
||||
for event in records:
|
||||
# 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, records)._cancel_microsoft()
|
||||
attendees = (self - records).attendee_ids.filtered(lambda a: a.partner_id == user.partner_id)
|
||||
attendees.do_decline()
|
||||
|
||||
|
|
@ -661,3 +702,8 @@ class Meeting(models.Model):
|
|||
if user_id and self.with_user(user_id).sudo()._check_microsoft_sync_status():
|
||||
return user_id
|
||||
return self.env.user
|
||||
|
||||
def _is_microsoft_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"."microsoft_id" IS NULL'
|
||||
base = super()._get_notify_alert_extra_conditions()
|
||||
if self.env.context.get('alarm_type') == 'email':
|
||||
return SQL("%s AND event.microsoft_id IS NULL", base)
|
||||
return base
|
||||
|
|
|
|||
|
|
@ -1,25 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
from odoo import models
|
||||
|
||||
from odoo.addons.microsoft_calendar.models.microsoft_sync import microsoft_calendar_token
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
||||
|
||||
|
||||
class Attendee(models.Model):
|
||||
_name = 'calendar.attendee'
|
||||
class CalendarAttendee(models.Model):
|
||||
_inherit = 'calendar.attendee'
|
||||
|
||||
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
|
||||
Otherwise, Microsoft Outlook will send them
|
||||
"""
|
||||
with microsoft_calendar_token(self.env.user.sudo()) as token:
|
||||
if not token:
|
||||
super()._send_mail_to_attendees(mail_template, force_send)
|
||||
|
||||
def do_tentative(self):
|
||||
# Synchronize event after state change
|
||||
res = super().do_tentative()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.osv import expression
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class RecurrenceRule(models.Model):
|
||||
class CalendarRecurrence(models.Model):
|
||||
_name = 'calendar.recurrence'
|
||||
_inherit = ['calendar.recurrence', 'microsoft.calendar.sync']
|
||||
|
||||
|
|
@ -13,8 +12,6 @@ class RecurrenceRule(models.Model):
|
|||
# Don't sync by default. Sync only when the recurrence is applied
|
||||
need_sync_m = fields.Boolean(default=False)
|
||||
|
||||
microsoft_id = fields.Char('Microsoft Calendar Recurrence Id')
|
||||
|
||||
def _compute_rrule(self):
|
||||
# Note: 'need_sync_m' is set to False to avoid syncing the updated recurrence with
|
||||
# Outlook, as this update may already come from Outlook. If not, this modification will
|
||||
|
|
@ -43,14 +40,15 @@ class RecurrenceRule(models.Model):
|
|||
vals += [{
|
||||
'name': event.name,
|
||||
'microsoft_id': event.microsoft_id,
|
||||
'ms_universal_event_id': event.ms_universal_event_id,
|
||||
'start': event.start,
|
||||
'stop': event.stop,
|
||||
'active': False,
|
||||
'need_sync_m': True,
|
||||
}]
|
||||
event._microsoft_delete(event.user_id, event.ms_organizer_event_id)
|
||||
event._microsoft_delete(event.user_id, event.microsoft_id)
|
||||
event.ms_universal_event_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_m = False
|
||||
return detached_events
|
||||
|
||||
|
|
@ -108,9 +106,10 @@ class RecurrenceRule(models.Model):
|
|||
# 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).ms_universal_event_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
|
||||
new_event_values, microsoft_id=False, ms_universal_event_id=False, need_sync_m=False
|
||||
))
|
||||
if self.rrule == current_rrule:
|
||||
# if the rrule has changed, it will be recalculated below
|
||||
|
|
@ -132,13 +131,12 @@ class RecurrenceRule(models.Model):
|
|||
# to base_event start datetime.
|
||||
if self.rrule != current_rrule:
|
||||
detached_events = self._apply_recurrence()
|
||||
detached_events.microsoft_id = False
|
||||
detached_events.ms_universal_event_id = False
|
||||
detached_events.unlink()
|
||||
|
||||
def _get_microsoft_sync_domain(self):
|
||||
# Do not sync Odoo recurrences with Outlook Calendar anymore.
|
||||
domain = expression.FALSE_DOMAIN
|
||||
return self._extend_microsoft_domain(domain)
|
||||
return self._extend_microsoft_domain(Domain.FALSE)
|
||||
|
||||
def _cancel_microsoft(self):
|
||||
self.calendar_event_ids.with_context(dont_notify=True)._cancel_microsoft()
|
||||
|
|
@ -151,18 +149,18 @@ class RecurrenceRule(models.Model):
|
|||
if with_ids:
|
||||
recurrence = {
|
||||
**recurrence,
|
||||
'ms_organizer_event_id': microsoft_recurrence.id,
|
||||
'microsoft_id': microsoft_recurrence.id,
|
||||
'ms_universal_event_id': microsoft_recurrence.iCalUId,
|
||||
}
|
||||
|
||||
return recurrence
|
||||
|
||||
def _microsoft_values(self, fields_to_sync):
|
||||
def _microsoft_values(self, fields_to_sync, initial_values=()):
|
||||
"""
|
||||
Get values to update the whole Outlook event recurrence.
|
||||
(done through the first event of the Outlook recurrence).
|
||||
"""
|
||||
return self.base_event_id._microsoft_values(fields_to_sync, initial_values={'type': 'seriesMaster'})
|
||||
return self.base_event_id._microsoft_values(fields_to_sync, initial_values={**dict(initial_values), 'type': 'seriesMaster'})
|
||||
|
||||
def _ensure_attendees_have_email(self):
|
||||
self.calendar_event_ids.filtered(lambda e: e.active)._ensure_attendees_have_email()
|
||||
|
|
@ -178,7 +176,7 @@ class RecurrenceRule(models.Model):
|
|||
if new_recurrence and new_recurrence.base_event_id.microsoft_id:
|
||||
new_recurrence.base_event_id._microsoft_delete(
|
||||
new_recurrence.base_event_id._get_organizer(),
|
||||
new_recurrence.base_event_id.ms_organizer_event_id
|
||||
new_recurrence.base_event_id.microsoft_id
|
||||
)
|
||||
|
||||
return new_recurrence
|
||||
|
|
@ -190,3 +188,9 @@ class RecurrenceRule(models.Model):
|
|||
if event:
|
||||
return event._get_event_user_m(user_id)
|
||||
return self.env.user
|
||||
|
||||
def _is_microsoft_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,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
|
@ -8,15 +7,13 @@ import pytz
|
|||
from dateutil.parser import parse
|
||||
from datetime import timedelta
|
||||
|
||||
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 import api, fields, models
|
||||
from odoo.fields import Domain
|
||||
from odoo.modules.registry import Registry
|
||||
from odoo.sql_db import BaseCursor
|
||||
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
|
||||
from odoo.addons.microsoft_calendar.utils.event_id_storage import IDS_SEPARATOR, combine_ids, split_ids
|
||||
from odoo.addons.microsoft_account.models.microsoft_service import TIMEOUT
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
|
@ -40,7 +37,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:
|
||||
|
|
@ -55,48 +52,38 @@ def after_commit(func):
|
|||
def microsoft_calendar_token(user):
|
||||
yield user._get_microsoft_calendar_token()
|
||||
|
||||
class MicrosoftSync(models.AbstractModel):
|
||||
|
||||
class MicrosoftCalendarSync(models.AbstractModel):
|
||||
_name = 'microsoft.calendar.sync'
|
||||
_description = "Synchronize a record with Microsoft Calendar"
|
||||
|
||||
microsoft_id = fields.Char('Microsoft Calendar Id', copy=False)
|
||||
|
||||
ms_organizer_event_id = fields.Char(
|
||||
'Organizer event Id',
|
||||
compute='_compute_organizer_event_id',
|
||||
inverse='_set_event_id',
|
||||
search='_search_organizer_event_id',
|
||||
)
|
||||
ms_universal_event_id = fields.Char(
|
||||
'Universal event Id',
|
||||
compute='_compute_universal_event_id',
|
||||
inverse='_set_event_id',
|
||||
search='_search_universal_event_id',
|
||||
)
|
||||
microsoft_id = fields.Char('Organizer event Id', copy=False, index=True)
|
||||
ms_universal_event_id = fields.Char('Universal event Id', copy=False, index=True)
|
||||
|
||||
# This field helps to know 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):
|
||||
if 'ms_universal_event_id' in vals:
|
||||
self._from_uids.clear_cache(self)
|
||||
|
||||
fields_to_sync = [x for x in vals.keys() if x in self._get_microsoft_synced_fields()]
|
||||
if fields_to_sync and 'need_sync_m' not in vals and not self.env.user.microsoft_synchronization_stopped:
|
||||
fields_to_sync = [x for x in vals if x in self._get_microsoft_synced_fields()]
|
||||
if fields_to_sync and 'need_sync_m' not in vals and self.env.user._get_microsoft_sync_status() == "sync_active":
|
||||
vals['need_sync_m'] = True
|
||||
|
||||
result = super().write(vals)
|
||||
|
||||
for record in self.filtered(lambda e: e.need_sync_m and e.ms_organizer_event_id):
|
||||
if not vals.get('active', True):
|
||||
# We need to delete the event. Cancel is not sufficant. Errors may occurs
|
||||
record._microsoft_delete(record._get_organizer(), record.ms_organizer_event_id, timeout=3)
|
||||
elif fields_to_sync:
|
||||
values = record._microsoft_values(fields_to_sync)
|
||||
if not values:
|
||||
continue
|
||||
record._microsoft_patch(record._get_organizer(), record.ms_organizer_event_id, values, timeout=3)
|
||||
if self.env.user._get_microsoft_sync_status() != "sync_paused":
|
||||
timeout = self._get_microsoft_graph_timeout()
|
||||
|
||||
for record in self:
|
||||
if record.need_sync_m and record.microsoft_id:
|
||||
if not vals.get('active', True):
|
||||
# We need to delete the event. Cancel is not sufficient. Errors may occur.
|
||||
record._microsoft_delete(record._get_organizer(), record.microsoft_id, timeout=timeout)
|
||||
elif fields_to_sync:
|
||||
values = record._microsoft_values(fields_to_sync)
|
||||
if not values:
|
||||
continue
|
||||
record._microsoft_patch(record._get_organizer(), record.microsoft_id, values, timeout=timeout)
|
||||
|
||||
return result
|
||||
|
||||
|
|
@ -107,53 +94,14 @@ class MicrosoftSync(models.AbstractModel):
|
|||
vals.update({'need_sync_m': False})
|
||||
records = super().create(vals_list)
|
||||
|
||||
records_to_sync = records.filtered(lambda r: r.need_sync_m and r.active)
|
||||
for record in records_to_sync:
|
||||
record._microsoft_insert(record._microsoft_values(self._get_microsoft_synced_fields()), timeout=3)
|
||||
if self.env.user._get_microsoft_sync_status() != "sync_paused":
|
||||
timeout = self._get_microsoft_graph_timeout()
|
||||
|
||||
for record in records:
|
||||
if record.need_sync_m and record.active:
|
||||
record._microsoft_insert(record._microsoft_values(self._get_microsoft_synced_fields()), timeout=timeout)
|
||||
return records
|
||||
|
||||
@api.depends('microsoft_id')
|
||||
def _compute_organizer_event_id(self):
|
||||
for event in self:
|
||||
event.ms_organizer_event_id = split_ids(event.microsoft_id)[0] if event.microsoft_id else False
|
||||
|
||||
@api.depends('microsoft_id')
|
||||
def _compute_universal_event_id(self):
|
||||
for event in self:
|
||||
event.ms_universal_event_id = split_ids(event.microsoft_id)[1] if event.microsoft_id else False
|
||||
|
||||
def _set_event_id(self):
|
||||
for event in self:
|
||||
event.microsoft_id = combine_ids(event.ms_organizer_event_id, event.ms_universal_event_id)
|
||||
|
||||
def _search_event_id(self, operator, value, with_uid):
|
||||
def _domain(v):
|
||||
return ('microsoft_id', '=like', f'%{IDS_SEPARATOR}{v}' if with_uid else f'{v}%')
|
||||
|
||||
if operator == '=' and not value:
|
||||
return (
|
||||
['|', ('microsoft_id', '=', False), ('microsoft_id', '=ilike', f'%{IDS_SEPARATOR}')]
|
||||
if with_uid
|
||||
else [('microsoft_id', '=', False)]
|
||||
)
|
||||
elif operator == '!=' and not value:
|
||||
return (
|
||||
[('microsoft_id', 'ilike', f'{IDS_SEPARATOR}_')]
|
||||
if with_uid
|
||||
else [('microsoft_id', '!=', False)]
|
||||
)
|
||||
return (
|
||||
['|'] * (len(value) - 1) + [_domain(v) for v in value]
|
||||
if operator.lower() == 'in'
|
||||
else [_domain(value)]
|
||||
)
|
||||
|
||||
def _search_organizer_event_id(self, operator, value):
|
||||
return self._search_event_id(operator, value, with_uid=False)
|
||||
|
||||
def _search_universal_event_id(self, operator, value):
|
||||
return self._search_event_id(operator, value, with_uid=True)
|
||||
|
||||
@api.model
|
||||
def _get_microsoft_service(self):
|
||||
return MicrosoftCalendarService(self.env['microsoft.service'])
|
||||
|
|
@ -166,8 +114,9 @@ class MicrosoftSync(models.AbstractModel):
|
|||
|
||||
def unlink(self):
|
||||
synced = self._get_synced_events()
|
||||
for ev in synced:
|
||||
ev._microsoft_delete(ev._get_organizer(), ev.ms_organizer_event_id)
|
||||
if self.env.user._get_microsoft_sync_status() != "sync_paused":
|
||||
for ev in synced:
|
||||
ev._microsoft_delete(ev._get_organizer(), ev.microsoft_id)
|
||||
return super().unlink()
|
||||
|
||||
def _write_from_microsoft(self, microsoft_event, vals):
|
||||
|
|
@ -175,14 +124,7 @@ class MicrosoftSync(models.AbstractModel):
|
|||
|
||||
@api.model
|
||||
def _create_from_microsoft(self, microsoft_event, vals_list):
|
||||
return self.with_context(dont_notify=True).create(vals_list)
|
||||
|
||||
@api.model
|
||||
@ormcache_context('uids', keys=('active_test',))
|
||||
def _from_uids(self, uids):
|
||||
if not uids:
|
||||
return self.browse()
|
||||
return self.search([('ms_universal_event_id', 'in', uids)])
|
||||
return self.with_context(dont_notify=True, skip_contact_description=True).create(vals_list)
|
||||
|
||||
def _sync_odoo2microsoft(self):
|
||||
if not self:
|
||||
|
|
@ -198,9 +140,12 @@ class MicrosoftSync(models.AbstractModel):
|
|||
new_records = records_to_sync - updated_records
|
||||
|
||||
for record in cancelled_records._get_synced_events():
|
||||
record._microsoft_delete(record._get_organizer(), record.ms_organizer_event_id)
|
||||
record._microsoft_delete(record._get_organizer(), record.microsoft_id)
|
||||
for record in new_records:
|
||||
values = record._microsoft_values(self._get_microsoft_synced_fields())
|
||||
sender_user = record._get_event_user_m()
|
||||
if record._is_microsoft_insertion_blocked(sender_user):
|
||||
continue
|
||||
if isinstance(values, dict):
|
||||
record._microsoft_insert(values)
|
||||
else:
|
||||
|
|
@ -210,10 +155,11 @@ class MicrosoftSync(models.AbstractModel):
|
|||
values = record._microsoft_values(self._get_microsoft_synced_fields())
|
||||
if not values:
|
||||
continue
|
||||
record._microsoft_patch(record._get_organizer(), record.ms_organizer_event_id, values)
|
||||
record._microsoft_patch(record._get_organizer(), record.microsoft_id, values)
|
||||
|
||||
def _cancel_microsoft(self):
|
||||
self.microsoft_id = False
|
||||
self.ms_universal_event_id = False
|
||||
self.unlink()
|
||||
|
||||
def _sync_recurrence_microsoft2odoo(self, microsoft_events, new_events=None):
|
||||
|
|
@ -231,7 +177,7 @@ class MicrosoftSync(models.AbstractModel):
|
|||
need_sync_m=False
|
||||
)
|
||||
to_create = recurrents.filter(
|
||||
lambda e: e.seriesMasterId == new_calendar_recurrence['ms_organizer_event_id']
|
||||
lambda e: e.seriesMasterId == new_calendar_recurrence['microsoft_id']
|
||||
)
|
||||
recurrents -= to_create
|
||||
base_values = dict(
|
||||
|
|
@ -261,10 +207,7 @@ class MicrosoftSync(models.AbstractModel):
|
|||
# is specific to the Microsoft user calendar.
|
||||
ms_recurrence_ids = list({x.seriesMasterId for x in recurrents})
|
||||
ms_recurrence_uids = {r.id: r.iCalUId for r in microsoft_events if r.id in ms_recurrence_ids}
|
||||
|
||||
recurrences = self.env['calendar.recurrence'].search([
|
||||
('ms_universal_event_id', 'in', ms_recurrence_uids.values())
|
||||
])
|
||||
recurrences = self.env['calendar.recurrence'].search([('ms_universal_event_id', 'in', microsoft_events.uids)])
|
||||
for recurrent_master_id in ms_recurrence_ids:
|
||||
recurrence_id = recurrences.filtered(
|
||||
lambda ev: ev.ms_universal_event_id == ms_recurrence_uids[recurrent_master_id]
|
||||
|
|
@ -294,7 +237,7 @@ class MicrosoftSync(models.AbstractModel):
|
|||
Update Odoo events from Outlook recurrence and events.
|
||||
"""
|
||||
# get the list of events to update ...
|
||||
events_to_update = events.filter(lambda e: e.seriesMasterId == self.ms_organizer_event_id)
|
||||
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]
|
||||
|
||||
|
|
@ -345,7 +288,7 @@ class MicrosoftSync(models.AbstractModel):
|
|||
dict(self._microsoft_to_odoo_values(e, with_ids=True), need_sync_m=False)
|
||||
for e in (new - new_recurrence)
|
||||
]
|
||||
synced_events = self.with_context(dont_notify=True)._create_from_microsoft(new, odoo_values)
|
||||
synced_events = self.with_context(dont_notify=True, skip_contact_description=True)._create_from_microsoft(new, odoo_values)
|
||||
synced_recurrences, updated_events = self._sync_recurrence_microsoft2odoo(existing, new_recurrence)
|
||||
synced_events |= updated_events
|
||||
|
||||
|
|
@ -353,12 +296,12 @@ class MicrosoftSync(models.AbstractModel):
|
|||
cancelled_recurrences = self.env['calendar.recurrence'].search([
|
||||
'|',
|
||||
('ms_universal_event_id', 'in', cancelled.uids),
|
||||
('ms_organizer_event_id', 'in', cancelled.ids),
|
||||
('microsoft_id', 'in', cancelled.ids),
|
||||
])
|
||||
cancelled_events = self.browse([
|
||||
e.odoo_id(self.env)
|
||||
for e in cancelled
|
||||
if e.id not in [r.ms_organizer_event_id for r in cancelled_recurrences]
|
||||
if e.id not in [r.microsoft_id for r in cancelled_recurrences]
|
||||
])
|
||||
cancelled_recurrences._cancel_microsoft()
|
||||
cancelled_events = cancelled_events.exists()
|
||||
|
|
@ -419,11 +362,6 @@ class MicrosoftSync(models.AbstractModel):
|
|||
stop_date_condition = any(event.stop >= lower_bound for event in self.calendar_event_ids)
|
||||
return stop_date_condition or update_time_diff >= timedelta(hours=1)
|
||||
|
||||
def _impersonate_user(self, user_id):
|
||||
""" Impersonate a user (mainly the event organizer) to be able to call the Outlook API with its token """
|
||||
# This method is obsolete, as it has been replaced by the `_get_event_user_m` method, which gets the user who will make the request.
|
||||
return user_id.with_user(user_id)
|
||||
|
||||
@after_commit
|
||||
def _microsoft_delete(self, user_id, event_id, timeout=TIMEOUT):
|
||||
"""
|
||||
|
|
@ -433,8 +371,8 @@ class MicrosoftSync(models.AbstractModel):
|
|||
'self' won't exist when this method will be really called due to @after_commit decorator.
|
||||
"""
|
||||
microsoft_service = self._get_microsoft_service()
|
||||
sender_user = self._get_event_user_m(user_id)
|
||||
with microsoft_calendar_token(sender_user.sudo()) as token:
|
||||
sender_user = self._get_event_user_m(user_id).sudo()
|
||||
with microsoft_calendar_token(sender_user) as token:
|
||||
if token and not sender_user.microsoft_synchronization_stopped:
|
||||
microsoft_service.delete(event_id, token=token, timeout=timeout)
|
||||
|
||||
|
|
@ -475,7 +413,8 @@ class MicrosoftSync(models.AbstractModel):
|
|||
self._ensure_attendees_have_email()
|
||||
event_id, uid = microsoft_service.insert(values, token=token, timeout=timeout)
|
||||
self.with_context(dont_notify=True).write({
|
||||
'microsoft_id': combine_ids(event_id, uid),
|
||||
'microsoft_id': event_id,
|
||||
'ms_universal_event_id': uid,
|
||||
'need_sync_m': False,
|
||||
})
|
||||
|
||||
|
|
@ -518,7 +457,19 @@ class MicrosoftSync(models.AbstractModel):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _microsoft_values(self, fields_to_sync):
|
||||
@api.model
|
||||
def _get_microsoft_graph_timeout(self):
|
||||
"""Return Microsoft Graph request timeout (seconds).
|
||||
|
||||
Keep current behavior by default (5s), but allow admins to increase it
|
||||
through a system parameter.
|
||||
"""
|
||||
timeout = self.env['ir.config_parameter'].sudo().get_param('microsoft_calendar.graph_timeout')
|
||||
if not timeout or not timeout.isdigit():
|
||||
return 5
|
||||
return max(1, int(timeout))
|
||||
|
||||
def _microsoft_values(self, fields_to_sync, initial_values=()):
|
||||
"""
|
||||
Implements this method to return a dict with values formatted
|
||||
according to the Microsoft Calendar API
|
||||
|
|
@ -550,19 +501,19 @@ class MicrosoftSync(models.AbstractModel):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _extend_microsoft_domain(self, domain):
|
||||
def _extend_microsoft_domain(self, domain: Domain):
|
||||
""" Extends the sync domain based on the full_sync_m context parameter.
|
||||
In case of full sync it shouldn't include already synced events.
|
||||
"""
|
||||
if self._context.get('full_sync_m', True):
|
||||
domain = expression.AND([domain, [('ms_universal_event_id', '=', False)]])
|
||||
if self.env.context.get('full_sync_m', True):
|
||||
domain &= Domain('ms_universal_event_id', '=', False)
|
||||
else:
|
||||
is_active_clause = (self._active_name, '=', True) if self._active_name else expression.TRUE_LEAF
|
||||
domain = expression.AND([domain, [
|
||||
'|',
|
||||
'&', ('ms_universal_event_id', '=', False), is_active_clause,
|
||||
('need_sync_m', '=', True),
|
||||
]])
|
||||
is_active_clause = Domain(self._active_name, '=', True) if self._active_name else Domain.TRUE
|
||||
domain &= (Domain('ms_universal_event_id', '=', False) & is_active_clause) | Domain('need_sync_m', '=', True)
|
||||
# Sync only events created/updated after last sync date (with 5 min of time acceptance).
|
||||
if self.env.user.microsoft_last_sync_date:
|
||||
time_offset = timedelta(minutes=5)
|
||||
domain &= Domain('write_date', '>=', self.env.user.microsoft_last_sync_date - time_offset)
|
||||
return domain
|
||||
|
||||
def _get_event_user_m(self, user_id=None):
|
||||
|
|
@ -572,3 +523,21 @@ class MicrosoftSync(models.AbstractModel):
|
|||
the appropriate user accordingly.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
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
|
||||
|
||||
def _is_microsoft_insertion_blocked(self, sender_user):
|
||||
"""
|
||||
Returns True if the record insertion to Microsoft should be blocked.
|
||||
This is a necessary step for ensuring data match between Odoo and Microsoft,
|
||||
as it prevents attendees to synchronize new records on behalf of the owners,
|
||||
otherwise the event ownership would be lost in Outlook and it would block the
|
||||
future record synchronization for the original owner.
|
||||
"""
|
||||
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_microsoft_client_id = fields.Char("Microsoft Client_id", config_parameter='microsoft_calendar_client_id', default='')
|
||||
cal_microsoft_client_secret = fields.Char("Microsoft Client_key", config_parameter='microsoft_calendar_client_secret', default='')
|
||||
cal_microsoft_sync_paused = fields.Boolean("Microsoft Synchronization Paused", config_parameter='microsoft_calendar_sync_paused',
|
||||
help="Indicates if synchronization with Outlook Calendar is paused or not.")
|
||||
|
|
|
|||
|
|
@ -1,33 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import requests
|
||||
from odoo.addons.microsoft_calendar.models.microsoft_sync import microsoft_calendar_token
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import api, fields, models, _, Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.loglevels import exception_to_unicode
|
||||
from odoo.addons.microsoft_account.models.microsoft_service import DEFAULT_MICROSOFT_TOKEN_ENDPOINT
|
||||
from odoo.addons.microsoft_account.models import microsoft_service
|
||||
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import InvalidSyncToken
|
||||
from odoo.tools import str2bool
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
microsoft_calendar_sync_token = fields.Char('Microsoft Next Sync Token', copy=False)
|
||||
microsoft_synchronization_stopped = fields.Boolean('Outlook Synchronization stopped', copy=False)
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + ['microsoft_synchronization_stopped']
|
||||
|
||||
@property
|
||||
def SELF_WRITEABLE_FIELDS(self):
|
||||
return super().SELF_WRITEABLE_FIELDS + ['microsoft_synchronization_stopped']
|
||||
microsoft_calendar_sync_token = fields.Char(related='res_users_settings_id.microsoft_calendar_sync_token', groups='base.group_system')
|
||||
microsoft_synchronization_stopped = fields.Boolean(related='res_users_settings_id.microsoft_synchronization_stopped', readonly=False, groups='base.group_system')
|
||||
microsoft_last_sync_date = fields.Datetime(related='res_users_settings_id.microsoft_last_sync_date', readonly=False, groups='base.group_system')
|
||||
|
||||
def _microsoft_calendar_authenticated(self):
|
||||
return bool(self.sudo().microsoft_calendar_rtoken)
|
||||
|
|
@ -37,48 +30,32 @@ class User(models.Model):
|
|||
return None
|
||||
|
||||
self.ensure_one()
|
||||
if self.microsoft_calendar_rtoken and not self._is_microsoft_calendar_valid():
|
||||
if self.sudo().microsoft_calendar_rtoken and not self._is_microsoft_calendar_valid():
|
||||
self._refresh_microsoft_calendar_token()
|
||||
return self.microsoft_calendar_token
|
||||
return self.sudo().microsoft_calendar_token
|
||||
|
||||
def _is_microsoft_calendar_valid(self):
|
||||
return self.microsoft_calendar_token_validity and self.microsoft_calendar_token_validity >= (fields.Datetime.now() + timedelta(minutes=1))
|
||||
return self.sudo().microsoft_calendar_token_validity and self.sudo().microsoft_calendar_token_validity >= (fields.Datetime.now() + timedelta(minutes=1))
|
||||
|
||||
def _refresh_microsoft_calendar_token(self):
|
||||
def _refresh_microsoft_calendar_token(self, service='calendar'):
|
||||
self.ensure_one()
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
client_id = get_param('microsoft_calendar_client_id')
|
||||
client_secret = get_param('microsoft_calendar_client_secret')
|
||||
|
||||
if not client_id or not client_secret:
|
||||
raise UserError(_("The account for the Outlook Calendar service is not configured."))
|
||||
|
||||
headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
data = {
|
||||
'refresh_token': self.microsoft_calendar_rtoken,
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'grant_type': 'refresh_token',
|
||||
}
|
||||
|
||||
try:
|
||||
dummy, response, dummy = self.env['microsoft.service']._do_request(
|
||||
DEFAULT_MICROSOFT_TOKEN_ENDPOINT, params=data, headers=headers, method='POST', preuri=''
|
||||
)
|
||||
ttl = response.get('expires_in')
|
||||
self.write({
|
||||
'microsoft_calendar_token': response.get('access_token'),
|
||||
access_token, ttl = self.env['microsoft.service']._refresh_microsoft_token('calendar', self.sudo().microsoft_calendar_rtoken)
|
||||
self.sudo().write({
|
||||
'microsoft_calendar_token': access_token,
|
||||
'microsoft_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.write({
|
||||
self.sudo().write({
|
||||
'microsoft_calendar_rtoken': False,
|
||||
'microsoft_calendar_token': False,
|
||||
'microsoft_calendar_token_validity': False,
|
||||
'microsoft_calendar_sync_token': False,
|
||||
})
|
||||
self.res_users_settings_id.sudo().write({
|
||||
'microsoft_calendar_sync_token': False
|
||||
})
|
||||
self.env.cr.commit()
|
||||
error_key = error.response.json().get("error", "nc")
|
||||
|
|
@ -88,9 +65,21 @@ class User(models.Model):
|
|||
error_key)
|
||||
raise UserError(error_msg)
|
||||
|
||||
def _get_microsoft_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("microsoft_calendar_sync_paused"), default=False):
|
||||
status = "sync_paused"
|
||||
elif self.sudo().microsoft_calendar_token and not self.sudo().microsoft_synchronization_stopped:
|
||||
status = "sync_active"
|
||||
elif self.sudo().microsoft_synchronization_stopped:
|
||||
status = "sync_stopped"
|
||||
return status
|
||||
|
||||
def _sync_microsoft_calendar(self):
|
||||
self.ensure_one()
|
||||
if self.microsoft_synchronization_stopped:
|
||||
self.sudo().microsoft_last_sync_date = datetime.now()
|
||||
if self._get_microsoft_sync_status() != "sync_active":
|
||||
return False
|
||||
|
||||
# Set the first synchronization date as an ICP parameter before writing the variable
|
||||
|
|
@ -98,14 +87,14 @@ class User(models.Model):
|
|||
self._set_ICP_first_synchronization_date(fields.Datetime.now())
|
||||
|
||||
calendar_service = self.env["calendar.event"]._get_microsoft_service()
|
||||
full_sync = not bool(self.microsoft_calendar_sync_token)
|
||||
full_sync = not bool(self.sudo().microsoft_calendar_sync_token)
|
||||
with microsoft_calendar_token(self) as token:
|
||||
try:
|
||||
events, next_sync_token = calendar_service.get_events(self.microsoft_calendar_sync_token, token=token)
|
||||
events, next_sync_token = calendar_service.get_events(self.sudo().microsoft_calendar_sync_token, token=token)
|
||||
except InvalidSyncToken:
|
||||
events, next_sync_token = calendar_service.get_events(token=token)
|
||||
full_sync = True
|
||||
self.microsoft_calendar_sync_token = next_sync_token
|
||||
self.res_users_settings_id.sudo().microsoft_calendar_sync_token = next_sync_token
|
||||
|
||||
# Microsoft -> Odoo
|
||||
synced_events, synced_recurrences = self.env['calendar.event']._sync_microsoft2odoo(events) if events else (self.env['calendar.event'], self.env['calendar.recurrence'])
|
||||
|
|
@ -118,32 +107,80 @@ class User(models.Model):
|
|||
|
||||
events = self.env['calendar.event']._get_microsoft_records_to_sync(full_sync=full_sync)
|
||||
(events - synced_events)._sync_odoo2microsoft()
|
||||
self.sudo().microsoft_last_sync_date = datetime.now()
|
||||
|
||||
return bool(events | synced_events) or bool(recurrences | synced_recurrences)
|
||||
|
||||
@api.model
|
||||
def _sync_all_microsoft_calendar(self):
|
||||
""" Cron job """
|
||||
users = self.env['res.users'].search([('microsoft_calendar_rtoken', '!=', False), ('microsoft_synchronization_stopped', '=', False)])
|
||||
users = self.env['res.users'].sudo().search([('microsoft_calendar_rtoken', '!=', False), ('microsoft_synchronization_stopped', '=', False)])
|
||||
for user in users:
|
||||
_logger.info("Calendar Synchro - Starting synchronization for %s", user)
|
||||
try:
|
||||
user.with_user(user).sudo()._sync_microsoft_calendar()
|
||||
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 stop_microsoft_synchronization(self):
|
||||
self.ensure_one()
|
||||
self.microsoft_synchronization_stopped = True
|
||||
self.sudo().microsoft_synchronization_stopped = True
|
||||
self.sudo().microsoft_last_sync_date = None
|
||||
|
||||
def restart_microsoft_synchronization(self):
|
||||
self.ensure_one()
|
||||
self.microsoft_synchronization_stopped = False
|
||||
self.sudo().microsoft_last_sync_date = datetime.now()
|
||||
self.sudo().microsoft_synchronization_stopped = False
|
||||
self.env['calendar.recurrence']._restart_microsoft_sync()
|
||||
self.env['calendar.event']._restart_microsoft_sync()
|
||||
|
||||
def unpause_microsoft_synchronization(self):
|
||||
self.env['ir.config_parameter'].sudo().set_param("microsoft_calendar_sync_paused", False)
|
||||
|
||||
def pause_microsoft_synchronization(self):
|
||||
self.env['ir.config_parameter'].sudo().set_param("microsoft_calendar_sync_paused", True)
|
||||
|
||||
@api.model
|
||||
def _has_setup_microsoft_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['microsoft.service']._get_microsoft_client_id('calendar')
|
||||
client_secret = microsoft_service._get_microsoft_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['microsoft_calendar'] = self._has_setup_microsoft_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('microsoft_calendar'):
|
||||
sync_status = self._get_microsoft_sync_status()
|
||||
if sync_status == 'sync_active' and not self.sudo().microsoft_calendar_token:
|
||||
sync_status = 'sync_stopped'
|
||||
res['microsoft_calendar'] = sync_status
|
||||
return res
|
||||
|
||||
def _has_any_active_synchronization(self):
|
||||
"""
|
||||
Check if synchronization is active for Microsoft Calendar.
|
||||
This function retrieves the synchronization status from the user's environment
|
||||
and checks if the Microsoft 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('microsoft_calendar') == 'sync_active':
|
||||
return True
|
||||
return res
|
||||
|
||||
def _set_ICP_first_synchronization_date(self, now):
|
||||
"""
|
||||
Set the first synchronization date as an ICP parameter when applicable (param not defined yet
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
_inherit = "res.users.settings"
|
||||
|
||||
# Microsoft Calendar settings.
|
||||
microsoft_calendar_sync_token = fields.Char('Microsoft Next Sync Token', copy=False, groups='base.group_system')
|
||||
microsoft_synchronization_stopped = fields.Boolean('Outlook Synchronization stopped', copy=False, groups='base.group_system')
|
||||
microsoft_last_sync_date = fields.Datetime('Last Sync Date', copy=False, help='Last synchronization date with Outlook Calendar', groups='base.group_system')
|
||||
|
||||
@api.model
|
||||
def _get_fields_blacklist(self):
|
||||
""" Get list of microsoft fields that won't be formatted in session_info. """
|
||||
microsoft_fields_blacklist = [
|
||||
'microsoft_calendar_sync_token',
|
||||
'microsoft_synchronization_stopped',
|
||||
'microsoft_last_sync_date',
|
||||
]
|
||||
return super()._get_fields_blacklist() + microsoft_fields_blacklist
|
||||
Loading…
Add table
Add a link
Reference in a new issue