From 4bf90586edfde3b9d8419c4a6f811698051ddcbf Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Mon, 30 Jun 2025 10:05:13 +0000 Subject: [PATCH 1/2] [ADD] hr_holidays_tier_validation: New module New module to overrides default holidays validation process to allow holidays validation based on a custom tier system. --- hr_holidays_tier_validation/README.rst | 76 ++++ hr_holidays_tier_validation/__init__.py | 1 + hr_holidays_tier_validation/__manifest__.py | 20 + hr_holidays_tier_validation/i18n/fr.po | 127 ++++++ .../i18n/hr_holidays_tier_validation.pot | 124 +++++ .../models/__init__.py | 3 + .../models/hr_leave.py | 138 ++++++ .../models/hr_leave_type.py | 11 + .../models/tier_definition.py | 11 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../static/description/index.html | 423 +++++++++++++++++ hr_holidays_tier_validation/tests/__init__.py | 1 + .../tests/test_hr_holidays_tier_validation.py | 426 ++++++++++++++++++ .../views/hr_leave_views.xml | 77 ++++ .../odoo/addons/hr_holidays_tier_validation | 1 + setup/hr_holidays_tier_validation/setup.py | 6 + 17 files changed, 1447 insertions(+) create mode 100644 hr_holidays_tier_validation/README.rst create mode 100644 hr_holidays_tier_validation/__init__.py create mode 100644 hr_holidays_tier_validation/__manifest__.py create mode 100644 hr_holidays_tier_validation/i18n/fr.po create mode 100644 hr_holidays_tier_validation/i18n/hr_holidays_tier_validation.pot create mode 100644 hr_holidays_tier_validation/models/__init__.py create mode 100644 hr_holidays_tier_validation/models/hr_leave.py create mode 100644 hr_holidays_tier_validation/models/hr_leave_type.py create mode 100644 hr_holidays_tier_validation/models/tier_definition.py create mode 100644 hr_holidays_tier_validation/readme/CONTRIBUTORS.rst create mode 100644 hr_holidays_tier_validation/readme/DESCRIPTION.rst create mode 100644 hr_holidays_tier_validation/static/description/index.html create mode 100644 hr_holidays_tier_validation/tests/__init__.py create mode 100644 hr_holidays_tier_validation/tests/test_hr_holidays_tier_validation.py create mode 100644 hr_holidays_tier_validation/views/hr_leave_views.xml create mode 120000 setup/hr_holidays_tier_validation/odoo/addons/hr_holidays_tier_validation create mode 100644 setup/hr_holidays_tier_validation/setup.py diff --git a/hr_holidays_tier_validation/README.rst b/hr_holidays_tier_validation/README.rst new file mode 100644 index 000000000..1b4edfbfc --- /dev/null +++ b/hr_holidays_tier_validation/README.rst @@ -0,0 +1,76 @@ +=========================== +HR holidays tier validation +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4aaca41166eb74188b0de71105edacd0ba2c0de6cece2ac4d396b58bb99985bc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.0/hr_holidays_tier_validation + :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-16-0/hr-holidays-16-0-hr_holidays_tier_validation + :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=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module overrides default holidays validation process to allow holidays validation based on a custom tier system. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* OpenStudio SAS + +Contributors +~~~~~~~~~~~~ + +* Simon Maillard + +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. + +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_tier_validation/__init__.py b/hr_holidays_tier_validation/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/hr_holidays_tier_validation/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_holidays_tier_validation/__manifest__.py b/hr_holidays_tier_validation/__manifest__.py new file mode 100644 index 000000000..5c28485ad --- /dev/null +++ b/hr_holidays_tier_validation/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "HR holidays tier validation", + "version": "16.0.1.0.0", + "category": "Human Resources", + "summary": "Tier Validation for HR Holidays", + "author": "Odoo Community Association (OCA), OpenStudio SAS", + "website": "https://github.com/OCA/hr-holidays", + "depends": [ + "hr_holidays", + "base_automation", + "base_tier_validation", + "base_tier_validation_waiting", # To alert only the next approver + "base_tier_validation_allow_disable_restart", # To avoid breaking the flow + ], + "data": [ + "views/hr_leave_views.xml", + ], + "installable": True, + "license": "AGPL-3", +} diff --git a/hr_holidays_tier_validation/i18n/fr.po b/hr_holidays_tier_validation/i18n/fr.po new file mode 100644 index 000000000..5efb5c5f7 --- /dev/null +++ b/hr_holidays_tier_validation/i18n/fr.po @@ -0,0 +1,127 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_holidays_tier_validation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-07-16 10:10+0000\n" +"PO-Revision-Date: 2025-07-16 10:10+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_holidays_tier_validation +#: model_terms:ir.ui.view,arch_db:hr_holidays_tier_validation.view_hr_leave_kanban_tier_validation +msgid " Reject" +msgstr " Rejeter" + +#. module: hr_holidays_tier_validation +#: model_terms:ir.ui.view,arch_db:hr_holidays_tier_validation.view_hr_leave_kanban_tier_validation +msgid " Validate" +msgstr " Valider" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__can_review +msgid "Can Review" +msgstr "Peut examiner" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__disable_validation_restart +msgid "Disable Validation Restart" +msgstr "Désactiver le redémarrage de la validation" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__has_comment +msgid "Has Comment" +msgstr "A un commentaire" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,help:hr_holidays_tier_validation.field_hr_leave__disable_validation_restart +msgid "If checked, disable the restart feature." +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__is_reevaluation_required +msgid "Is Reevaluation Required" +msgstr "Réévaluation requise" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave_type__leave_validation_type +msgid "Leave Validation" +msgstr "Validation des congés" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__need_validation +msgid "Need Validation" +msgstr "Besoin de validation" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__next_review +msgid "Next Review" +msgstr "Prochaine validation" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__rejected +msgid "Rejected" +msgstr "Refusé" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__rejected_message +msgid "Rejected Message" +msgstr "Message de refus" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__reviewer_ids +msgid "Reviewers" +msgstr "Examinateurs" + +#. module: hr_holidays_tier_validation +#: model:ir.model,name:hr_holidays_tier_validation.model_tier_definition +msgid "Tier Definition" +msgstr "Définition de niveau" + +#. module: hr_holidays_tier_validation +#: model:ir.model,name:hr_holidays_tier_validation.model_hr_leave +msgid "Time Off" +msgstr "Congés" + +#. module: hr_holidays_tier_validation +#: model:ir.model,name:hr_holidays_tier_validation.model_hr_leave_type +msgid "Time Off Type" +msgstr "Type de congé" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__to_validate_message +msgid "To Validate Message" +msgstr "Message à valider" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields.selection,name:hr_holidays_tier_validation.selection__hr_leave_type__leave_validation_type__tier_validation +msgid "Use Tier Validation System" +msgstr "Utiliser le système de validation par étapes" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__validated +msgid "Validated" +msgstr "Validé" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__validated_message +msgid "Validated Message" +msgstr "Message validé" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__validation_status +msgid "Validation Status" +msgstr "Statut de validation" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__review_ids +msgid "Validations" +msgstr "Validations" diff --git a/hr_holidays_tier_validation/i18n/hr_holidays_tier_validation.pot b/hr_holidays_tier_validation/i18n/hr_holidays_tier_validation.pot new file mode 100644 index 000000000..9dc9890bf --- /dev/null +++ b/hr_holidays_tier_validation/i18n/hr_holidays_tier_validation.pot @@ -0,0 +1,124 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_holidays_tier_validation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \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_tier_validation +#: model_terms:ir.ui.view,arch_db:hr_holidays_tier_validation.view_hr_leave_kanban_tier_validation +msgid " Reject" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model_terms:ir.ui.view,arch_db:hr_holidays_tier_validation.view_hr_leave_kanban_tier_validation +msgid " Validate" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__can_review +msgid "Can Review" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__disable_validation_restart +msgid "Disable Validation Restart" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__has_comment +msgid "Has Comment" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,help:hr_holidays_tier_validation.field_hr_leave__disable_validation_restart +msgid "If checked, disable the restart feature." +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__is_reevaluation_required +msgid "Is Reevaluation Required" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave_type__leave_validation_type +msgid "Leave Validation" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__need_validation +msgid "Need Validation" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__next_review +msgid "Next Review" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__rejected +msgid "Rejected" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__rejected_message +msgid "Rejected Message" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__reviewer_ids +msgid "Reviewers" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model,name:hr_holidays_tier_validation.model_tier_definition +msgid "Tier Definition" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model,name:hr_holidays_tier_validation.model_hr_leave +msgid "Time Off" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model,name:hr_holidays_tier_validation.model_hr_leave_type +msgid "Time Off Type" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__to_validate_message +msgid "To Validate Message" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields.selection,name:hr_holidays_tier_validation.selection__hr_leave_type__leave_validation_type__tier_validation +msgid "Use Tier Validation System" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__validated +msgid "Validated" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__validated_message +msgid "Validated Message" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__validation_status +msgid "Validation Status" +msgstr "" + +#. module: hr_holidays_tier_validation +#: model:ir.model.fields,field_description:hr_holidays_tier_validation.field_hr_leave__review_ids +msgid "Validations" +msgstr "" diff --git a/hr_holidays_tier_validation/models/__init__.py b/hr_holidays_tier_validation/models/__init__.py new file mode 100644 index 000000000..921aeed42 --- /dev/null +++ b/hr_holidays_tier_validation/models/__init__.py @@ -0,0 +1,3 @@ +from . import hr_leave +from . import hr_leave_type +from . import tier_definition diff --git a/hr_holidays_tier_validation/models/hr_leave.py b/hr_holidays_tier_validation/models/hr_leave.py new file mode 100644 index 000000000..c5c6056c5 --- /dev/null +++ b/hr_holidays_tier_validation/models/hr_leave.py @@ -0,0 +1,138 @@ +from odoo import api, models + + +class HrLeave(models.Model): + _name = "hr.leave" + _inherit = ["hr.leave", "tier.validation"] + _tier_validation_manual_config = False + _state_field = "state" + _state_from = ["confirm"] + _state_to = ["validate"] + _cancel_state = "refuse" + + @api.model + def create(self, vals_list): + if isinstance(vals_list, dict): + vals_list = [vals_list] + + records = super().create(vals_list) + + # Request validation for records that need tier validation and are in confirm state + for record in records: + if ( + record.holiday_status_id.leave_validation_type == "tier_validation" + and record.state == "confirm" + and record.need_validation + ): + record.request_validation() + + return records + + def action_confirm(self): + """Override to request tier validation on confirm if needed.""" + res = super().action_confirm() + + for rec in self: + if ( + rec.holiday_status_id.leave_validation_type == "tier_validation" + and rec.need_validation + ): + rec.request_validation() + + return res + + # ### Tier validation waiting Overrides ### + + @api.depends("review_ids.status", "holiday_status_id.leave_validation_type") + def _compute_need_validation(self): + """Override to check for tier validation only if leave type validation + is set to tier_validation.""" + for rec in self: + if rec.holiday_status_id.leave_validation_type == "tier_validation": + # Only use tier validation logic for tier validation leave types + return super()._compute_need_validation() + else: + # For other validation types, no tier validation is needed + rec.need_validation = False + + def _get_validation_exceptions(self, extra_domain=None, add_base_exceptions=True): + """Add fields to tier validation exceptions.""" + res = super()._get_validation_exceptions(extra_domain, add_base_exceptions) + res.extend( + ["state", "meeting_id", "first_approver_id", "second_approver_id", "active"] + ) + return res + + def _check_state_conditions(self, vals): + """Override to handle state conditions to respect + skip_check_state_condition context.""" + if self.env.context.get("skip_check_state_condition", False): + return False + + return super()._check_state_conditions(vals) + + def _validate_tier(self, tiers): + """Validate leave if all reviews are approved.""" + + res = super()._validate_tier(tiers) + if all(review.status == "approved" for review in self.review_ids): + + # Trigger leave validation if all reviews are approved + self.with_context( + mail_activity_automation_skip=True, + skip_check_state_condition=True, + ).action_validate() + + return res + + def _rejected_tier(self, tiers): + """Override to handle tier rejection logic.""" + + res = super()._rejected_tier(tiers) + # Trigger leave refusal if the tier is rejected + self.with_context(skip_check_state_condition=True).action_refuse() + + return res + + def _notify_review_available(self, tier_reviews): + if hasattr(self, "message_post") and hasattr(self, "message_subscribe"): + for rec in self.sudo(): + # Limit the message dest. to the users that have to do the next review + users_to_notify = tier_reviews.filtered( + lambda r: r.definition_id.notify_on_pending + and r.res_id == rec.id # and r.validation_status == "pending" + ).mapped("reviewer_ids") + + # Subscribe reviewers and notify + if len(users_to_notify) > 0: + rec.message_subscribe( + partner_ids=users_to_notify.mapped("partner_id").ids, + subtype_ids=self.env.ref( + self._get_requested_notification_subtype() + ).ids, + ) + # Do not subscribe recipients to avoid future notifications + rec.with_context(mail_post_autofollow=False).message_post( + subtype_xmlid=self._get_requested_notification_subtype(), + body=rec._notify_requested_review_body(), + partner_ids=users_to_notify.mapped("partner_id").ids, + message_type="email", + ) + + def _notify_get_recipients_groups(self, msg_vals=None): + """Disable direct action links, force user to go to the leave request page + The main goal is to avoid users to use upstream validation actions and + bypass tier validation + + TODO: + - Disable controllers of the upstream validation/refusal actions + """ + + # Get the default groups from parent + groups = super()._notify_get_recipients_groups(msg_vals) + + # Update all groups to have empty actions list + for _group_name, _group_func, group_data in groups: + group_data["actions"] = [] + + return groups diff --git a/hr_holidays_tier_validation/models/hr_leave_type.py b/hr_holidays_tier_validation/models/hr_leave_type.py new file mode 100644 index 000000000..7acad48f2 --- /dev/null +++ b/hr_holidays_tier_validation/models/hr_leave_type.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class HrLeaveType(models.Model): + _inherit = "hr.leave.type" + + leave_validation_type = fields.Selection( + selection_add=[ + ("tier_validation", "Use Tier Validation System"), + ] + ) diff --git a/hr_holidays_tier_validation/models/tier_definition.py b/hr_holidays_tier_validation/models/tier_definition.py new file mode 100644 index 000000000..fd2fb9a2f --- /dev/null +++ b/hr_holidays_tier_validation/models/tier_definition.py @@ -0,0 +1,11 @@ +from odoo import api, models + + +class TierDefinition(models.Model): + _inherit = "tier.definition" + + @api.model + def _get_tier_validation_model_names(self): + res = super()._get_tier_validation_model_names() + res.append("hr.leave") + return res diff --git a/hr_holidays_tier_validation/readme/CONTRIBUTORS.rst b/hr_holidays_tier_validation/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..5756920fa --- /dev/null +++ b/hr_holidays_tier_validation/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simon Maillard diff --git a/hr_holidays_tier_validation/readme/DESCRIPTION.rst b/hr_holidays_tier_validation/readme/DESCRIPTION.rst new file mode 100644 index 000000000..14ed7796a --- /dev/null +++ b/hr_holidays_tier_validation/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module overrides default holidays validation process to allow holidays validation based on a custom tier system. diff --git a/hr_holidays_tier_validation/static/description/index.html b/hr_holidays_tier_validation/static/description/index.html new file mode 100644 index 000000000..53c40c1e0 --- /dev/null +++ b/hr_holidays_tier_validation/static/description/index.html @@ -0,0 +1,423 @@ + + + + + +HR holidays tier validation + + + +
+

HR holidays tier validation

+ + +

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

+

This module overrides default holidays validation process to allow holidays validation based on a custom tier system.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • OpenStudio SAS
  • +
+
+
+

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.

+

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_tier_validation/tests/__init__.py b/hr_holidays_tier_validation/tests/__init__.py new file mode 100644 index 000000000..87afeac21 --- /dev/null +++ b/hr_holidays_tier_validation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_holidays_tier_validation diff --git a/hr_holidays_tier_validation/tests/test_hr_holidays_tier_validation.py b/hr_holidays_tier_validation/tests/test_hr_holidays_tier_validation.py new file mode 100644 index 000000000..8aa8a6c18 --- /dev/null +++ b/hr_holidays_tier_validation/tests/test_hr_holidays_tier_validation.py @@ -0,0 +1,426 @@ +# Copyright 2025 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +class TestHrHolidaysTierValidation(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + + # Clean up any existing tier definitions for hr.leave to avoid conflicts + existing_tier_defs = cls.env["tier.definition"].search( + [("model_id", "=", cls.env.ref("hr_holidays.model_hr_leave").id)] + ) + existing_tier_defs.unlink() + + # Clean up any existing stress days to avoid conflicts with other tests + existing_stress_days = cls.env["hr.leave.stress.day"].search([]) + existing_stress_days.unlink() + + # Models + cls.user_model = cls.env["res.users"] + cls.employee_model = cls.env["hr.employee"] + cls.leave_type_model = cls.env["hr.leave.type"] + cls.leave_model = cls.env["hr.leave"] + cls.leave_allocation_model = cls.env["hr.leave.allocation"] + cls.tier_definition_model = cls.env["tier.definition"] + cls.review_model = cls.env["tier.review"] + + # Create test users + cls.hr_manager = cls.user_model.create( + { + "name": "HR Manager Test", + "login": "hr_manager_test", + "email": "hr.manager.test@example.com", + "groups_id": [ + ( + 6, + 0, + [ + cls.env.ref("hr_holidays.group_hr_holidays_manager").id, + cls.env.ref("base.group_user").id, + ], + ) + ], + } + ) + + cls.leave_manager = cls.user_model.create( + { + "name": "Leave Manager Test", + "login": "leave_manager_test", + "email": "leave.manager.test@example.com", + "groups_id": [ + ( + 6, + 0, + [ + cls.env.ref("hr_holidays.group_hr_holidays_user").id, + cls.env.ref("base.group_user").id, + ], + ) + ], + } + ) + + cls.employee_user = cls.user_model.create( + { + "name": "Employee User Test", + "login": "employee_user_test", + "email": "employee.test@example.com", + "groups_id": [(6, 0, [cls.env.ref("base.group_user").id])], + } + ) + + # Create employees + cls.employee = cls.employee_model.create( + { + "name": "Test Employee", + "user_id": cls.employee_user.id, + "leave_manager_id": cls.leave_manager.id, + } + ) + + # Create leave type with tier validation + cls.leave_type_tier_validation = cls.leave_type_model.create( + { + "name": "Leave Type with Tier Validation Test", + "leave_validation_type": "tier_validation", + "allocation_validation_type": "no", + } + ) + + # Create leave type without tier validation + cls.leave_type_standard = cls.leave_type_model.create( + { + "name": "Leave Type Standard Test", + "leave_validation_type": "both", + "allocation_validation_type": "no", + } + ) + + # Create tier definitions + cls.tier_def_1 = cls.tier_definition_model.create( + { + "model_id": cls.env.ref("hr_holidays.model_hr_leave").id, + "review_type": "individual", + "reviewer_id": cls.leave_manager.id, + "definition_domain": "[('state', '=', 'confirm'), " + "('holiday_status_id.leave_validation_type', '=', 'tier_validation')]", + "sequence": 10, + "notify_on_pending": True, + "approve_sequence": True, + } + ) + + cls.tier_def_2 = cls.tier_definition_model.create( + { + "model_id": cls.env.ref("hr_holidays.model_hr_leave").id, + "review_type": "individual", + "reviewer_id": cls.hr_manager.id, + "definition_domain": "[('state', '=', 'confirm'), " + "('holiday_status_id.leave_validation_type', '=', 'tier_validation')]", # noqa B950 + "sequence": 20, + "notify_on_pending": True, + "approve_sequence": True, + } + ) + + # Create leave allocations + cls.leave_allocation_tier = cls.leave_allocation_model.create( + { + "name": "Test Allocation Tier", + "holiday_status_id": cls.leave_type_tier_validation.id, + "employee_id": cls.employee.id, + "number_of_days": 20, + } + ) + cls.leave_allocation_tier.with_user(cls.hr_manager).action_validate() + + cls.leave_allocation_standard = cls.leave_allocation_model.create( + { + "name": "Test Allocation Standard", + "holiday_status_id": cls.leave_type_standard.id, + "employee_id": cls.employee.id, + "number_of_days": 20, + } + ) + cls.leave_allocation_standard.with_user(cls.hr_manager).action_validate() + + def test_01_leave_type_tier_validation_selection(self): + """Test that the leave type has the tier_validation option.""" + leave_type = self.leave_type_model.create( + { + "name": "Test Leave Type", + "leave_validation_type": "tier_validation", + } + ) + self.assertEqual(leave_type.leave_validation_type, "tier_validation") + + def test_02_tier_definition_model_names(self): + """Test that hr.leave is included in tier validation model names.""" + model_names = self.tier_definition_model._get_tier_validation_model_names() + self.assertIn("hr.leave", model_names) + + def test_03_leave_request_tier_validation_enabled(self): + """Test leave request with tier validation enabled.""" + today = datetime.today() + + # Verify we have exactly 2 tier definitions for our leave type + tier_defs = self.tier_definition_model.search( + [("model_id", "=", self.env.ref("hr_holidays.model_hr_leave").id)] + ) + self.assertEqual( + len(tier_defs), 2, f"Expected 2 tier definitions, found {len(tier_defs)}" + ) + + leave = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Request", + "holiday_status_id": self.leave_type_tier_validation.id, + "employee_id": self.employee.id, + "date_from": today, + "date_to": today + timedelta(days=2), + } + ) + + # Should be in confirm state after creation with tier validation + self.assertEqual(leave.state, "confirm") + + # Should need validation + self.assertTrue(leave.need_validation) + + # Should have tier reviews created + self.assertEqual( + len(leave.review_ids), + 2, + f"Expected 2 reviews, found {len(leave.review_ids)}. " + f"Reviews: {[(r.reviewer_id.name, r.definition_id.sequence) for r in leave.review_ids]}", # noqa B950 + ) + + def test_04_leave_request_standard_validation(self): + """Test leave request with standard validation (no tier validation).""" + today = datetime.today() + leave = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Request Standard", + "holiday_status_id": self.leave_type_standard.id, + "employee_id": self.employee.id, + "date_from": today + timedelta(days=5), + "date_to": today + timedelta(days=7), + "number_of_days": 3, + } + ) + + # Should be in confirm state after creation + self.assertEqual(leave.state, "confirm") + + # Should not need tier validation + self.assertFalse(leave.need_validation) + + # HR Manager should be able to validate directly + leave.with_user(self.hr_manager).action_validate() + self.assertEqual(leave.state, "validate") + + def test_05_tier_validation_request_and_approve(self): + """Test the complete tier validation flow: request, review, approve.""" + today = datetime.today() + leave = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Tier Validation", + "holiday_status_id": self.leave_type_tier_validation.id, + "employee_id": self.employee.id, + "date_from": today + timedelta(days=10), + "date_to": today + timedelta(days=12), + "number_of_days": 3, + } + ) + + # Should be in confirm state and need validation + self.assertEqual(leave.state, "confirm") + self.assertTrue(leave.need_validation) + + # Should have reviews created automatically + self.assertEqual( + len(leave.review_ids), + 2, + f"Expected 2 reviews, found {len(leave.review_ids)}. " + f"Reviews: {[(r.reviewer_id.name, r.definition_id.sequence) for r in leave.review_ids]}", # noqa B950 + ) + + # Check that we have pending reviews + pending_reviews = leave.review_ids.filtered(lambda r: r.status == "pending") + self.assertTrue(pending_reviews) + + # First reviewer (Leave Manager) approves - should be sequence 10 + first_review = leave.review_ids.filtered( + lambda r: r.definition_id.sequence == 10 + ) + self.assertTrue(first_review, "No review found with sequence 10") + self.assertEqual(first_review.reviewer_id, self.leave_manager) + + leave.with_user(self.leave_manager).validate_tier() + + leave.invalidate_recordset() + + # Check that the first review is approved but leave is still pending + self.assertEqual(first_review.status, "approved") + self.assertFalse(leave.validated) + + # Second reviewer (HR Manager) approves - should be sequence 20 + second_review = leave.review_ids.filtered( + lambda r: r.definition_id.sequence == 20 + ) + self.assertTrue(second_review, "No review found with sequence 20") + self.assertEqual(second_review.reviewer_id, self.hr_manager) + + leave.with_user(self.hr_manager).validate_tier() + + leave.invalidate_recordset() + + # Now the leave should be fully validated + self.assertEqual(second_review.status, "approved") + + self.assertTrue( + leave.validated, + "Leave should be validated after both reviews\n" + f"Leave Reviews: {[(r.reviewer_id.name, r.status) for r in leave.review_ids]}", + ) + + self.assertEqual(leave.state, "validate") + + def test_06_tier_validation_rejection(self): + """Test tier validation rejection and restart.""" + today = datetime.today() + leave = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Rejection", + "holiday_status_id": self.leave_type_tier_validation.id, + "employee_id": self.employee.id, + "date_from": today + timedelta(days=15), + "date_to": today + timedelta(days=17), + "number_of_days": 3, + } + ) + + # Should be in confirm state and have reviews + self.assertEqual(leave.state, "confirm") + self.assertTrue(leave.review_ids) + + # HR Officer rejects + review = leave.review_ids.filtered( + lambda r: r.reviewer_id == self.leave_manager + ) + self.assertTrue(review) + leave.with_user(self.leave_manager).reject_tier() + + leave.invalidate_recordset() + + # Leave should be rejected + # Note: review record is deleted after rejection, so we can't check review.status + # self.assertEqual(review.status, "rejected") + # self.assertTrue(leave.rejected) + self.assertEqual(leave.state, "refuse") + + def test_07_under_validation_write_protection(self): + """Test that records under validation cannot be modified.""" + today = datetime.today() + leave = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Write Protection", + "holiday_status_id": self.leave_type_tier_validation.id, + "employee_id": self.employee.id, + "date_from": today + timedelta(days=20), + "date_to": today + timedelta(days=22), + "number_of_days": 3, + } + ) + + # Should be in confirm state and under validation + self.assertEqual(leave.state, "confirm") + self.assertTrue(leave.need_validation) + + # Should not be able to modify certain fields while under validation + # Note: write protection may depend on specific implementation + # This test checks if the leave is in a state where it needs validation + self.assertTrue(leave.review_ids.filtered(lambda r: r.status == "pending")) + + # Try to modify date_from + with self.assertRaises(ValidationError): + leave.with_user(self.employee_user).write( + {"date_from": today + timedelta(days=25)} + ) + + def test_08_notification_system(self): + """Test the notification system for pending reviews.""" + today = datetime.today() + leave = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Notification", + "holiday_status_id": self.leave_type_tier_validation.id, + "employee_id": self.employee.id, + "date_from": today + timedelta(days=25), + "date_to": today + timedelta(days=27), + "number_of_days": 3, + } + ) + + reviews = leave.with_user(self.employee_user).review_ids + + # Check that reviewers are notified + pending_reviews = reviews.filtered(lambda r: r.status == "pending") + self.assertTrue(pending_reviews) + + # Check that the _notify_review_available method works + leave._notify_review_available(pending_reviews) + + # Verify followers have been updated (reviewers should be subscribed) + reviewer_partners = pending_reviews.mapped("reviewer_ids.partner_id") + follower_partners = leave.message_follower_ids.mapped("partner_id") + for partner in reviewer_partners: + self.assertIn(partner, follower_partners) + + def test_09_action_methods_with_context(self): + """Test that action methods properly handle mail_activity_automation_skip context.""" + today = datetime.today() + leave = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Context", + "holiday_status_id": self.leave_type_tier_validation.id, + "employee_id": self.employee.id, + "date_from": today + timedelta(days=30), + "date_to": today + timedelta(days=32), + "number_of_days": 3, + } + ) + + # Test that create method adds the context + leave2 = self.leave_model.with_user(self.employee_user).create( + { + "name": "Test Leave Context 2", + "holiday_status_id": self.leave_type_tier_validation.id, + "employee_id": self.employee.id, + "date_from": today + timedelta(days=35), + "date_to": today + timedelta(days=37), + "number_of_days": 3, + } + ) + self.assertTrue(leave2) + + # Request validation and approve to test action_validate context + leave.with_user(self.employee_user).request_validation() + + # Approve by both reviewers + leave.with_user(self.leave_manager).validate_tier() + leave.with_user(self.hr_manager).validate_tier() + + # Should be validated + self.assertEqual(leave.state, "validate") diff --git a/hr_holidays_tier_validation/views/hr_leave_views.xml b/hr_holidays_tier_validation/views/hr_leave_views.xml new file mode 100644 index 000000000..c83997a7a --- /dev/null +++ b/hr_holidays_tier_validation/views/hr_leave_views.xml @@ -0,0 +1,77 @@ + + + + hr_leave_form_tier_validation + hr.leave + + + + + + + + {"invisible": [('validation_type', '=', 'tier_validation')]} + + + + + + { + 'invisible': [ + '|', + '&', ('validation_type', '=', 'tier_validation'), ('state', '!=', 'validate'), + '|','|','|', + ('id', '=', False), + ('active', '=', False), + ('can_approve', '=', False), + ('state', 'not in', ('confirm','validate1','validate')) + ] + } + + + + + + + + hr_leave_kanban_tier_validation + hr.leave + + + + + + + + + + + + + + + + + diff --git a/setup/hr_holidays_tier_validation/odoo/addons/hr_holidays_tier_validation b/setup/hr_holidays_tier_validation/odoo/addons/hr_holidays_tier_validation new file mode 120000 index 000000000..3125b5e9f --- /dev/null +++ b/setup/hr_holidays_tier_validation/odoo/addons/hr_holidays_tier_validation @@ -0,0 +1 @@ +../../../../hr_holidays_tier_validation \ No newline at end of file diff --git a/setup/hr_holidays_tier_validation/setup.py b/setup/hr_holidays_tier_validation/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/hr_holidays_tier_validation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 33d3408f82895fc2542e941334244bb16a96ca79 Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Wed, 10 Dec 2025 10:53:54 +0000 Subject: [PATCH 2/2] [WIP] TO REMOVE. Add dependency to unmerged PR --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 test-requirements.txt diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..43d9e7733 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-base_tier_validation_allow_disable_restart @ git+https://github.com/OCA/server-ux.git@refs/pull/1125/head#subdirectory=setup/base_tier_validation_allow_disable_restart