Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions microsoft_calendar_filter/models/calendar_event.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,101 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import logging

from odoo import models
from odoo.osv.expression import AND
from odoo.tools.safe_eval import safe_eval

from odoo.addons.microsoft_calendar.models.microsoft_sync import (
microsoft_calendar_token,
)

from .res_config_settings import FILTER_ODOO_EVENTS

_logger = logging.getLogger(__name__)


class CalendarEvent(models.Model):
_inherit = "calendar.event"

def _sync_odoo2microsoft(self):
self._remove_events_not_in_filter()
return super()._sync_odoo2microsoft()

def _remove_events_not_in_filter(self):
"""Remove those events from Microsoft that should not have been synced.

When defining a filter to limit the records to be sent to MS, records
already in the filter might already have been synced. We want to
remove those records from the MS Calendar, but only when they have
been created by Odoo.

Unfortunately, we do not always know for sure where records have been
created, but when res_model in calendar.event is filled, it are
definitely Odoo records.
"""
microsoft_service = self._get_microsoft_service()
sender_user = self.env.user
with microsoft_calendar_token(sender_user) as token:
if token and not sender_user.microsoft_synchronization_stopped:
filter_domain = self._get_filter_domain()
if filter_domain:
synchronized_events = self.with_context(
remove_events_not_in_filter=True
)._get_microsoft_records_to_sync()
# Events is already filtered on partner and date ranges.
for event in synchronized_events:
# If event is not in filter, remove it:
event_domain = AND([filter_domain, [("id", "=", event.id)]])
if self.search(event_domain, limit=1):
continue
try:
microsoft_service.delete(
# ms_organizer_event_id is computed from microsoft_id.
event.ms_organizer_event_id,
token=token,
timeout=15,
)
event.write(
{
"microsoft_id": False,
"need_sync_m": False,
"microsoft_recurrence_master_id": False,
}
)
except Exception:
_logger.warn(
"Could not remove event %(event)s from MS Calendar",
{"event": event.name},
)

def _extend_microsoft_domain(self, domain):
"""Only need this for simple calendar.event.

Recurrent events are not sent to Outlook anyway.
"""
if self.env.context.get("remove_events_not_in_filter", False):
# Find Odoo records that have already been synced.
return AND(
[
domain,
[
("microsoft_id", "!=", False),
("res_model", "!=", False),
],
]
)
filter_domain = self._get_filter_domain()
extended_domain = super()._extend_microsoft_domain(domain)
if not filter_domain:
return extended_domain
return AND([extended_domain, filter_domain])

def _get_filter_domain(self):
"""Get filter domain. Return [] if not set."""
ICP = self.env["ir.config_parameter"].sudo()
extra_filter = ICP.get_param(FILTER_ODOO_EVENTS)
domain_text = extra_filter.strip() if extra_filter else ""
if domain_text in ("", "[]"):
return extended_domain
filter_domain = safe_eval(domain_text)
return AND([extended_domain, filter_domain])
return []
return safe_eval(domain_text)
4 changes: 3 additions & 1 deletion microsoft_calendar_filter/models/res_config_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ class ResConfigSettings(models.TransientModel):
config_parameter=FILTER_ODOO_EVENTS,
default="[]",
help="Limit Odoo events synchronized to records satisfying domain."
" When no filter is set, all Odoo events will be synchronized.",
" When no filter is set, all Odoo events will be synchronized."
" When a filter is set, events NOT satisfying the domain, but"
" already synchronized, will be removed from Microsoft",
)
1 change: 1 addition & 0 deletions microsoft_calendar_filter/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

from . import test_filter_odoo_events
from . import test_filter_private_events
from . import test_remove_events_not_in_filter
19 changes: 5 additions & 14 deletions microsoft_calendar_filter/tests/test_filter_private_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@

from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import (
MicrosoftCalendarService,
)
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent

from ..models.res_config_settings import FILTER_PRIVATE_EVENTS
Expand All @@ -26,7 +23,7 @@ def test_filter_private_events(self):
existing_records = Calendar.search([])
expected_event = dict(self.expected_odoo_event_from_outlook, user_id=False)
microsoft_events = self._get_public_and_private_event()
self._synchronize_events(microsoft_events)
Calendar.with_user(self.organizer_user)._sync_microsoft2odoo(microsoft_events)
# Only the public event should have been synchronized.
records = Calendar.search([])
new_records = records - existing_records
Expand All @@ -39,27 +36,21 @@ def test_remove_ms_private_events(self):
Calendar = self.env["calendar.event"]
existing_records = Calendar.search([])
public_event_dict = self._get_public_event()
self._synchronize_events(MicrosoftEvent([public_event_dict]))
microsoft_events = MicrosoftEvent([public_event_dict])
Calendar.with_user(self.organizer_user)._sync_microsoft2odoo(microsoft_events)
records = Calendar.search([])
new_records = records - existing_records
self.assertEqual(len(new_records), 1)
self.assertEqual(new_records.privacy, "public")
# Make the public event private and test removal
to_be_removed_id = new_records.id
public_event_dict["sensitivity"] = "private"
self._synchronize_events(MicrosoftEvent([public_event_dict]))
microsoft_events = MicrosoftEvent([public_event_dict])
Calendar.with_user(self.organizer_user)._sync_microsoft2odoo(microsoft_events)
# The record should have been removed now.
record_still_there = Calendar.search([("id", "=", to_be_removed_id)])
self.assertEqual(len(record_still_there), 0)

@patch.object(MicrosoftCalendarService, "get_events")
def _synchronize_events(self, microsoft_events, mock_get_events):
"""Synchronize the given MS events."""
mock_get_events.return_value = (microsoft_events, None)
self.organizer_user.with_user(
self.organizer_user
).sudo()._sync_microsoft_calendar()

def test_filter_events(self):
CalendarSync = self.env["microsoft.calendar.sync"]
microsoft_events = self._get_public_and_private_event()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from datetime import date, datetime
from unittest.mock import ANY, patch

from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.tests.common import (
TestCommon,
mock_get_token,
patch_api,
)
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import (
MicrosoftCalendarService,
)

from ..models.res_config_settings import FILTER_ODOO_EVENTS


@patch.object(User, "_get_microsoft_calendar_token", mock_get_token)
class TestRemoveEventsNotInFilter(TestCommon):
@patch_api
def setUp(self):
super(TestRemoveEventsNotInFilter, self).setUp()
self.ICP = self.env["ir.config_parameter"].sudo()
self.create_events_for_tests()
res_model_id = self.env["ir.model"]._get_id("res.partner")
year = date.today().year
self.simple_event.write(
{
"res_model_id": res_model_id,
"res_model": "res.partner",
"res_id": self.organizer_user.partner_id.id,
"start": datetime(year, 1, 15, 8, 0),
"stop": datetime(year, 1, 15, 18, 0),
}
)

@patch.object(MicrosoftCalendarService, "delete")
def test_delete_simple_event_from_odoo_organizer_calendar(self, mock_delete):
# We now define to exclude res.partner related events from synchronization.
self.ICP.set_param(
FILTER_ODOO_EVENTS,
"[('res_model', '!=', 'res.partner')]",
)
event_id = self.simple_event.ms_organizer_event_id
# Now remove the event that no longer satisfies the filter.
self.simple_event.with_user(self.organizer_user)._remove_events_not_in_filter()
self.assertFalse(self.simple_event.microsoft_id)
mock_delete.assert_called_once_with(
event_id, token=mock_get_token(self.organizer_user), timeout=ANY
)