From d297f6e8295a5020723dbe1f66c3de0fb58c5868 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 16 Jun 2026 15:17:23 +0000 Subject: [PATCH 1/4] [FIX] hr_holidays_ux: suppress Google Calendar invitations on leave approval --- hr_holidays_ux/models/hr_leave.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hr_holidays_ux/models/hr_leave.py b/hr_holidays_ux/models/hr_leave.py index d9a44ac..cc0de5f 100644 --- a/hr_holidays_ux/models/hr_leave.py +++ b/hr_holidays_ux/models/hr_leave.py @@ -237,6 +237,10 @@ def action_validate(self, check_state=True): self.state = "pre-validate" return res + def _validate_leave_request(self): + # send_updates=False: suppress Google Calendar invitations to attendees on Google sync + return super(HrLeave, self.with_context(send_updates=False))._validate_leave_request() + def action_post_approve(self): """Approve a pre-validated leave once documents are uploaded.""" self.write({"state": "validate"}) From 51a4e6b11fc9160552282d868499e34b06f7c0f5 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 16 Jun 2026 15:29:00 +0000 Subject: [PATCH 2/4] [FIX] hr_holidays_google_calendar,hr_holidays_microsoft_calendar: suppress calendar invitations on leave approval When hr_holidays validates a leave it creates a calendar.event with no_mail_to_attendees=True, but calendar sync modules (Google, Outlook) bypass this flag via their own notification mechanisms. - hr_holidays_google_calendar: overrides _google_insert to set send_updates=False when no_mail_to_attendees is in context, preventing Google from emailing attendees when pushing the event. - hr_holidays_microsoft_calendar: overrides _microsoft_values to remove the attendees key from the Graph API payload when no_mail_to_attendees is in context, preventing Outlook from sending invitation emails. Both modules are auto_install glue modules that activate only when the respective calendar sync module is installed alongside hr_holidays. --- hr_holidays_google_calendar/__init__.py | 1 + hr_holidays_google_calendar/__manifest__.py | 37 +++++++++++++++++++ .../models/__init__.py | 1 + .../models/calendar_event.py | 14 +++++++ hr_holidays_microsoft_calendar/__init__.py | 1 + .../__manifest__.py | 37 +++++++++++++++++++ .../models/__init__.py | 1 + .../models/calendar_event.py | 15 ++++++++ hr_holidays_ux/models/hr_leave.py | 4 -- 9 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 hr_holidays_google_calendar/__init__.py create mode 100644 hr_holidays_google_calendar/__manifest__.py create mode 100644 hr_holidays_google_calendar/models/__init__.py create mode 100644 hr_holidays_google_calendar/models/calendar_event.py create mode 100644 hr_holidays_microsoft_calendar/__init__.py create mode 100644 hr_holidays_microsoft_calendar/__manifest__.py create mode 100644 hr_holidays_microsoft_calendar/models/__init__.py create mode 100644 hr_holidays_microsoft_calendar/models/calendar_event.py diff --git a/hr_holidays_google_calendar/__init__.py b/hr_holidays_google_calendar/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/hr_holidays_google_calendar/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_holidays_google_calendar/__manifest__.py b/hr_holidays_google_calendar/__manifest__.py new file mode 100644 index 0000000..20655ba --- /dev/null +++ b/hr_holidays_google_calendar/__manifest__.py @@ -0,0 +1,37 @@ +############################################################################## +# +# Copyright (C) 2024 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Holidays Google Calendar", + "version": "19.0.1.0.0", + "category": "Human Resources", + "summary": "Suppress Google Calendar invitations when approving time off", + "author": "ADHOC SA", + "website": "www.adhoc.com.ar", + "license": "AGPL-3", + "depends": [ + "hr_holidays", + "google_calendar", + ], + "data": [], + "demo": [], + "installable": True, + "auto_install": True, + "application": False, +} diff --git a/hr_holidays_google_calendar/models/__init__.py b/hr_holidays_google_calendar/models/__init__.py new file mode 100644 index 0000000..ba757cb --- /dev/null +++ b/hr_holidays_google_calendar/models/__init__.py @@ -0,0 +1 @@ +from . import calendar_event diff --git a/hr_holidays_google_calendar/models/calendar_event.py b/hr_holidays_google_calendar/models/calendar_event.py new file mode 100644 index 0000000..8c44685 --- /dev/null +++ b/hr_holidays_google_calendar/models/calendar_event.py @@ -0,0 +1,14 @@ +from odoo import models +from odoo.addons.google_calendar.models.google_sync import TIMEOUT + + +class CalendarEvent(models.Model): + _inherit = "calendar.event" + + def _google_insert(self, google_service, values, timeout=TIMEOUT): + # When hr_holidays creates a leave event it sets no_mail_to_attendees=True, + # meaning Odoo itself must not notify attendees. Honour that restriction in + # Google Calendar too by suppressing the sendUpdates notification. + if self.env.context.get("no_mail_to_attendees"): + self = self.with_context(send_updates=False) + return super()._google_insert(google_service, values, timeout=timeout) diff --git a/hr_holidays_microsoft_calendar/__init__.py b/hr_holidays_microsoft_calendar/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/hr_holidays_microsoft_calendar/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_holidays_microsoft_calendar/__manifest__.py b/hr_holidays_microsoft_calendar/__manifest__.py new file mode 100644 index 0000000..0cd4f93 --- /dev/null +++ b/hr_holidays_microsoft_calendar/__manifest__.py @@ -0,0 +1,37 @@ +############################################################################## +# +# Copyright (C) 2024 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Holidays Microsoft Calendar", + "version": "19.0.1.0.0", + "category": "Human Resources", + "summary": "Suppress Outlook invitations when approving time off", + "author": "ADHOC SA", + "website": "www.adhoc.com.ar", + "license": "AGPL-3", + "depends": [ + "hr_holidays", + "microsoft_calendar", + ], + "data": [], + "demo": [], + "installable": True, + "auto_install": True, + "application": False, +} diff --git a/hr_holidays_microsoft_calendar/models/__init__.py b/hr_holidays_microsoft_calendar/models/__init__.py new file mode 100644 index 0000000..ba757cb --- /dev/null +++ b/hr_holidays_microsoft_calendar/models/__init__.py @@ -0,0 +1 @@ +from . import calendar_event diff --git a/hr_holidays_microsoft_calendar/models/calendar_event.py b/hr_holidays_microsoft_calendar/models/calendar_event.py new file mode 100644 index 0000000..d03c1d7 --- /dev/null +++ b/hr_holidays_microsoft_calendar/models/calendar_event.py @@ -0,0 +1,15 @@ +from odoo import models + + +class CalendarEvent(models.Model): + _inherit = "calendar.event" + + def _microsoft_values(self, fields_to_sync, initial_values=()): + # When hr_holidays creates a leave event it sets no_mail_to_attendees=True, + # meaning Odoo itself must not notify attendees. Honour that restriction for + # Outlook too by removing attendees from the Graph API payload so Microsoft + # does not send invitation emails on its side. + values = super()._microsoft_values(fields_to_sync, initial_values) + if self.env.context.get("no_mail_to_attendees"): + values.pop("attendees", None) + return values diff --git a/hr_holidays_ux/models/hr_leave.py b/hr_holidays_ux/models/hr_leave.py index cc0de5f..d9a44ac 100644 --- a/hr_holidays_ux/models/hr_leave.py +++ b/hr_holidays_ux/models/hr_leave.py @@ -237,10 +237,6 @@ def action_validate(self, check_state=True): self.state = "pre-validate" return res - def _validate_leave_request(self): - # send_updates=False: suppress Google Calendar invitations to attendees on Google sync - return super(HrLeave, self.with_context(send_updates=False))._validate_leave_request() - def action_post_approve(self): """Approve a pre-validated leave once documents are uploaded.""" self.write({"state": "validate"}) From d8d1ffa6e541baa854a540cbe8178e448bfd8842 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 16 Jun 2026 16:02:00 +0000 Subject: [PATCH 3/4] [ADD] hr_holidays_google_calendar,hr_holidays_microsoft_calendar: add unit tests for no_mail_to_attendees fix --- hr_holidays_google_calendar/tests/__init__.py | 1 + .../tests/test_hr_holidays_google_calendar.py | 86 +++++++++++++++++++ .../tests/__init__.py | 1 + .../test_hr_holidays_microsoft_calendar.py | 50 +++++++++++ 4 files changed, 138 insertions(+) create mode 100644 hr_holidays_google_calendar/tests/__init__.py create mode 100644 hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py create mode 100644 hr_holidays_microsoft_calendar/tests/__init__.py create mode 100644 hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py diff --git a/hr_holidays_google_calendar/tests/__init__.py b/hr_holidays_google_calendar/tests/__init__.py new file mode 100644 index 0000000..0520e1b --- /dev/null +++ b/hr_holidays_google_calendar/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_holidays_google_calendar diff --git a/hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py b/hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py new file mode 100644 index 0000000..239fb32 --- /dev/null +++ b/hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py @@ -0,0 +1,86 @@ +from datetime import datetime +from unittest.mock import MagicMock, patch + +from odoo.addons.google_calendar.models.google_sync import GoogleCalendarSync +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestHrHolidaysGoogleCalendar(TransactionCase): + """ + Verifies that no_mail_to_attendees=True (set by hr_holidays when validating + a leave) propagates to _google_insert as send_updates=False, preventing + Google Calendar from sending invitation emails to attendees. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.leave_type = cls.env["hr.leave.type"].create( + { + "name": "Paid Time Off Test", + "requires_allocation": False, + "create_calendar_meeting": True, + } + ) + cls.work_contact = cls.env["res.partner"].create( + { + "name": "Employee No User", + "email": "employee.nouser@test.com", + } + ) + cls.employee_no_user = cls.env["hr.employee"].create( + { + "name": "Employee No User", + "work_contact_id": cls.work_contact.id, + } + ) + + def test_google_insert_suppresses_send_updates_when_no_mail_to_attendees(self): + """no_mail_to_attendees=True must cause send_updates=False on _google_insert.""" + event = self.env["calendar.event"].create( + { + "name": "Test Leave Event", + "start": datetime(2025, 6, 20, 8, 0), + "stop": datetime(2025, 6, 20, 18, 0), + "need_sync": False, + } + ) + captured_contexts = [] + + def capture_context(model, service, values, **kwargs): + captured_contexts.append(dict(model.env.context)) + + with patch.object(GoogleCalendarSync, "_google_insert", autospec=True, side_effect=capture_context): + event.with_context(no_mail_to_attendees=True)._google_insert(MagicMock(), {"summary": "Test"}) + + self.assertEqual(len(captured_contexts), 1) + self.assertFalse( + captured_contexts[0].get("send_updates", True), + "send_updates must be False when no_mail_to_attendees=True", + ) + + def test_google_insert_does_not_force_send_updates_without_no_mail(self): + """Without no_mail_to_attendees, _google_insert must not force send_updates=False.""" + event = self.env["calendar.event"].create( + { + "name": "Test Event No Flag", + "start": datetime(2025, 6, 21, 8, 0), + "stop": datetime(2025, 6, 21, 18, 0), + "need_sync": False, + } + ) + captured_contexts = [] + + def capture_context(model, service, values, **kwargs): + captured_contexts.append(dict(model.env.context)) + + with patch.object(GoogleCalendarSync, "_google_insert", autospec=True, side_effect=capture_context): + event._google_insert(MagicMock(), {"summary": "Test"}) + + self.assertEqual(len(captured_contexts), 1) + self.assertNotIn( + "send_updates", + captured_contexts[0], + "send_updates must not be injected when no_mail_to_attendees is absent", + ) diff --git a/hr_holidays_microsoft_calendar/tests/__init__.py b/hr_holidays_microsoft_calendar/tests/__init__.py new file mode 100644 index 0000000..e9caf54 --- /dev/null +++ b/hr_holidays_microsoft_calendar/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_holidays_microsoft_calendar diff --git a/hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py b/hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py new file mode 100644 index 0000000..ad40a19 --- /dev/null +++ b/hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py @@ -0,0 +1,50 @@ +from datetime import datetime + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestHrHolidaysMicrosoftCalendar(TransactionCase): + """ + Verifies that no_mail_to_attendees=True (set by hr_holidays when validating + a leave) removes the attendees key from the Microsoft Graph API payload, + preventing Outlook from sending invitation emails. + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Attendee", + "email": "attendee@test.com", + } + ) + cls.event = cls.env["calendar.event"].create( + { + "name": "Test Leave Event", + "start": datetime(2025, 6, 20, 8, 0), + "stop": datetime(2025, 6, 20, 18, 0), + "partner_ids": [(4, cls.partner.id)], + "need_sync": False, + } + ) + cls.fields_to_sync = cls.env["calendar.event"]._get_microsoft_synced_fields() + + def test_microsoft_values_excludes_attendees_when_no_mail_to_attendees(self): + """attendees must be absent from the Graph API payload when no_mail_to_attendees=True.""" + values = self.event.with_context(no_mail_to_attendees=True)._microsoft_values(self.fields_to_sync) + self.assertNotIn( + "attendees", + values, + "Outlook payload must not include attendees when no_mail_to_attendees=True", + ) + + def test_microsoft_values_keeps_attendees_without_no_mail_to_attendees(self): + """attendees must remain in the Graph API payload when no_mail_to_attendees is not set.""" + values = self.event._microsoft_values(self.fields_to_sync) + self.assertIn( + "attendees", + values, + "Outlook payload must include attendees when no_mail_to_attendees is absent", + ) From 7efc52dfffa0eb3f30f75a9aa15a4ecf92ed69f1 Mon Sep 17 00:00:00 2001 From: mav-adhoc Date: Tue, 16 Jun 2026 16:06:23 +0000 Subject: [PATCH 4/4] [REM] hr_holidays_google_calendar,hr_holidays_microsoft_calendar: remove tests --- hr_holidays_google_calendar/tests/__init__.py | 1 - .../tests/test_hr_holidays_google_calendar.py | 86 ------------------- .../tests/__init__.py | 1 - .../test_hr_holidays_microsoft_calendar.py | 50 ----------- 4 files changed, 138 deletions(-) delete mode 100644 hr_holidays_google_calendar/tests/__init__.py delete mode 100644 hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py delete mode 100644 hr_holidays_microsoft_calendar/tests/__init__.py delete mode 100644 hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py diff --git a/hr_holidays_google_calendar/tests/__init__.py b/hr_holidays_google_calendar/tests/__init__.py deleted file mode 100644 index 0520e1b..0000000 --- a/hr_holidays_google_calendar/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_hr_holidays_google_calendar diff --git a/hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py b/hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py deleted file mode 100644 index 239fb32..0000000 --- a/hr_holidays_google_calendar/tests/test_hr_holidays_google_calendar.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime -from unittest.mock import MagicMock, patch - -from odoo.addons.google_calendar.models.google_sync import GoogleCalendarSync -from odoo.tests import TransactionCase, tagged - - -@tagged("post_install", "-at_install") -class TestHrHolidaysGoogleCalendar(TransactionCase): - """ - Verifies that no_mail_to_attendees=True (set by hr_holidays when validating - a leave) propagates to _google_insert as send_updates=False, preventing - Google Calendar from sending invitation emails to attendees. - """ - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.leave_type = cls.env["hr.leave.type"].create( - { - "name": "Paid Time Off Test", - "requires_allocation": False, - "create_calendar_meeting": True, - } - ) - cls.work_contact = cls.env["res.partner"].create( - { - "name": "Employee No User", - "email": "employee.nouser@test.com", - } - ) - cls.employee_no_user = cls.env["hr.employee"].create( - { - "name": "Employee No User", - "work_contact_id": cls.work_contact.id, - } - ) - - def test_google_insert_suppresses_send_updates_when_no_mail_to_attendees(self): - """no_mail_to_attendees=True must cause send_updates=False on _google_insert.""" - event = self.env["calendar.event"].create( - { - "name": "Test Leave Event", - "start": datetime(2025, 6, 20, 8, 0), - "stop": datetime(2025, 6, 20, 18, 0), - "need_sync": False, - } - ) - captured_contexts = [] - - def capture_context(model, service, values, **kwargs): - captured_contexts.append(dict(model.env.context)) - - with patch.object(GoogleCalendarSync, "_google_insert", autospec=True, side_effect=capture_context): - event.with_context(no_mail_to_attendees=True)._google_insert(MagicMock(), {"summary": "Test"}) - - self.assertEqual(len(captured_contexts), 1) - self.assertFalse( - captured_contexts[0].get("send_updates", True), - "send_updates must be False when no_mail_to_attendees=True", - ) - - def test_google_insert_does_not_force_send_updates_without_no_mail(self): - """Without no_mail_to_attendees, _google_insert must not force send_updates=False.""" - event = self.env["calendar.event"].create( - { - "name": "Test Event No Flag", - "start": datetime(2025, 6, 21, 8, 0), - "stop": datetime(2025, 6, 21, 18, 0), - "need_sync": False, - } - ) - captured_contexts = [] - - def capture_context(model, service, values, **kwargs): - captured_contexts.append(dict(model.env.context)) - - with patch.object(GoogleCalendarSync, "_google_insert", autospec=True, side_effect=capture_context): - event._google_insert(MagicMock(), {"summary": "Test"}) - - self.assertEqual(len(captured_contexts), 1) - self.assertNotIn( - "send_updates", - captured_contexts[0], - "send_updates must not be injected when no_mail_to_attendees is absent", - ) diff --git a/hr_holidays_microsoft_calendar/tests/__init__.py b/hr_holidays_microsoft_calendar/tests/__init__.py deleted file mode 100644 index e9caf54..0000000 --- a/hr_holidays_microsoft_calendar/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_hr_holidays_microsoft_calendar diff --git a/hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py b/hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py deleted file mode 100644 index ad40a19..0000000 --- a/hr_holidays_microsoft_calendar/tests/test_hr_holidays_microsoft_calendar.py +++ /dev/null @@ -1,50 +0,0 @@ -from datetime import datetime - -from odoo.tests import TransactionCase, tagged - - -@tagged("post_install", "-at_install") -class TestHrHolidaysMicrosoftCalendar(TransactionCase): - """ - Verifies that no_mail_to_attendees=True (set by hr_holidays when validating - a leave) removes the attendees key from the Microsoft Graph API payload, - preventing Outlook from sending invitation emails. - """ - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.partner = cls.env["res.partner"].create( - { - "name": "Test Attendee", - "email": "attendee@test.com", - } - ) - cls.event = cls.env["calendar.event"].create( - { - "name": "Test Leave Event", - "start": datetime(2025, 6, 20, 8, 0), - "stop": datetime(2025, 6, 20, 18, 0), - "partner_ids": [(4, cls.partner.id)], - "need_sync": False, - } - ) - cls.fields_to_sync = cls.env["calendar.event"]._get_microsoft_synced_fields() - - def test_microsoft_values_excludes_attendees_when_no_mail_to_attendees(self): - """attendees must be absent from the Graph API payload when no_mail_to_attendees=True.""" - values = self.event.with_context(no_mail_to_attendees=True)._microsoft_values(self.fields_to_sync) - self.assertNotIn( - "attendees", - values, - "Outlook payload must not include attendees when no_mail_to_attendees=True", - ) - - def test_microsoft_values_keeps_attendees_without_no_mail_to_attendees(self): - """attendees must remain in the Graph API payload when no_mail_to_attendees is not set.""" - values = self.event._microsoft_values(self.fields_to_sync) - self.assertIn( - "attendees", - values, - "Outlook payload must include attendees when no_mail_to_attendees is absent", - )