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..f5ca72e5d --- /dev/null +++ b/volunteer_holiday/README.rst @@ -0,0 +1,99 @@ +================= +Volunteer Holiday +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/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_holiday + :alt: beescoop/Obeesdoo + +|badge1| |badge2| |badge3| + +Add volunteer and company holidays to volunteer app, and manage shifts and generators accordingly. + +**Table of contents** + +.. 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. + + 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. + + +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-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 new file mode 100644 index 000000000..11a4be221 --- /dev/null +++ b/volunteer_holiday/__init__.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# 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 new file mode 100644 index 000000000..2d038b27d --- /dev/null +++ b/volunteer_holiday/__manifest__.py @@ -0,0 +1,34 @@ +# 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", + "maintainers": ["remytms"], + "license": "AGPL-3", + "application": False, + "depends": ["base", "volunteer", "mail"], + "data": [ + "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", + "views/volunteer_volunteer_view.xml", + "views/volunteer_volunteer_leave_type_view.xml", + "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/data/cron.xml b/volunteer_holiday/data/cron.xml new file mode 100644 index 000000000..a9e68562e --- /dev/null +++ b/volunteer_holiday/data/cron.xml @@ -0,0 +1,48 @@ + + + + + + Volunteer: Cancel Generated Shifts During Company Holidays + + code + model._cancel_holiday_shift() + 24 + hours + -1 + + + + + Volunteer: Cancel Participations During Volunteer Leaves + + code + model._cancel_volunteer_leave_participation() + 24 + hours + -1 + + + + + Notification: Send a Reminder to Volunteers Before their Leave Ends + + code + model._send_notification_end_leave() + 24 + hours + -1 + + + + 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..94b7d370a --- /dev/null +++ b/volunteer_holiday/demo/volunteer_company_holiday_demo.xml @@ -0,0 +1,25 @@ + + + + + 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..0ee014e4e --- /dev/null +++ b/volunteer_holiday/demo/volunteer_volunteer_leave_demo.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + 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 + + diff --git a/volunteer_holiday/models/__init__.py b/volunteer_holiday/models/__init__.py new file mode 100644 index 000000000..cad294e94 --- /dev/null +++ b/volunteer_holiday/models/__init__.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# 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 +from . import volunteer_volunteer_leave_type +from . import volunteer_volunteer_leave +from . import volunteer_shift diff --git a/volunteer_holiday/models/res_company.py b/volunteer_holiday/models/res_company.py new file mode 100644 index 000000000..b66e3bfa9 --- /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 new file mode 100644 index 000000000..5521f0ca1 --- /dev/null +++ b/volunteer_holiday/models/volunteer_company_holiday.py @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import datetime +from itertools import groupby + +from odoo import api, fields, models + + +class VolunteerCompanyHoliday(models.Model): + _name = "volunteer.company.holiday" + _description = "Company Holidays" + _order = "start_date, company_id, id" + _inherit = ["mail.thread", "mail.activity.mixin"] + + # Fields + + name = fields.Char(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, 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.", + ), + ] + + # Override 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_create_multi + def create(self, vals_list): + result = super().create(vals_list) + if "start_date" in vals_list or "end_date" in vals_list: + 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 + + 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 + ) + + future_company_holidays = self.env["volunteer.company.holiday"].search( + [("start_date", ">=", today_midnight)], + order="company_id", + ) + + for company_id, grouped_holidays_by_company in groupby( + future_company_holidays, key=lambda holiday: holiday.company_id + ): + same_company_shifts = self.env["volunteer.shift"].search( + [ + ("company_id", "=", company_id.id), + ("state", "=", "confirmed"), + ("start_time", ">=", today_midnight), + ("generator_id", "!=", False), + ("generator_id.is_maintained_during_holiday", "=", False), + ] + ) + for hol in grouped_holidays_by_company: + 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( + 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/models/volunteer_shift.py b/volunteer_holiday/models/volunteer_shift.py new file mode 100644 index 000000000..09ec880af --- /dev/null +++ b/volunteer_holiday/models/volunteer_shift.py @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + + +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, + ) + + # Compute Method + + @api.depends("start_time", "end_time", "company_id") + def _compute_overlap_holiday(self): + """Compute if shift overlaps with any company holiday period.""" + Holiday = self.env["volunteer.company.holiday"] + for shift in self: + # Setting local variable + found_overlap = False + company_holidays = self.env["volunteer.company.holiday"].search( + [ + ("company_id", "=", shift.company_id.id), + ], + ) + for hol in company_holidays: + if Holiday._shift_covers_holiday( + shift.start_time, + shift.end_time, + hol.start_date, + hol.end_date, + ): + found_overlap = True + break + + shift.overlaps_holiday = found_overlap 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..62030d34d --- /dev/null +++ b/volunteer_holiday/models/volunteer_shift_recurrent_generator.py @@ -0,0 +1,13 @@ +# 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", tracking=True + ) diff --git a/volunteer_holiday/models/volunteer_volunteer.py b/volunteer_holiday/models/volunteer_volunteer.py new file mode 100644 index 000000000..32fbe3844 --- /dev/null +++ b/volunteer_holiday/models/volunteer_volunteer.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo import fields, models +from odoo.tools.translate import _ + + +class VolunteerVolunteer(models.Model): + _inherit = "volunteer.volunteer" + + volunteer_leave_ids = fields.One2many( + comodel_name="volunteer.volunteer.leave", + inverse_name="volunteer_id", + string="Leaves", + ) + + 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 new file mode 100644 index 000000000..ef99424b7 --- /dev/null +++ b/volunteer_holiday/models/volunteer_volunteer_leave.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from datetime import date, datetime + +from odoo import fields, models + + +class VolunteerVolunteerLeave(models.Model): + _name = "volunteer.volunteer.leave" + _description = "Volunteer Leave" + _order = "start_date desc, volunteer_id, id" + _inherit = ["mail.thread", "mail.activity.mixin"] + + # Relational Fields + + volunteer_id = fields.Many2one( + comodel_name="volunteer.volunteer", + string="Volunteer", + required=True, + ) + company_id = fields.Many2one( + comodel_name="res.company", + 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, 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 + + 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 + ) + all_future_volunteer_leaves = self.search([("end_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 Holiday._shift_covers_holiday( + participation.shift_id.start_time, + participation.shift_id.end_time, + leave.start_date, + leave.end_date, + ): + participation.write({"registration_state": "canceled"}) 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..7d3bb30b4 --- /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, + ) 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/readme/ROADMAP.rst b/volunteer_holiday/readme/ROADMAP.rst new file mode 100644 index 000000000..2fd0afe5a --- /dev/null +++ b/volunteer_holiday/readme/ROADMAP.rst @@ -0,0 +1,25 @@ +- 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. + diff --git a/volunteer_holiday/security/ir.model.access.csv b/volunteer_holiday/security/ir.model.access.csv new file mode 100644 index 000000000..be173409c --- /dev/null +++ b/volunteer_holiday/security/ir.model.access.csv @@ -0,0 +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,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/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/static/description/index.html b/volunteer_holiday/static/description/index.html new file mode 100644 index 000000000..e2e4ce7eb --- /dev/null +++ b/volunteer_holiday/static/description/index.html @@ -0,0 +1,458 @@ + + + + + +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

+ +
+

Known issues / Roadmap

+
    +
  • 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.

    +
    +
  • +
+
+
+

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

+

Current maintainer:

+

remytms

+

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

+

You are welcome to contribute.

+
+
+
+ + diff --git a/volunteer_holiday/tests/__init__.py b/volunteer_holiday/tests/__init__.py new file mode 100644 index 000000000..d0f31e5f1 --- /dev/null +++ b/volunteer_holiday/tests/__init__.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 Coop IT Easy SC +# +# 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 new file mode 100644 index 000000000..435ee0afc --- /dev/null +++ b/volunteer_holiday/tests/test_company_holiday.py @@ -0,0 +1,191 @@ +# 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 TestCompanyHoliday(TestVolunteerHolidayCommon): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + self.Generator = self.env["volunteer.shift.recurrent.generator"] + + # 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", + "is_maintained_during_holiday": False, + "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_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", + "state": "confirmed", + "is_maintained_during_holiday": False, + "until_date": date(2026, 3, 2), + "interval_type": "days", + "interval": 1, + "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, + } + ) + + self.gen_one_long_shift = self.Generator.create( + { + "name": "genOneLongShift", + "state": "confirmed", + "is_maintained_during_holiday": False, + "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), + "tz": "Europe/Brussels", + "max_volunteer_nb": 6, + "type_id": self.type1.id, + } + ) + + self.gen_overlap_holiday = self.Generator.create( + { + "name": "genOverlapHoliday", + "state": "confirmed", + "is_maintained_during_holiday": False, + "until_date": date(2026, 3, 6), + "interval_type": "days", + "interval": 1, + "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, + } + ) + + # Create holidays + self.march_holiday = self.Holiday.create( + { + "name": "marchHolidays", + "start_date": date(2026, 3, 3), + "end_date": date(2026, 3, 5), + } + ) + + def test_cancel_holiday_shift(self): + """Test that holidays do cancel confirmed generated shifts""" + + # Checks before test + 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 + for shift in shifts_without_holiday: + self.assertEqual(shift.state, "confirmed") + + one_long_shift = self.gen_one_long_shift.volunteer_shift_ids + 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") + + # Call function + self.Holiday._cancel_holiday_shift() + + # All shifts in shifts_on_holiday cover the march_holidays period, + # thus should be canceled + for shift in shifts_on_holiday: + self.assertEqual(shift.state, "canceled") + + # 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") + + # 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 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): + 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): + self.assertEqual(shift.state, "confirmed") + + # 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, + # 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) + ) + self.assertEqual(march_6_shift.state, "confirmed") 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..978c11e2f --- /dev/null +++ b/volunteer_holiday/tests/test_send_notification_end_leave.py @@ -0,0 +1,67 @@ +# 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 .test_volunteer_holiday_common import TestVolunteerHolidayCommon + + +class TestNotificationEndLeave(TestVolunteerHolidayCommon): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + self.test_company = self.Company.create( + { + "name": "LeavingTestCompany", + "nb_days_before_leave_end": 3, + } + ) + + # Create required 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.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, 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 + 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), 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), 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), 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 new file mode 100644 index 000000000..6bc733d5d --- /dev/null +++ b/volunteer_holiday/tests/test_volunteer_leave.py @@ -0,0 +1,123 @@ +# 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-03-01 10:00:00") +class TestVolunteerLeave(TestVolunteerHolidayCommon): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + # Needed models + + self.Participation = self.env["volunteer.shift.participation"] + + # 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" + ) diff --git a/volunteer_holiday/tests/test_volunteer_shift.py b/volunteer_holiday/tests/test_volunteer_shift.py new file mode 100644 index 000000000..38feffa99 --- /dev/null +++ b/volunteer_holiday/tests/test_volunteer_shift.py @@ -0,0 +1,72 @@ +# 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 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) 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/views/volunteer_company_holiday_view.xml b/volunteer_holiday/views/volunteer_company_holiday_view.xml new file mode 100644 index 000000000..f1cf443d5 --- /dev/null +++ b/volunteer_holiday/views/volunteer_company_holiday_view.xml @@ -0,0 +1,103 @@ + + + + + + Company Holiday Form + volunteer.company.holiday + +
+ +
+

+ +

+
+ + + + + + + +
+ +
+ + + +
+ +
+
+ + + Company Holiday List + volunteer.company.holiday + + + + + + + + + + + + volunteer.company.holiday + + + + + + + + + Company Holiday + volunteer.company.holiday + {"search_default_filter_future": 1} + tree,form + + +
diff --git a/volunteer_holiday/views/volunteer_menu.xml b/volunteer_holiday/views/volunteer_menu.xml new file mode 100644 index 000000000..c8f29d327 --- /dev/null +++ b/volunteer_holiday/views/volunteer_menu.xml @@ -0,0 +1,35 @@ + + + + + + + + + + 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 + + + + + + + + + + + + + + + + + + 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 + +
+
+
+
+
+
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..06724a73f --- /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..6c3555017 --- /dev/null +++ b/volunteer_holiday/views/volunteer_volunteer_leave_view.xml @@ -0,0 +1,98 @@ + + + + + Volunteer Leaves List + volunteer.volunteer.leave + + + + + + + + + + + + + Volunteer Leaves Form + volunteer.volunteer.leave + +
+ +
+

+

+
+ + + + + + + +
+ +
+
+ + + volunteer.volunteer.leave + + + + + + + + + Volunteer Leaves + volunteer.volunteer.leave + {"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 new file mode 100644 index 000000000..fbd3333c9 --- /dev/null +++ b/volunteer_holiday/views/volunteer_volunteer_view.xml @@ -0,0 +1,29 @@ + + + + + Volunteer: Add page Vacation + volunteer.volunteer + + + + + + + + + + + + + + + + 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, + )