diff --git a/hr_holidays_public_regional/README.rst b/hr_holidays_public_regional/README.rst new file mode 100644 index 000000000..84f10541b --- /dev/null +++ b/hr_holidays_public_regional/README.rst @@ -0,0 +1,137 @@ +=========================== +HR Holidays Public Regional +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:bf25fc19d33af2c0f86e0fbe57aa1132e1bba1d1834a3ab851ef22f9943f1a09 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr--holidays-lightgray.png?logo=github + :target: https://github.com/OCA/hr-holidays/tree/17.0/hr_holidays_public_regional + :alt: OCA/hr-holidays +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-holidays-17-0/hr-holidays-17-0-hr_holidays_public_regional + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-holidays&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +| This module allows you to manage public regional holidays by defining + them in dedicated calendars and assigning them to employees. +| When enabled on a leave type, public regional holidays linked to the + employee are automatically excluded from the leave duration + calculation by default. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Go to *Time Off -> Configuration -> Time Off Types* and open a Leave +Type + +- Check "Exclude Public Regional Holidays" to exclude public holidays. + +Usage +===== + +For adding public regional holidays: + +1. Go to the menu *Leaves > Public Holidays > Public Regional Holidays*. +2. Create your public regional holidays. + +For using public regional holidays on leaves: + +1. Go to *Time Off > Dashboard*. +2. Select dragging on the calendar the days you want to be on leave, or + go to the form view for selecting start and end dates. +3. Select the proper "Leave Type" that has "Exclude Public Regional + Holidays" checked. +4. If no leave type is yet specified, then default configuration is to + exclude public regional holidays. +5. Go to Employees > Select an employee and assign that public regional + holidays +6. The number of days will be computed excluding public regional + holidays that match the selected employee, including global and state + holidays. + +In calendar views in HR holiday app public will be display (likes other +unusual days) according the current user employee regional calendar. + +- When an employee requests a leave using a leave type with **“Exclude + Public Regional Holidays”** enabled, + the system automatically subtracts any matching regional holidays + (from the assigned calendar) from the leave duration. +- The calculation is based on the employee’s assigned regional calendar + and only considers holidays within the leave period. +- If no regional calendar is assigned to the employee, the leave + duration will be computed normally. + +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 +------- + +* APSL-Nagarro + +Contributors +------------ + +- `APSL-Nagarro `__: + + - Miquel Alzanillas + - Antoni Marroig + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-peluko00| image:: https://github.com/peluko00.png?size=40px + :target: https://github.com/peluko00 + :alt: peluko00 +.. |maintainer-miquelalzanillas| image:: https://github.com/miquelalzanillas.png?size=40px + :target: https://github.com/miquelalzanillas + :alt: miquelalzanillas + +Current `maintainers `__: + +|maintainer-peluko00| |maintainer-miquelalzanillas| + +This module is part of the `OCA/hr-holidays `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_holidays_public_regional/__init__.py b/hr_holidays_public_regional/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/hr_holidays_public_regional/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/hr_holidays_public_regional/__manifest__.py b/hr_holidays_public_regional/__manifest__.py new file mode 100644 index 000000000..45561e7c3 --- /dev/null +++ b/hr_holidays_public_regional/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "HR Holidays Public Regional", + "summary": """ + Manage regional public holidays by assigning dedicated calendars to employees. + Leaves can optionally exclude regional holidays from their duration calculation. + """, + "version": "17.0.1.0.0", + "category": "Human Resources", + "author": "APSL-Nagarro, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/hr-holidays", + "depends": ["hr_holidays_public"], + "data": [ + "data/data.xml", + "security/ir.model.access.csv", + "views/hr_employee.xml", + "views/hr_holidays_public_regional.xml", + "views/hr_leave_type.xml", + "wizards/holidays_public_regional_next_year_wizard.xml", + ], + "maintainers": ["peluko00", "miquelalzanillas"], + "license": "AGPL-3", + "installable": True, +} diff --git a/hr_holidays_public_regional/data/data.xml b/hr_holidays_public_regional/data/data.xml new file mode 100644 index 000000000..ce07b0105 --- /dev/null +++ b/hr_holidays_public_regional/data/data.xml @@ -0,0 +1,8 @@ + + + + + Regional Holidays + + diff --git a/hr_holidays_public_regional/i18n/es.po b/hr_holidays_public_regional/i18n/es.po new file mode 100644 index 000000000..58ed1a4d9 --- /dev/null +++ b/hr_holidays_public_regional/i18n/es.po @@ -0,0 +1,272 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_holidays_public_regional +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-12-11 08:42+0000\n" +"PO-Revision-Date: 2025-12-11 08:42+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_hr_employee_base +msgid "Basic Employee" +msgstr "Empleado básico" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__year +msgid "Calendar Year" +msgstr "Año del calendario" + +#. module: hr_holidays_public_regional +#: model_terms:ir.ui.view,arch_db:hr_holidays_public_regional.holidays_public_regional_next_year_wizard_view +msgid "Cancel" +msgstr "Cancelar" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_res_partner +msgid "Contact" +msgstr "Contacto" + +#. module: hr_holidays_public_regional +#: model_terms:ir.ui.view,arch_db:hr_holidays_public_regional.holidays_public_regional_next_year_wizard_view +msgid "Create" +msgstr "Crear" + +#. module: hr_holidays_public_regional +#: model:ir.actions.act_window,name:hr_holidays_public_regional.action_create_next_year_public_regional_holidays +#: model:ir.ui.menu,name:hr_holidays_public_regional.menu_create_next_year_public_regional_holidays +#: model_terms:ir.ui.view,arch_db:hr_holidays_public_regional.holidays_public_regional_next_year_wizard_view +msgid "Create Next Year Public Regional Holidays" +msgstr "Crear Próximo Calendario Regional de Festivos" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__create_uid +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__create_uid +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__create_uid +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__create_date +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__create_date +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__create_date +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_public_regional_holidays_wizard +msgid "Creates public regional holidays from existing ones" +msgstr "" +"Crea el calendario de festivos regionales a partir de uno ya existente" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__date +msgid "Date" +msgstr "Fecha" + +#. module: hr_holidays_public_regional +#. odoo-python +#: code:addons/hr_holidays_public_regional/models/hr_holidays_public_regional.py:0 +#, python-format +msgid "" +"Dates of holidays should be the same year as the calendar year they are " +"being assigned to" +msgstr "" +"Las fechas de las vacaciones deberían ser el mismo año que el año de " +"calendario al que están siendo asignadas" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__display_name +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__display_name +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__display_name +msgid "Display Name" +msgstr "Mostrar nombre" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_hr_employee +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__employee_id +msgid "Employee" +msgstr "Empleado" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_hr_employee_regional_calendar +msgid "Employee Regional Calendar" +msgstr "Calendario Regional del Empleado" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__end_date +msgid "End Date" +msgstr "Fecha de Fin" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_leave_type__exclude_public_regional_holidays +msgid "Exclude Public Regional Holidays" +msgstr "Excluir Festivos Regionales" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__id +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__id +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__id +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__id +msgid "ID" +msgstr "" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,help:hr_holidays_public_regional.field_hr_leave_type__exclude_public_regional_holidays +msgid "" +"If enabled, public regional holidays are skipped in leave days calculation." +msgstr "" +"Si se marca, los festivos regionales no se tienen en cuenta en el cálculo de" +" días de ausencia." + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__write_uid +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__write_uid +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__write_uid +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__write_date +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__write_date +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__write_date +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__meeting_id +msgid "Meeting" +msgstr "Reunión" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__display_name +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__name +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__name +msgid "Name" +msgstr "Nombe" + +#. module: hr_holidays_public_regional +#. odoo-python +#: code:addons/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.py:0 +#, python-format +msgid "New public regional holidays" +msgstr "Nuevos Calendarios Festivos Regionales" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee__is_public_regional_holiday +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_base__is_public_regional_holiday +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_public__is_public_regional_holiday +msgid "Public Holiday Today" +msgstr "" + +#. module: hr_holidays_public_regional +#: model_terms:ir.ui.view,arch_db:hr_holidays_public_regional.edit_holiday_status_form +msgid "Public Holidays" +msgstr "Festivos" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_hr_holidays_public_regional +msgid "Public Holidays Regional" +msgstr "Festivos Regionales" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_hr_holidays_public_regional_line +msgid "Public Holidays Regional Lines" +msgstr "Líneas de Festivos Regionales" + +#. module: hr_holidays_public_regional +#: model:ir.actions.act_window,name:hr_holidays_public_regional.open_holidays_public_regional_view +#: model:ir.ui.menu,name:hr_holidays_public_regional.menu_holidays_public_regional_view +#: model_terms:ir.ui.view,arch_db:hr_holidays_public_regional.view_holidays_public_regional_form +msgid "Public Regional Holidays" +msgstr "Festivos Regionales" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__calendar_id +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional_line__calendar_id +msgid "Regional Calendar" +msgstr "Calendario Regional" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee__regional_holiday_calendar_ids +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_base__regional_holiday_calendar_ids +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_public__regional_holiday_calendar_ids +msgid "Regional Calendars" +msgstr "Calendarios Regionales" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_holidays_public_regional__line_ids +msgid "Regional Holidays Dates" +msgstr "Fechas Festivos Regionales" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_resource_calendar +msgid "Resource Working Time" +msgstr "Tiempo de trabajo de recursos" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_hr_employee_regional_calendar__start_date +msgid "Start Date" +msgstr "Fecha de Inicio" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__template_ids +msgid "Templates" +msgstr "Plantillas" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_hr_leave +msgid "Time Off" +msgstr "Ausencias" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_hr_leave_type +msgid "Time Off Type" +msgstr "Tipo de ausencia" + +#. module: hr_holidays_public_regional +#: model:ir.model,name:hr_holidays_public_regional.model_res_users +msgid "User" +msgstr "Usuario" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,field_description:hr_holidays_public_regional.field_public_regional_holidays_wizard__year +#: model_terms:ir.ui.view,arch_db:hr_holidays_public_regional.view_search_holidays_public_regional +msgid "Year" +msgstr "Año" + +#. module: hr_holidays_public_regional +#: model:ir.model.fields,help:hr_holidays_public_regional.field_public_regional_holidays_wizard__year +msgid "Year for which you want to create the public regional holidays. " +msgstr "Año para el que quieres crear el calendario de festivos regionales." + +#. module: hr_holidays_public_regional +#. odoo-python +#: code:addons/hr_holidays_public_regional/models/hr_holidays_public_regional.py:0 +#, python-format +msgid "You can't create duplicate public holidays for the date %s." +msgstr "No se pueden crear días festivos duplicados para la fecha %s." + +#. module: hr_holidays_public_regional +#. odoo-python +#: code:addons/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.py:0 +#, python-format +msgid "" +"You cannot use as template the public regional holidays of a year that " +"includes public regional holidays on 29th of February (2016, 2020...), " +"please select a template from another year." +msgstr "" +"No se puede utilizar como plantilla un calendario que incluye un festivo " +"regional el 29 de Febrero, por favor seleccione una plantilla de otro año." diff --git a/hr_holidays_public_regional/models/__init__.py b/hr_holidays_public_regional/models/__init__.py new file mode 100644 index 000000000..d600c59ff --- /dev/null +++ b/hr_holidays_public_regional/models/__init__.py @@ -0,0 +1,8 @@ +from . import hr_employee +from . import hr_employee_regional_calendar +from . import hr_holidays_public_regional +from . import hr_leave +from . import hr_leave_type +from . import resource_calendar +from . import res_partner +from . import res_users diff --git a/hr_holidays_public_regional/models/hr_employee.py b/hr_holidays_public_regional/models/hr_employee.py new file mode 100644 index 000000000..ed8d5da16 --- /dev/null +++ b/hr_holidays_public_regional/models/hr_employee.py @@ -0,0 +1,79 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from datetime import datetime + +from odoo import api, fields, models + + +class HREmployee(models.Model): + _inherit = "hr.employee" + + def _get_public_regional_holiday_lines(self, date_start, date_end): + """Just get the employees holidays""" + domain = self.env["hr.leave"]._get_domain_from_get_unusual_days_regional( + date_from=date_start, date_to=date_end + ) + return self.env["hr.holidays.public.regional.line"].search(domain) + + @api.model + def get_public_holidays_data(self, date_start, date_end): + # Include public holidays in the calendar summary + res = super().get_public_holidays_data(date_start, date_end) + self = self._get_contextual_employee() + public_holidays = self._get_public_regional_holiday_lines( + date_start, date_end + ).sorted("date") + res += list( + map( + lambda bh: { + "id": -bh.id, + "colorIndex": 0, + "end": (datetime.combine(bh.date, datetime.max.time())).isoformat(), + "endType": "datetime", + "isAllDay": True, + "start": ( + datetime.combine(bh.date, datetime.min.time()) + ).isoformat(), + "startType": "datetime", + "title": bh.name, + }, + public_holidays, + ) + ) + return sorted(res, key=lambda x: x["start"]) + + +class HrEmployeeBase(models.AbstractModel): + _inherit = "hr.employee.base" + + regional_holiday_calendar_ids = fields.One2many( + "hr.employee.regional.calendar", "employee_id", string="Regional Calendars" + ) + is_public_regional_holiday = fields.Boolean( + string="Public Holiday Today", compute="_compute_is_public_regional_holiday" + ) + + def _compute_is_public_regional_holiday(self): + holiday_public_regional = self.env["hr.holidays.public.regional"] + for item in self: + item.is_public_regional_holiday = ( + holiday_public_regional.is_public_regional_holiday( + fields.Date.context_today(item), employee=item + ) + ) + + def _get_im_status_hr_holidays_public_regional(self, key): + im_status_mapped = { + "online": "leave_online", + "away": "leave_away", + "offline": "leave_offline", + } + return im_status_mapped[key] + + def _compute_leave_status(self): + res = super()._compute_leave_status() + for item in self.filtered( + lambda x: not x.is_absent and x.is_public_regional_holiday + ): + item.is_absent = True + return res diff --git a/hr_holidays_public_regional/models/hr_employee_regional_calendar.py b/hr_holidays_public_regional/models/hr_employee_regional_calendar.py new file mode 100644 index 000000000..eda8eb70a --- /dev/null +++ b/hr_holidays_public_regional/models/hr_employee_regional_calendar.py @@ -0,0 +1,19 @@ +from odoo import fields, models + + +class EmployeeRegionalCalendar(models.Model): + _name = "hr.employee.regional.calendar" + _description = "Employee Regional Calendar" + _order = "start_date desc" + + employee_id = fields.Many2one( + "hr.employee", string="Employee", required=True, ondelete="cascade" + ) + calendar_id = fields.Many2one( + "hr.holidays.public.regional", + string="Regional Calendar", + required=True, + ondelete="restrict", + ) + start_date = fields.Date(required=True) + end_date = fields.Date(required=True) diff --git a/hr_holidays_public_regional/models/hr_holidays_public_regional.py b/hr_holidays_public_regional/models/hr_holidays_public_regional.py new file mode 100644 index 000000000..59d7e4550 --- /dev/null +++ b/hr_holidays_public_regional/models/hr_holidays_public_regional.py @@ -0,0 +1,159 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import datetime +from datetime import date + +from odoo import SUPERUSER_ID, _, api, fields, models +from odoo.exceptions import ValidationError + + +class HRHolidaysPublicRegional(models.Model): + _name = "hr.holidays.public.regional" + _description = "Public Holidays Regional" + _rec_name = "name" + _order = "name" + + name = fields.Char(required=True) + display_name = fields.Char("Name", compute="_compute_display_name", store=True) + line_ids = fields.One2many( + "hr.holidays.public.regional.line", "calendar_id", "Regional Holidays Dates" + ) + year = fields.Integer("Calendar Year", required=True, default=date.today().year) + + @api.depends("name", "year") + def _compute_display_name(self): + for line in self: + line.display_name = f"{line.name} ({line.year})" + + @api.model + @api.returns("hr.holidays.public.regional.line") + def get_regional_holidays_list( + self, employee, year=None, start_dt=None, end_dt=None + ): + """ + Returns recordset of hr.holidays.public.line + for the current year and employee + :param year: year as string (optional if start_dt and end_dt defined) + :param start_dt: start_dt as date + :param end_dt: end_dt as date + :param employee_id: ID of the employee + :return: recordset of hr.holidays.public.line + """ + if not start_dt and not end_dt: + year = year or datetime.date.today().year + start_dt = datetime.date(year, 1, 1) + end_dt = datetime.date(year, 12, 31) + if employee: + employee_regional_calendar_ids = self.env[ + "hr.employee.regional.calendar" + ].search( + [ + ("employee_id", "=", employee.id), + ] + ) + employee_calendar_ids = ( + employee_regional_calendar_ids.mapped("calendar_id").ids + if employee_regional_calendar_ids + else [] + ) + + if employee_calendar_ids: + domain = [ + ("date", ">=", start_dt), + ("date", "<=", end_dt), + ("calendar_id", "in", employee_calendar_ids), + ] + return self.env["hr.holidays.public.regional.line"].search(domain) + return self.env["hr.holidays.public.regional.line"].browse() + + @api.model + def is_public_regional_holiday(self, selected_date, employee): + """ + Returns True if selected_date is a public holiday for the employee + :param selected_date: datetime object + :param employee_id: ID of the employee + :return: bool + """ + holidays_lines = self.get_regional_holidays_list( + employee=employee, year=selected_date.year + ) + if holidays_lines: + hol_date = holidays_lines.filtered(lambda r: r.date == selected_date) + if hol_date: + return True + return False + + +class HrHolidaysPublicRegionalLine(models.Model): + _name = "hr.holidays.public.regional.line" + _description = "Public Holidays Regional Lines" + _order = "date, name desc" + + name = fields.Char(required=True) + date = fields.Date(required=True) + calendar_id = fields.Many2one( + "hr.holidays.public.regional", + "Regional Calendar", + required=True, + ondelete="cascade", + ) + meeting_id = fields.Many2one("calendar.event", string="Meeting", copy=False) + + @api.constrains("date") + def _check_date_state(self): + for line in self: + if line.date.year != line.calendar_id.year: + raise ValidationError( + _( + "Dates of holidays should be the same year as the calendar" + " year they are being assigned to" + ) + ) + + if line.date and line.calendar_id: + if line.calendar_id.line_ids.filtered_domain( + [("date", "=", line.date), ("id", "!=", line.id)] + ): + raise ValidationError( + _("You can't create duplicate public holidays for the date %s.") + % line.date + ) + + def _prepare_regional_holidays_meeting_values(self): + self.ensure_one() + categ_id = self.env.ref("hr_holidays_public_regional.event_type_holiday", False) + meeting_values = { + "name": ( + f"{self.name} ({self.calendar_id.name})" + if self.calendar_id.name + else self.name + ), + "start": self.date, + "stop": self.date, + "allday": True, + "user_id": SUPERUSER_ID, + "privacy": "confidential", + "show_as": "busy", + } + if categ_id: + meeting_values.update({"categ_ids": [(6, 0, categ_id.ids)]}) + return meeting_values + + @api.constrains("date", "name", "calendar_id") + def _update_calendar_event(self): + for rec in self: + if rec.meeting_id: + rec.meeting_id.write(rec._prepare_regional_holidays_meeting_values()) + + @api.model + def create(self, values): + res = super().create(values) + res.meeting_id = self.env["calendar.event"].create( + res._prepare_regional_holidays_meeting_values() + ) + return res + + def unlink(self): + self.mapped("meeting_id").unlink() + return super().unlink() diff --git a/hr_holidays_public_regional/models/hr_leave.py b/hr_holidays_public_regional/models/hr_leave.py new file mode 100644 index 000000000..86d01de25 --- /dev/null +++ b/hr_holidays_public_regional/models/hr_leave.py @@ -0,0 +1,99 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class HrLeave(models.Model): + _inherit = "hr.leave" + + def action_validate(self): + """Inject the needed context for excluding public holidays + (if applicable) on the actions derived from this validation. + This is required for example for `project_timesheet_holidays` + for not generating the timesheet on the public holiday. + Unfortunately, no regression test can be added, being in a separate module.""" + for leave in self: + if ( + leave.holiday_status_id.exclude_public_regional_holidays + or not leave.holiday_status_id + ): + leave = self.with_context( + employee_id=leave.employee_id.id, + exclude_public_regional_holidays=True, + ) + super(HrLeave, leave).action_validate() + return True + + def _get_duration(self, check_leave_type=True, resource_calendar=None): + if ( + self.holiday_status_id.exclude_public_regional_holidays + or not self.holiday_status_id + ): + instance = self.with_context(exclude_public_regional_holidays=True) + else: + instance = self + return super(HrLeave, instance)._get_duration( + check_leave_type, resource_calendar + ) + + @api.depends("number_of_days") + def _compute_number_of_hours_display(self): + to_serialize = self.filtered( + lambda x: x.state == "validate" + and x.holiday_status_id.exclude_public_regional_holidays + ) + for leave in to_serialize: + leave = leave.with_context( + exclude_public_regional_holidays=True, employee_id=leave.employee_id.id + ) + super(HrLeave, leave)._compute_number_of_hours_display() + return super(HrLeave, self - to_serialize)._compute_number_of_hours_display() + + def _get_domain_from_get_unusual_days_regional(self, date_from, date_to=None): + domain = [("date", ">=", date_from)] + # Use the employee of the user or the one who has the context + employee_id = self.env.context.get("employee_id", False) + employee = ( + self.env["hr.employee"].browse(employee_id) + if employee_id + else self.env.user.employee_id + ) + employee_regional_calendar_ids = self.env[ + "hr.employee.regional.calendar" + ].search( + [ + ("employee_id", "=", employee.id), + ] + ) + employee_calendar_ids = ( + employee_regional_calendar_ids.mapped("calendar_id").ids + if employee_regional_calendar_ids + else [] + ) + domain = [ + ("date", ">=", date_from), + ("calendar_id", "in", employee_calendar_ids), + ] + if date_to: + domain.append( + ( + "date", + "<=", + date_to, + ) + ) + return domain + + @api.model + def get_unusual_days(self, date_from, date_to=None): + res = super().get_unusual_days(date_from, date_to=date_to) + domain = self._get_domain_from_get_unusual_days_regional( + date_from=date_from, date_to=date_to + ) + public_regional_holidays = self.env["hr.holidays.public.regional.line"].search( + domain + ) + for public_regional_holiday in public_regional_holidays: + res[fields.Date.to_string(public_regional_holiday.date)] = True + return res diff --git a/hr_holidays_public_regional/models/hr_leave_type.py b/hr_holidays_public_regional/models/hr_leave_type.py new file mode 100644 index 000000000..358cae434 --- /dev/null +++ b/hr_holidays_public_regional/models/hr_leave_type.py @@ -0,0 +1,16 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class HrLeaveType(models.Model): + _inherit = "hr.leave.type" + + exclude_public_regional_holidays = fields.Boolean( + default=True, + help=( + "If enabled, public regional holidays are " + "skipped in leave days calculation." + ), + ) diff --git a/hr_holidays_public_regional/models/res_partner.py b/hr_holidays_public_regional/models/res_partner.py new file mode 100644 index 000000000..e868f8e91 --- /dev/null +++ b/hr_holidays_public_regional/models/res_partner.py @@ -0,0 +1,21 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + def _compute_im_status(self): + res = super()._compute_im_status() + for item in self.filtered( + lambda x: x.user_ids.employee_id.is_public_regional_holiday + and "leave_" not in x.im_status + ): + item.im_status = ( + item.user_ids.employee_id._get_im_status_hr_holidays_public_regional( + item.im_status + ) + ) + return res diff --git a/hr_holidays_public_regional/models/res_users.py b/hr_holidays_public_regional/models/res_users.py new file mode 100644 index 000000000..c545f286c --- /dev/null +++ b/hr_holidays_public_regional/models/res_users.py @@ -0,0 +1,21 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class User(models.Model): + _inherit = "res.users" + + def _compute_im_status(self): + res = super()._compute_im_status() + for user in self.filtered( + lambda x: x.employee_id.is_public_regional_holiday + and "leave_" not in x.im_status + ): + user.im_status = ( + user.employee_id._get_im_status_hr_holidays_public_regional( + user.im_status + ) + ) + return res diff --git a/hr_holidays_public_regional/models/resource_calendar.py b/hr_holidays_public_regional/models/resource_calendar.py new file mode 100644 index 000000000..941dbb38b --- /dev/null +++ b/hr_holidays_public_regional/models/resource_calendar.py @@ -0,0 +1,50 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +from odoo.addons.resource.models.utils import Intervals + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + def _attendance_intervals_batch_exclude_public_holidays_regional( + self, start_dt, end_dt, intervals, resources, tz + ): + employee_id = self.env.context.get("employee_id", False) + employee = self.env["hr.employee"].browse(employee_id) + list_by_dates = ( + self.env["hr.holidays.public.regional"] + .get_regional_holidays_list( + start_dt=start_dt.date(), + end_dt=end_dt.date(), + employee=employee, + ) + .mapped("date") + ) + for resource in resources: + interval_resource = intervals[resource.id] + attendances = [] + for attendance in interval_resource._items: + if attendance[0].date() not in list_by_dates: + attendances.append(attendance) + intervals[resource.id] = Intervals(attendances) + return intervals + + def _attendance_intervals_batch( + self, start_dt, end_dt, resources=None, domain=None, tz=None, lunch=False + ): + res = super()._attendance_intervals_batch( + start_dt=start_dt, + end_dt=end_dt, + resources=resources, + domain=domain, + tz=tz, + lunch=lunch, + ) + if self.env.context.get("exclude_public_regional_holidays") and resources: + return self._attendance_intervals_batch_exclude_public_holidays_regional( + start_dt, end_dt, res, resources, tz + ) + return res diff --git a/hr_holidays_public_regional/pyproject.toml b/hr_holidays_public_regional/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_holidays_public_regional/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_holidays_public_regional/readme/CONFIGURE.md b/hr_holidays_public_regional/readme/CONFIGURE.md new file mode 100644 index 000000000..77bcea165 --- /dev/null +++ b/hr_holidays_public_regional/readme/CONFIGURE.md @@ -0,0 +1,4 @@ +Go to *Time Off -\> Configuration -\> Time Off Types* and open a Leave +Type + +- Check "Exclude Public Regional Holidays" to exclude public holidays. diff --git a/hr_holidays_public_regional/readme/CONTRIBUTORS.md b/hr_holidays_public_regional/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..5eb18a2d7 --- /dev/null +++ b/hr_holidays_public_regional/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [APSL-Nagarro](https://www.apsl.tech): + - Miquel Alzanillas \<\> + - Antoni Marroig \<\> \ No newline at end of file diff --git a/hr_holidays_public_regional/readme/DESCRIPTION.md b/hr_holidays_public_regional/readme/DESCRIPTION.md new file mode 100644 index 000000000..caf5be8bb --- /dev/null +++ b/hr_holidays_public_regional/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allows you to manage public regional holidays by defining them in dedicated calendars and assigning them to employees. +When enabled on a leave type, public regional holidays linked to the employee are automatically excluded from the leave duration calculation by default. diff --git a/hr_holidays_public_regional/readme/USAGE.md b/hr_holidays_public_regional/readme/USAGE.md new file mode 100644 index 000000000..d94214901 --- /dev/null +++ b/hr_holidays_public_regional/readme/USAGE.md @@ -0,0 +1,26 @@ +For adding public regional holidays: + +1. Go to the menu *Leaves \> Public Holidays \> Public Regional Holidays*. +2. Create your public regional holidays. + +For using public regional holidays on leaves: + +1. Go to *Time Off \> Dashboard*. +2. Select dragging on the calendar the days you want to be on leave, or + go to the form view for selecting start and end dates. +3. Select the proper "Leave Type" that has "Exclude Public Regional Holidays" + checked. +4. If no leave type is yet specified, then default configuration is to + exclude public regional holidays. +5. Go to Employees > Select an employee and assign that public regional holidays +5. The number of days will be computed excluding public regional holidays that + match the selected employee, including global and state + holidays. + +In calendar views in HR holiday app public will be display (likes other +unusual days) according the current user employee regional calendar. + +- When an employee requests a leave using a leave type with **“Exclude Public Regional Holidays”** enabled, + the system automatically subtracts any matching regional holidays (from the assigned calendar) from the leave duration. +- The calculation is based on the employee’s assigned regional calendar and only considers holidays within the leave period. +- If no regional calendar is assigned to the employee, the leave duration will be computed normally. diff --git a/hr_holidays_public_regional/security/ir.model.access.csv b/hr_holidays_public_regional/security/ir.model.access.csv new file mode 100644 index 000000000..1800ee9a1 --- /dev/null +++ b/hr_holidays_public_regional/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_holidays_public_regional_user,access_hr_holidays_public_regional,model_hr_holidays_public_regional,base.group_user,1,0,0,0 +access_hr_holidays_public_regional_manager,access_hr_holidays_public_regional,model_hr_holidays_public_regional,hr_holidays.group_hr_holidays_manager,1,1,1,1 +access_hr_holidays_public_regional_line_user,access_hr_holidays_public_regional_line,model_hr_holidays_public_regional_line,base.group_user,1,0,0,0 +access_hr_holidays_public_regional_line_manager,access_hr_holidays_public_regional_line,model_hr_holidays_public_regional_line,hr_holidays.group_hr_holidays_manager,1,1,1,1 +access_hr_holidays_public_regional_manager_wizard,access_hr_holidays_public_regional_wizard,model_public_regional_holidays_wizard,hr_holidays.group_hr_holidays_manager,1,1,1,1 +acess_hr_employee_regional_calendar_user,acess_hr_employee_regional_calendar_user,model_hr_employee_regional_calendar,base.group_user,1,0,0,0 +acess_hr_employee_regional_calendar_manager,acess_hr_employee_regional_calendar_manager,model_hr_employee_regional_calendar,hr_holidays.group_hr_holidays_manager,1,1,1,1 diff --git a/hr_holidays_public_regional/static/description/icon.png b/hr_holidays_public_regional/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/hr_holidays_public_regional/static/description/icon.png differ diff --git a/hr_holidays_public_regional/static/description/icon.svg b/hr_holidays_public_regional/static/description/icon.svg new file mode 100644 index 000000000..a7a26d093 --- /dev/null +++ b/hr_holidays_public_regional/static/description/icon.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/hr_holidays_public_regional/static/description/index.html b/hr_holidays_public_regional/static/description/index.html new file mode 100644 index 000000000..35d914727 --- /dev/null +++ b/hr_holidays_public_regional/static/description/index.html @@ -0,0 +1,480 @@ + + + + + +HR Holidays Public Regional + + + +
+

HR Holidays Public Regional

+ + +

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

+
+
This module allows you to manage public regional holidays by defining +them in dedicated calendars and assigning them to employees.
+
When enabled on a leave type, public regional holidays linked to the +employee are automatically excluded from the leave duration +calculation by default.
+
+

Table of contents

+ +
+

Configuration

+

Go to Time Off -> Configuration -> Time Off Types and open a Leave +Type

+
    +
  • Check “Exclude Public Regional Holidays” to exclude public holidays.
  • +
+
+
+

Usage

+

For adding public regional holidays:

+
    +
  1. Go to the menu Leaves > Public Holidays > Public Regional Holidays.
  2. +
  3. Create your public regional holidays.
  4. +
+

For using public regional holidays on leaves:

+
    +
  1. Go to Time Off > Dashboard.
  2. +
  3. Select dragging on the calendar the days you want to be on leave, or +go to the form view for selecting start and end dates.
  4. +
  5. Select the proper “Leave Type” that has “Exclude Public Regional +Holidays” checked.
  6. +
  7. If no leave type is yet specified, then default configuration is to +exclude public regional holidays.
  8. +
  9. Go to Employees > Select an employee and assign that public regional +holidays
  10. +
  11. The number of days will be computed excluding public regional +holidays that match the selected employee, including global and state +holidays.
  12. +
+

In calendar views in HR holiday app public will be display (likes other +unusual days) according the current user employee regional calendar.

+
    +
  • When an employee requests a leave using a leave type with “Exclude +Public Regional Holidays” enabled, +the system automatically subtracts any matching regional holidays +(from the assigned calendar) from the leave duration.
  • +
  • The calculation is based on the employee’s assigned regional calendar +and only considers holidays within the leave period.
  • +
  • If no regional calendar is assigned to the employee, the leave +duration will be computed normally.
  • +
+
+
+

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

+
    +
  • APSL-Nagarro
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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

+

Current maintainers:

+

peluko00 miquelalzanillas

+

This module is part of the OCA/hr-holidays project on GitHub.

+

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

+
+
+
+ + diff --git a/hr_holidays_public_regional/tests/__init__.py b/hr_holidays_public_regional/tests/__init__.py new file mode 100644 index 000000000..91c3a3020 --- /dev/null +++ b/hr_holidays_public_regional/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_holidays_calculation +from . import test_holidays_public_regional diff --git a/hr_holidays_public_regional/tests/test_holidays_calculation.py b/hr_holidays_public_regional/tests/test_holidays_calculation.py new file mode 100644 index 000000000..e313df3dc --- /dev/null +++ b/hr_holidays_public_regional/tests/test_holidays_calculation.py @@ -0,0 +1,191 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +class TestRegionalHolidaysComputeDaysBase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + cls.HrLeave = cls.env["hr.leave"] + cls.HrLeaveType = cls.env["hr.leave.type"] + cls.HrHolidaysPublicRegional = cls.env["hr.holidays.public.regional"] + cls.calendar = cls.env["resource.calendar"].create( + {"name": "Calendar", "attendance_ids": []} + ) + for day in range(5): # From monday to friday + cls.calendar.attendance_ids = [ + ( + 0, + 0, + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "08", + "hour_to": "12", + }, + ), + ( + 0, + 0, + { + "name": "Attendance", + "dayofweek": str(day), + "hour_from": "14", + "hour_to": "18", + }, + ), + ] + # Use a very old year for avoiding to collapse with current data + cls.public_regional_holiday1 = cls.HrHolidaysPublicRegional.create( + { + "name": "Public Regional Holiday 1", + "year": 1946, + "line_ids": [(0, 0, {"name": "Christmas", "date": "1946-12-25"})], + } + ) + cls.public_regional_holiday2 = cls.HrHolidaysPublicRegional.create( + { + "name": "Public Regional Holiday 2", + "year": 1946, + "line_ids": [ + (0, 0, {"name": "Before Christmas", "date": "1946-12-24"}), + ( + 0, + 0, + { + "name": "Even More Before Christmas", + "date": "1946-12-23", + }, + ), + ], + } + ) + + cls.public_holiday_global_1947 = cls.HrHolidaysPublicRegional.create( + { + "name": "Public Holiday Global 1947", + "year": 1947, + "line_ids": [ + (0, 0, {"name": "New Eve", "date": "1947-01-01"}), + (0, 0, {"name": "New Eve extended", "date": "1947-01-02"}), + ], + } + ) + + cls.holiday_type = cls.HrLeaveType.create({"name": "Leave Type Test"}) + cls.holiday_type_no_excludes = cls.HrLeaveType.create( + { + "name": "Leave Type Test Without excludes", + "exclude_public_holidays": False, + } + ) + + cls.employee_1 = cls.env["hr.employee"].create( + { + "name": "Employee 1", + "resource_calendar_id": cls.calendar.id, + } + ) + cls.env["hr.employee.regional.calendar"].create( + { + "employee_id": cls.employee_1.id, + "calendar_id": cls.public_regional_holiday1.id, + "start_date": "1946-01-01", + "end_date": "1946-12-31", + } + ) + cls.employee_2 = cls.env["hr.employee"].create( + { + "name": "Employee 2", + "resource_calendar_id": cls.calendar.id, + } + ) + cls.env["hr.employee.regional.calendar"].create( + { + "employee_id": cls.employee_2.id, + "calendar_id": cls.public_regional_holiday2.id, + "start_date": "1946-01-01", + "end_date": "1946-12-31", + } + ) + + +class TestHolidaysRegionalComputeDays(TestRegionalHolidaysComputeDaysBase): + def test_number_days_excluding_employee_1(self): + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1946-12-29 23:59:59", # Sunday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_1.id, + } + ) + leave_request._compute_duration() + self.assertEqual(leave_request.number_of_days, 4) + + def _test_number_days_excluding_employee_2(self): + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1946-12-29 23:59:59", # Sunday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_2.id, + } + ) + leave_request._compute_duration() + self.assertEqual(leave_request.number_of_days, 2) + + def test_number_days_not_excluding(self): + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1946-12-29 23:59:59", # Sunday + "holiday_status_id": self.holiday_type_no_excludes.id, + "employee_id": self.employee_1.id, + } + ) + leave_request._compute_duration() + self.assertEqual(leave_request.number_of_days, 5) + + def test_number_days_across_year(self): + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1947-01-03 23:59:59", # Friday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_1.id, + } + ) + leave_request._compute_duration() + self.assertEqual(leave_request.number_of_days, 9) + + def test_number_days_across_year_2(self): + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1947-01-03 23:59:59", # Friday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_2.id, + } + ) + leave_request._compute_duration() + self.assertEqual(leave_request.number_of_days, 8) + + def test_number_of_hours_excluding_employee_2(self): + self.holiday_type.request_unit = "hour" + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1946-12-29 23:59:59", # Sunday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_2.id, + } + ) + + self.assertEqual(leave_request.number_of_days, 3) + self.assertEqual(leave_request.number_of_hours_display, 24) diff --git a/hr_holidays_public_regional/tests/test_holidays_public_regional.py b/hr_holidays_public_regional/tests/test_holidays_public_regional.py new file mode 100644 index 000000000..5a45bb9ad --- /dev/null +++ b/hr_holidays_public_regional/tests/test_holidays_public_regional.py @@ -0,0 +1,201 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import date + +from freezegun import freeze_time + +from odoo.exceptions import UserError, ValidationError +from odoo.tests import new_test_user + +from odoo.addons.base.tests.common import BaseCommon + + +class TestHolidaysPublicRegionalBase(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.holiday_model = cls.env["hr.holidays.public.regional"] + cls.holiday_model_line = cls.env["hr.holidays.public.regional.line"] + cls.employee_model = cls.env["hr.employee"] + cls.wizard_next_year = cls.env["public.regional.holidays.wizard"] + cls.leave_model = cls.env["hr.leave"] + + # Create holidays + holiday2 = cls.holiday_model.create({"name": "Holiday 2", "year": 1994}) + cls.holiday_model_line.create( + {"name": "holiday 5", "date": "1994-10-14", "calendar_id": holiday2.id} + ) + + holiday3 = cls.holiday_model.create({"name": "Holiday 3", "year": 1994}) + cls.holiday_model_line.create( + {"name": "holiday 6", "date": "1994-11-14", "calendar_id": holiday3.id} + ) + + cls.holiday1 = cls.holiday_model.create({"name": "Holiday 1", "year": 1995}) + for dt in ["1995-10-14", "1995-12-31", "1995-01-01"]: + cls.holiday_model_line.create( + {"name": "holiday x", "date": dt, "calendar_id": cls.holiday1.id} + ) + + cls.employee = cls.employee_model.create({"name": "Test Employee"}) + cls.env["hr.employee.regional.calendar"].create( + { + "employee_id": cls.employee.id, + "calendar_id": holiday2.id, + "start_date": "1994-01-01", + "end_date": "1994-12-31", + } + ) + + +class TestHolidaysPublicRegional(TestHolidaysPublicRegionalBase): + def test_duplicate_date_state_fail(self): + holiday4 = self.holiday_model.create({"name": "Holiday 4", "year": 1994}) + self.holiday_model_line.create( + {"name": "holiday x", "date": "1994-11-14", "calendar_id": holiday4.id} + ) + with self.assertRaises(ValidationError): + self.holiday_model_line.create( + {"name": "holiday x", "date": "1994-11-14", "calendar_id": holiday4.id} + ) + + def test_isnot_holiday(self): + self.assertFalse( + self.holiday_model.is_public_regional_holiday( + date(1995, 12, 10), self.employee + ) + ) + + def test_is_holiday(self): + self.assertTrue( + self.holiday_model.is_public_regional_holiday( + date(1994, 10, 14), self.employee + ) + ) + + def test_holiday_line_year(self): + holiday4 = self.holiday_model.create({"name": "Holiday 4", "year": 1994}) + with self.assertRaises(ValidationError): + self.holiday_model_line.create( + {"name": "holiday x", "date": "1995-11-14", "calendar_id": holiday4.id} + ) + + def test_list_holidays_in_list(self): + lines = self.holiday_model.get_regional_holidays_list( + employee=self.employee, year=1994 + ) + res = lines.filtered(lambda r: r.date == date(1994, 10, 14)) + self.assertEqual(len(res), 1) + self.assertEqual(len(lines), 1) + + def test_create_year_2000_public_holidays(self): + ph_start_ids = self.holiday_model.search([("year", "=", 1994)]) + val = {"template_ids": ph_start_ids, "year": 2000} + wz_create_ph = self.wizard_next_year.new(values=val) + + wz_create_ph.create_public_regional_holidays() + + lines = self.holiday_model_line.search([("calendar_id.year", "=", 2000)]) + self.assertEqual(len(lines), 2) + + def test_february_29th(self): + holiday_tw_2016 = self.holiday_model.create( + {"name": "Holiday TW 2016", "year": 2016} + ) + + self.holiday_model_line.create( + { + "name": "Peace Memorial Holiday", + "date": "2016-02-29", + "calendar_id": holiday_tw_2016.id, + } + ) + + val = {"template_ids": holiday_tw_2016} + wz_create_ph = self.wizard_next_year.new(values=val) + + with self.assertRaises(UserError): + wz_create_ph.create_public_regional_holidays() + + def test_calendar_event_created(self): + holiday = self.holiday_model.create({"name": "Holiday 2019", "year": 2019}) + hline = self.holiday_model_line.create( + {"name": "holiday x", "date": "2019-07-30", "calendar_id": holiday.id} + ) + meeting_id = hline.meeting_id + self.assertTrue(meeting_id) + hline.unlink() + self.assertFalse(meeting_id.exists()) + + def assertPublicHolidayIsUnusualDay(self, expected): + self.assertFalse( + self.leave_model.with_user(self.env.ref("base.user_demo").id) + .get_unusual_days("2019-07-01", date_to="2019-07-31") + .get("2019-07-30", False) + ) + holiday = self.holiday_model.create({"year": 2019}) + self.holiday_model_line.create( + { + "name": "holiday x", + "date": "2019-07-30", + "calendar_id": holiday.id, + } + ) + self.env["hr.employee.regional.calendar"].create( + { + "employee_id": self.employee.id, + "calendar_id": holiday.id, + "start_date": "2019-01-01", + "end_date": "2019-12-31", + } + ) + self.assertEqual( + self.leave_model.with_user( + self.env.ref("base.user_demo").id + ).get_unusual_days("2019-07-01", date_to="2019-07-31")["2019-07-30"], + expected, + ) + + def test_public_holidays_context(self): + self.leave_model = self.leave_model.with_context(employee_id=self.employee.id) + self.assertFalse( + self.leave_model.with_user(self.env.ref("base.user_demo").id) + .get_unusual_days("2019-07-01", date_to="2019-07-31") + .get("2019-07-30", False) + ) + holiday = self.holiday_model.create({"name": "Holiday 2019", "year": 2019}) + self.holiday_model_line.create( + { + "name": "holiday x", + "date": "2019-07-30", + "calendar_id": holiday.id, + } + ) + self.env["hr.employee.regional.calendar"].create( + { + "employee_id": self.employee.id, + "calendar_id": holiday.id, + "start_date": "2019-01-01", + "end_date": "2019-12-31", + } + ) + self.assertTrue( + self.leave_model.with_user( + self.env.ref("base.user_demo").id + ).get_unusual_days("2019-07-01", date_to="2019-07-31")["2019-07-30"], + ) + + @freeze_time("1994-10-14") + def test_user_im_status(self): + self.assertTrue(self.employee.is_public_regional_holiday) + self.assertEqual(self.employee.hr_icon_display, "presence_holiday_absent") + self.assertTrue(self.employee.is_absent) + user = new_test_user(self.env, login="test-user") + self.assertEqual(user.im_status, "offline") + self.assertEqual(user.partner_id.im_status, "offline") + self.employee.user_id = user + user.invalidate_recordset() + self.assertEqual(user.im_status, "leave_offline") + user.partner_id.invalidate_recordset() + self.assertEqual(user.partner_id.im_status, "leave_offline") diff --git a/hr_holidays_public_regional/views/hr_employee.xml b/hr_holidays_public_regional/views/hr_employee.xml new file mode 100644 index 000000000..8cba0ae2f --- /dev/null +++ b/hr_holidays_public_regional/views/hr_employee.xml @@ -0,0 +1,37 @@ + + + + + hr.employee.inherit + hr.employee + + + + + + + + + + + + + + + hr.employee.public.inherit + hr.employee.public + + + + + + + + + + + + + + diff --git a/hr_holidays_public_regional/views/hr_holidays_public_regional.xml b/hr_holidays_public_regional/views/hr_holidays_public_regional.xml new file mode 100644 index 000000000..e26eb949a --- /dev/null +++ b/hr_holidays_public_regional/views/hr_holidays_public_regional.xml @@ -0,0 +1,62 @@ + + + + + hr.holidays.public.regional.tree + hr.holidays.public.regional + + + + + + + + + + Holidays Public Regional Filters + hr.holidays.public.regional + + + + + + + + + hr.holidays.public.regional.form + hr.holidays.public.regional + +
+ + + + + + + + + + + + + + + + +
+
+ + + Public Regional Holidays + hr.holidays.public.regional + tree,form + + + +
diff --git a/hr_holidays_public_regional/views/hr_leave_type.xml b/hr_holidays_public_regional/views/hr_leave_type.xml new file mode 100644 index 000000000..e63dc686c --- /dev/null +++ b/hr_holidays_public_regional/views/hr_leave_type.xml @@ -0,0 +1,17 @@ + + + + + hr.leave.type.form + hr.leave.type + + + + + + + + + + diff --git a/hr_holidays_public_regional/wizards/__init__.py b/hr_holidays_public_regional/wizards/__init__.py new file mode 100644 index 000000000..a256e0abc --- /dev/null +++ b/hr_holidays_public_regional/wizards/__init__.py @@ -0,0 +1 @@ +from . import holidays_public_regional_next_year_wizard diff --git a/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.py b/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.py new file mode 100644 index 000000000..02c2d64a1 --- /dev/null +++ b/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.py @@ -0,0 +1,70 @@ +# Copyright 2025 APSL-Nagarro Miquel Alzanillas, Antoni Marroig +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class HolidaysPublicNextYearWizard(models.TransientModel): + _name = "public.regional.holidays.wizard" + _description = "Creates public regional holidays from existing ones" + + template_ids = fields.Many2many( + comodel_name="hr.holidays.public.regional", string="Templates", required=True + ) + year = fields.Integer( + help="Year for which you want to create the public regional holidays. ", + default=lambda self: fields.Date.today().year + 1, + required=True, + ) + + def create_public_regional_holidays(self): + self.ensure_one() + + ph_env = self.env["hr.holidays.public.regional"] + phs = ph_env.search([("id", "in", self.template_ids.ids)]) + + new_ph_ids = [] + for ph in phs: + new_ph = ph.copy() + new_ph.year = self.year + + new_ph_ids.append(new_ph.id) + + for last_ph_line in ph.line_ids: + feb_29 = last_ph_line.date.month == 2 and last_ph_line.date.day == 29 + + if feb_29: + # Handling this rare case would mean quite a lot of + # complexity because previous or next day might also be a + # public holiday. + raise UserError( + _( + "You cannot use as template the public regional holidays " + "of a year that " + "includes public regional holidays on 29th of February " + "(2016, 2020...), please select a template from " + "another year." + ) + ) + + new_date = last_ph_line.date.replace(year=self.year) + + new_ph_line_vals = {"date": new_date, "calendar_id": new_ph.id} + last_ph_line.copy(new_ph_line_vals) + + domain = [["id", "in", new_ph_ids]] + + action = { + "type": "ir.actions.act_window", + "name": _("New public regional holidays"), + "view_mode": "tree,form", + "res_model": "hr.holidays.public.regional", + "domain": domain, + } + + return action diff --git a/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.xml b/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.xml new file mode 100644 index 000000000..2749f8949 --- /dev/null +++ b/hr_holidays_public_regional/wizards/holidays_public_regional_next_year_wizard.xml @@ -0,0 +1,44 @@ + + + + + Create Next Year Public Regional Holidays + public.regional.holidays.wizard + +
+ + + + + + +
+
+
+
+
+ + Create Next Year Public Regional Holidays + public.regional.holidays.wizard + form + new + + +