From a6b1a6d8af25c898c570139f15efa42fb92bf1b1 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Thu, 19 Feb 2026 10:58:02 +0100 Subject: [PATCH 01/24] added volunteer_company_holiday, extended recurrent_generator, draft for _cancel_holiday_shift(), draft for test_company_holiday --- .../odoo/addons/volunteer_holiday | 1 + setup/volunteer_holiday/setup.py | 6 + volunteer_holiday/README.rst | 77 ++++++++++ volunteer_holiday/__init__.py | 5 + volunteer_holiday/__manifest__.py | 28 ++++ volunteer_holiday/controllers/__init__.py | 1 + volunteer_holiday/data/cron.xml | 19 +++ volunteer_holiday/demo/demo.xml | 30 ++++ volunteer_holiday/models/__init__.py | 6 + .../models/volunteer_company_holiday.py | 74 +++++++++ .../volunteer_shift_recurrent_generator.py | 11 ++ .../security/ir.model.access.csv | 4 + volunteer_holiday/tests/__init__.py | 5 + .../tests/test_company_holiday.py | 142 ++++++++++++++++++ .../views/volunteer_company_holiday_view.xml | 27 ++++ volunteer_holiday/views/volunteer_menu.xml | 17 +++ .../views/volunteer_shift_generator_views.xml | 36 +++++ 17 files changed, 489 insertions(+) create mode 120000 setup/volunteer_holiday/odoo/addons/volunteer_holiday create mode 100644 setup/volunteer_holiday/setup.py create mode 100644 volunteer_holiday/README.rst create mode 100644 volunteer_holiday/__init__.py create mode 100644 volunteer_holiday/__manifest__.py create mode 100644 volunteer_holiday/controllers/__init__.py create mode 100644 volunteer_holiday/data/cron.xml create mode 100644 volunteer_holiday/demo/demo.xml create mode 100644 volunteer_holiday/models/__init__.py create mode 100644 volunteer_holiday/models/volunteer_company_holiday.py create mode 100644 volunteer_holiday/models/volunteer_shift_recurrent_generator.py create mode 100644 volunteer_holiday/security/ir.model.access.csv create mode 100644 volunteer_holiday/tests/__init__.py create mode 100644 volunteer_holiday/tests/test_company_holiday.py create mode 100644 volunteer_holiday/views/volunteer_company_holiday_view.xml create mode 100644 volunteer_holiday/views/volunteer_menu.xml create mode 100644 volunteer_holiday/views/volunteer_shift_generator_views.xml diff --git a/setup/volunteer_holiday/odoo/addons/volunteer_holiday b/setup/volunteer_holiday/odoo/addons/volunteer_holiday new file mode 120000 index 000000000..c2314afcc --- /dev/null +++ b/setup/volunteer_holiday/odoo/addons/volunteer_holiday @@ -0,0 +1 @@ +../../../../volunteer_holiday \ No newline at end of file diff --git a/setup/volunteer_holiday/setup.py b/setup/volunteer_holiday/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/volunteer_holiday/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/volunteer_holiday/README.rst b/volunteer_holiday/README.rst new file mode 100644 index 000000000..899623b3f --- /dev/null +++ b/volunteer_holiday/README.rst @@ -0,0 +1,77 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========= +Volunteer +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a3d4eec071c4f3516c0c04b4d8801127c2b5ebd85c4eda456fc0cdc8dfeb8f43 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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-beescoop%2FObeesdoo-lightgray.png?logo=github + :target: https://github.com/beescoop/Obeesdoo/tree/16.0/volunteer + :alt: beescoop/Obeesdoo + +|badge1| |badge2| |badge3| + +Add holidays for companies and volunteers, and manage shifts accordingly. + +**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 +~~~~~~~ + +* Coop IT Easy SC + +Contributors +~~~~~~~~~~~~ + +* `Coop IT Easy SC `_: + + * Geneviève Ernould + +Maintainers +~~~~~~~~~~~ + +.. |maintainer-aydrpm| image:: https://github.com/aydrpm.png?size=40px + :target: https://github.com/aydrpm + :alt: aydrpm +.. |maintainer-remytms| image:: https://github.com/remytms.png?size=40px + :target: https://github.com/remytms + :alt: remytms + +Current maintainers: + +|maintainer-genepi314| |maintainer-remytms| + +This module is part of the `beescoop/Obeesdoo `_ project on GitHub. + +You are welcome to contribute. diff --git a/volunteer_holiday/__init__.py b/volunteer_holiday/__init__.py new file mode 100644 index 000000000..a6c9053e3 --- /dev/null +++ b/volunteer_holiday/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import models diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py new file mode 100644 index 000000000..bb0f3d227 --- /dev/null +++ b/volunteer_holiday/__manifest__.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +{ + "name": "Volunteer Holiday", + "summary": "Add holidays for companies and volunteers", + "version": "16.0.0.5.0", + "category": "Volunteer management", + "website": "https://github.com/beescoop/Obeesdoo", + "author": "Coop IT Easy SC", + "license": "AGPL-3", + "application": False, + "depends": ["base", "volunteer", "mail"], + "data": [ + # "data/cron.xml", + "security/ir.model.access.csv", + "views/volunteer_shift_generator_views.xml", + "views/volunteer_company_holiday_view.xml", + "views/volunteer_menu.xml", + ], + "demo": [], + "assets": { + "web.assets_backend": [ + "volunteer/static/src/scss/volunteer_shift.scss", + ], + }, +} diff --git a/volunteer_holiday/controllers/__init__.py b/volunteer_holiday/controllers/__init__.py new file mode 100644 index 000000000..e046e49fb --- /dev/null +++ b/volunteer_holiday/controllers/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/volunteer_holiday/data/cron.xml b/volunteer_holiday/data/cron.xml new file mode 100644 index 000000000..dde0323cb --- /dev/null +++ b/volunteer_holiday/data/cron.xml @@ -0,0 +1,19 @@ + + + + + + Cancel generated shifts during holidays + + code + model._cancel_holiday_shift() + 24 + hours + -1 + + + diff --git a/volunteer_holiday/demo/demo.xml b/volunteer_holiday/demo/demo.xml new file mode 100644 index 000000000..8b2cb8d36 --- /dev/null +++ b/volunteer_holiday/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/volunteer_holiday/models/__init__.py b/volunteer_holiday/models/__init__.py new file mode 100644 index 000000000..7d4950704 --- /dev/null +++ b/volunteer_holiday/models/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import volunteer_company_holiday +from . import volunteer_shift_recurrent_generator diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py new file mode 100644 index 000000000..cc8dd4374 --- /dev/null +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class VolunteerCompanyHoliday(models.Model): + _name = "volunteer.company.holiday" + _description = "Company Holidays" + _order = "start_date" + + name = fields.Char(required=True) + + # # C'est mal, mais je l'ai mis en commentaire pour faire mon test... + # company_id = fields.Many2one( + # comodel_name="res.company", + # string="Company", + # default=lambda self: self.env.user.company_id, + # # required=True, + # ) + + start_date = fields.Date(required=True) + + end_date = fields.Date(required=True) + + # # Problem with this function, work needed + + # def _cancel_holiday_shift(self, months=3): + # """Cancel shifts if they cover holiday period within time range""" + # # Setting the time range we want to work with + # date_time_range = datetime.today() + relativedelta(months=months) + + # confirmed_future_generated_shifts_in_range = self.env["volunteer.shift"].search( + # [('start_time', '>=', datetime.today()), + # ('start_time', '<=', date_time_range), + # ('generator_id', '!=', None), + # ('state', '=', 'confirmed')] + # ) + + # # Getting list of shifts to be cancel in case of holidays through generator_id + # potential_shifts_to_cancel = [] + # for shift in confirmed_future_generated_shifts_in_range: + # if shift.generator_id.is_maintained_during_holiday == False: + # potential_shifts_to_cancel.append(shift) + + # future_company_holidays_in_range = self.env["volunteer.company.holiday"].search( + # [('start_date', '>=', date.today()), + # ('start_date', '<=', date_time_range.date())] + # ) + + # for shift in potential_shifts_to_cancel: + # for holiday in future_company_holidays_in_range: + # if shift.state != "canceled" and self._shift_covers_holiday( + # shift.start_time, shift.end_time, holiday.start_date, holiday.end_date + # ): + # # Danger ! Corrupted the database : + # # shift.stage_id.state = "canceled" + # shift.write({'state': 'canceled'}) + + # # Wonder if this @api.model creates a problem + # # @api.model + # def _shift_covers_holiday( + # self, shift_start_time, shift_end_time, holiday_start_date, holiday_end_date + # ): + # shift_start_date = shift_start_time.date() + # shift_end_date = shift_end_time.date() + + # if (shift_start_date >= holiday_start_date and shift_start_date <= holiday_end_date): + # return True + # if (shift_end_date >= holiday_start_date and shift_start_date <= holiday_end_date): + # return True + # if (shift_start_date <= holiday_start_date and shift_end_date >= holiday_end_date): + # return True diff --git a/volunteer_holiday/models/volunteer_shift_recurrent_generator.py b/volunteer_holiday/models/volunteer_shift_recurrent_generator.py new file mode 100644 index 000000000..61168748b --- /dev/null +++ b/volunteer_holiday/models/volunteer_shift_recurrent_generator.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class VolunteerShiftRecurrentGenerator(models.Model): + _inherit = "volunteer.shift.recurrent.generator" + + is_maintained_during_holiday = fields.Boolean("Maintain During Holidays") diff --git a/volunteer_holiday/security/ir.model.access.csv b/volunteer_holiday/security/ir.model.access.csv new file mode 100644 index 000000000..f667cc4f3 --- /dev/null +++ b/volunteer_holiday/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_company_holiday_user,CompanyHolidayUser,model_volunteer_company_holiday,volunteer.volunteer_group_user,1,0,0,0 +access_company_holiday_manager,CompanyHolidayManager,model_volunteer_company_holiday,volunteer.volunteer_group_manager,1,1,0,0 +access_company_holiday_admin,CompanyHolidayAdmin,model_volunteer_company_holiday,volunteer.volunteer_group_admin,1,1,1,1 diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py new file mode 100644 index 000000000..5e8e2c9b9 --- /dev/null +++ b/volunteer_holiday/tests/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import test_company_holiday diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py new file mode 100644 index 000000000..f66922b45 --- /dev/null +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import date, datetime + +from odoo.tests.common import TransactionCase + + +class TestCronHoliday(TransactionCase): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + # Force all operations to run as admin + self.env = self.env(user=self.env.ref("base.user_admin")) + + # Set up the environment + self.env = self.env( + context=dict( + self.env.context, + mail_create_nolog=True, + mail_create_nosubscribe=True, + mail_notrack=True, + no_reset_password=True, + tracking_disable=True, + ) + ) + + # Models + + self.Holiday = self.env["volunteer.company.holiday"] + self.Shift = self.env["volunteer.shift"] + self.Type = self.env["volunteer.shift.type"] + self.Generator = self.env["volunteer.shift.recurrent.generator"] + + # Stages + + self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") + self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") + + # Create required type + self.type1 = self.Type.create( + { + "name": "TypeTest", + "description": "Type for autotests", + } + ) + + # Fix number of occurrences for all tests + self.env.company.shift_nb_occurrence = 10 + + # Create recurrent generators + self.gen_with_holiday = self.Generator.create( + { + "name": "GenWithHoliday", + "state": "confirmed", + "until_date": date(2026, 3, 5), + "interval_type": "days", + "interval": 1, + "start_time": datetime(2026, 3, 4, 10, 5), + "end_time": datetime(2026, 3, 4, 12, 5), + "tz": "Europe/Brussels", + "max_volunteer_nb": 6, + "type_id": self.type1.id, + } + ) + + self.gen_without_holiday = self.Generator.create( + { + "name": "genWithoutHoliday", + "state": "confirmed", + "until_date": date(2026, 3, 3), + "interval_type": "days", + "interval": 1, + "start_time": datetime(2026, 3, 1, 10, 5), + "end_time": datetime(2026, 3, 1, 12, 5), + "tz": "Europe/Brussels", + "max_volunteer_nb": 6, + "type_id": self.type1.id, + } + ) + self.gen_overlap_holiday = self.Generator.create( + { + "name": "genOverlapHoliday", + "state": "confirmed", + "until_date": date(2026, 3, 5), + "interval_type": "days", + "interval": 1, + "start_time": datetime(2026, 3, 1, 10, 5), + "end_time": datetime(2026, 3, 1, 12, 5), + "tz": "Europe/Brussels", + "max_volunteer_nb": 6, + "type_id": self.type1.id, + } + ) + + # Create holidays + + self.march_holiday = self.Holiday.create( + { + "name": "marchHolidays", + # "company_id": self.env.user.company_id, + "start_date": date(2026, 3, 4), + "end_date": date(2026, 3, 5), + } + ) + + def test_cancel_holiday_shift(self): + """Test that holidays do cancel confirmed generated shifts""" + + # Start of the test + shifts_with_holiday = self.gen_with_holiday.volunteer_shift_ids + for shift in shifts_with_holiday: + self.assertEqual(shift.state, "confirmed") + + shifts_without_holiday = self.gen_without_holiday.volunteer_shift_ids + for shift in shifts_without_holiday: + self.assertEqual(shift.state, "confirmed") + + shifts_overlap_holiday = self.gen_overlap_holiday.volunteer_shift_ids + for shift in shifts_overlap_holiday: + self.assertEqual(shift.state, "confirmed") + + self.Holiday._cancel_holiday_shift() + + # All shifts in shifts_with_holiday cover the march_holidays period + for shift in shifts_with_holiday: + self.assertEqual(shift.state, "canceled") + + # No shift in shifts_without_holiday cover holiday period + for shift in shifts_without_holiday: + self.assertEqual(shift.state, "confirmed") + + # Certain shifts in shifts_overlap_holiday cover holiday period + for shift in shifts_overlap_holiday: + if ( + shift.start_time.date() == self.march_holiday.start_date + or shift.start_time.date() == self.march_holiday.end_date + ): + self.assertEqual(shift.state, "canceled") + else: + self.assertEqual(shift.state, "confirmed") diff --git a/volunteer_holiday/views/volunteer_company_holiday_view.xml b/volunteer_holiday/views/volunteer_company_holiday_view.xml new file mode 100644 index 000000000..cf1bcb00a --- /dev/null +++ b/volunteer_holiday/views/volunteer_company_holiday_view.xml @@ -0,0 +1,27 @@ + + + + + + Holiday List + volunteer.company.holiday + + + + + + + + + + + + Company Holiday + volunteer.company.holiday + tree + + diff --git a/volunteer_holiday/views/volunteer_menu.xml b/volunteer_holiday/views/volunteer_menu.xml new file mode 100644 index 000000000..051abd850 --- /dev/null +++ b/volunteer_holiday/views/volunteer_menu.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/volunteer_holiday/views/volunteer_shift_generator_views.xml b/volunteer_holiday/views/volunteer_shift_generator_views.xml new file mode 100644 index 000000000..a3980dbf9 --- /dev/null +++ b/volunteer_holiday/views/volunteer_shift_generator_views.xml @@ -0,0 +1,36 @@ + + + + + Shift Generator: Add info about holiday-cancellable shifts + volunteer.shift.recurrent.generator + + + + + + + + + + + + + + + + + + From fe0dfa498157d6b217abf487c908ea2fa633e15e Mon Sep 17 00:00:00 2001 From: genepi314 Date: Fri, 20 Feb 2026 10:22:29 +0100 Subject: [PATCH 02/24] cancel holiday shift operational, still lacks company implementation, draft unit tests --- volunteer_holiday/__manifest__.py | 2 +- .../models/volunteer_company_holiday.py | 141 +++++++++++------- .../tests/test_company_holiday.py | 33 +++- .../views/volunteer_company_holiday_view.xml | 2 +- 4 files changed, 113 insertions(+), 65 deletions(-) diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py index bb0f3d227..33f76bfc1 100644 --- a/volunteer_holiday/__manifest__.py +++ b/volunteer_holiday/__manifest__.py @@ -13,7 +13,7 @@ "application": False, "depends": ["base", "volunteer", "mail"], "data": [ - # "data/cron.xml", + "data/cron.xml", "security/ir.model.access.csv", "views/volunteer_shift_generator_views.xml", "views/volunteer_company_holiday_view.xml", diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index cc8dd4374..96ff5d794 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -2,6 +2,10 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later +from datetime import date, datetime + +from dateutil.relativedelta import relativedelta + from odoo import fields, models @@ -10,65 +14,92 @@ class VolunteerCompanyHoliday(models.Model): _description = "Company Holidays" _order = "start_date" + # Fields + name = fields.Char(required=True) - # # C'est mal, mais je l'ai mis en commentaire pour faire mon test... - # company_id = fields.Many2one( - # comodel_name="res.company", - # string="Company", - # default=lambda self: self.env.user.company_id, - # # required=True, - # ) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.user.company_id, + # required=True, + ) start_date = fields.Date(required=True) end_date = fields.Date(required=True) - # # Problem with this function, work needed - - # def _cancel_holiday_shift(self, months=3): - # """Cancel shifts if they cover holiday period within time range""" - # # Setting the time range we want to work with - # date_time_range = datetime.today() + relativedelta(months=months) - - # confirmed_future_generated_shifts_in_range = self.env["volunteer.shift"].search( - # [('start_time', '>=', datetime.today()), - # ('start_time', '<=', date_time_range), - # ('generator_id', '!=', None), - # ('state', '=', 'confirmed')] - # ) - - # # Getting list of shifts to be cancel in case of holidays through generator_id - # potential_shifts_to_cancel = [] - # for shift in confirmed_future_generated_shifts_in_range: - # if shift.generator_id.is_maintained_during_holiday == False: - # potential_shifts_to_cancel.append(shift) - - # future_company_holidays_in_range = self.env["volunteer.company.holiday"].search( - # [('start_date', '>=', date.today()), - # ('start_date', '<=', date_time_range.date())] - # ) - - # for shift in potential_shifts_to_cancel: - # for holiday in future_company_holidays_in_range: - # if shift.state != "canceled" and self._shift_covers_holiday( - # shift.start_time, shift.end_time, holiday.start_date, holiday.end_date - # ): - # # Danger ! Corrupted the database : - # # shift.stage_id.state = "canceled" - # shift.write({'state': 'canceled'}) - - # # Wonder if this @api.model creates a problem - # # @api.model - # def _shift_covers_holiday( - # self, shift_start_time, shift_end_time, holiday_start_date, holiday_end_date - # ): - # shift_start_date = shift_start_time.date() - # shift_end_date = shift_end_time.date() - - # if (shift_start_date >= holiday_start_date and shift_start_date <= holiday_end_date): - # return True - # if (shift_end_date >= holiday_start_date and shift_start_date <= holiday_end_date): - # return True - # if (shift_start_date <= holiday_start_date and shift_end_date >= holiday_end_date): - # return True + # Methods + + def _cancel_holiday_shift(self, time_in_months=3): + """Cancel shifts if they cover holiday period within time range""" + # Forcing admin because of clause in volunteer.volunteer_shift.py + previous_env = self.env + self.env = self.env(user=self.env.ref("base.user_admin")) + + # Setting the time range we want to work with + date_time_range = datetime.today() + relativedelta(months=time_in_months) + + confirmed_future_generated_shifts_in_range = self.env["volunteer.shift"].search( + [ + ("start_time", ">=", datetime.today()), + ("start_time", "<=", date_time_range), + ("generator_id", "!=", None), + ("state", "=", "confirmed"), + ] + ) + + # Getting list of shifts to be canceled in case of holidays through generator_id + potential_shifts_to_cancel = [] + for shift in confirmed_future_generated_shifts_in_range: + if not shift.generator_id.is_maintained_during_holiday: + potential_shifts_to_cancel.append(shift) + + future_company_holidays_in_range = self.env["volunteer.company.holiday"].search( + [ + ("start_date", ">=", date.today()), + ("start_date", "<=", date_time_range.date()), + ] + ) + + # Cancel shifts if they're not canceled yet and cover holiday period + for shift in potential_shifts_to_cancel: + for holiday in future_company_holidays_in_range: + if shift.state != "canceled" and self._shift_covers_holiday( + shift.start_time, + shift.end_time, + holiday.start_date, + holiday.end_date, + ): + shift.write( + { + "stage_id": self.env.ref( + "volunteer.volunteer_shift_stage_canceled" + ).id + } + ) + # Back to original user + self.env = previous_env + + def _shift_covers_holiday( + self, shift_start_time, shift_end_time, holiday_start_date, holiday_end_date + ): + """Compare two periods of time, return true if they overlap.""" + shift_start_date = shift_start_time.date() + shift_end_date = shift_end_time.date() + + if ( + ( + shift_start_date >= holiday_start_date + and shift_start_date <= holiday_end_date + ) + or ( + shift_end_date >= holiday_start_date + and shift_start_date <= holiday_end_date + ) + or ( + shift_start_date <= holiday_start_date + and shift_end_date >= holiday_end_date + ) + ): + return True diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index f66922b45..6f5f1ed45 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -38,6 +38,9 @@ def setUp(self, *args, **kwargs): self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") + # Create other company + self.anoter_company = self.env["res.company"].create({"name": "AnotherCompany"}) + # Create required type self.type1 = self.Type.create( { @@ -57,8 +60,8 @@ def setUp(self, *args, **kwargs): "until_date": date(2026, 3, 5), "interval_type": "days", "interval": 1, - "start_time": datetime(2026, 3, 4, 10, 5), - "end_time": datetime(2026, 3, 4, 12, 5), + "start_time": datetime(2026, 3, 4, 10, 0), + "end_time": datetime(2026, 3, 4, 12, 0), "tz": "Europe/Brussels", "max_volunteer_nb": 6, "type_id": self.type1.id, @@ -72,8 +75,8 @@ def setUp(self, *args, **kwargs): "until_date": date(2026, 3, 3), "interval_type": "days", "interval": 1, - "start_time": datetime(2026, 3, 1, 10, 5), - "end_time": datetime(2026, 3, 1, 12, 5), + "start_time": datetime(2026, 3, 1, 10, 0), + "end_time": datetime(2026, 3, 1, 12, 0), "tz": "Europe/Brussels", "max_volunteer_nb": 6, "type_id": self.type1.id, @@ -83,19 +86,33 @@ def setUp(self, *args, **kwargs): { "name": "genOverlapHoliday", "state": "confirmed", - "until_date": date(2026, 3, 5), + "until_date": date(2026, 3, 7), "interval_type": "days", "interval": 1, - "start_time": datetime(2026, 3, 1, 10, 5), - "end_time": datetime(2026, 3, 1, 12, 5), + "start_time": datetime(2026, 3, 1, 10, 0), + "end_time": datetime(2026, 3, 1, 12, 0), "tz": "Europe/Brussels", "max_volunteer_nb": 6, "type_id": self.type1.id, } ) - # Create holidays + self.gen_long_shift = self.Generator.create( + { + "name": "genOverlapHoliday", + "state": "confirmed", + "until_date": date(2026, 3, 7), + "interval_type": "days", + "interval": 1, + "start_time": datetime(2026, 3, 1, 10, 0), + "end_time": datetime(2026, 3, 7, 10, 0), + "tz": "Europe/Brussels", + "max_volunteer_nb": 6, + "type_id": self.type1.id, + } + ) + # Create holidays self.march_holiday = self.Holiday.create( { "name": "marchHolidays", diff --git a/volunteer_holiday/views/volunteer_company_holiday_view.xml b/volunteer_holiday/views/volunteer_company_holiday_view.xml index cf1bcb00a..8f70ac429 100644 --- a/volunteer_holiday/views/volunteer_company_holiday_view.xml +++ b/volunteer_holiday/views/volunteer_company_holiday_view.xml @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later - + From 79748eee7ecc1e6655299d11bf9ccdd567f25062 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Mon, 23 Feb 2026 16:43:02 +0100 Subject: [PATCH 03/24] commit before switching branch --- volunteer_holiday/__manifest__.py | 5 ---- .../models/volunteer_company_holiday.py | 9 ++++-- .../tests/test_company_holiday.py | 28 ++++++++----------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py index 33f76bfc1..8adc8501e 100644 --- a/volunteer_holiday/__manifest__.py +++ b/volunteer_holiday/__manifest__.py @@ -20,9 +20,4 @@ "views/volunteer_menu.xml", ], "demo": [], - "assets": { - "web.assets_backend": [ - "volunteer/static/src/scss/volunteer_shift.scss", - ], - }, } diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 96ff5d794..89d568370 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -31,16 +31,20 @@ class VolunteerCompanyHoliday(models.Model): # Methods + # Retirer la limite de temps, regarder les shifts futurs def _cancel_holiday_shift(self, time_in_months=3): """Cancel shifts if they cover holiday period within time range""" - # Forcing admin because of clause in volunteer.volunteer_shift.py + # Forcing admin because of clause in volunteer.volunteer_shift.py, + # couldn't run the function otherwise. + # Retester avec sudo previous_env = self.env self.env = self.env(user=self.env.ref("base.user_admin")) - # Setting the time range we want to work with + # Setting the time range we want to work with, here 3 months. date_time_range = datetime.today() + relativedelta(months=time_in_months) confirmed_future_generated_shifts_in_range = self.env["volunteer.shift"].search( + # Here we'll have to include domain where holiday.company_id == shift.company_id [ ("start_time", ">=", datetime.today()), ("start_time", "<=", date_time_range), @@ -50,6 +54,7 @@ def _cancel_holiday_shift(self, time_in_months=3): ) # Getting list of shifts to be canceled in case of holidays through generator_id + # Faire la meme chose avec filtered, lambda rec: potential_shifts_to_cancel = [] for shift in confirmed_future_generated_shifts_in_range: if not shift.generator_id.is_maintained_during_holiday: diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index 6f5f1ed45..d3063debe 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -60,8 +60,8 @@ def setUp(self, *args, **kwargs): "until_date": date(2026, 3, 5), "interval_type": "days", "interval": 1, - "start_time": datetime(2026, 3, 4, 10, 0), - "end_time": datetime(2026, 3, 4, 12, 0), + "start_time": datetime(2026, 3, 3, 10, 0), + "end_time": datetime(2026, 3, 3, 12, 0), "tz": "Europe/Brussels", "max_volunteer_nb": 6, "type_id": self.type1.id, @@ -72,7 +72,7 @@ def setUp(self, *args, **kwargs): { "name": "genWithoutHoliday", "state": "confirmed", - "until_date": date(2026, 3, 3), + "until_date": date(2026, 3, 2), "interval_type": "days", "interval": 1, "start_time": datetime(2026, 3, 1, 10, 0), @@ -117,7 +117,7 @@ def setUp(self, *args, **kwargs): { "name": "marchHolidays", # "company_id": self.env.user.company_id, - "start_date": date(2026, 3, 4), + "start_date": date(2026, 3, 3), "end_date": date(2026, 3, 5), } ) @@ -125,7 +125,7 @@ def setUp(self, *args, **kwargs): def test_cancel_holiday_shift(self): """Test that holidays do cancel confirmed generated shifts""" - # Start of the test + # Check before test shifts_with_holiday = self.gen_with_holiday.volunteer_shift_ids for shift in shifts_with_holiday: self.assertEqual(shift.state, "confirmed") @@ -138,22 +138,18 @@ def test_cancel_holiday_shift(self): for shift in shifts_overlap_holiday: self.assertEqual(shift.state, "confirmed") + # Call function self.Holiday._cancel_holiday_shift() - # All shifts in shifts_with_holiday cover the march_holidays period + # All shifts in shifts_with_holiday cover the march_holidays period, + # thus should be canceled for shift in shifts_with_holiday: self.assertEqual(shift.state, "canceled") - # No shift in shifts_without_holiday cover holiday period + # No shift in shifts_without_holiday cover holiday period, + # thus shouldn't be canceled for shift in shifts_without_holiday: self.assertEqual(shift.state, "confirmed") - # Certain shifts in shifts_overlap_holiday cover holiday period - for shift in shifts_overlap_holiday: - if ( - shift.start_time.date() == self.march_holiday.start_date - or shift.start_time.date() == self.march_holiday.end_date - ): - self.assertEqual(shift.state, "canceled") - else: - self.assertEqual(shift.state, "confirmed") + # Certain shifts in shifts_overlap_holiday cover holiday period, + # thus only certain shifts should be canceled From e7198ae0ef55037ce29d287c07f16ea65f8c86f5 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 24 Feb 2026 17:08:44 +0100 Subject: [PATCH 04/24] draft test_cancel_holiday_shift with company + correct README --- volunteer_holiday/README.rst | 33 +- .../models/volunteer_company_holiday.py | 16 +- volunteer_holiday/readme/CONTRIBUTORS.rst | 3 + volunteer_holiday/readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 419 ++++++++++++++++++ .../tests/test_company_holiday.py | 70 ++- 6 files changed, 499 insertions(+), 43 deletions(-) create mode 100644 volunteer_holiday/readme/CONTRIBUTORS.rst create mode 100644 volunteer_holiday/readme/DESCRIPTION.rst create mode 100644 volunteer_holiday/static/description/index.html diff --git a/volunteer_holiday/README.rst b/volunteer_holiday/README.rst index 899623b3f..8f5692c36 100644 --- a/volunteer_holiday/README.rst +++ b/volunteer_holiday/README.rst @@ -1,32 +1,28 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - -========= -Volunteer -========= +================= +Volunteer Holiday +================= .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:a3d4eec071c4f3516c0c04b4d8801127c2b5ebd85c4eda456fc0cdc8dfeb8f43 + !! source digest: sha256:d68150a24904527787d7cde7a5cb68c88f101992b43ec7c25f9a3450eb8d6184 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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/license-AGPL--3-blue.png +.. |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-beescoop%2FObeesdoo-lightgray.png?logo=github - :target: https://github.com/beescoop/Obeesdoo/tree/16.0/volunteer + :target: https://github.com/beescoop/Obeesdoo/tree/16.0/volunteer_holiday :alt: beescoop/Obeesdoo |badge1| |badge2| |badge3| -Add holidays for companies and volunteers, and manage shifts accordingly. +Add volunteer and company holidays to volunteer app, and manage shifts and generators accordingly. **Table of contents** @@ -39,7 +35,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -61,17 +57,6 @@ Contributors Maintainers ~~~~~~~~~~~ -.. |maintainer-aydrpm| image:: https://github.com/aydrpm.png?size=40px - :target: https://github.com/aydrpm - :alt: aydrpm -.. |maintainer-remytms| image:: https://github.com/remytms.png?size=40px - :target: https://github.com/remytms - :alt: remytms - -Current maintainers: - -|maintainer-genepi314| |maintainer-remytms| - -This module is part of the `beescoop/Obeesdoo `_ project on GitHub. +This module is part of the `beescoop/Obeesdoo `_ project on GitHub. You are welcome to contribute. diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 89d568370..e7485d690 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -22,7 +22,7 @@ class VolunteerCompanyHoliday(models.Model): comodel_name="res.company", string="Company", default=lambda self: self.env.user.company_id, - # required=True, + required=True, ) start_date = fields.Date(required=True) @@ -70,11 +70,15 @@ def _cancel_holiday_shift(self, time_in_months=3): # Cancel shifts if they're not canceled yet and cover holiday period for shift in potential_shifts_to_cancel: for holiday in future_company_holidays_in_range: - if shift.state != "canceled" and self._shift_covers_holiday( - shift.start_time, - shift.end_time, - holiday.start_date, - holiday.end_date, + if ( + shift.state != "canceled" + and shift.company_id == holiday.company_id + and self._shift_covers_holiday( + shift.start_time, + shift.end_time, + holiday.start_date, + holiday.end_date, + ) ): shift.write( { diff --git a/volunteer_holiday/readme/CONTRIBUTORS.rst b/volunteer_holiday/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..8caa3ae1c --- /dev/null +++ b/volunteer_holiday/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Coop IT Easy SC `_: + + * Geneviève Ernould diff --git a/volunteer_holiday/readme/DESCRIPTION.rst b/volunteer_holiday/readme/DESCRIPTION.rst new file mode 100644 index 000000000..4c3e093ae --- /dev/null +++ b/volunteer_holiday/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Add volunteer and company holidays to volunteer app, and manage shifts and generators accordingly. diff --git a/volunteer_holiday/static/description/index.html b/volunteer_holiday/static/description/index.html new file mode 100644 index 000000000..9e6ea2004 --- /dev/null +++ b/volunteer_holiday/static/description/index.html @@ -0,0 +1,419 @@ + + + + + +Volunteer Holiday + + + +
+

Volunteer Holiday

+ + +

Beta License: AGPL-3 beescoop/Obeesdoo

+

Add volunteer and company holidays to volunteer app, and manage shifts and generators accordingly.

+

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

+
    +
  • Coop IT Easy SC
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the beescoop/Obeesdoo project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index d3063debe..e07f81b85 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -28,6 +28,7 @@ def setUp(self, *args, **kwargs): # Models + self.Company = self.env["res.company"] self.Holiday = self.env["volunteer.company.holiday"] self.Shift = self.env["volunteer.shift"] self.Type = self.env["volunteer.shift.type"] @@ -38,9 +39,6 @@ def setUp(self, *args, **kwargs): self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") - # Create other company - self.anoter_company = self.env["res.company"].create({"name": "AnotherCompany"}) - # Create required type self.type1 = self.Type.create( { @@ -82,30 +80,31 @@ def setUp(self, *args, **kwargs): "type_id": self.type1.id, } ) - self.gen_overlap_holiday = self.Generator.create( + + self.gen_one_long_shift = self.Generator.create( { - "name": "genOverlapHoliday", + "name": "genOneLongShift", "state": "confirmed", "until_date": date(2026, 3, 7), "interval_type": "days", "interval": 1, "start_time": datetime(2026, 3, 1, 10, 0), - "end_time": datetime(2026, 3, 1, 12, 0), + "end_time": datetime(2026, 3, 7, 10, 0), "tz": "Europe/Brussels", "max_volunteer_nb": 6, "type_id": self.type1.id, } ) - self.gen_long_shift = self.Generator.create( + self.gen_overlap_holiday = self.Generator.create( { "name": "genOverlapHoliday", "state": "confirmed", - "until_date": date(2026, 3, 7), + "until_date": date(2026, 3, 6), "interval_type": "days", "interval": 1, - "start_time": datetime(2026, 3, 1, 10, 0), - "end_time": datetime(2026, 3, 7, 10, 0), + "start_time": datetime(2026, 3, 2, 10, 0), + "end_time": datetime(2026, 3, 2, 12, 0), "tz": "Europe/Brussels", "max_volunteer_nb": 6, "type_id": self.type1.id, @@ -116,7 +115,6 @@ def setUp(self, *args, **kwargs): self.march_holiday = self.Holiday.create( { "name": "marchHolidays", - # "company_id": self.env.user.company_id, "start_date": date(2026, 3, 3), "end_date": date(2026, 3, 5), } @@ -134,6 +132,12 @@ def test_cancel_holiday_shift(self): for shift in shifts_without_holiday: self.assertEqual(shift.state, "confirmed") + one_long_shift = self.gen_one_long_shift.volunteer_shift_ids + # print(f"Here is one_long_shift : {one_long_shift}") + for shift in one_long_shift: + self.assertEqual(shift.state, "confirmed") + + # Here generator overlapping with holiday period shifts_overlap_holiday = self.gen_overlap_holiday.volunteer_shift_ids for shift in shifts_overlap_holiday: self.assertEqual(shift.state, "confirmed") @@ -151,5 +155,45 @@ def test_cancel_holiday_shift(self): for shift in shifts_without_holiday: self.assertEqual(shift.state, "confirmed") - # Certain shifts in shifts_overlap_holiday cover holiday period, - # thus only certain shifts should be canceled + # This long shift should be canceled entirely as it overlaps with holiday period + for shift in one_long_shift: + self.assertEqual(shift.state, "canceled") + + # Overlapping shifts should be canceled, the others should stay + for shift in shifts_overlap_holiday: + # Holidays last from 4/3/26 to 5/3/2026 + if shift.start_time == datetime(2026, 3, 2, 10, 0): + self.assertEqual(shift.state, "confirmed") + if shift.start_time == datetime(2026, 3, 3, 10, 0): + self.assertEqual(shift.state, "canceled") + if shift.start_time == datetime(2026, 3, 4, 10, 0): + self.assertEqual(shift.state, "canceled") + if shift.start_time == datetime(2026, 3, 5, 10, 0): + self.assertEqual(shift.state, "canceled") + if shift.start_time == datetime(2026, 3, 6, 10, 0): + # print(f"shift start time : {shift.start_time}, holiday end date : {}") + self.assertEqual(shift.state, "confirmed") + + # # Create other company + # self.anoter_company = self.Company.sudo().create({"name": "AnotherCompany"}) + + # # Create holidays of another company + # self.other_company_holiday = self.Holiday.sudo().create( + # { + # "name": "marchOtherHolidays", + # "company_id": self.anoter_company.id, + # "start_date": date(2026, 3, 6), + # "end_date": date(2026, 3, 6), + # } + # ) + + # # Call function again + # self.Holiday._cancel_holiday_shift() + + # # Check that shift of March 6 wasn't canceled, + # # because March 6 holiday is registered for a different company + # # than March 6 shift + # march_6_shift = self.shifts_overlap_holiday.filtered( + # lambda shift: shift.start_time == datetime(2026, 3, 6, 10, 0) + # ) + # self.assertEqual(march_6_shift.state, "confirmed") From acc87b7cf5bb5ab0f563af23c7adae01c8c463d1 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 3 Mar 2026 10:03:02 +0100 Subject: [PATCH 05/24] Add models and views for volunteer leaves, volunteer leave types, and some changes to access rights. --- volunteer_holiday/__manifest__.py | 4 +- volunteer_holiday/models/__init__.py | 3 + .../models/volunteer_volunteer.py | 15 ++++ .../models/volunteer_volunteer_leave.py | 29 ++++++ .../models/volunteer_volunteer_leave_type.py | 20 +++++ .../security/ir.model.access.csv | 8 +- .../tests/test_company_holiday.py | 88 ++++++++++++------- .../views/volunteer_company_holiday_view.xml | 2 +- volunteer_holiday/views/volunteer_menu.xml | 32 +++++-- .../volunteer_volunteer_leave_type_view.xml | 25 ++++++ .../views/volunteer_volunteer_leave_view.xml | 26 ++++++ .../views/volunteer_volunteer_view.xml | 29 ++++++ 12 files changed, 238 insertions(+), 43 deletions(-) create mode 100644 volunteer_holiday/models/volunteer_volunteer.py create mode 100644 volunteer_holiday/models/volunteer_volunteer_leave.py create mode 100644 volunteer_holiday/models/volunteer_volunteer_leave_type.py create mode 100644 volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml create mode 100644 volunteer_holiday/views/volunteer_volunteer_leave_view.xml create mode 100644 volunteer_holiday/views/volunteer_volunteer_view.xml diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py index 8adc8501e..6d66d8390 100644 --- a/volunteer_holiday/__manifest__.py +++ b/volunteer_holiday/__manifest__.py @@ -17,7 +17,9 @@ "security/ir.model.access.csv", "views/volunteer_shift_generator_views.xml", "views/volunteer_company_holiday_view.xml", + "views/volunteer_volunteer_view.xml", + "views/volunteer_volunteer_leave_type_view.xml", + "views/volunteer_volunteer_leave_view.xml", "views/volunteer_menu.xml", ], - "demo": [], } diff --git a/volunteer_holiday/models/__init__.py b/volunteer_holiday/models/__init__.py index 7d4950704..6f1de5c8a 100644 --- a/volunteer_holiday/models/__init__.py +++ b/volunteer_holiday/models/__init__.py @@ -4,3 +4,6 @@ from . import volunteer_company_holiday from . import volunteer_shift_recurrent_generator +from . import volunteer_volunteer +from . import volunteer_volunteer_leave_type +from . import volunteer_volunteer_leave diff --git a/volunteer_holiday/models/volunteer_volunteer.py b/volunteer_holiday/models/volunteer_volunteer.py new file mode 100644 index 000000000..ec3a32817 --- /dev/null +++ b/volunteer_holiday/models/volunteer_volunteer.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class VolunteerVolunteer(models.Model): + _inherit = "volunteer.volunteer" + + volunteer_leave_ids = fields.One2many( + comodel_name="volunteer.volunteer.leave", + inverse_name="volunteer_id", + string="Leave", + ) diff --git a/volunteer_holiday/models/volunteer_volunteer_leave.py b/volunteer_holiday/models/volunteer_volunteer_leave.py new file mode 100644 index 000000000..4f7bad4bf --- /dev/null +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class VolunteerVolunteerLeave(models.Model): + _name = "volunteer.volunteer.leave" + _description = "Volunteer Leave" + + # Fields + + volunteer_id = fields.Many2one( + comodel_name="volunteer.volunteer", + string="Volunteer", + required=True, + ) + start_date = fields.Date( + required=True, + ) + end_date = fields.Date( + required=True, + ) + type_id = fields.Many2one( + comodel_name="volunteer.volunteer.leave.type", + string="Leave Type", + required="True", + ) diff --git a/volunteer_holiday/models/volunteer_volunteer_leave_type.py b/volunteer_holiday/models/volunteer_volunteer_leave_type.py new file mode 100644 index 000000000..282bf1e18 --- /dev/null +++ b/volunteer_holiday/models/volunteer_volunteer_leave_type.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class VolunteerVolunteerLeaveType(models.Model): + _name = "volunteer.volunteer.leave.type" + _description = "Volunteer Leave" + _order = "name" + + name = fields.Char(string="Leave Type", required="True") + description = fields.Char() + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + default=lambda self: self.env.user.company_id, + required=True, + ) diff --git a/volunteer_holiday/security/ir.model.access.csv b/volunteer_holiday/security/ir.model.access.csv index f667cc4f3..be173409c 100644 --- a/volunteer_holiday/security/ir.model.access.csv +++ b/volunteer_holiday/security/ir.model.access.csv @@ -1,4 +1,10 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_company_holiday_user,CompanyHolidayUser,model_volunteer_company_holiday,volunteer.volunteer_group_user,1,0,0,0 -access_company_holiday_manager,CompanyHolidayManager,model_volunteer_company_holiday,volunteer.volunteer_group_manager,1,1,0,0 +access_company_holiday_manager,CompanyHolidayManager,model_volunteer_company_holiday,volunteer.volunteer_group_manager,1,1,1,0 access_company_holiday_admin,CompanyHolidayAdmin,model_volunteer_company_holiday,volunteer.volunteer_group_admin,1,1,1,1 +access_volunteer_leave_user,VolunteerLeaveUser,model_volunteer_volunteer_leave,volunteer.volunteer_group_user,1,0,0,0 +access_volunteer_leave_manager,VolunteerLeaveManager,model_volunteer_volunteer_leave,volunteer.volunteer_group_user,1,1,1,0 +access_volunteer_leave_admin,VolunteerLeaveAdmin,model_volunteer_volunteer_leave,volunteer.volunteer_group_user,1,1,1,1 +access_volunteer_leave_type_user,VolunteerLeaveTypeUser,model_volunteer_volunteer_leave_type,volunteer.volunteer_group_user,1,0,0,0 +access_volunteer_leave_type_manager,VolunteerLeaveTypeManager,model_volunteer_volunteer_leave_type,volunteer.volunteer_group_manager,1,1,1,0 +access_volunteer_leave_type_admin,VolunteerLeaveTypeAdmin,model_volunteer_volunteer_leave_type,volunteer.volunteer_group_admin,1,1,1,1 diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index e07f81b85..00439518d 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -66,6 +66,22 @@ def setUp(self, *args, **kwargs): } ) + self.gen_maintained_during_holiday = self.Generator.create( + { + "name": "GenMaintainedDuringHoliday", + "state": "confirmed", + "is_maintained_during_holiday": True, + "until_date": date(2026, 3, 5), + "interval_type": "days", + "interval": 1, + "start_time": datetime(2026, 3, 3, 10, 0), + "end_time": datetime(2026, 3, 3, 12, 0), + "tz": "Europe/Brussels", + "max_volunteer_nb": 6, + "type_id": self.type1.id, + } + ) + self.gen_without_holiday = self.Generator.create( { "name": "genWithoutHoliday", @@ -85,8 +101,8 @@ def setUp(self, *args, **kwargs): { "name": "genOneLongShift", "state": "confirmed", - "until_date": date(2026, 3, 7), - "interval_type": "days", + "until_date": date(2026, 3, 8), + "interval_type": "months", "interval": 1, "start_time": datetime(2026, 3, 1, 10, 0), "end_time": datetime(2026, 3, 7, 10, 0), @@ -124,8 +140,12 @@ def test_cancel_holiday_shift(self): """Test that holidays do cancel confirmed generated shifts""" # Check before test - shifts_with_holiday = self.gen_with_holiday.volunteer_shift_ids - for shift in shifts_with_holiday: + shifts_on_holiday = self.gen_with_holiday.volunteer_shift_ids + for shift in shifts_on_holiday: + self.assertEqual(shift.state, "confirmed") + + maintained_shifts = self.gen_maintained_during_holiday.volunteer_shift_ids + for shift in maintained_shifts: self.assertEqual(shift.state, "confirmed") shifts_without_holiday = self.gen_without_holiday.volunteer_shift_ids @@ -133,7 +153,6 @@ def test_cancel_holiday_shift(self): self.assertEqual(shift.state, "confirmed") one_long_shift = self.gen_one_long_shift.volunteer_shift_ids - # print(f"Here is one_long_shift : {one_long_shift}") for shift in one_long_shift: self.assertEqual(shift.state, "confirmed") @@ -145,12 +164,16 @@ def test_cancel_holiday_shift(self): # Call function self.Holiday._cancel_holiday_shift() - # All shifts in shifts_with_holiday cover the march_holidays period, + # All shifts in shifts_on_holiday cover the march_holidays period, # thus should be canceled - for shift in shifts_with_holiday: + for shift in shifts_on_holiday: self.assertEqual(shift.state, "canceled") - # No shift in shifts_without_holiday cover holiday period, + # Maintained shifts shouldn't be canceled + for shift in maintained_shifts: + self.assertEqual(shift.state, "confirmed") + + # No shift in shifts_without_holiday covers holiday period, # thus shouldn't be canceled for shift in shifts_without_holiday: self.assertEqual(shift.state, "confirmed") @@ -161,7 +184,7 @@ def test_cancel_holiday_shift(self): # Overlapping shifts should be canceled, the others should stay for shift in shifts_overlap_holiday: - # Holidays last from 4/3/26 to 5/3/2026 + # Holidays last from 3/3/26 to 5/3/2026 if shift.start_time == datetime(2026, 3, 2, 10, 0): self.assertEqual(shift.state, "confirmed") if shift.start_time == datetime(2026, 3, 3, 10, 0): @@ -171,29 +194,28 @@ def test_cancel_holiday_shift(self): if shift.start_time == datetime(2026, 3, 5, 10, 0): self.assertEqual(shift.state, "canceled") if shift.start_time == datetime(2026, 3, 6, 10, 0): - # print(f"shift start time : {shift.start_time}, holiday end date : {}") self.assertEqual(shift.state, "confirmed") - # # Create other company - # self.anoter_company = self.Company.sudo().create({"name": "AnotherCompany"}) - - # # Create holidays of another company - # self.other_company_holiday = self.Holiday.sudo().create( - # { - # "name": "marchOtherHolidays", - # "company_id": self.anoter_company.id, - # "start_date": date(2026, 3, 6), - # "end_date": date(2026, 3, 6), - # } - # ) - - # # Call function again - # self.Holiday._cancel_holiday_shift() - - # # Check that shift of March 6 wasn't canceled, - # # because March 6 holiday is registered for a different company - # # than March 6 shift - # march_6_shift = self.shifts_overlap_holiday.filtered( - # lambda shift: shift.start_time == datetime(2026, 3, 6, 10, 0) - # ) - # self.assertEqual(march_6_shift.state, "confirmed") + # Create other company + self.anoter_company = self.Company.sudo().create({"name": "AnotherCompany"}) + + # Create holidays of another company + self.other_company_holiday = self.Holiday.sudo().create( + { + "name": "marchOtherHolidays", + "company_id": self.anoter_company.id, + "start_date": date(2026, 3, 6), + "end_date": date(2026, 3, 6), + } + ) + + # Call function again + self.Holiday._cancel_holiday_shift() + + # Check that shift of March 6 wasn't canceled, + # because March 6 holiday is registered for a different company + # than March 6 shift + march_6_shift = shifts_overlap_holiday.filtered( + lambda shift: shift.start_time == datetime(2026, 3, 6, 10, 0) + ) + self.assertEqual(march_6_shift.state, "confirmed") diff --git a/volunteer_holiday/views/volunteer_company_holiday_view.xml b/volunteer_holiday/views/volunteer_company_holiday_view.xml index 8f70ac429..f95cf6fce 100644 --- a/volunteer_holiday/views/volunteer_company_holiday_view.xml +++ b/volunteer_holiday/views/volunteer_company_holiday_view.xml @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
- + Company Holiday volunteer.company.holiday tree diff --git a/volunteer_holiday/views/volunteer_menu.xml b/volunteer_holiday/views/volunteer_menu.xml index 051abd850..c8f29d327 100644 --- a/volunteer_holiday/views/volunteer_menu.xml +++ b/volunteer_holiday/views/volunteer_menu.xml @@ -5,13 +5,31 @@ SPDX-FileCopyrightText: 2025 Coop IT Easy SC SPDX-License-Identifier: AGPL-3.0-or-later --> - + id="volunteer_holiday_separator_menu" + name="Time Off" + parent="volunteer.volunteer_configuration_menu" + sequence="30" + groups="volunteer.volunteer_group_admin" + > + + + + diff --git a/volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml b/volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml new file mode 100644 index 000000000..b2601af2e --- /dev/null +++ b/volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml @@ -0,0 +1,25 @@ + + + + + Leave Type List + volunteer.volunteer.leave.type + + + + + + + + + + + Leave Types + volunteer.volunteer.leave.type + tree + + diff --git a/volunteer_holiday/views/volunteer_volunteer_leave_view.xml b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml new file mode 100644 index 000000000..a88de7668 --- /dev/null +++ b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml @@ -0,0 +1,26 @@ + + + + + Volunteer Leaves List + volunteer.volunteer.leave + + + + + + + + + + + + Volunteer Leaves + volunteer.volunteer.leave + tree + + diff --git a/volunteer_holiday/views/volunteer_volunteer_view.xml b/volunteer_holiday/views/volunteer_volunteer_view.xml new file mode 100644 index 000000000..4ac7225fb --- /dev/null +++ b/volunteer_holiday/views/volunteer_volunteer_view.xml @@ -0,0 +1,29 @@ + + + + + Volunteer: Add page Vacation + volunteer.volunteer + + + + + + + + + + + + + + + + From a58453447edf095b47c24bfc5ad77c5f32c757ce Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 3 Mar 2026 14:50:21 +0100 Subject: [PATCH 06/24] Refacto _cancel_holiday_shift, now using a dictionary and filters by company. --- .../models/volunteer_company_holiday.py | 87 +++++++++---------- .../tests/test_company_holiday.py | 4 + 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index e7485d690..04de4f8b6 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -from datetime import date, datetime +from datetime import datetime from dateutil.relativedelta import relativedelta @@ -34,61 +34,58 @@ class VolunteerCompanyHoliday(models.Model): # Retirer la limite de temps, regarder les shifts futurs def _cancel_holiday_shift(self, time_in_months=3): """Cancel shifts if they cover holiday period within time range""" - # Forcing admin because of clause in volunteer.volunteer_shift.py, - # couldn't run the function otherwise. - # Retester avec sudo - previous_env = self.env - self.env = self.env(user=self.env.ref("base.user_admin")) - - # Setting the time range we want to work with, here 3 months. - date_time_range = datetime.today() + relativedelta(months=time_in_months) - - confirmed_future_generated_shifts_in_range = self.env["volunteer.shift"].search( - # Here we'll have to include domain where holiday.company_id == shift.company_id - [ - ("start_time", ">=", datetime.today()), - ("start_time", "<=", date_time_range), - ("generator_id", "!=", None), - ("state", "=", "confirmed"), - ] + today_midnight = datetime.today().replace( + hour=0, minute=0, second=0, microsecond=0 + ) + # Setting the time range we want to work with, here 3 months + date_time_range = today_midnight + relativedelta(months=time_in_months) + + confirmed_future_generated_shifts_in_range = ( + self.env["volunteer.shift"] + .sudo() + .search( + [ + ("state", "=", "confirmed"), + ("start_time", ">=", today_midnight), + ("start_time", "<=", date_time_range), + ("generator_id", "!=", None), + ("generator_id.is_maintained_during_holiday", "=", False), + ] + ) ) - # Getting list of shifts to be canceled in case of holidays through generator_id - # Faire la meme chose avec filtered, lambda rec: - potential_shifts_to_cancel = [] - for shift in confirmed_future_generated_shifts_in_range: - if not shift.generator_id.is_maintained_during_holiday: - potential_shifts_to_cancel.append(shift) - - future_company_holidays_in_range = self.env["volunteer.company.holiday"].search( - [ - ("start_date", ">=", date.today()), - ("start_date", "<=", date_time_range.date()), - ] + future_company_holidays_in_range = ( + self.env["volunteer.company.holiday"] + .sudo() + .search( + [ + ("start_date", ">=", today_midnight), + ("start_date", "<=", date_time_range.date()), + ] + ) ) - # Cancel shifts if they're not canceled yet and cover holiday period - for shift in potential_shifts_to_cancel: - for holiday in future_company_holidays_in_range: - if ( - shift.state != "canceled" - and shift.company_id == holiday.company_id - and self._shift_covers_holiday( - shift.start_time, - shift.end_time, - holiday.start_date, - holiday.end_date, - ) + # Create dictionary key-company for list of values-shifts + shifts_by_company = {} + for shift in confirmed_future_generated_shifts_in_range: + shifts_by_company.setdefault(shift.company_id, []).append(shift) + + for holiday in future_company_holidays_in_range: + same_company_shifts = shifts_by_company.get(holiday.company_id, []) + for shift in same_company_shifts: + if shift.state != "canceled" and self._shift_covers_holiday( + shift.start_time, + shift.end_time, + holiday.start_date, + holiday.end_date, ): - shift.write( + shift.sudo().write( { "stage_id": self.env.ref( "volunteer.volunteer_shift_stage_canceled" ).id } ) - # Back to original user - self.env = previous_env def _shift_covers_holiday( self, shift_start_time, shift_end_time, holiday_start_date, holiday_end_date diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index 00439518d..8879812dc 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -55,6 +55,7 @@ def setUp(self, *args, **kwargs): { "name": "GenWithHoliday", "state": "confirmed", + "is_maintained_during_holiday": False, "until_date": date(2026, 3, 5), "interval_type": "days", "interval": 1, @@ -86,6 +87,7 @@ def setUp(self, *args, **kwargs): { "name": "genWithoutHoliday", "state": "confirmed", + "is_maintained_during_holiday": False, "until_date": date(2026, 3, 2), "interval_type": "days", "interval": 1, @@ -101,6 +103,7 @@ def setUp(self, *args, **kwargs): { "name": "genOneLongShift", "state": "confirmed", + "is_maintained_during_holiday": False, "until_date": date(2026, 3, 8), "interval_type": "months", "interval": 1, @@ -116,6 +119,7 @@ def setUp(self, *args, **kwargs): { "name": "genOverlapHoliday", "state": "confirmed", + "is_maintained_during_holiday": False, "until_date": date(2026, 3, 6), "interval_type": "days", "interval": 1, From 130eeb9eea8143b201078d0809ed3460e6185bcb Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 3 Mar 2026 16:42:18 +0100 Subject: [PATCH 07/24] Add tracking to is_maintained_during_holiday (bool) in generator. --- .../models/volunteer_shift_recurrent_generator.py | 4 +++- volunteer_holiday/views/volunteer_company_holiday_view.xml | 3 ++- volunteer_holiday/views/volunteer_volunteer_leave_view.xml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/volunteer_holiday/models/volunteer_shift_recurrent_generator.py b/volunteer_holiday/models/volunteer_shift_recurrent_generator.py index 61168748b..62030d34d 100644 --- a/volunteer_holiday/models/volunteer_shift_recurrent_generator.py +++ b/volunteer_holiday/models/volunteer_shift_recurrent_generator.py @@ -8,4 +8,6 @@ class VolunteerShiftRecurrentGenerator(models.Model): _inherit = "volunteer.shift.recurrent.generator" - is_maintained_during_holiday = fields.Boolean("Maintain During Holidays") + is_maintained_during_holiday = fields.Boolean( + "Maintain During Holidays", tracking=True + ) diff --git a/volunteer_holiday/views/volunteer_company_holiday_view.xml b/volunteer_holiday/views/volunteer_company_holiday_view.xml index f95cf6fce..8bdcbc915 100644 --- a/volunteer_holiday/views/volunteer_company_holiday_view.xml +++ b/volunteer_holiday/views/volunteer_company_holiday_view.xml @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later Holiday List volunteer.company.holiday - + @@ -24,4 +24,5 @@ SPDX-License-Identifier: AGPL-3.0-or-later volunteer.company.holiday tree + diff --git a/volunteer_holiday/views/volunteer_volunteer_leave_view.xml b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml index a88de7668..983a75641 100644 --- a/volunteer_holiday/views/volunteer_volunteer_leave_view.xml +++ b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later Volunteer Leaves List volunteer.volunteer.leave - + From 5218d916610f0d3f890455c762ff223a019c0976 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Wed, 4 Mar 2026 16:00:00 +0100 Subject: [PATCH 08/24] Add freezegun for test_company_holiday --- volunteer_holiday/tests/test_company_holiday.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index 8879812dc..7bf05023f 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -4,9 +4,12 @@ from datetime import date, datetime +from freezegun import freeze_time + from odoo.tests.common import TransactionCase +@freeze_time("2026-01-01 10:00:00") class TestCronHoliday(TransactionCase): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) @@ -217,7 +220,7 @@ def test_cancel_holiday_shift(self): self.Holiday._cancel_holiday_shift() # Check that shift of March 6 wasn't canceled, - # because March 6 holiday is registered for a different company + # as March 6 holiday is registered for a different company # than March 6 shift march_6_shift = shifts_overlap_holiday.filtered( lambda shift: shift.start_time == datetime(2026, 3, 6, 10, 0) From 642f6cf496dcd77e7b21642bf4d83598d5ba51e1 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Wed, 4 Mar 2026 16:12:08 +0100 Subject: [PATCH 09/24] =?UTF-8?q?Syntax=20and=20decorator=20fixes=20in=20v?= =?UTF-8?q?olunteer=5Fcompany=5Fholiday=C3=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- volunteer_holiday/models/volunteer_company_holiday.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 04de4f8b6..8930fd3b1 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -6,7 +6,7 @@ from dateutil.relativedelta import relativedelta -from odoo import fields, models +from odoo import api, fields, models class VolunteerCompanyHoliday(models.Model): @@ -87,6 +87,7 @@ def _cancel_holiday_shift(self, time_in_months=3): } ) + @api.model def _shift_covers_holiday( self, shift_start_time, shift_end_time, holiday_start_date, holiday_end_date ): @@ -94,7 +95,7 @@ def _shift_covers_holiday( shift_start_date = shift_start_time.date() shift_end_date = shift_end_time.date() - if ( + return ( ( shift_start_date >= holiday_start_date and shift_start_date <= holiday_end_date @@ -107,5 +108,4 @@ def _shift_covers_holiday( shift_start_date <= holiday_start_date and shift_end_date >= holiday_end_date ) - ): - return True + ) From be54c46fe8d08ec22d91164ac0b99d1dbc7e942b Mon Sep 17 00:00:00 2001 From: genepi314 Date: Wed, 4 Mar 2026 17:53:05 +0100 Subject: [PATCH 10/24] Add company for volunteer_leave, record rules for all models, row company present but hideable in all views. --- volunteer_holiday/__manifest__.py | 1 + .../models/volunteer_volunteer_leave.py | 7 +++++ .../models/volunteer_volunteer_leave_type.py | 1 + .../security/volunteer_security.xml | 26 +++++++++++++++++++ .../views/volunteer_company_holiday_view.xml | 2 +- .../volunteer_volunteer_leave_type_view.xml | 2 +- .../views/volunteer_volunteer_leave_view.xml | 1 + 7 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 volunteer_holiday/security/volunteer_security.xml diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py index 6d66d8390..defdf419d 100644 --- a/volunteer_holiday/__manifest__.py +++ b/volunteer_holiday/__manifest__.py @@ -15,6 +15,7 @@ "data": [ "data/cron.xml", "security/ir.model.access.csv", + "security/volunteer_security.xml", "views/volunteer_shift_generator_views.xml", "views/volunteer_company_holiday_view.xml", "views/volunteer_volunteer_view.xml", diff --git a/volunteer_holiday/models/volunteer_volunteer_leave.py b/volunteer_holiday/models/volunteer_volunteer_leave.py index 4f7bad4bf..aa005db9c 100644 --- a/volunteer_holiday/models/volunteer_volunteer_leave.py +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -16,12 +16,19 @@ class VolunteerVolunteerLeave(models.Model): string="Volunteer", required=True, ) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + related="volunteer_id.company_id", + ) + start_date = fields.Date( required=True, ) end_date = fields.Date( required=True, ) + type_id = fields.Many2one( comodel_name="volunteer.volunteer.leave.type", string="Leave Type", diff --git a/volunteer_holiday/models/volunteer_volunteer_leave_type.py b/volunteer_holiday/models/volunteer_volunteer_leave_type.py index 282bf1e18..0e4469760 100644 --- a/volunteer_holiday/models/volunteer_volunteer_leave_type.py +++ b/volunteer_holiday/models/volunteer_volunteer_leave_type.py @@ -12,6 +12,7 @@ class VolunteerVolunteerLeaveType(models.Model): name = fields.Char(string="Leave Type", required="True") description = fields.Char() + company_id = fields.Many2one( comodel_name="res.company", string="Company", diff --git a/volunteer_holiday/security/volunteer_security.xml b/volunteer_holiday/security/volunteer_security.xml new file mode 100644 index 000000000..88a5322f6 --- /dev/null +++ b/volunteer_holiday/security/volunteer_security.xml @@ -0,0 +1,26 @@ + + + + + Volunteer Company Holiday: Company Access + + [('company_id', 'in', company_ids + [False])] + + + + Volunteer Leaves: Company Access + + [('company_id', 'in', company_ids + [False])] + + + + Volunteer Leave Type: Company Access + + [('company_id', 'in', company_ids + [False])] + + + diff --git a/volunteer_holiday/views/volunteer_company_holiday_view.xml b/volunteer_holiday/views/volunteer_company_holiday_view.xml index 8bdcbc915..1be08b18a 100644 --- a/volunteer_holiday/views/volunteer_company_holiday_view.xml +++ b/volunteer_holiday/views/volunteer_company_holiday_view.xml @@ -12,9 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-or-later - + diff --git a/volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml b/volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml index b2601af2e..06724a73f 100644 --- a/volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml +++ b/volunteer_holiday/views/volunteer_volunteer_leave_type_view.xml @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later - + diff --git a/volunteer_holiday/views/volunteer_volunteer_leave_view.xml b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml index 983a75641..238f6cbb2 100644 --- a/volunteer_holiday/views/volunteer_volunteer_leave_view.xml +++ b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml @@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later + From 57a9f43aa25b2ca84205788ea83122959b5b9510 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Thu, 5 Mar 2026 14:59:48 +0100 Subject: [PATCH 11/24] Add cron and function to cancel volunteer participations during their leaves. --- volunteer_holiday/data/cron.xml | 16 ++++- .../models/volunteer_volunteer_leave.py | 71 +++++++++++++++++-- volunteer_holiday/tests/__init__.py | 2 + .../tests/test_volunteer_leave.py | 0 4 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 volunteer_holiday/tests/test_volunteer_leave.py diff --git a/volunteer_holiday/data/cron.xml b/volunteer_holiday/data/cron.xml index dde0323cb..1bf1a886c 100644 --- a/volunteer_holiday/data/cron.xml +++ b/volunteer_holiday/data/cron.xml @@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later --> - - Cancel generated shifts during holidays + + Cancel Generated Shifts During Company Holidays code model._cancel_holiday_shift() @@ -16,4 +16,16 @@ SPDX-License-Identifier: AGPL-3.0-or-later -1 + + + Cancel Participations During Volunteer Leaves + + code + model._cancel_volunteer_leave_participation() + 24 + hours + -1 + + + diff --git a/volunteer_holiday/models/volunteer_volunteer_leave.py b/volunteer_holiday/models/volunteer_volunteer_leave.py index aa005db9c..a843ad93f 100644 --- a/volunteer_holiday/models/volunteer_volunteer_leave.py +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -2,14 +2,17 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -from odoo import fields, models +from datetime import date, datetime + +from odoo import api, fields, models class VolunteerVolunteerLeave(models.Model): _name = "volunteer.volunteer.leave" _description = "Volunteer Leave" + _order = "start_date" - # Fields + # Relational Fields volunteer_id = fields.Many2one( comodel_name="volunteer.volunteer", @@ -21,6 +24,13 @@ class VolunteerVolunteerLeave(models.Model): string="Company", related="volunteer_id.company_id", ) + type_id = fields.Many2one( + comodel_name="volunteer.volunteer.leave.type", + string="Leave Type", + required="True", + ) + + # Time fields start_date = fields.Date( required=True, @@ -29,8 +39,55 @@ class VolunteerVolunteerLeave(models.Model): required=True, ) - type_id = fields.Many2one( - comodel_name="volunteer.volunteer.leave.type", - string="Leave Type", - required="True", - ) + # Methods + + def _cancel_volunteer_leave_participation(self): + """Cancel participations of volunteers if they overlap with their time off""" + today_midnight = datetime.today().replace( + hour=0, minute=0, second=0, microsecond=0 + ) + all_future_volunteer_leaves = ( + self.env["volunteer.volunteer.leave"] + .sudo() + .search([("start_date", ">=", date.today())]) + ) + + for leave in all_future_volunteer_leaves: + future_confirmed_participations = ( + leave.volunteer_id.shift_participation_ids.filtered( + lambda participation: participation.registration_state != "canceled" + and participation.shift_id.state != "canceled" + and participation.shift_id.start_time >= today_midnight + ) + ) + for participation in future_confirmed_participations: + if self._shift_covers_holiday( + participation.shift_id.start_time, + participation.shift_id.end_time, + leave.start_date, + leave.end_date, + ): + participation.sudo().write({"registration_state": "canceled"}) + + @api.model + def _shift_covers_holiday( + self, shift_start_time, shift_end_time, holiday_start_date, holiday_end_date + ): + """Compare two periods of time, return true if they overlap.""" + shift_start_date = shift_start_time.date() + shift_end_date = shift_end_time.date() + + return ( + ( + shift_start_date >= holiday_start_date + and shift_start_date <= holiday_end_date + ) + or ( + shift_end_date >= holiday_start_date + and shift_start_date <= holiday_end_date + ) + or ( + shift_start_date <= holiday_start_date + and shift_end_date >= holiday_end_date + ) + ) diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py index 5e8e2c9b9..c34a0ade4 100644 --- a/volunteer_holiday/tests/__init__.py +++ b/volunteer_holiday/tests/__init__.py @@ -3,3 +3,5 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from . import test_company_holiday + +# from . import test_volunteer_leave diff --git a/volunteer_holiday/tests/test_volunteer_leave.py b/volunteer_holiday/tests/test_volunteer_leave.py new file mode 100644 index 000000000..e69de29bb From 5e83781a984933a8902804a7a2039a6b76c9d159 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Thu, 5 Mar 2026 15:55:53 +0100 Subject: [PATCH 12/24] small fix _cancel_volunteer_leave_participation(). --- volunteer_holiday/models/volunteer_volunteer_leave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volunteer_holiday/models/volunteer_volunteer_leave.py b/volunteer_holiday/models/volunteer_volunteer_leave.py index a843ad93f..206c52cdf 100644 --- a/volunteer_holiday/models/volunteer_volunteer_leave.py +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -49,7 +49,7 @@ def _cancel_volunteer_leave_participation(self): all_future_volunteer_leaves = ( self.env["volunteer.volunteer.leave"] .sudo() - .search([("start_date", ">=", date.today())]) + .search([("end_date", ">=", date.today())]) ) for leave in all_future_volunteer_leaves: From e260401fb58af7b0c566e13ebd713035d312677f Mon Sep 17 00:00:00 2001 From: genepi314 Date: Thu, 5 Mar 2026 18:25:15 +0100 Subject: [PATCH 13/24] Add test for _cancel_volunteer_leave_participation(). --- volunteer_holiday/tests/__init__.py | 3 +- .../tests/test_company_holiday.py | 10 +- .../tests/test_volunteer_leave.py | 161 ++++++++++++++++++ 3 files changed, 167 insertions(+), 7 deletions(-) diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py index c34a0ade4..5619e703a 100644 --- a/volunteer_holiday/tests/__init__.py +++ b/volunteer_holiday/tests/__init__.py @@ -2,6 +2,5 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later +from . import test_volunteer_leave from . import test_company_holiday - -# from . import test_volunteer_leave diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index 7bf05023f..a2a38ed71 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -10,7 +10,7 @@ @freeze_time("2026-01-01 10:00:00") -class TestCronHoliday(TransactionCase): +class TestCompanyHoliday(TransactionCase): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) @@ -33,14 +33,14 @@ def setUp(self, *args, **kwargs): self.Company = self.env["res.company"] self.Holiday = self.env["volunteer.company.holiday"] - self.Shift = self.env["volunteer.shift"] + # self.Shift = self.env["volunteer.shift"] self.Type = self.env["volunteer.shift.type"] self.Generator = self.env["volunteer.shift.recurrent.generator"] # Stages - self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") - self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") + # self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") + # self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") # Create required type self.type1 = self.Type.create( @@ -146,7 +146,7 @@ def setUp(self, *args, **kwargs): def test_cancel_holiday_shift(self): """Test that holidays do cancel confirmed generated shifts""" - # Check before test + # Checks before test shifts_on_holiday = self.gen_with_holiday.volunteer_shift_ids for shift in shifts_on_holiday: self.assertEqual(shift.state, "confirmed") diff --git a/volunteer_holiday/tests/test_volunteer_leave.py b/volunteer_holiday/tests/test_volunteer_leave.py index e69de29bb..5d01ba219 100644 --- a/volunteer_holiday/tests/test_volunteer_leave.py +++ b/volunteer_holiday/tests/test_volunteer_leave.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import date, datetime + +from freezegun import freeze_time + +from odoo.tests.common import TransactionCase + + +@freeze_time("2026-03-01 10:00:00") +class TestVolunteerLeave(TransactionCase): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + # Force all operations to run as admin + self.env = self.env(user=self.env.ref("base.user_admin")) + + # Set up the environment + self.env = self.env( + context=dict( + self.env.context, + mail_create_nolog=True, + mail_create_nosubscribe=True, + mail_notrack=True, + no_reset_password=True, + tracking_disable=True, + ) + ) + + # Models + + self.Volunteer = self.env["volunteer.volunteer"] + self.Shift = self.env["volunteer.shift"] + self.Type = self.env["volunteer.shift.type"] + self.VolunteerLeave = self.env["volunteer.volunteer.leave"] + self.VolunteerLeaveType = self.env["volunteer.volunteer.leave.type"] + self.Participation = self.env["volunteer.shift.participation"] + + # Create required stages + self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") + self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") + + # Create required types + self.type1 = self.Type.create( + { + "name": "TypeTest", + "description": "Type for autotests", + } + ) + self.volunteer_leave_type1 = self.VolunteerLeaveType.create( + { + "name": "LeaveTypeTest", + "description": "Type for autotests", + } + ) + + # Create shifts + self.shift1 = self.Shift.create( + { + "name": "Shift1", + "stage_id": self.stage_confirmed.id, + "start_time": datetime(2026, 4, 1, 10, 5), + "end_time": datetime(2026, 4, 1, 12, 5), + "tz": "Europe/Brussels", + "max_volunteer_nb": 2, + "type_id": self.type1.id, + } + ) + self.shift2 = self.Shift.create( + { + "name": "Shift2", + "stage_id": self.stage_confirmed.id, + "start_time": datetime(2026, 4, 2, 10, 5), + "end_time": datetime(2026, 4, 2, 12, 5), + "tz": "Europe/Brussels", + "max_volunteer_nb": 2, + "type_id": self.type1.id, + } + ) + + # Create volunteers, one that has time off, one that doesn't + self.leaving_volunteer = self.Volunteer.create({"name": "LeavingVolunteer"}) + self.staying_volunteer = self.Volunteer.create({"name": "StayingVolunteer"}) + + # Create confirmed participations + self.leaving_volunteer_participation1 = self.Participation.create( + { + "volunteer_id": self.leaving_volunteer.id, + "shift_id": self.shift1.id, + "registration_state": "confirmed", + } + ) + self.leaving_volunteer_participation2 = self.Participation.create( + { + "volunteer_id": self.leaving_volunteer.id, + "shift_id": self.shift2.id, + "registration_state": "confirmed", + } + ) + self.staying_volunteer_participation1 = self.Participation.create( + { + "volunteer_id": self.staying_volunteer.id, + "shift_id": self.shift1.id, + "registration_state": "confirmed", + } + ) + self.staying_volunteer_participation2 = self.Participation.create( + { + "volunteer_id": self.staying_volunteer.id, + "shift_id": self.shift2.id, + "registration_state": "confirmed", + } + ) + + # Create leaves for leaving volunteer + self.leaving_volunteer_leave = self.VolunteerLeave.create( + { + "volunteer_id": self.leaving_volunteer.id, + "type_id": self.volunteer_leave_type1.id, + "start_date": date(2026, 4, 1), + "end_date": date(2026, 4, 2), + } + ) + + def test_cancel_volunteer_leave_participation(self): + """Test that volunteer participations are canceled + if the associated shifts overlap with the volunteer's time off""" + + # Checks before test + self.assertEqual( + self.leaving_volunteer_participation1.registration_state, "confirmed" + ) + self.assertEqual( + self.leaving_volunteer_participation2.registration_state, "confirmed" + ) + self.assertEqual( + self.staying_volunteer_participation1.registration_state, "confirmed" + ) + self.assertEqual( + self.staying_volunteer_participation2.registration_state, "confirmed" + ) + + # Call function + self.VolunteerLeave._cancel_volunteer_leave_participation() + + # Participations of the leaving volunteer should be canceled + self.assertEqual( + self.leaving_volunteer_participation1.registration_state, "canceled" + ) + self.assertEqual( + self.leaving_volunteer_participation2.registration_state, "canceled" + ) + # Participations of the staying volunteer should be confirmed + self.assertEqual( + self.staying_volunteer_participation1.registration_state, "confirmed" + ) + self.assertEqual( + self.staying_volunteer_participation2.registration_state, "confirmed" + ) From 8488f36fe3d83332fa49e556af7eea4fbfa301aa Mon Sep 17 00:00:00 2001 From: genepi314 Date: Mon, 9 Mar 2026 15:17:28 +0100 Subject: [PATCH 14/24] Add demo data for company holiday, volunteer leave and volunteer leave type. --- volunteer_holiday/__manifest__.py | 5 +++ volunteer_holiday/demo/demo.xml | 30 ------------- .../demo/volunteer_company_holiday_demo.xml | 37 ++++++++++++++++ .../demo/volunteer_volunteer_leave_demo.xml | 42 +++++++++++++++++++ .../volunteer_volunteer_leave_type_demo.xml | 31 ++++++++++++++ 5 files changed, 115 insertions(+), 30 deletions(-) delete mode 100644 volunteer_holiday/demo/demo.xml create mode 100644 volunteer_holiday/demo/volunteer_company_holiday_demo.xml create mode 100644 volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml create mode 100644 volunteer_holiday/demo/volunteer_volunteer_leave_type_demo.xml diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py index defdf419d..056355337 100644 --- a/volunteer_holiday/__manifest__.py +++ b/volunteer_holiday/__manifest__.py @@ -23,4 +23,9 @@ "views/volunteer_volunteer_leave_view.xml", "views/volunteer_menu.xml", ], + "demo": [ + "demo/volunteer_company_holiday_demo.xml", + "demo/volunteer_volunteer_leave_type_demo.xml", + "demo/volunteer_volunteer_leave_demo.xml", + ], } diff --git a/volunteer_holiday/demo/demo.xml b/volunteer_holiday/demo/demo.xml deleted file mode 100644 index 8b2cb8d36..000000000 --- a/volunteer_holiday/demo/demo.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/volunteer_holiday/demo/volunteer_company_holiday_demo.xml b/volunteer_holiday/demo/volunteer_company_holiday_demo.xml new file mode 100644 index 000000000..997f7d2d9 --- /dev/null +++ b/volunteer_holiday/demo/volunteer_company_holiday_demo.xml @@ -0,0 +1,37 @@ + + + + + Christmas Holidays + + + + + + Store Inventory + + + + diff --git a/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml b/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml new file mode 100644 index 000000000..b98e54af3 --- /dev/null +++ b/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/volunteer_holiday/demo/volunteer_volunteer_leave_type_demo.xml b/volunteer_holiday/demo/volunteer_volunteer_leave_type_demo.xml new file mode 100644 index 000000000..6237e6149 --- /dev/null +++ b/volunteer_holiday/demo/volunteer_volunteer_leave_type_demo.xml @@ -0,0 +1,31 @@ + + + + + Sick Leave + Leave due to illness + + + + Parental Leave + Leave due to pregnancy/childcare + + + + Vacation + Vacation + + From 15a6db182c3e029b68934a0bd5ab4b427eb86599 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 10 Mar 2026 09:08:24 +0100 Subject: [PATCH 15/24] draft new function to uncancel shifts in volunteer_company_holiday (for now commented). --- .../models/volunteer_company_holiday.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 8930fd3b1..d13a25c1d 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -109,3 +109,33 @@ def _shift_covers_holiday( and shift_end_date >= holiday_end_date ) ) + + # Override Methods + + # def write(self, vals): + # self.ensure_one() + # if "start_date" in vals or "end_date" in vals: + # old_start_date = self.start_date + # old_end_date = self.end_date + + # if vals["start_date"] > old_start_date: + # shifts_to_uncancel = self.env["volunteer_shift"].sudo().search( + # [ + # ("shift.state", "=", "canceled"), + # ("shift.start_time", ">", old_start_date), + # ("shift.start_time", "<", vals["start_date"]), + # ("shift.generator_id", "!=", None), + # ("shift.generator_id.is_maintained_during_holiday", "=", False), + # # Ajouter un bool canceled_through_cron ? + # ] + # ) + # for shift in shifts_to_uncancel: + # shift.super().write( + # { + # "stage_id": self.env.ref( + # "volunteer.volunteer_shift_stage_confirmed" + # ).id + # } + # ) + + # # if vals["end_date"] < old_end_date: From e9688bd26f4ce0853fd4691c5aed9dd6d0e802d0 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Thu, 12 Mar 2026 12:12:36 +0100 Subject: [PATCH 16/24] removed copy of method in volunteer_leave, add config for notification to volunteers, corrected README. --- volunteer_holiday/README.rst | 8 +++ volunteer_holiday/__init__.py | 1 + volunteer_holiday/__manifest__.py | 2 + volunteer_holiday/data/cron.xml | 25 +++++++-- volunteer_holiday/models/__init__.py | 1 + volunteer_holiday/models/res_company.py | 22 ++++++++ .../models/volunteer_company_holiday.py | 54 ++++++++++++------- .../models/volunteer_volunteer.py | 23 ++++++++ .../models/volunteer_volunteer_leave.py | 28 ++-------- .../static/description/index.html | 2 + .../views/res_config_settings_views.xml | 50 +++++++++++++++++ volunteer_holiday/wizards/__init__.py | 5 ++ .../wizards/res_config_settings.py | 14 +++++ 13 files changed, 186 insertions(+), 49 deletions(-) create mode 100644 volunteer_holiday/models/res_company.py create mode 100644 volunteer_holiday/views/res_config_settings_views.xml create mode 100644 volunteer_holiday/wizards/__init__.py create mode 100644 volunteer_holiday/wizards/res_config_settings.py diff --git a/volunteer_holiday/README.rst b/volunteer_holiday/README.rst index 8f5692c36..a2b1ff77a 100644 --- a/volunteer_holiday/README.rst +++ b/volunteer_holiday/README.rst @@ -57,6 +57,14 @@ Contributors Maintainers ~~~~~~~~~~~ +.. |maintainer-remytms| image:: https://github.com/remytms.png?size=40px + :target: https://github.com/remytms + :alt: remytms + +Current maintainer: + +|maintainer-remytms| + This module is part of the `beescoop/Obeesdoo `_ project on GitHub. You are welcome to contribute. diff --git a/volunteer_holiday/__init__.py b/volunteer_holiday/__init__.py index a6c9053e3..11a4be221 100644 --- a/volunteer_holiday/__init__.py +++ b/volunteer_holiday/__init__.py @@ -3,3 +3,4 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from . import models +from . import wizards diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py index 056355337..bbc0a35fb 100644 --- a/volunteer_holiday/__manifest__.py +++ b/volunteer_holiday/__manifest__.py @@ -9,6 +9,7 @@ "category": "Volunteer management", "website": "https://github.com/beescoop/Obeesdoo", "author": "Coop IT Easy SC", + "maintainers": ["remytms"], "license": "AGPL-3", "application": False, "depends": ["base", "volunteer", "mail"], @@ -16,6 +17,7 @@ "data/cron.xml", "security/ir.model.access.csv", "security/volunteer_security.xml", + "views/res_config_settings_views.xml", "views/volunteer_shift_generator_views.xml", "views/volunteer_company_holiday_view.xml", "views/volunteer_volunteer_view.xml", diff --git a/volunteer_holiday/data/cron.xml b/volunteer_holiday/data/cron.xml index 1bf1a886c..c50a467b0 100644 --- a/volunteer_holiday/data/cron.xml +++ b/volunteer_holiday/data/cron.xml @@ -4,10 +4,12 @@ SPDX-FileCopyrightText: 2025 Coop IT Easy SC SPDX-License-Identifier: AGPL-3.0-or-later --> - - + + - Cancel Generated Shifts During Company Holidays + Volunteer: Cancel Generated Shifts During Company Holidays code model._cancel_holiday_shift() @@ -18,7 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-or-later - Cancel Participations During Volunteer Leaves + Volunteer: Cancel Participations During Volunteer Leaves code model._cancel_volunteer_leave_participation() @@ -28,4 +32,17 @@ SPDX-License-Identifier: AGPL-3.0-or-later + + Notification: Send a Reminder to Volunteers Before their Leave Ends + + code + model._send_notification_end_leave() + 24 + hours + -1 + + + diff --git a/volunteer_holiday/models/__init__.py b/volunteer_holiday/models/__init__.py index 6f1de5c8a..1507f7e0f 100644 --- a/volunteer_holiday/models/__init__.py +++ b/volunteer_holiday/models/__init__.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later +from . import res_company from . import volunteer_company_holiday from . import volunteer_shift_recurrent_generator from . import volunteer_volunteer diff --git a/volunteer_holiday/models/res_company.py b/volunteer_holiday/models/res_company.py new file mode 100644 index 000000000..8abc19343 --- /dev/null +++ b/volunteer_holiday/models/res_company.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = ["res.company"] + + nb_days_before_leave_end = fields.Integer( + string="Number of days for notification before end of leave", + default=7, + ) + + _sql_constraints = [ + ( + "nb_days_is_pos", + "check (nb_days_before_leave_end > 0)", + "The number of days cannot be null or negative.", + ), + ] diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index d13a25c1d..48d41ae71 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -33,7 +33,7 @@ class VolunteerCompanyHoliday(models.Model): # Retirer la limite de temps, regarder les shifts futurs def _cancel_holiday_shift(self, time_in_months=3): - """Cancel shifts if they cover holiday period within time range""" + """Cancel shifts if they cover holiday period within time range.""" today_midnight = datetime.today().replace( hour=0, minute=0, second=0, microsecond=0 ) @@ -110,32 +110,46 @@ def _shift_covers_holiday( ) ) - # Override Methods + # Override Methods : DRAFT ! il faut encore préciser des conditions qui + # vérifieront que ça vaut la peine de lancer la fonction (qu'il y a bien + # des shifts à annuler dans l'interval de temps entre les anciennes dates + # de début et de fin des vacances). # def write(self, vals): - # self.ensure_one() # if "start_date" in vals or "end_date" in vals: - # old_start_date = self.start_date - # old_end_date = self.end_date - - # if vals["start_date"] > old_start_date: - # shifts_to_uncancel = self.env["volunteer_shift"].sudo().search( - # [ - # ("shift.state", "=", "canceled"), - # ("shift.start_time", ">", old_start_date), - # ("shift.start_time", "<", vals["start_date"]), - # ("shift.generator_id", "!=", None), - # ("shift.generator_id.is_maintained_during_holiday", "=", False), - # # Ajouter un bool canceled_through_cron ? - # ] - # ) + # canceled_shifts_during_holiday = self.env["volunteer_shift"].sudo().search( + # [ + # ("shift.state", "=", "canceled"), + # ("shift.generator_id", "!=", None), + # ("shift.generator_id.is_maintained_during_holiday", "=", False), + # # the 2 last domains could be replaced by one if we add the bool + # # is_canceled_by_holiday + # ] + # ) + # for holiday in self: + # old_start_date = holiday.start_date + # old_end_date = holiday.end_date + # shifts_to_uncancel = [] + # if vals["start_date"] > old_start_date: + # temp_shifts_to_uncancel = canceled_shifts_during_holiday.filtered( + # lambda shift: shift.start_time.date() > old_start_date + # and shift.start_time.date() < vals["start_date"] + # ) + # shifts_to_uncancel.extend(temp_shifts_to_uncancel) + # elif vals["end_date"] < old_end_date: + # temp_shifts_to_uncancel = canceled_shifts_during_holiday.filtered( + # lambda shift: shift.end_time.date() < old_end_date + # and shift.end_time.date() > vals["end_date"] + # ) + # shifts_to_uncancel.extend(temp_shifts_to_uncancel) # for shift in shifts_to_uncancel: - # shift.super().write( + # shift.write( # { # "stage_id": self.env.ref( # "volunteer.volunteer_shift_stage_confirmed" # ).id # } # ) - - # # if vals["end_date"] < old_end_date: + # TODO: Aussi penser aux participations de chaque shift, + # si on les repasse en confirmé, lesquelles, etc + # super().write(vals) diff --git a/volunteer_holiday/models/volunteer_volunteer.py b/volunteer_holiday/models/volunteer_volunteer.py index ec3a32817..d945d70bc 100644 --- a/volunteer_holiday/models/volunteer_volunteer.py +++ b/volunteer_holiday/models/volunteer_volunteer.py @@ -2,6 +2,10 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later +from datetime import datetime + +from dateutil.relativedelta import relativedelta + from odoo import fields, models @@ -13,3 +17,22 @@ class VolunteerVolunteer(models.Model): inverse_name="volunteer_id", string="Leave", ) + + def _send_notification_end_leave(self): + """Send a notification to volunteers before their leave ends.""" + all_companies = self.env["res.company"].search([]) + for company in all_companies: + nb_days = company.nb_days_before_leave_end + message_body = ("Your leave ends in {} days.").format(nb_days) + ending_soon_leaves = self.env["volunteer.volunteer.leave"].search( + [ + ("company_id", "=", company.id), + ( + "end_date", + "=", + datetime.today().date() + relativedelta(days=nb_days), + ), + ] + ) + for leave in ending_soon_leaves: + leave.volunteer_id.message_post(body=message_body) diff --git a/volunteer_holiday/models/volunteer_volunteer_leave.py b/volunteer_holiday/models/volunteer_volunteer_leave.py index 206c52cdf..e1cd71198 100644 --- a/volunteer_holiday/models/volunteer_volunteer_leave.py +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -4,7 +4,7 @@ from datetime import date, datetime -from odoo import api, fields, models +from odoo import fields, models class VolunteerVolunteerLeave(models.Model): @@ -43,6 +43,7 @@ class VolunteerVolunteerLeave(models.Model): def _cancel_volunteer_leave_participation(self): """Cancel participations of volunteers if they overlap with their time off""" + Holiday = self.env["volunteer.company.holiday"] today_midnight = datetime.today().replace( hour=0, minute=0, second=0, microsecond=0 ) @@ -61,33 +62,10 @@ def _cancel_volunteer_leave_participation(self): ) ) for participation in future_confirmed_participations: - if self._shift_covers_holiday( + if Holiday._shift_covers_holiday( participation.shift_id.start_time, participation.shift_id.end_time, leave.start_date, leave.end_date, ): participation.sudo().write({"registration_state": "canceled"}) - - @api.model - def _shift_covers_holiday( - self, shift_start_time, shift_end_time, holiday_start_date, holiday_end_date - ): - """Compare two periods of time, return true if they overlap.""" - shift_start_date = shift_start_time.date() - shift_end_date = shift_end_time.date() - - return ( - ( - shift_start_date >= holiday_start_date - and shift_start_date <= holiday_end_date - ) - or ( - shift_end_date >= holiday_start_date - and shift_start_date <= holiday_end_date - ) - or ( - shift_start_date <= holiday_start_date - and shift_end_date >= holiday_end_date - ) - ) diff --git a/volunteer_holiday/static/description/index.html b/volunteer_holiday/static/description/index.html index 9e6ea2004..5a726cce5 100644 --- a/volunteer_holiday/static/description/index.html +++ b/volunteer_holiday/static/description/index.html @@ -410,6 +410,8 @@

Contributors

Maintainers

+

Current maintainer:

+

remytms

This module is part of the beescoop/Obeesdoo project on GitHub.

You are welcome to contribute.

diff --git a/volunteer_holiday/views/res_config_settings_views.xml b/volunteer_holiday/views/res_config_settings_views.xml new file mode 100644 index 000000000..4d8ff388f --- /dev/null +++ b/volunteer_holiday/views/res_config_settings_views.xml @@ -0,0 +1,50 @@ + + + + + Volunteer Settings: Add customization for notifications to volunteers + res.config.settings + + + + +

Notification settings

+
+
+
+ + Number of days for notification before end of leave + +
+ A notification will be sent to every volunteer some time before the end of their leave. Set the amount of days. +
+
+
+
+
+
+
+
+
+
+
+
diff --git a/volunteer_holiday/wizards/__init__.py b/volunteer_holiday/wizards/__init__.py new file mode 100644 index 000000000..d742162a0 --- /dev/null +++ b/volunteer_holiday/wizards/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from . import res_config_settings diff --git a/volunteer_holiday/wizards/res_config_settings.py b/volunteer_holiday/wizards/res_config_settings.py new file mode 100644 index 000000000..6473571e6 --- /dev/null +++ b/volunteer_holiday/wizards/res_config_settings.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + nb_days_before_leave_end = fields.Integer( + related="company_id.nb_days_before_leave_end", + readonly=False, + ) From 8b1dc76d2c379486abf01adab4e760405fa0189f Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 17 Mar 2026 10:48:38 +0100 Subject: [PATCH 17/24] Add method to send notification for end of volunteer leave, add configuration in General settings, views, test, and corrected views for company holidays and volunteer leaves. --- volunteer_holiday/models/res_company.py | 2 +- .../models/volunteer_company_holiday.py | 15 +++- .../models/volunteer_volunteer_leave.py | 21 +++-- volunteer_holiday/tests/__init__.py | 6 +- .../tests/test_company_holiday.py | 6 -- .../tests/test_send_notification_end_leave.py | 84 +++++++++++++++++++ .../views/volunteer_company_holiday_view.xml | 81 +++++++++++++++++- .../views/volunteer_volunteer_leave_view.xml | 75 ++++++++++++++++- .../views/volunteer_volunteer_view.xml | 2 +- 9 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 volunteer_holiday/tests/test_send_notification_end_leave.py diff --git a/volunteer_holiday/models/res_company.py b/volunteer_holiday/models/res_company.py index 8abc19343..b66e3bfa9 100644 --- a/volunteer_holiday/models/res_company.py +++ b/volunteer_holiday/models/res_company.py @@ -16,7 +16,7 @@ class ResCompany(models.Model): _sql_constraints = [ ( "nb_days_is_pos", - "check (nb_days_before_leave_end > 0)", + "CHECK (nb_days_before_leave_end > 0)", "The number of days cannot be null or negative.", ), ] diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 48d41ae71..40c6dfc01 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -13,6 +13,7 @@ class VolunteerCompanyHoliday(models.Model): _name = "volunteer.company.holiday" _description = "Company Holidays" _order = "start_date" + _inherit = ["mail.thread", "mail.activity.mixin"] # Fields @@ -25,9 +26,19 @@ class VolunteerCompanyHoliday(models.Model): required=True, ) - start_date = fields.Date(required=True) + start_date = fields.Date(required=True, tracking=True) - end_date = fields.Date(required=True) + end_date = fields.Date(required=True, tracking=True) + + # SQL Constraints + + _sql_constraints = [ + ( + "start_d_smaller_than_end_d", + "CHECK (start_date <= end_date)", + "Start date shouldn't be greater than end date.", + ), + ] # Methods diff --git a/volunteer_holiday/models/volunteer_volunteer_leave.py b/volunteer_holiday/models/volunteer_volunteer_leave.py index e1cd71198..39345dd87 100644 --- a/volunteer_holiday/models/volunteer_volunteer_leave.py +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -10,7 +10,8 @@ class VolunteerVolunteerLeave(models.Model): _name = "volunteer.volunteer.leave" _description = "Volunteer Leave" - _order = "start_date" + _order = "start_date desc" + _inherit = ["mail.thread", "mail.activity.mixin"] # Relational Fields @@ -32,12 +33,18 @@ class VolunteerVolunteerLeave(models.Model): # Time fields - start_date = fields.Date( - required=True, - ) - end_date = fields.Date( - required=True, - ) + start_date = fields.Date(required=True, tracking=True) + end_date = fields.Date(required=True, tracking=True) + + # SQL Constraints + + _sql_constraints = [ + ( + "start_d_smaller_than_end_d", + "CHECK (start_date <= end_date)", + "Start date shouldn't be greater than end date.", + ), + ] # Methods diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py index 5619e703a..61ecf7ba9 100644 --- a/volunteer_holiday/tests/__init__.py +++ b/volunteer_holiday/tests/__init__.py @@ -2,5 +2,7 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later -from . import test_volunteer_leave -from . import test_company_holiday +from . import test_send_notification_end_leave + +# from . import test_volunteer_leave +# from . import test_company_holiday diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index a2a38ed71..8868874a5 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -33,15 +33,9 @@ def setUp(self, *args, **kwargs): self.Company = self.env["res.company"] self.Holiday = self.env["volunteer.company.holiday"] - # self.Shift = self.env["volunteer.shift"] self.Type = self.env["volunteer.shift.type"] self.Generator = self.env["volunteer.shift.recurrent.generator"] - # Stages - - # self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") - # self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") - # Create required type self.type1 = self.Type.create( { diff --git a/volunteer_holiday/tests/test_send_notification_end_leave.py b/volunteer_holiday/tests/test_send_notification_end_leave.py new file mode 100644 index 000000000..ccb088097 --- /dev/null +++ b/volunteer_holiday/tests/test_send_notification_end_leave.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import date + +from freezegun import freeze_time + +from odoo.tests.common import TransactionCase + + +# @freeze_time("2026-01-01 10:00:00") +class TestNotificationEndLeave(TransactionCase): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + # Force all operations to run as admin + self.env = self.env(user=self.env.ref("base.user_admin")) + + # Models + + self.Volunteer = self.env["volunteer.volunteer"] + self.Leave = self.env["volunteer.volunteer.leave"] + self.VolunteerLeaveType = self.env["volunteer.volunteer.leave.type"] + self.Company = self.env["res.company"] + + # Records + + # Set number of days before the end of a volunteer's leave + # to send notification, here 3, for example. + self.test_company = self.Company.create( + { + "name": "LeavingTestCompany", + "nb_days_before_leave_end": 3, + } + ) + + # Create necessary leave type. + self.volunteer_leave_type1 = self.VolunteerLeaveType.create( + { + "name": "LeaveTypeTest", + "description": "Type for autotests", + } + ) + + self.leaving_volunteer = self.Volunteer.create( + { + "name": "LeavingVolunteer", + "company_id": self.test_company.id, + } + ) + + self.leaving_volunteer_leave = self.Leave.create( + { + "volunteer_id": self.leaving_volunteer.id, + "type_id": self.volunteer_leave_type1.id, + "start_date": date(2026, 4, 1), + "end_date": date(2026, 4, 7), + } + ) + + def test_send_notification_end_leave(self): + """Check that a notification is sent to volunteers a certain time + before the end of their leave.""" + # Check before using the tested method + # Note that there's already a message sent automatically from OdooBot + # at creation of volunteer. We thus need to check there is not more than 1 message. + self.assertEqual(len(self.leaving_volunteer.message_ids), 1) + + # 4 days before end of leave, there shouldn't be any more message yet. + with freeze_time("2026-04-03 10:00:00"): + self.Volunteer._send_notification_end_leave() + self.assertEqual(len(self.leaving_volunteer.message_ids), 1) + + # 3 days before end of leave, a message should be posted. + with freeze_time("2026-04-04 10:00:00"): + self.Volunteer._send_notification_end_leave() + self.assertEqual(len(self.leaving_volunteer.message_ids), 2) + + # 2 days before end of leave, there shouldn't be any more + # message sent. + with freeze_time("2026-04-05 10:00:00"): + self.Volunteer._send_notification_end_leave() + self.assertEqual(len(self.leaving_volunteer.message_ids), 2) diff --git a/volunteer_holiday/views/volunteer_company_holiday_view.xml b/volunteer_holiday/views/volunteer_company_holiday_view.xml index 1be08b18a..f1cf443d5 100644 --- a/volunteer_holiday/views/volunteer_company_holiday_view.xml +++ b/volunteer_holiday/views/volunteer_company_holiday_view.xml @@ -6,11 +6,72 @@ SPDX-License-Identifier: AGPL-3.0-or-later --> + + Company Holiday Form + volunteer.company.holiday + +
+ +
+

+ +

+
+ + + + + + + +
+ +
+ + + +
+ +
+
+ - Holiday List + Company Holiday List volunteer.company.holiday - + @@ -19,10 +80,24 @@ SPDX-License-Identifier: AGPL-3.0-or-later + + volunteer.company.holiday + + + + + + + Company Holiday volunteer.company.holiday - tree + {"search_default_filter_future": 1} + tree,form
diff --git a/volunteer_holiday/views/volunteer_volunteer_leave_view.xml b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml index 238f6cbb2..6c3555017 100644 --- a/volunteer_holiday/views/volunteer_volunteer_leave_view.xml +++ b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later Volunteer Leaves List volunteer.volunteer.leave - + @@ -19,9 +19,80 @@ SPDX-License-Identifier: AGPL-3.0-or-later + + Volunteer Leaves Form + volunteer.volunteer.leave + +
+ +
+

+

+
+ + + + + + + +
+ +
+
+ + + volunteer.volunteer.leave + + + + + + + Volunteer Leaves volunteer.volunteer.leave - tree + {"search_default_filter_future": 1} + tree,form +
diff --git a/volunteer_holiday/views/volunteer_volunteer_view.xml b/volunteer_holiday/views/volunteer_volunteer_view.xml index 4ac7225fb..fbd3333c9 100644 --- a/volunteer_holiday/views/volunteer_volunteer_view.xml +++ b/volunteer_holiday/views/volunteer_volunteer_view.xml @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later > - + From 6992be1410b41fbdd0e405b9425c2f6330100987 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Sat, 21 Mar 2026 13:46:09 +0100 Subject: [PATCH 18/24] add badge on shift form view if on holiday, models volunteer_shift and volunteer_company_holiday updated. --- volunteer_holiday/__manifest__.py | 1 + volunteer_holiday/models/__init__.py | 1 + .../models/volunteer_company_holiday.py | 62 +++++---------- volunteer_holiday/models/volunteer_shift.py | 79 +++++++++++++++++++ volunteer_holiday/tests/__init__.py | 5 +- .../views/volunteer_shift_views.xml | 27 +++++++ 6 files changed, 128 insertions(+), 47 deletions(-) create mode 100644 volunteer_holiday/models/volunteer_shift.py create mode 100644 volunteer_holiday/views/volunteer_shift_views.xml diff --git a/volunteer_holiday/__manifest__.py b/volunteer_holiday/__manifest__.py index bbc0a35fb..2d038b27d 100644 --- a/volunteer_holiday/__manifest__.py +++ b/volunteer_holiday/__manifest__.py @@ -17,6 +17,7 @@ "data/cron.xml", "security/ir.model.access.csv", "security/volunteer_security.xml", + "views/volunteer_shift_views.xml", "views/res_config_settings_views.xml", "views/volunteer_shift_generator_views.xml", "views/volunteer_company_holiday_view.xml", diff --git a/volunteer_holiday/models/__init__.py b/volunteer_holiday/models/__init__.py index 1507f7e0f..cad294e94 100644 --- a/volunteer_holiday/models/__init__.py +++ b/volunteer_holiday/models/__init__.py @@ -8,3 +8,4 @@ from . import volunteer_volunteer from . import volunteer_volunteer_leave_type from . import volunteer_volunteer_leave +from . import volunteer_shift diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 40c6dfc01..487345fdc 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -94,7 +94,7 @@ def _cancel_holiday_shift(self, time_in_months=3): { "stage_id": self.env.ref( "volunteer.volunteer_shift_stage_canceled" - ).id + ).id, } ) @@ -121,46 +121,20 @@ def _shift_covers_holiday( ) ) - # Override Methods : DRAFT ! il faut encore préciser des conditions qui - # vérifieront que ça vaut la peine de lancer la fonction (qu'il y a bien - # des shifts à annuler dans l'interval de temps entre les anciennes dates - # de début et de fin des vacances). - - # def write(self, vals): - # if "start_date" in vals or "end_date" in vals: - # canceled_shifts_during_holiday = self.env["volunteer_shift"].sudo().search( - # [ - # ("shift.state", "=", "canceled"), - # ("shift.generator_id", "!=", None), - # ("shift.generator_id.is_maintained_during_holiday", "=", False), - # # the 2 last domains could be replaced by one if we add the bool - # # is_canceled_by_holiday - # ] - # ) - # for holiday in self: - # old_start_date = holiday.start_date - # old_end_date = holiday.end_date - # shifts_to_uncancel = [] - # if vals["start_date"] > old_start_date: - # temp_shifts_to_uncancel = canceled_shifts_during_holiday.filtered( - # lambda shift: shift.start_time.date() > old_start_date - # and shift.start_time.date() < vals["start_date"] - # ) - # shifts_to_uncancel.extend(temp_shifts_to_uncancel) - # elif vals["end_date"] < old_end_date: - # temp_shifts_to_uncancel = canceled_shifts_during_holiday.filtered( - # lambda shift: shift.end_time.date() < old_end_date - # and shift.end_time.date() > vals["end_date"] - # ) - # shifts_to_uncancel.extend(temp_shifts_to_uncancel) - # for shift in shifts_to_uncancel: - # shift.write( - # { - # "stage_id": self.env.ref( - # "volunteer.volunteer_shift_stage_confirmed" - # ).id - # } - # ) - # TODO: Aussi penser aux participations de chaque shift, - # si on les repasse en confirmé, lesquelles, etc - # super().write(vals) + def write(self, vals): + result = super().write(vals) + if "start_date" in vals or "end_date" in vals: + self.env["volunteer.shift"]._compute_overlap_holiday() + return result + + @api.model + def create(self, vals): + result = super().create(vals) + if "start_date" in vals or "end_date" in vals: + self.env["volunteer.shift"]._compute_overlap_holiday() + return result + + def unlink(self): + result = super().unlink() + self.env["volunteer.shift"]._compute_overlap_holiday() + return result diff --git a/volunteer_holiday/models/volunteer_shift.py b/volunteer_holiday/models/volunteer_shift.py new file mode 100644 index 000000000..695a22525 --- /dev/null +++ b/volunteer_holiday/models/volunteer_shift.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import datetime + +from odoo import api, fields, models + + +class VolunteerShift(models.Model): + _inherit = "volunteer.shift" + + overlaps_holiday = fields.Boolean( + string="Overlaps with Company Holidays", + compute="_compute_overlap_holiday", + tracking=True, + store=True, + ) + + # # This field needs more thinking. See method below. + # overlapping_holiday_ids = fields.Many2many( + # comodel_name="volunteer.company.holiday", + # string="Overlap with: ", + # compute="_compute_overlap_holiday", + # store=True, + # ) + + @api.depends("start_time", "end_time", "company_id") + def _compute_overlap_holiday(self): + """Compute if shift overlaps with any company holiday period.""" + # # For now, this function only applies to future shifts and holidays. + # # Some thinking will be needed regarding the update of the fields + # # overlapping_holiday_ids and overlaps_holiday over time, and whether + # # changing dates in the past should be possible or not. + date_today = datetime.today().date() + Holiday = self.env["volunteer.company.holiday"] + future_shifts = self.env["volunteer.shift"].search( + [("end_time", ">=", datetime.today())] + ) + for shift in future_shifts: + # old_overlapping_holidays = shift.overlapping_holiday_ids + # new_overlapping_holidays = [] + found_overlap = False + this_company_future_holidays = self.env["volunteer.company.holiday"].search( + [ + ("end_date", ">=", date_today), + ("company_id", "=", shift.company_id.id), + ] + ) + for holiday in this_company_future_holidays: + if Holiday._shift_covers_holiday( + shift.start_time, + shift.end_time, + holiday.start_date, + holiday.end_date, + ): + found_overlap = True + shift.overlaps_holiday = True + # new_overlapping_holidays.extend(holiday) + # shift.overlapping_holiday_ids += holiday.id + + if not found_overlap: + shift.overlaps_holiday = False + + # # Can't get to write this with proper syntax for now. Considering this + # # is only one side of the function, as there needs to be another one to do + # # the same job in volunteer.company.holiday. + + # elif old_overlapping_holidays != new_overlapping_holidays: + # old_and_new = list(set(old_overlapping_holidays + new_overlapping_holidays)) + # for holiday in old_and_new: + # if (holiday in old_overlapping_holidays + # and holiday not in new_overlapping_holidays): + # shift.write({"overlapping_holiday_ids": [(3, holiday.id)]}) + # elif (holiday in new_overlapping_holidays + # and holiday not in old_overlapping_holidays): + # shift.write({"overlapping_holiday_ids": [(4, holiday.id)]}) + + # print(shift.overlapping_holiday_ids) diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py index 61ecf7ba9..1cdcb16de 100644 --- a/volunteer_holiday/tests/__init__.py +++ b/volunteer_holiday/tests/__init__.py @@ -3,6 +3,5 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from . import test_send_notification_end_leave - -# from . import test_volunteer_leave -# from . import test_company_holiday +from . import test_volunteer_leave +from . import test_company_holiday diff --git a/volunteer_holiday/views/volunteer_shift_views.xml b/volunteer_holiday/views/volunteer_shift_views.xml new file mode 100644 index 000000000..fdbccfb1f --- /dev/null +++ b/volunteer_holiday/views/volunteer_shift_views.xml @@ -0,0 +1,27 @@ + + + + + Shift: Add info if shift overlaps with holidays + volunteer.shift + + + + +
+
+ Overlap with company holiday + +
+
+
+
+
+
From 432576cf9ab41246456c49f064077b9615265633 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Sat, 21 Mar 2026 15:20:52 +0100 Subject: [PATCH 19/24] Changed time range to 12 months, for demonstration purpose. --- volunteer_holiday/models/volunteer_company_holiday.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 487345fdc..6243ccffb 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -43,7 +43,7 @@ class VolunteerCompanyHoliday(models.Model): # Methods # Retirer la limite de temps, regarder les shifts futurs - def _cancel_holiday_shift(self, time_in_months=3): + def _cancel_holiday_shift(self, time_in_months=12): """Cancel shifts if they cover holiday period within time range.""" today_midnight = datetime.today().replace( hour=0, minute=0, second=0, microsecond=0 From e8b3d4606f492bb5846c9e5e7195e1ae29c3a498 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 24 Mar 2026 15:59:08 +0100 Subject: [PATCH 20/24] Add test for computed field overlaps_holiday, add common for all tests and clean all tests. --- volunteer_holiday/models/volunteer_shift.py | 20 ------ volunteer_holiday/tests/__init__.py | 3 + .../tests/test_company_holiday.py | 35 +-------- .../tests/test_send_notification_end_leave.py | 32 +++------ .../tests/test_volunteer_holiday_common.py | 57 +++++++++++++++ .../tests/test_volunteer_leave.py | 44 +----------- .../tests/test_volunteer_shift.py | 71 +++++++++++++++++++ 7 files changed, 144 insertions(+), 118 deletions(-) create mode 100644 volunteer_holiday/tests/test_volunteer_holiday_common.py create mode 100644 volunteer_holiday/tests/test_volunteer_shift.py diff --git a/volunteer_holiday/models/volunteer_shift.py b/volunteer_holiday/models/volunteer_shift.py index 695a22525..6f6278077 100644 --- a/volunteer_holiday/models/volunteer_shift.py +++ b/volunteer_holiday/models/volunteer_shift.py @@ -38,8 +38,6 @@ def _compute_overlap_holiday(self): [("end_time", ">=", datetime.today())] ) for shift in future_shifts: - # old_overlapping_holidays = shift.overlapping_holiday_ids - # new_overlapping_holidays = [] found_overlap = False this_company_future_holidays = self.env["volunteer.company.holiday"].search( [ @@ -56,24 +54,6 @@ def _compute_overlap_holiday(self): ): found_overlap = True shift.overlaps_holiday = True - # new_overlapping_holidays.extend(holiday) - # shift.overlapping_holiday_ids += holiday.id if not found_overlap: shift.overlaps_holiday = False - - # # Can't get to write this with proper syntax for now. Considering this - # # is only one side of the function, as there needs to be another one to do - # # the same job in volunteer.company.holiday. - - # elif old_overlapping_holidays != new_overlapping_holidays: - # old_and_new = list(set(old_overlapping_holidays + new_overlapping_holidays)) - # for holiday in old_and_new: - # if (holiday in old_overlapping_holidays - # and holiday not in new_overlapping_holidays): - # shift.write({"overlapping_holiday_ids": [(3, holiday.id)]}) - # elif (holiday in new_overlapping_holidays - # and holiday not in old_overlapping_holidays): - # shift.write({"overlapping_holiday_ids": [(4, holiday.id)]}) - - # print(shift.overlapping_holiday_ids) diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py index 1cdcb16de..b2519169d 100644 --- a/volunteer_holiday/tests/__init__.py +++ b/volunteer_holiday/tests/__init__.py @@ -2,6 +2,9 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later + +from . import test_volunteer_holiday_common +from . import test_volunteer_shift from . import test_send_notification_end_leave from . import test_volunteer_leave from . import test_company_holiday diff --git a/volunteer_holiday/tests/test_company_holiday.py b/volunteer_holiday/tests/test_company_holiday.py index 8868874a5..435ee0afc 100644 --- a/volunteer_holiday/tests/test_company_holiday.py +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -6,44 +6,16 @@ from freezegun import freeze_time -from odoo.tests.common import TransactionCase +from .test_volunteer_holiday_common import TestVolunteerHolidayCommon @freeze_time("2026-01-01 10:00:00") -class TestCompanyHoliday(TransactionCase): +class TestCompanyHoliday(TestVolunteerHolidayCommon): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) - # Force all operations to run as admin - self.env = self.env(user=self.env.ref("base.user_admin")) - - # Set up the environment - self.env = self.env( - context=dict( - self.env.context, - mail_create_nolog=True, - mail_create_nosubscribe=True, - mail_notrack=True, - no_reset_password=True, - tracking_disable=True, - ) - ) - - # Models - - self.Company = self.env["res.company"] - self.Holiday = self.env["volunteer.company.holiday"] - self.Type = self.env["volunteer.shift.type"] self.Generator = self.env["volunteer.shift.recurrent.generator"] - # Create required type - self.type1 = self.Type.create( - { - "name": "TypeTest", - "description": "Type for autotests", - } - ) - # Fix number of occurrences for all tests self.env.company.shift_nb_occurrence = 10 @@ -197,9 +169,6 @@ def test_cancel_holiday_shift(self): if shift.start_time == datetime(2026, 3, 6, 10, 0): self.assertEqual(shift.state, "confirmed") - # Create other company - self.anoter_company = self.Company.sudo().create({"name": "AnotherCompany"}) - # Create holidays of another company self.other_company_holiday = self.Holiday.sudo().create( { diff --git a/volunteer_holiday/tests/test_send_notification_end_leave.py b/volunteer_holiday/tests/test_send_notification_end_leave.py index ccb088097..db366ca65 100644 --- a/volunteer_holiday/tests/test_send_notification_end_leave.py +++ b/volunteer_holiday/tests/test_send_notification_end_leave.py @@ -6,28 +6,14 @@ from freezegun import freeze_time -from odoo.tests.common import TransactionCase +from .test_volunteer_holiday_common import TestVolunteerHolidayCommon # @freeze_time("2026-01-01 10:00:00") -class TestNotificationEndLeave(TransactionCase): +class TestNotificationEndLeave(TestVolunteerHolidayCommon): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) - # Force all operations to run as admin - self.env = self.env(user=self.env.ref("base.user_admin")) - - # Models - - self.Volunteer = self.env["volunteer.volunteer"] - self.Leave = self.env["volunteer.volunteer.leave"] - self.VolunteerLeaveType = self.env["volunteer.volunteer.leave.type"] - self.Company = self.env["res.company"] - - # Records - - # Set number of days before the end of a volunteer's leave - # to send notification, here 3, for example. self.test_company = self.Company.create( { "name": "LeavingTestCompany", @@ -35,7 +21,7 @@ def setUp(self, *args, **kwargs): } ) - # Create necessary leave type. + # Create required leave type. self.volunteer_leave_type1 = self.VolunteerLeaveType.create( { "name": "LeaveTypeTest", @@ -50,7 +36,7 @@ def setUp(self, *args, **kwargs): } ) - self.leaving_volunteer_leave = self.Leave.create( + self.leaving_volunteer_leave = self.VolunteerLeave.create( { "volunteer_id": self.leaving_volunteer.id, "type_id": self.volunteer_leave_type1.id, @@ -63,22 +49,20 @@ def test_send_notification_end_leave(self): """Check that a notification is sent to volunteers a certain time before the end of their leave.""" # Check before using the tested method - # Note that there's already a message sent automatically from OdooBot - # at creation of volunteer. We thus need to check there is not more than 1 message. - self.assertEqual(len(self.leaving_volunteer.message_ids), 1) + self.assertEqual(len(self.leaving_volunteer.message_ids), 0) # 4 days before end of leave, there shouldn't be any more message yet. with freeze_time("2026-04-03 10:00:00"): self.Volunteer._send_notification_end_leave() - self.assertEqual(len(self.leaving_volunteer.message_ids), 1) + self.assertEqual(len(self.leaving_volunteer.message_ids), 0) # 3 days before end of leave, a message should be posted. with freeze_time("2026-04-04 10:00:00"): self.Volunteer._send_notification_end_leave() - self.assertEqual(len(self.leaving_volunteer.message_ids), 2) + self.assertEqual(len(self.leaving_volunteer.message_ids), 1) # 2 days before end of leave, there shouldn't be any more # message sent. with freeze_time("2026-04-05 10:00:00"): self.Volunteer._send_notification_end_leave() - self.assertEqual(len(self.leaving_volunteer.message_ids), 2) + self.assertEqual(len(self.leaving_volunteer.message_ids), 1) diff --git a/volunteer_holiday/tests/test_volunteer_holiday_common.py b/volunteer_holiday/tests/test_volunteer_holiday_common.py new file mode 100644 index 000000000..047fdc93d --- /dev/null +++ b/volunteer_holiday/tests/test_volunteer_holiday_common.py @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + + +from odoo.tests.common import TransactionCase + + +class TestVolunteerHolidayCommon(TransactionCase): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + # Force all operations to run as admin + self.env = self.env(user=self.env.ref("base.user_admin")) + + # Set up the environment + self.env = self.env( + context=dict( + self.env.context, + mail_create_nolog=True, + mail_create_nosubscribe=True, + mail_notrack=True, + no_reset_password=True, + tracking_disable=True, + ) + ) + + # Models + + self.Shift = self.env["volunteer.shift"] + self.Type = self.env["volunteer.shift.type"] + self.Volunteer = self.env["volunteer.volunteer"] + self.Holiday = self.env["volunteer.company.holiday"] + self.Company = self.env["res.company"] + self.VolunteerLeave = self.env["volunteer.volunteer.leave"] + self.VolunteerLeaveType = self.env["volunteer.volunteer.leave.type"] + + # Recurrent records + + self.anoter_company = self.Company.create({"name": "AnotherCompany"}) + + # Mandatory records to use for required fields in other recs + + self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") + self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") + self.type1 = self.Type.create( + { + "name": "TypeTest", + "description": "Type for autotests", + } + ) + self.volunteer_leave_type1 = self.VolunteerLeaveType.create( + { + "name": "LeaveTypeTest", + "description": "Type for autotests", + } + ) diff --git a/volunteer_holiday/tests/test_volunteer_leave.py b/volunteer_holiday/tests/test_volunteer_leave.py index 5d01ba219..6bc733d5d 100644 --- a/volunteer_holiday/tests/test_volunteer_leave.py +++ b/volunteer_holiday/tests/test_volunteer_leave.py @@ -6,56 +6,18 @@ from freezegun import freeze_time -from odoo.tests.common import TransactionCase +from .test_volunteer_holiday_common import TestVolunteerHolidayCommon @freeze_time("2026-03-01 10:00:00") -class TestVolunteerLeave(TransactionCase): +class TestVolunteerLeave(TestVolunteerHolidayCommon): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) - # Force all operations to run as admin - self.env = self.env(user=self.env.ref("base.user_admin")) + # Needed models - # Set up the environment - self.env = self.env( - context=dict( - self.env.context, - mail_create_nolog=True, - mail_create_nosubscribe=True, - mail_notrack=True, - no_reset_password=True, - tracking_disable=True, - ) - ) - - # Models - - self.Volunteer = self.env["volunteer.volunteer"] - self.Shift = self.env["volunteer.shift"] - self.Type = self.env["volunteer.shift.type"] - self.VolunteerLeave = self.env["volunteer.volunteer.leave"] - self.VolunteerLeaveType = self.env["volunteer.volunteer.leave.type"] self.Participation = self.env["volunteer.shift.participation"] - # Create required stages - self.stage_confirmed = self.env.ref("volunteer.volunteer_shift_stage_confirmed") - self.stage_canceled = self.env.ref("volunteer.volunteer_shift_stage_canceled") - - # Create required types - self.type1 = self.Type.create( - { - "name": "TypeTest", - "description": "Type for autotests", - } - ) - self.volunteer_leave_type1 = self.VolunteerLeaveType.create( - { - "name": "LeaveTypeTest", - "description": "Type for autotests", - } - ) - # Create shifts self.shift1 = self.Shift.create( { diff --git a/volunteer_holiday/tests/test_volunteer_shift.py b/volunteer_holiday/tests/test_volunteer_shift.py new file mode 100644 index 000000000..aaf34eca8 --- /dev/null +++ b/volunteer_holiday/tests/test_volunteer_shift.py @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import date, datetime + +from freezegun import freeze_time + +from .test_volunteer_holiday_common import TestVolunteerHolidayCommon + + +@freeze_time("2026-01-01 10:00:00") +class TestVolunteerShift(TestVolunteerHolidayCommon): + def setUp(self): + super().setUp() + + # Records + self.easter_holiday = self.Holiday.create( + { + "name": "Easter", + "start_date": date(2026, 4, 6), + "end_date": date(2026, 4, 6), + } + ) + + self.shift_no_overlap = self.Shift.create( + { + "name": "ShiftNoOverlap", + "stage_id": self.stage_confirmed.id, + "start_time": datetime(2026, 4, 5, 10, 0), + "end_time": datetime(2026, 4, 5, 12, 0), + "tz": "Europe/Brussels", + "max_volunteer_nb": 2, + "type_id": self.type1.id, + } + ) + + self.shift_overlap = self.Shift.create( + { + "name": "ShiftOverlap", + "stage_id": self.stage_confirmed.id, + "start_time": datetime(2026, 4, 6, 10, 0), + "end_time": datetime(2026, 4, 6, 12, 0), + "tz": "Europe/Brussels", + "max_volunteer_nb": 2, + "type_id": self.type1.id, + } + ) + + self.shift_other_company = self.Shift.create( + { + "name": "ShiftOtherCompany", + "stage_id": self.stage_confirmed.id, + "start_time": datetime(2026, 4, 6, 10, 0), + "end_time": datetime(2026, 4, 6, 12, 0), + "tz": "Europe/Brussels", + "max_volunteer_nb": 2, + "type_id": self.type1.id, + "company_id": self.anoter_company.id, + } + ) + + def test_compute_overlap_holiday(self): + """Test that overlaps_holiday does change according + current shifts and holidays""" + # The compute_overlap_holiday is triggered when holidays are + # created, written or unlinked, as well as when start_time + # and end_time are modificated on shifts. + self.assertFalse(self.shift_no_overlap.overlaps_holiday) + self.assertTrue(self.shift_overlap.overlaps_holiday) + self.assertFalse(self.shift_other_company.overlaps_holiday) From bfdcddff8e2c751237fd8bafbca1772860fec18b Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 24 Mar 2026 17:24:04 +0100 Subject: [PATCH 21/24] Add ROADMAP. --- volunteer_holiday/README.rst | 30 +++++++++++ volunteer_holiday/readme/ROADMAP.rst | 26 ++++++++++ .../static/description/index.html | 50 +++++++++++++++---- 3 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 volunteer_holiday/readme/ROADMAP.rst diff --git a/volunteer_holiday/README.rst b/volunteer_holiday/README.rst index a2b1ff77a..a84292dfb 100644 --- a/volunteer_holiday/README.rst +++ b/volunteer_holiday/README.rst @@ -29,6 +29,36 @@ Add volunteer and company holidays to volunteer app, and manage shifts and gener .. contents:: :local: +Known issues / Roadmap +====================== + +- Currently, shifts that were canceled due to company holidays are not marked as such. It can be a source of confusion. + +Suggestion: Add message in the chatter of the shift form view stating that it was canceled because of company holidays. + +- A label indicates whether a shift overlaps with any holiday period ONLY in the shift form view. + +Suggestion: Add the same label in all shift views, especially in the shift list view. + +- No reminder that a shift has been created through a Generator with the bool is_maintained_during_holiday=True is visible on the shift. + +Suggestion: Add a field to the volunteer shift model and views to show this information. + +- Currently, there is no warning or any form of control in the cancellation-of-shifts-due-to-holidays process. + +Suggestions: +Add a wizard to control every step of the process when creating company holidays. +Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually. + +- A volunteer's participations are not marked when they overlap with any of their leave. + +Suggestion: Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations. + +- For now, it is still possible for a volunteer to participate in a shift that over laps withtheir leave. + +Suggestion: Add constraints and an error message if it is attempted. + + Bug Tracker =========== diff --git a/volunteer_holiday/readme/ROADMAP.rst b/volunteer_holiday/readme/ROADMAP.rst new file mode 100644 index 000000000..d012065c5 --- /dev/null +++ b/volunteer_holiday/readme/ROADMAP.rst @@ -0,0 +1,26 @@ +- Currently, shifts that were canceled due to company holidays are not marked as such. It can be a source of confusion. + +Suggestion: Add message in the chatter of the shift form view stating that it was canceled because of company holidays. + +- A label indicates whether a shift overlaps with any holiday period ONLY in the shift form view. + +Suggestion: Add the same label in all shift views, especially in the shift list view. + +- No reminder that a shift has been created through a Generator with the bool is_maintained_during_holiday=True is visible on the shift. + +Suggestion: Add a field to the volunteer shift model and views to show this information. + +- Currently, there is no warning or any form of control in the cancellation-of-shifts-due-to-holidays process. + +Suggestions: +Add a wizard to control every step of the process when creating company holidays. +Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually. + +- A volunteer's participations are not marked when they overlap with any of their leave. + +Suggestion: Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations. + +- For now, it is still possible for a volunteer to participate in a shift that over laps withtheir leave. + +Suggestion: Add constraints and an error message if it is attempted. + diff --git a/volunteer_holiday/static/description/index.html b/volunteer_holiday/static/description/index.html index 5a726cce5..2a51a09dd 100644 --- a/volunteer_holiday/static/description/index.html +++ b/volunteer_holiday/static/description/index.html @@ -374,17 +374,47 @@

Volunteer Holiday

Table of contents

+
+

Known issues / Roadmap

+
    +
  • Currently, shifts that were canceled due to company holidays are not marked as such. It can be a source of confusion.
  • +
+

Suggestion: Add message in the chatter of the shift form view stating that it was canceled because of company holidays.

+
    +
  • A label indicates whether a shift overlaps with any holiday period ONLY in the shift form view.
  • +
+

Suggestion: Add the same label in all shift views, especially in the shift list view.

+
    +
  • No reminder that a shift has been created through a Generator with the bool is_maintained_during_holiday=True is visible on the shift.
  • +
+

Suggestion: Add a field to the volunteer shift model and views to show this information.

+
    +
  • Currently, there is no warning or any form of control in the cancellation-of-shifts-due-to-holidays process.
  • +
+

Suggestions: +Add a wizard to control every step of the process when creating company holidays. +Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually.

+
    +
  • A volunteer’s participations are not marked when they overlap with any of their leave.
  • +
+

Suggestion: Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations.

+
    +
  • For now, it is still possible for a volunteer to participate in a shift that over laps withtheir leave.
  • +
+

Suggestion: Add constraints and an error message if it is attempted.

+
-

Bug Tracker

+

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 @@ -392,15 +422,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Coop IT Easy SC
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

Current maintainer:

remytms

This module is part of the beescoop/Obeesdoo project on GitHub.

From 396fddfba1e97a331527c83760a93e417624aa68 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Tue, 24 Mar 2026 17:26:52 +0100 Subject: [PATCH 22/24] reformat ROADMAP. --- volunteer_holiday/README.rst | 15 +++-- volunteer_holiday/readme/ROADMAP.rst | 15 +++-- .../static/description/index.html | 57 +++++++++++-------- 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/volunteer_holiday/README.rst b/volunteer_holiday/README.rst index a84292dfb..f5ca72e5d 100644 --- a/volunteer_holiday/README.rst +++ b/volunteer_holiday/README.rst @@ -34,29 +34,28 @@ Known issues / Roadmap - Currently, shifts that were canceled due to company holidays are not marked as such. It can be a source of confusion. -Suggestion: Add message in the chatter of the shift form view stating that it was canceled because of company holidays. + Add message in the chatter of the shift form view stating that it was canceled because of company holidays. - A label indicates whether a shift overlaps with any holiday period ONLY in the shift form view. -Suggestion: Add the same label in all shift views, especially in the shift list view. + Add the same label in all shift views, especially in the shift list view. - No reminder that a shift has been created through a Generator with the bool is_maintained_during_holiday=True is visible on the shift. -Suggestion: Add a field to the volunteer shift model and views to show this information. + Add a field to the volunteer shift model and views to show this information. - Currently, there is no warning or any form of control in the cancellation-of-shifts-due-to-holidays process. -Suggestions: -Add a wizard to control every step of the process when creating company holidays. -Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually. + Add a wizard to control every step of the process when creating company holidays. + AND/OR Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually. - A volunteer's participations are not marked when they overlap with any of their leave. -Suggestion: Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations. + Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations. - For now, it is still possible for a volunteer to participate in a shift that over laps withtheir leave. -Suggestion: Add constraints and an error message if it is attempted. + Add constraints and an error message if it is attempted. Bug Tracker diff --git a/volunteer_holiday/readme/ROADMAP.rst b/volunteer_holiday/readme/ROADMAP.rst index d012065c5..2fd0afe5a 100644 --- a/volunteer_holiday/readme/ROADMAP.rst +++ b/volunteer_holiday/readme/ROADMAP.rst @@ -1,26 +1,25 @@ - Currently, shifts that were canceled due to company holidays are not marked as such. It can be a source of confusion. -Suggestion: Add message in the chatter of the shift form view stating that it was canceled because of company holidays. + Add message in the chatter of the shift form view stating that it was canceled because of company holidays. - A label indicates whether a shift overlaps with any holiday period ONLY in the shift form view. -Suggestion: Add the same label in all shift views, especially in the shift list view. + Add the same label in all shift views, especially in the shift list view. - No reminder that a shift has been created through a Generator with the bool is_maintained_during_holiday=True is visible on the shift. -Suggestion: Add a field to the volunteer shift model and views to show this information. + Add a field to the volunteer shift model and views to show this information. - Currently, there is no warning or any form of control in the cancellation-of-shifts-due-to-holidays process. -Suggestions: -Add a wizard to control every step of the process when creating company holidays. -Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually. + Add a wizard to control every step of the process when creating company holidays. + AND/OR Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually. - A volunteer's participations are not marked when they overlap with any of their leave. -Suggestion: Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations. + Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations. - For now, it is still possible for a volunteer to participate in a shift that over laps withtheir leave. -Suggestion: Add constraints and an error message if it is attempted. + Add constraints and an error message if it is attempted. diff --git a/volunteer_holiday/static/description/index.html b/volunteer_holiday/static/description/index.html index 2a51a09dd..e2e4ce7eb 100644 --- a/volunteer_holiday/static/description/index.html +++ b/volunteer_holiday/static/description/index.html @@ -386,32 +386,39 @@

Volunteer Holiday

Known issues / Roadmap

-
    -
  • Currently, shifts that were canceled due to company holidays are not marked as such. It can be a source of confusion.
  • -
-

Suggestion: Add message in the chatter of the shift form view stating that it was canceled because of company holidays.

-
    -
  • A label indicates whether a shift overlaps with any holiday period ONLY in the shift form view.
  • -
-

Suggestion: Add the same label in all shift views, especially in the shift list view.

-
    -
  • No reminder that a shift has been created through a Generator with the bool is_maintained_during_holiday=True is visible on the shift.
  • -
-

Suggestion: Add a field to the volunteer shift model and views to show this information.

-
    -
  • Currently, there is no warning or any form of control in the cancellation-of-shifts-due-to-holidays process.
  • -
-

Suggestions: -Add a wizard to control every step of the process when creating company holidays. -Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually.

-
    -
  • A volunteer’s participations are not marked when they overlap with any of their leave.
  • -
-

Suggestion: Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations.

-
    -
  • For now, it is still possible for a volunteer to participate in a shift that over laps withtheir leave.
  • +
      +
    • Currently, shifts that were canceled due to company holidays are not marked as such. It can be a source of confusion.

      +
      +

      Add message in the chatter of the shift form view stating that it was canceled because of company holidays.

      +
      +
    • +
    • A label indicates whether a shift overlaps with any holiday period ONLY in the shift form view.

      +
      +

      Add the same label in all shift views, especially in the shift list view.

      +
      +
    • +
    • No reminder that a shift has been created through a Generator with the bool is_maintained_during_holiday=True is visible on the shift.

      +
      +

      Add a field to the volunteer shift model and views to show this information.

      +
      +
    • +
    • Currently, there is no warning or any form of control in the cancellation-of-shifts-due-to-holidays process.

      +
      +

      Add a wizard to control every step of the process when creating company holidays. +AND/OR Add a button in the company holidays view that would allow to trigger the function that cancels shifts manually.

      +
      +
    • +
    • A volunteer’s participations are not marked when they overlap with any of their leave.

      +
      +

      Add a computed field similar to overlaps_holiday in volunteer.shift, to display on all views with participations.

      +
      +
    • +
    • For now, it is still possible for a volunteer to participate in a shift that over laps withtheir leave.

      +
      +

      Add constraints and an error message if it is attempted.

      +
      +
    -

    Suggestion: Add constraints and an error message if it is attempted.

Bug Tracker

From 52f09f1b14faf0052b2b813d1ce9b4a0be9c7177 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Wed, 25 Mar 2026 12:29:51 +0100 Subject: [PATCH 23/24] Refactor _cancel_holiday_shift with itertools + remove unnecessary .sudo() + remove time_range. --- .../models/volunteer_company_holiday.py | 114 ++++++++---------- 1 file changed, 50 insertions(+), 64 deletions(-) diff --git a/volunteer_holiday/models/volunteer_company_holiday.py b/volunteer_holiday/models/volunteer_company_holiday.py index 6243ccffb..76e5168fd 100644 --- a/volunteer_holiday/models/volunteer_company_holiday.py +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -3,8 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from datetime import datetime - -from dateutil.relativedelta import relativedelta +from itertools import groupby from odoo import api, fields, models @@ -40,63 +39,68 @@ class VolunteerCompanyHoliday(models.Model): ), ] + # Overrride Methods + + def write(self, vals): + result = super().write(vals) + if "start_date" in vals or "end_date" in vals: + self.env["volunteer.shift"]._compute_overlap_holiday() + return result + + @api.model + def create(self, vals): + result = super().create(vals) + if "start_date" in vals or "end_date" in vals: + self.env["volunteer.shift"]._compute_overlap_holiday() + return result + + def unlink(self): + result = super().unlink() + self.env["volunteer.shift"]._compute_overlap_holiday() + return result + # Methods - # Retirer la limite de temps, regarder les shifts futurs - def _cancel_holiday_shift(self, time_in_months=12): + def _cancel_holiday_shift(self): """Cancel shifts if they cover holiday period within time range.""" today_midnight = datetime.today().replace( hour=0, minute=0, second=0, microsecond=0 ) - # Setting the time range we want to work with, here 3 months - date_time_range = today_midnight + relativedelta(months=time_in_months) - confirmed_future_generated_shifts_in_range = ( - self.env["volunteer.shift"] - .sudo() - .search( + future_company_holidays = self.env["volunteer.company.holiday"].search( + [("start_date", ">=", today_midnight)] + ) + + for company, grouped_holidays_by_company in groupby( + future_company_holidays, key=lambda holiday: holiday.company_id.id + ): + grouped_holidays = list(grouped_holidays_by_company) + same_company_shifts = self.env["volunteer.shift"].search( [ + ("company_id", "=", company), ("state", "=", "confirmed"), ("start_time", ">=", today_midnight), - ("start_time", "<=", date_time_range), - ("generator_id", "!=", None), + ("generator_id", "!=", False), ("generator_id.is_maintained_during_holiday", "=", False), ] ) - ) - - future_company_holidays_in_range = ( - self.env["volunteer.company.holiday"] - .sudo() - .search( - [ - ("start_date", ">=", today_midnight), - ("start_date", "<=", date_time_range.date()), - ] - ) - ) - - # Create dictionary key-company for list of values-shifts - shifts_by_company = {} - for shift in confirmed_future_generated_shifts_in_range: - shifts_by_company.setdefault(shift.company_id, []).append(shift) - - for holiday in future_company_holidays_in_range: - same_company_shifts = shifts_by_company.get(holiday.company_id, []) - for shift in same_company_shifts: - if shift.state != "canceled" and self._shift_covers_holiday( - shift.start_time, - shift.end_time, - holiday.start_date, - holiday.end_date, - ): - shift.sudo().write( - { - "stage_id": self.env.ref( - "volunteer.volunteer_shift_stage_canceled" - ).id, - } - ) + for hol in grouped_holidays: + for shift in same_company_shifts: + if self._shift_covers_holiday( + shift.start_time, + shift.end_time, + hol.start_date, + hol.end_date, + ): + shift.write( + { + "stage_id": self.env.ref( + "volunteer.volunteer_shift_stage_canceled" + ).id, + } + ) + + # Helper method @api.model def _shift_covers_holiday( @@ -120,21 +124,3 @@ def _shift_covers_holiday( and shift_end_date >= holiday_end_date ) ) - - def write(self, vals): - result = super().write(vals) - if "start_date" in vals or "end_date" in vals: - self.env["volunteer.shift"]._compute_overlap_holiday() - return result - - @api.model - def create(self, vals): - result = super().create(vals) - if "start_date" in vals or "end_date" in vals: - self.env["volunteer.shift"]._compute_overlap_holiday() - return result - - def unlink(self): - result = super().unlink() - self.env["volunteer.shift"]._compute_overlap_holiday() - return result From 7896929b090eac5a15c007f312f3f0774b3c7af7 Mon Sep 17 00:00:00 2001 From: genepi314 Date: Wed, 25 Mar 2026 15:31:36 +0100 Subject: [PATCH 24/24] Corrections after Remy's comments in PR + cleaned code overall. --- volunteer_holiday/controllers/__init__.py | 1 - volunteer_holiday/data/cron.xml | 2 +- .../demo/volunteer_company_holiday_demo.xml | 20 ++++--------------- .../demo/volunteer_volunteer_leave_demo.xml | 11 ++++------ volunteer_holiday/models/volunteer_shift.py | 12 ----------- .../models/volunteer_volunteer.py | 2 +- .../models/volunteer_volunteer_leave.py | 2 +- volunteer_holiday/tests/__init__.py | 1 - .../tests/test_send_notification_end_leave.py | 1 - .../tests/test_volunteer_shift.py | 3 ++- 10 files changed, 13 insertions(+), 42 deletions(-) delete mode 100644 volunteer_holiday/controllers/__init__.py diff --git a/volunteer_holiday/controllers/__init__.py b/volunteer_holiday/controllers/__init__.py deleted file mode 100644 index e046e49fb..000000000 --- a/volunteer_holiday/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import controllers diff --git a/volunteer_holiday/data/cron.xml b/volunteer_holiday/data/cron.xml index c50a467b0..a9e68562e 100644 --- a/volunteer_holiday/data/cron.xml +++ b/volunteer_holiday/data/cron.xml @@ -4,7 +4,7 @@ SPDX-FileCopyrightText: 2025 Coop IT Easy SC SPDX-License-Identifier: AGPL-3.0-or-later --> - + Christmas Holidays - - + + Store Inventory - - + + diff --git a/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml b/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml index b98e54af3..0ee014e4e 100644 --- a/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml +++ b/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml @@ -13,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-or-later @@ -30,13 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-or-later name="type_id" ref="volunteer_volunteer_leave_type_demo_parental_leave" /> - + diff --git a/volunteer_holiday/models/volunteer_shift.py b/volunteer_holiday/models/volunteer_shift.py index 6f6278077..e86d8aa56 100644 --- a/volunteer_holiday/models/volunteer_shift.py +++ b/volunteer_holiday/models/volunteer_shift.py @@ -17,21 +17,9 @@ class VolunteerShift(models.Model): store=True, ) - # # This field needs more thinking. See method below. - # overlapping_holiday_ids = fields.Many2many( - # comodel_name="volunteer.company.holiday", - # string="Overlap with: ", - # compute="_compute_overlap_holiday", - # store=True, - # ) - @api.depends("start_time", "end_time", "company_id") def _compute_overlap_holiday(self): """Compute if shift overlaps with any company holiday period.""" - # # For now, this function only applies to future shifts and holidays. - # # Some thinking will be needed regarding the update of the fields - # # overlapping_holiday_ids and overlaps_holiday over time, and whether - # # changing dates in the past should be possible or not. date_today = datetime.today().date() Holiday = self.env["volunteer.company.holiday"] future_shifts = self.env["volunteer.shift"].search( diff --git a/volunteer_holiday/models/volunteer_volunteer.py b/volunteer_holiday/models/volunteer_volunteer.py index d945d70bc..84aeb9316 100644 --- a/volunteer_holiday/models/volunteer_volunteer.py +++ b/volunteer_holiday/models/volunteer_volunteer.py @@ -15,7 +15,7 @@ class VolunteerVolunteer(models.Model): volunteer_leave_ids = fields.One2many( comodel_name="volunteer.volunteer.leave", inverse_name="volunteer_id", - string="Leave", + string="Leaves", ) def _send_notification_end_leave(self): diff --git a/volunteer_holiday/models/volunteer_volunteer_leave.py b/volunteer_holiday/models/volunteer_volunteer_leave.py index 39345dd87..e6370bc24 100644 --- a/volunteer_holiday/models/volunteer_volunteer_leave.py +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -10,7 +10,7 @@ class VolunteerVolunteerLeave(models.Model): _name = "volunteer.volunteer.leave" _description = "Volunteer Leave" - _order = "start_date desc" + _order = "start_date desc, volunteer_id, id" _inherit = ["mail.thread", "mail.activity.mixin"] # Relational Fields diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py index b2519169d..d0f31e5f1 100644 --- a/volunteer_holiday/tests/__init__.py +++ b/volunteer_holiday/tests/__init__.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: AGPL-3.0-or-later - from . import test_volunteer_holiday_common from . import test_volunteer_shift from . import test_send_notification_end_leave diff --git a/volunteer_holiday/tests/test_send_notification_end_leave.py b/volunteer_holiday/tests/test_send_notification_end_leave.py index db366ca65..978c11e2f 100644 --- a/volunteer_holiday/tests/test_send_notification_end_leave.py +++ b/volunteer_holiday/tests/test_send_notification_end_leave.py @@ -9,7 +9,6 @@ from .test_volunteer_holiday_common import TestVolunteerHolidayCommon -# @freeze_time("2026-01-01 10:00:00") class TestNotificationEndLeave(TestVolunteerHolidayCommon): def setUp(self, *args, **kwargs): super().setUp(*args, **kwargs) diff --git a/volunteer_holiday/tests/test_volunteer_shift.py b/volunteer_holiday/tests/test_volunteer_shift.py index aaf34eca8..38feffa99 100644 --- a/volunteer_holiday/tests/test_volunteer_shift.py +++ b/volunteer_holiday/tests/test_volunteer_shift.py @@ -15,6 +15,7 @@ def setUp(self): super().setUp() # Records + self.easter_holiday = self.Holiday.create( { "name": "Easter", @@ -65,7 +66,7 @@ def test_compute_overlap_holiday(self): current shifts and holidays""" # The compute_overlap_holiday is triggered when holidays are # created, written or unlinked, as well as when start_time - # and end_time are modificated on shifts. + # and end_time are modificated in shifts. self.assertFalse(self.shift_no_overlap.overlaps_holiday) self.assertTrue(self.shift_overlap.overlaps_holiday) self.assertFalse(self.shift_other_company.overlaps_holiday)