diff --git a/microsoft_calendar_logging/README.rst b/microsoft_calendar_logging/README.rst new file mode 100644 index 00000000..15d3f115 --- /dev/null +++ b/microsoft_calendar_logging/README.rst @@ -0,0 +1,91 @@ +========================================== +Microsoft Calendar Synchronization Logging +========================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3a3a56b20dd48418b39f82f6627a4a52938888c71710efac94d5a31f333c1278 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcalendar-lightgray.png?logo=github + :target: https://github.com/OCA/calendar/tree/16.0/microsoft_calendar_logging + :alt: OCA/calendar +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/calendar-16-0/calendar-16-0-microsoft_calendar_logging + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/calendar&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +When debugging Microsoft Calender synchronization, it would be usefull to +know wether events where added or updated from microsoft, or the other way +around, and when this happened. + +Synchronization events will be logged on the record , except for deletions, +that for obvious reasons will be logged in the system log. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Therp BV + +Contributors +~~~~~~~~~~~~ + +* `Therp BV `_: + + * Ronald Portier (NL66278) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-NL66278| image:: https://github.com/NL66278.png?size=40px + :target: https://github.com/NL66278 + :alt: NL66278 + +Current `maintainer `__: + +|maintainer-NL66278| + +This module is part of the `OCA/calendar `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/microsoft_calendar_logging/__init__.py b/microsoft_calendar_logging/__init__.py new file mode 100644 index 00000000..31660d6a --- /dev/null +++ b/microsoft_calendar_logging/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/microsoft_calendar_logging/__manifest__.py b/microsoft_calendar_logging/__manifest__.py new file mode 100644 index 00000000..51c7b01c --- /dev/null +++ b/microsoft_calendar_logging/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Microsoft Calendar Synchronization Logging", + "summary": "Log synchronization from Microsoft to Odoo and vice versa", + "version": "16.0.1.0.0", + "category": "Appointments", + "website": "https://github.com/OCA/calendar", + "author": "Odoo Community Association (OCA), Therp BV", + "maintainers": ["NL66278"], + "license": "AGPL-3", + "depends": [ + "microsoft_calendar", + ], + "data": [ + "views/calendar_event_views.xml", + ], +} diff --git a/microsoft_calendar_logging/models/__init__.py b/microsoft_calendar_logging/models/__init__.py new file mode 100644 index 00000000..46c809d1 --- /dev/null +++ b/microsoft_calendar_logging/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import microsoft_calendar_sync diff --git a/microsoft_calendar_logging/models/microsoft_calendar_sync.py b/microsoft_calendar_logging/models/microsoft_calendar_sync.py new file mode 100644 index 00000000..d26ee0bc --- /dev/null +++ b/microsoft_calendar_logging/models/microsoft_calendar_sync.py @@ -0,0 +1,89 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging +import pprint + +from odoo import api, fields, models + +from odoo.addons.microsoft_account.models.microsoft_service import TIMEOUT + +_logger = logging.getLogger(__name__) + + +class MicrosoftCalendarSync(models.AbstractModel): + """Track times and direction of synchronization.""" + + _inherit = "microsoft.calendar.sync" + + created_by_synchronization = fields.Boolean( + default=False, + readonly=True, + ) + datetime_updated_from_microsoft = fields.Datetime() + datetime_created_on_microsoft = fields.Datetime() + datetime_updated_on_microsoft = fields.Datetime() + + def _sync_recurrence_microsoft2odoo(self, microsoft_events, new_events=None): + synced_recurrences, updated_events = super()._sync_recurrence_microsoft2odoo( + microsoft_events, new_events=new_events + ) + if synced_recurrences: + synced_recurrences.write({"created_by_synchronization": True}) + return synced_recurrences, updated_events + + @api.model + def _create_from_microsoft(self, microsoft_event, vals_list): + for vals in vals_list: + vals["created_by_synchronization"] = True + return super()._create_from_microsoft(microsoft_event, vals_list) + + def _write_from_microsoft(self, microsoft_event, vals): + vals["datetime_updated_from_microsoft"] = fields.Datetime.now() + return super()._write_from_microsoft(microsoft_event, vals) + + def _cancel_microsoft(self): + for this in self: + _logger.info( + "Microsoft deleted %(model)s, (%(id)s, %(name)s)", + {"model": this._name, "id": this.id, "name": this.name}, + ) + return super()._cancel_microsoft() + + def _microsoft_delete(self, user_id, event_id, timeout=TIMEOUT): + """Log deletion of microsoft record, for either unlinked or archived record.""" + # Actual delete on microsoft will run after commit. + for this in self: + _logger.info( + "Deleting event on microsoft for %(model)s, (%(id)s, %(name)s)", + {"model": this._name, "id": this.id, "name": this.name}, + ) + return super()._microsoft_delete(user_id, event_id, timeout=timeout) + + def _microsoft_patch(self, user_id, event_id, values, timeout=TIMEOUT): + """Mark time record last updated on microsoft from Odoo.""" + # Actual update on microsoft will run after commit. + super().write({"datetime_updated_on_microsoft": fields.Datetime.now()}) + for this in self: + _logger.info( + "Updating event on microsoft for %(model)s, (%(id)s, %(name)s)", + {"model": this._name, "id": this.id, "name": this.name}, + ) + _logger.debug( + "Updating event with values: %(values)s", + {"values": pprint.pformat(values)}, + ) + return super()._microsoft_patch(user_id, event_id, values, timeout=timeout) + + def _microsoft_insert(self, values, timeout=TIMEOUT): + """Mark time record created on microsoft from Odoo.""" + # Actual insert on microsoft will run after commit. + super().write({"datetime_created_on_microsoft": fields.Datetime.now()}) + for this in self: + _logger.info( + "Inserting event on microsoft for %(model)s, (%(id)s, %(name)s)", + {"model": this._name, "id": this.id, "name": this.name}, + ) + _logger.debug( + "Inserting with values: %(values)s", {"values": pprint.pformat(values)} + ) + return super()._microsoft_insert(values, timeout=timeout) diff --git a/microsoft_calendar_logging/readme/CONTRIBUTORS.rst b/microsoft_calendar_logging/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..a5e152b7 --- /dev/null +++ b/microsoft_calendar_logging/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Therp BV `_: + + * Ronald Portier (NL66278) diff --git a/microsoft_calendar_logging/readme/DESCRIPTION.rst b/microsoft_calendar_logging/readme/DESCRIPTION.rst new file mode 100644 index 00000000..d3b26fc7 --- /dev/null +++ b/microsoft_calendar_logging/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +When debugging Microsoft Calender synchronization, it would be usefull to +know wether events where added or updated from microsoft, or the other way +around, and when this happened. + +Synchronization events will be logged on the record , except for deletions, +that for obvious reasons will be logged in the system log. diff --git a/microsoft_calendar_logging/static/description/index.html b/microsoft_calendar_logging/static/description/index.html new file mode 100644 index 00000000..d3d9c428 --- /dev/null +++ b/microsoft_calendar_logging/static/description/index.html @@ -0,0 +1,432 @@ + + + + + +Microsoft Calendar Synchronization Logging + + + +
+

Microsoft Calendar Synchronization Logging

+ + +

Beta License: AGPL-3 OCA/calendar Translate me on Weblate Try me on Runboat

+

When debugging Microsoft Calender synchronization, it would be usefull to +know wether events where added or updated from microsoft, or the other way +around, and when this happened.

+

Synchronization events will be logged on the record , except for deletions, +that for obvious reasons will be logged in the system log.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Therp BV
  • +
+
+
+

Contributors

+
    +
  • Therp BV:
      +
    • Ronald Portier (NL66278)
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

NL66278

+

This module is part of the OCA/calendar project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/microsoft_calendar_logging/tests/__init__.py b/microsoft_calendar_logging/tests/__init__.py new file mode 100644 index 00000000..1bfcb395 --- /dev/null +++ b/microsoft_calendar_logging/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_synchronization_logging diff --git a/microsoft_calendar_logging/tests/test_synchronization_logging.py b/microsoft_calendar_logging/tests/test_synchronization_logging.py new file mode 100644 index 00000000..5df69473 --- /dev/null +++ b/microsoft_calendar_logging/tests/test_synchronization_logging.py @@ -0,0 +1,154 @@ +# Copyright 2025 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from unittest.mock import ANY, patch + +from freezegun import freeze_time + +from odoo.addons.microsoft_account.models.microsoft_service import MicrosoftService +from odoo.addons.microsoft_calendar.models.res_users import User +from odoo.addons.microsoft_calendar.tests.common import ( + TestCommon, + _modified_date_in_the_future, + mock_get_token, +) +from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids +from odoo.addons.microsoft_calendar.utils.microsoft_calendar import ( + MicrosoftCalendarService, +) +from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent + + +@patch.object(User, "_get_microsoft_calendar_token", mock_get_token) +class TestSynchronizationLogging(TestCommon): + @freeze_time("2021-09-22") + def test_microsoft2odoo(self): + expected_event = dict(self.expected_odoo_event_from_outlook, user_id=False) + new_record = self._get_new_record_from_microsoft() + self.assertEqual(len(new_record), 1) + self.assertTrue(new_record.created_by_synchronization) + self.assert_odoo_event(new_record, expected_event) + # Modify the event. + modified_event = self._get_modifield_event(new_record) + self._synchronize_events(modified_event) + new_record.invalidate_recordset(["datetime_updated_from_microsoft"]) + self.assertTrue(new_record.datetime_updated_from_microsoft) # Should be set + self.assertEqual(new_record.name, "Update simple event") + + @freeze_time("2021-09-22") + @patch.object(MicrosoftService, "_do_request") + def test_odoo2microsoft_create(self, mock_do_request): + Calendar = self.env["calendar.event"] + oca_days = Calendar.create( + { + "name": "OCA Days", + "description": "

OCA Days

", + "location": "Luik/Liège", + "active": True, + "start": self.start_date, + "stop": self.end_date, + "user_id": self.organizer_user.id, + "partner_ids": [ + self.organizer_user.partner_id.id, + self.attendee_user.partner_id.id, + ], + } + ) + self.assertFalse(oca_days.created_by_synchronization) + mock_do_request.return_value = (200, {"id": 1, "iCalUId": 2}, None) + oca_days._sync_odoo2microsoft() + self.assertTrue(oca_days.datetime_created_on_microsoft) # Should be set + + @freeze_time("2021-09-22") + @patch.object(MicrosoftService, "_do_request") + def test_odoo2microsoft_update(self, mock_do_request): + # We test with an odoo event that is created from a + # microsoft event, because for a succesfull update a + # microsoft_id is required. Normall when an event is + # created on Odoo, it is first send to MS. Then in the + # next synchronization, the microsoft_id will be retrieved + # and the Odoo event updated with its value. + oca_days = self._get_new_record_from_microsoft() + self.assertEqual(len(oca_days), 1) + oca_days.write({"name": "The Great OCA Days"}) + self.assertTrue(oca_days.need_sync_m) + mock_do_request.return_value = (200, True, None) + oca_days.with_user(self.organizer_user)._sync_odoo2microsoft() + self.assertTrue(oca_days.datetime_updated_on_microsoft) # Should be set + + def _get_new_record_from_microsoft(self): + """Create a record from microsoft event.""" + Calendar = self.env["calendar.event"] + existing_records = Calendar.search([]) + microsoft_events = self._get_test_events() + self._synchronize_events(microsoft_events) + # We should have a new event. + records = Calendar.search([]) + new_record = records - existing_records + return new_record + + @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 _get_test_events(self): + public_event_data = self._get_public_event_data() + return MicrosoftEvent([public_event_data]) + + def _get_public_event_data(self): + return dict( + self.simple_event_from_outlook_attendee, + organizer={ + "emailAddress": {"address": "john.doe@odoo.com", "name": "John Doe"}, + }, + ) + + def _get_modifield_event(self, odoo_event): + modified_data = dict( + self._get_public_event_data(), + subject="Update simple event", + lastModifiedDateTime=_modified_date_in_the_future(odoo_event), + ) + return MicrosoftEvent([modified_data]) + + @patch.object(MicrosoftCalendarService, "delete") + def test_delete_simple_event_from_odoo_organizer_calendar(self, mock_delete): + # Copied (modified) from Odoo to check delete still works. + simple_event = self._get_simple_event() + event_id = simple_event.ms_organizer_event_id + simple_event.with_user(self.organizer_user).unlink() + self.call_post_commit_hooks() + simple_event.invalidate_recordset() + self.assertFalse(simple_event.exists()) + mock_delete.assert_called_once_with( + event_id, token=mock_get_token(self.organizer_user), timeout=ANY + ) + + @patch.object(MicrosoftCalendarService, "get_events") + def test_cancel_simple_event_from_outlook_organizer_calendar(self, mock_get_events): + # Copied (modified) from Odoo to check delete still works. + simple_event = self._get_simple_event() + event_id = simple_event.ms_organizer_event_id + mock_get_events.return_value = ( + MicrosoftEvent([{"id": event_id, "@removed": {"reason": "deleted"}}]), + None, + ) + self.organizer_user.with_user( + self.organizer_user + ).sudo()._sync_microsoft_calendar() + self.assertFalse(simple_event.exists()) + + def _get_simple_event(self): + Calendar = self.env["calendar.event"] + simple_event = Calendar.search( + [("name", "=", "simple_event")] + ) or Calendar.with_user(self.organizer_user).create( + dict( + self.simple_event_values, + microsoft_id=combine_ids("123", "456"), + ) + ) + return simple_event diff --git a/microsoft_calendar_logging/views/calendar_event_views.xml b/microsoft_calendar_logging/views/calendar_event_views.xml new file mode 100644 index 00000000..bcbb8a35 --- /dev/null +++ b/microsoft_calendar_logging/views/calendar_event_views.xml @@ -0,0 +1,24 @@ + + + + + calendar.event.form - microsoft_calendar_logging + calendar.event + + + + + + + + + + + + + + + + + + diff --git a/setup/microsoft_calendar_logging/odoo/addons/microsoft_calendar_logging b/setup/microsoft_calendar_logging/odoo/addons/microsoft_calendar_logging new file mode 120000 index 00000000..e73dd4f4 --- /dev/null +++ b/setup/microsoft_calendar_logging/odoo/addons/microsoft_calendar_logging @@ -0,0 +1 @@ +../../../../microsoft_calendar_logging \ No newline at end of file diff --git a/setup/microsoft_calendar_logging/setup.py b/setup/microsoft_calendar_logging/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/microsoft_calendar_logging/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)