From fe6f582046dfb54aeca2f7c1fa4c9331606ac20a Mon Sep 17 00:00:00 2001 From: Ronald Portier Date: Wed, 28 Jan 2026 19:06:35 +0100 Subject: [PATCH] [IMP] ms_calendar_filter: remove events not in filter --- .../models/calendar_event.py | 82 ++++++++++++++++++- .../models/res_config_settings.py | 4 +- microsoft_calendar_filter/tests/__init__.py | 1 + .../tests/test_filter_private_events.py | 19 ++--- .../tests/test_remove_events_not_in_filter.py | 51 ++++++++++++ 5 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 microsoft_calendar_filter/tests/test_remove_events_not_in_filter.py diff --git a/microsoft_calendar_filter/models/calendar_event.py b/microsoft_calendar_filter/models/calendar_event.py index 6206e853..d49fa309 100644 --- a/microsoft_calendar_filter/models/calendar_event.py +++ b/microsoft_calendar_filter/models/calendar_event.py @@ -1,25 +1,101 @@ # Copyright 2026 Therp BV . # 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) diff --git a/microsoft_calendar_filter/models/res_config_settings.py b/microsoft_calendar_filter/models/res_config_settings.py index 30c9c617..4a8c85ea 100644 --- a/microsoft_calendar_filter/models/res_config_settings.py +++ b/microsoft_calendar_filter/models/res_config_settings.py @@ -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", ) diff --git a/microsoft_calendar_filter/tests/__init__.py b/microsoft_calendar_filter/tests/__init__.py index b219ac8b..989a32c1 100644 --- a/microsoft_calendar_filter/tests/__init__.py +++ b/microsoft_calendar_filter/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_filter_odoo_events from . import test_filter_private_events +from . import test_remove_events_not_in_filter diff --git a/microsoft_calendar_filter/tests/test_filter_private_events.py b/microsoft_calendar_filter/tests/test_filter_private_events.py index bc0ecbde..5b6adab6 100644 --- a/microsoft_calendar_filter/tests/test_filter_private_events.py +++ b/microsoft_calendar_filter/tests/test_filter_private_events.py @@ -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 @@ -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 @@ -39,7 +36,8 @@ 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) @@ -47,19 +45,12 @@ def test_remove_ms_private_events(self): # 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() diff --git a/microsoft_calendar_filter/tests/test_remove_events_not_in_filter.py b/microsoft_calendar_filter/tests/test_remove_events_not_in_filter.py new file mode 100644 index 00000000..0a4b9b35 --- /dev/null +++ b/microsoft_calendar_filter/tests/test_remove_events_not_in_filter.py @@ -0,0 +1,51 @@ +# Copyright 2026 Therp BV . +# 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 + )