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
95 changes: 95 additions & 0 deletions microsoft_calendar_filters/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
=========================
Microsoft Calendar Filter
=========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c5bc398294a279702feaeead1ff962c9ee7b754a6bae724cf67ae113fafff013
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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_filters
: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_filters
: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|

The module at the moment offers to options:

#. Do not synchronize private events on Outlook to Odoo,
delete those that already have been synchronized.
#. Set a domain to limit the synchronization from Odoo to
Outlook to calender events satisfying that domain.

Just installing the module should not change anything. The
options have to be configured in the General Settings under
Integrations.

**Table of contents**

.. contents::
:local:

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/calendar/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 <https://github.com/OCA/calendar/issues/new?body=module:%20microsoft_calendar_filters%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
~~~~~~~

* Therp BV

Contributors
~~~~~~~~~~~~

* `Therp BV <https://therp.nl>`_:

* 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 <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-NL66278|

This module is part of the `OCA/calendar <https://github.com/OCA/calendar/tree/16.0/microsoft_calendar_filters>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
3 changes: 3 additions & 0 deletions microsoft_calendar_filters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import models
19 changes: 19 additions & 0 deletions microsoft_calendar_filters/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2025-2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Microsoft Calendar Filter",
"summary": "Limit the records that are synchronized from Outlook to Odoo",
"version": "16.0.1.1.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/res_config_settings_views.xml",
],
}
148 changes: 148 additions & 0 deletions microsoft_calendar_filters/doc/microsoft-odoo-calender-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
Microsoft -- Odoo Calender synchronization, how it works

# Preparation

Odoo sysadmin must connect server with Microsoft.

Follow the instructions in the link below to create a registered Microsoft Application
or Entra ID, which should result in a client ID and a client secret.

<https://www.odoo.com/documentation/18.0/applications/productivity/calendar/outlook.html>

Then in General Settings, under Integrations it is possible to enable Outlook Calender
and then fill in the Client ID and secret.

There is a cron for the Outlook Calender synchronization that needs to be configured to
run when and how often.

There are a few settings for finetuning, but basically this completes the general server
configuration.

Each user will have, if so desired, to activate (or stop) the calender synchronization
for his/her/its own calendar. To use that they need to go to the calender view of the
calendar app. In the sidebar on the right, under the month overview, there are buttons
to login to either google or outlook, so activate the synchronization.

Activation will set fields in the res.users model:

In microsoft_account module the fields are defined for the authentication of the user:

microsoft_calendar_rtoken

microsoft_calendar_token

microsoft_calendar_token_validity

Defined in microsoft_calendar module:

microsoft_calendar_sync_token: will be used to limit synchronization to what still needs
to be done;

microsoft_synchronization_stopped: will be set to False.

# High level overview of the synchronization

The synchronization is run from the res.users model. The method called is
\_sync_all_microsoft_calendar() which will select all users that have a valid refresh
token (defined in microsoft_account module) and where synchronization has not been
stopped. For all of those users the method \_sync_microsoft_calendar() will be called.

For each user to be synchronized a connection is made with the MS calender service:

calendar_service = self.env\[\"calendar.event\"\].\_get_microsoft_service()

Then either all microsoft events for this user are retrieved (when no sync token in
user), or everything that has changed since the last synchronization. Apart from the
events we get a new synchronization token, that will be used on a next call to not have
a full synchronization each time:

events, next_sync_token =
calendar_service.get_events(self.microsoft_calendar_sync_token, token=token)

If there are any MS events to be synchronized that will be done. So this is **from
Microsoft to Odoo**. This synchronization returns both normal calender events, as well
as recurring events:

synced_events, synced_recurrences =
self.env\[\'calendar.event\'\].\_sync_microsoft2odoo(events)

Now it is time to synchronize **from Odoo to Microsoft**. This will be done seperately
for recurring events and single calendar events:

First all recurrences that might be synchronized with Microsoft are searched. The
occurences that just have been synced from Microsoft are excluded. The synchronization
is run. Any calender events that belong to the recurrences are added to the already
synced events:

recurrences =
self.env\[\'calendar.recurrence\'\].\_get_microsoft_records_to_sync(full_sync=full_sync)

recurrences -= synced_recurrences

recurrences.\_sync_odoo2microsoft()

synced_events \|= recurrences.calendar_event_ids

Now it is time to synchronize the single calendar events, that are not part of a
recurrence, and that have not been synchronized from Microsoft to Odoo:

events =
self.env\[\'calendar.event\'\].\_get_microsoft_records_to_sync(full_sync=full_sync)

(events -- synced_events).\_sync_odoo2microsoft()

# Microsoft Events

To make it easier to deal with events retrieved from Microsoft, or to be created/updated
at Microsoft, these events are stored in an Event object that has many parallels with
the Odoo objects that reflect rows in the postgress database.

The classes are defined in:

odoo/addons/microsoft_calendar/utils/microsoft_event.py

# Synchronization from Microsoft to Odoo

The synchronization from Microsoft to Odoo is defined in a mixin that will be used to
extend both the calendar.event and the calendar.recurrence models:
microsoft.calendar.sync.

The basic sequence is as follows:

Odoo and microsoft events are matched. This separates MS Events that are new, from those
that can be updated.

-- Events cancelled on the Microsoft side are cancelled on Odoo;

-- New events that are not recurrences are added;

-- Recurrences and updated events are gathered;

-- Recurrences and events cancelled on the Odoo site are cancelled on Microsoft

(So this is actually from Odoo to Microsoft!)

-- Looping over the existing events (minus those cancelled), the Odoo events and
recurrences are updated if the update time on Microsoft is after the update time on
Odoo.

(This supposes the clocks on both systems are more or less in sync);

-- The synced events and synced recurrences are returned.

# Synchronization from Odoo to Microsoft

This is defined in the mixin class microsoft.calendar.sync in the method
\_sync_odoo2microsoft.

First all active records are selected, and all the others assigned to cancelled_records.

Then it is determined which records already exist on the Microsoft side. The rest is
new.

The cancelled records that exist on Microsoft are deleted there.

The new records are created on the Microsoft side.

The updated records are checked for the need to synchronize, and if they are the
Microsoft side is updated ('patched').
4 changes: 4 additions & 0 deletions microsoft_calendar_filters/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import microsoft_calendar_sync
from . import res_config_settings
24 changes: 24 additions & 0 deletions microsoft_calendar_filters/models/calendar_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models
from odoo.expression import AND
from odoo.tools.safe_eval import safe_eval


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

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

Recurrent events are not sent to Outlook anyway.
"""
extended_domain = super()._extend_microsoft_domain(domain)
ICP = self.env["ir.config_parameter"].sudo()
extra_filter = ICP.get_param("microsoft_calendar_filter.filter_odoo_events")
domain_text = extra_filter.strip()
if domain_text in ("", "[]"):
return extended_domain
filter_domain = safe_eval(domain_text)
return AND([extended_domain, filter_domain])
39 changes: 39 additions & 0 deletions microsoft_calendar_filters/models/microsoft_calendar_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright 2025 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models

from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent


class MicrosoftCalendarSync(models.AbstractModel):
_inherit = "microsoft.calendar.sync"

@api.model
def _sync_microsoft2odoo(self, microsoft_events: MicrosoftEvent):
ICP = self.env["ir.config_parameter"].sudo()
filter_private = ICP.get_param(
"microsoft_calendar_filter.filter_microsoft_private_events"
)
if bool(filter_private):
self._remove_private_events(microsoft_events)
filtered_microsoft_events = self._filter_microsoft_events(microsoft_events)
return super()._sync_microsoft2odoo(filtered_microsoft_events)

def _remove_private_events(self, microsoft_events):
"""This is to remove events that where public before but now private."""
private_events = microsoft_events.filter(lambda e: e.sensitivity == "private")
simple_events = private_events.filter(lambda e: not e.is_recurrence())
self._remove_recurrent_or_simple_events(simple_events)
recurrent_events = private_events.filter(lambda e: e.is_recurrence())
self._remove_recurrent_or_simple_events(recurrent_events)

def _remove_recurrent_or_simple_events(self, microsoft_events):
"""We need this because _load_odoo_ids_from_db does not accept mixed types."""
mapped_events = microsoft_events._load_odoo_ids_from_db(self.env)
for mevent in mapped_events:
odoo_event = self.browse(mevent.odoo_id(self.env)).exists()
if odoo_event:
odoo_event.unlink()

def _filter_microsoft_events(self, microsoft_events):
return microsoft_events.filter(lambda e: e.sensitivity != "private")
43 changes: 43 additions & 0 deletions microsoft_calendar_filters/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2026 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

filter_microsoft_private_events = fields.Boolean(
config_parameter="filter_microsoft_private_events",
default=False,
help="Do not synchronize microsoft private events to Odoo."
" Actually delete private events that have already been synchronized.",
)
filter_odoo_events = fields.Text(
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.",
)

@api.constrains("filter_odoo_events")
def _check_filter_odoo_events(self):
Calendar = self.env["calendar.event"]
for this in self:
domain_text = this.filter_odoo_events.strip()
if domain_text in ("", "[]"):
continue
try:
domain = safe_eval(domain_text)
Calendar.search(domain, limit=1)
except Exception as exc:
message = exc.msg if hasattr(exc, "msg") else str(exc)
raise ValidationError(
_(
"Domain %(domain_text)s is invalid: %(message)s",
domain_text=domain_text,
message=message,
)
) from exc
3 changes: 3 additions & 0 deletions microsoft_calendar_filters/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* `Therp BV <https://therp.nl>`_:

* Ronald Portier (NL66278)
10 changes: 10 additions & 0 deletions microsoft_calendar_filters/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
The module at the moment offers to options:

#. Do not synchronize private events on Outlook to Odoo,
delete those that already have been synchronized.
#. Set a domain to limit the synchronization from Odoo to
Outlook to calender events satisfying that domain.

Just installing the module should not change anything. The
options have to be configured in the General Settings under
Integrations.
Loading