diff --git a/base_tier_validation_delegation/README.rst b/base_tier_validation_delegation/README.rst new file mode 100644 index 0000000000..5972617e90 --- /dev/null +++ b/base_tier_validation_delegation/README.rst @@ -0,0 +1,100 @@ +=============================== +Base Tier Validation Delegation +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:80e0eb6d81b34912b48ec36fcb243ad8154a470253cd6509da92e245dbea4f91 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/16.0/base_tier_validation_delegation + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-16-0/server-ux-16-0-base_tier_validation_delegation + :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/server-ux&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides the core functionality for users to delegate their tier validation tasks to another user when they are out of the office. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure Tier Validations Delegation, you need to: + +1. Go to **Settings > Users & Companies > Users** and select their own user profile (or click their name in the top right corner and select **My Profile**). +2. Navigate to the **Delegation** tab. +3. Check the **On Holiday** box. +4. Optionally, set the **Holiday Start/End Dates**. If no dates are set, the delegation is considered active as long as the "On Holiday" box is checked. +5. Select a **Default Replacer**. This is the user who will receive all validation requests. + +The module includes two daily automated jobs: + +- Holiday Status Update: This job automatically checks the "On Holiday" box for any user whose Holiday Start Date is today. This activation triggers the same logic as a manual change, meaning all of their existing pending reviews are automatically reassigned to their replacer. It also unchecks the box for users whose Holiday End Date has passed. +- Delegation Reminder: This job sends a reminder notification to users 3 days before their scheduled holiday if they have not yet configured a replacer. + +Usage +===== + +To use this module, you need to: + +* Any new tier review assigned to the original user will be automatically assigned to the final user in the delegation chain. +* Any existing pending reviews will be reassigned when the "On Holiday" status is activated. +* A message is posted in the chatter of the related document to inform that the review has been delegated. + +Delegators can track the reviews they have delegated by going to **Settings > Tier Validations > My Delegated Reviews**. + +Administrators can manage all user delegations from **Settings > Users & Companies > Delegation Management**. + +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 +~~~~~~~ + +* 360 ERP + +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/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_tier_validation_delegation/__init__.py b/base_tier_validation_delegation/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/base_tier_validation_delegation/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_tier_validation_delegation/__manifest__.py b/base_tier_validation_delegation/__manifest__.py new file mode 100644 index 0000000000..a22ab593f3 --- /dev/null +++ b/base_tier_validation_delegation/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "Base Tier Validation Delegation", + "summary": "Allows users to delegate tier validation tasks when out of office.", + "version": "16.0.1.0.0", + "development_status": "Beta", + "category": "Tools", + "website": "https://github.com/OCA/server-ux", + "author": "360 ERP, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["base_tier_validation"], + "data": [ + "security/delegation_security.xml", + "data/cron.xml", + "views/res_users_views.xml", + "views/tier_review_views.xml", + ], +} diff --git a/base_tier_validation_delegation/data/cron.xml b/base_tier_validation_delegation/data/cron.xml new file mode 100644 index 0000000000..794eabe3cb --- /dev/null +++ b/base_tier_validation_delegation/data/cron.xml @@ -0,0 +1,38 @@ + + + + Delegation: Update Holiday Status + + code + model._cron_update_holiday_status() + + 1 + days + -1 + + Automatically activate or deactivate a user's 'On Holiday' status based on their configured start and end dates. + + + + Delegation: Send Holiday Reminder + + code + model._cron_send_delegation_reminder() + + 1 + days + -1 + + Notifies users 3 days before their scheduled holiday if they have not yet configured a replacer. + + diff --git a/base_tier_validation_delegation/models/__init__.py b/base_tier_validation_delegation/models/__init__.py new file mode 100644 index 0000000000..4463c67cff --- /dev/null +++ b/base_tier_validation_delegation/models/__init__.py @@ -0,0 +1,3 @@ +from . import res_users +from . import tier_review +from . import tier_validation diff --git a/base_tier_validation_delegation/models/res_users.py b/base_tier_validation_delegation/models/res_users.py new file mode 100644 index 0000000000..c123cdc732 --- /dev/null +++ b/base_tier_validation_delegation/models/res_users.py @@ -0,0 +1,174 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +import logging +from datetime import timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class ResUsers(models.Model): + _inherit = "res.users" + + on_holiday = fields.Boolean( + help="Check this box if you are out of office and want to delegate your " + "validation tasks.", + ) + holiday_start_date = fields.Date() + holiday_end_date = fields.Date() + validation_replacer_id = fields.Many2one( + "res.users", + string="Default Replacer", + help="This user will receive your validation requests while you are on holiday.", + ) + + @api.constrains("on_holiday", "holiday_start_date", "holiday_end_date") + def _check_holiday_dates(self): + """Ensure end date is not before start date.""" + for user in self: + if ( + user.on_holiday + and user.holiday_start_date + and user.holiday_end_date + and user.holiday_start_date > user.holiday_end_date + ): + raise ValidationError( + _("Holiday End Date cannot be before the Start Date.") + ) + + @api.constrains("on_holiday", "validation_replacer_id") + def _check_validation_replacer(self): + """Ensures a user does not delegate to themselves or create a circular loop.""" + for user in self: + if not user.on_holiday or not user.validation_replacer_id: + continue + if user.validation_replacer_id == user: + raise ValidationError( + _("You cannot delegate validation tasks to yourself.") + ) + # Check for circular delegation (e.g., A->B->C->A) + next_replacer = user.validation_replacer_id + path = {user} + while next_replacer: + if next_replacer in path: + raise ValidationError( + _("You cannot create a circular delegation path.") + ) + path.add(next_replacer) + next_replacer = next_replacer.validation_replacer_id + + def _is_currently_on_holiday(self, today=None): + """ + Checks if a user is considered on holiday right now, respecting date ranges. + """ + self.ensure_one() + if not today: + today = fields.Date.context_today(self) + return ( + self.on_holiday + and self.validation_replacer_id + and (not self.holiday_start_date or self.holiday_start_date <= today) + and (not self.holiday_end_date or self.holiday_end_date >= today) + ) + + def _get_final_validation_replacer(self): + """ + Recursively finds the final active user in a delegation chain. + """ + self.ensure_one() + delegation_path = {self} + current_user = self + today = fields.Date.context_today(self) + + while current_user._is_currently_on_holiday(today=today): + next_user_candidate = current_user.validation_replacer_id + + if not next_user_candidate or not next_user_candidate.active: + _logger.debug( + "Delegation chain broken, falling back to '%s'.", current_user.login + ) + return current_user + + if next_user_candidate in delegation_path: + _logger.warning( + "Circular delegation detected, falling back to '%s'.", + current_user.login, + ) + return current_user + + delegation_path.add(next_user_candidate) + current_user = next_user_candidate + return current_user + + def write(self, vals): + """ + If a user's holiday status or replacer changes, find all their pending + reviews and trigger a re-computation of the reviewers. + """ + holiday_fields = [ + "on_holiday", + "holiday_start_date", + "holiday_end_date", + "validation_replacer_id", + ] + if not any(field in holiday_fields for field in vals): + return super().write(vals) + + users_to_recompute = self + + res = super().write(vals) + + if users_to_recompute: + self.env["tier.review"]._recompute_reviews_for_users(users_to_recompute) + return res + + @api.model + def _cron_update_holiday_status(self): + """ + A daily cron job to automatically activate or deactivate a user's + holiday status based on the configured start and end dates. + """ + _logger.info("CRON: Running automatic holiday status update.") + today = fields.Date.context_today(self) + users_to_activate = self.search( + [("on_holiday", "=", False), ("holiday_start_date", "=", today)] + ) + if users_to_activate: + users_to_activate.write({"on_holiday": True}) + users_to_deactivate = self.search( + [("on_holiday", "=", True), ("holiday_end_date", "<", today)] + ) + if users_to_deactivate: + users_to_deactivate.write({"on_holiday": False}) + _logger.info("CRON: Finished holiday status update.") + + @api.model + def _cron_send_delegation_reminder(self): + """ + Sends a reminder to users whose holiday is starting soon but have not + configured a replacer. + """ + _logger.info("CRON: Running delegation reminder check.") + reminder_date = fields.Date.context_today(self) + timedelta(days=3) + users_to_remind = self.search( + [ + ("holiday_start_date", "=", reminder_date), + ("on_holiday", "=", False), + ("validation_replacer_id", "=", False), + ] + ) + for user in users_to_remind: + user.partner_id.message_post( + body=_( + "Your holiday is scheduled to start on %s. Please remember to " + "configure a validation replacer in your preferences to avoid " + "blocking any documents." + ) + % user.holiday_start_date, + message_type="notification", + subtype_xmlid="mail.mt_comment", + ) + _logger.info("CRON: Finished delegation reminder check.") diff --git a/base_tier_validation_delegation/models/tier_review.py b/base_tier_validation_delegation/models/tier_review.py new file mode 100644 index 0000000000..b32bbe73ad --- /dev/null +++ b/base_tier_validation_delegation/models/tier_review.py @@ -0,0 +1,119 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +import logging + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class TierReview(models.Model): + _inherit = "tier.review" + + delegated_by_ids = fields.Many2many( + comodel_name="res.users", + relation="tier_review_delegated_by_rel", + column1="review_id", + column2="user_id", + string="Delegated By", + compute="_compute_reviewer_ids", + store=True, + help="Original users who delegated this review to the current reviewers.", + ) + + def _get_original_reviewers(self): + """ + Helper method to get the reviewers as defined on the tier definition, + bypassing any delegation logic from this module's override of `_get_reviewers`. + This is a safe copy of the logic from the base `base_tier_validation` module. + """ + self.ensure_one() + if self.definition_id.review_type == "individual": + return self.definition_id.reviewer_id + if self.definition_id.review_type == "group": + return self.definition_id.reviewer_group_id.users + if self.definition_id.review_type == "field": + resource = self.env[self.model].browse(self.res_id) + reviewer_field = getattr( + resource, self.definition_id.reviewer_field_id.name, False + ) + if reviewer_field and reviewer_field._name == "res.users": + return reviewer_field + return self.env["res.users"] + + @api.depends(lambda self: self._get_reviewer_fields()) + def _compute_reviewer_ids(self): + """ + Computes the final reviewers after applying delegation logic and also + populates `delegated_by_ids` with any users whose reviews were reassigned. + """ + old_reviewers_map = {rec.id: rec.reviewer_ids for rec in self} + res = super()._compute_reviewer_ids() + for rec in self: + original_reviewers = rec._get_original_reviewers() + final_reviewers = rec.reviewer_ids + # The difference between original and final reviewers are the delegators + delegators = original_reviewers - final_reviewers + rec.delegated_by_ids = delegators + + # Post chatter message on change + old_reviewers = old_reviewers_map.get(rec.id) + if old_reviewers is not None and old_reviewers != final_reviewers: + added = final_reviewers - old_reviewers + removed = old_reviewers - final_reviewers + if added and removed: + record = self.env[rec.model].browse(rec.res_id) + if record: + from_names = ", ".join(removed.mapped("name")) + to_names = ", ".join(added.mapped("name")) + body = _( + f"Review task delegated from {from_names}" + f" to {to_names}." + ) + record.message_post(body=body) + return res + + def _get_reviewers(self): + """ + Overrides the base method to apply delegation logic. It gets the + original reviewers and then substitutes anyone who is on holiday with + their designated replacer. + """ + original_reviewers = super()._get_reviewers() + final_reviewers = self.env["res.users"] + for user in original_reviewers: + final_replacer = user._get_final_validation_replacer() + if user != final_replacer: + _logger.debug( + "Review ID %s: User '%s' delegated to '%s'.", + self.id, + user.login, + final_replacer.login, + ) + final_reviewers |= final_replacer + # Return a unique set of reviewers + return final_reviewers + + @api.model + def _recompute_reviews_for_users(self, users): + """ + Finds all pending reviews assigned to a given set of users (or delegated + by them) and triggers a re-computation of their reviewers. + """ + if not users: + return + + # Find all pending reviews where any of the given users are either + # a current reviewer OR the original delegator. This ensures we find + # reviews even after they have been delegated. + domain = [ + ("status", "=", "pending"), + "|", + ("reviewer_ids", "in", users.ids), + ("delegated_by_ids", "in", users.ids), + ] + affected_reviews = self.search(domain) + + if affected_reviews: + affected_reviews._compute_reviewer_ids() diff --git a/base_tier_validation_delegation/models/tier_validation.py b/base_tier_validation_delegation/models/tier_validation.py new file mode 100644 index 0000000000..947f7e89a9 --- /dev/null +++ b/base_tier_validation_delegation/models/tier_validation.py @@ -0,0 +1,127 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class TierValidation(models.AbstractModel): + _inherit = "tier.validation" + + def _find_review_for_delegate(self): + """ + Finds a pending review where the current user is a delegate for one of + the original reviewers. + :return: A tuple of (tier.review, res.users) for the review and the + original delegator, or empty recordsets if not found. + """ + self.ensure_one() + user = self.env.user + for review in self.review_ids.filtered(lambda r: r.status == "pending"): + for original_reviewer in review._get_original_reviewers(): + if original_reviewer._get_final_validation_replacer() == user: + return review, original_reviewer + return self.env["tier.review"], self.env["res.users"] + + def _execute_as_delegate(self, action): + """ + Helper to perform an action (validate or reject) on behalf of a delegator. + It temporarily adds the current user to the reviewers list to pass + the base method's security checks. + :param action: string, either 'validate' or 'reject' + :return: A tuple containing (result_of_action, delegator_user, review) + """ + review, delegator = self._find_review_for_delegate() + if not review: + return None, self.env["res.users"], self.env["tier.review"] + + _logger.debug( + "DELEGATION [%s]: User '%s' is a delegate for review %s.", + action, + self.env.user.login, + review.id, + ) + # Temporarily add the delegate to pass base security checks + review.sudo().reviewer_ids = [(4, self.env.user.id)] + + # Add a context key to prevent re-entrant calls + ctx = self.with_context(in_delegation_flow=True).env.context + + if action == "validate": + res = super(TierValidation, self.with_context(**ctx))._validate_tier( + tiers=review + ) + else: + res = super(TierValidation, self.with_context(**ctx))._rejected_tier( + tiers=review + ) + + return res, delegator, review + + def _validate_tier(self, tiers=False): + """ + Allows a delegate user to validate a tier. + """ + self.ensure_one() + # If we are already in the delegation flow, do not re-run the logic. + if self.env.context.get("in_delegation_flow"): + return super()._validate_tier(tiers=tiers) + + # If user is a direct reviewer, use the standard method. + if self.review_ids.filtered( + lambda r: self.env.user in r.reviewer_ids and r.status == "pending" + ): + return super()._validate_tier(tiers=tiers) + + # If not a direct reviewer, check if they are a delegate. + res, _delegator, _review = self._execute_as_delegate("validate") + if res is not None: + return res + + # Fallback to the standard method (which will likely raise an error) + return super()._validate_tier(tiers=tiers) + + def _rejected_tier(self, tiers=False): + """ + Allows a delegate user to reject a tier. Advanced policies are + handled in the 'policy' module. + This method now returns context for inheriting modules. + :return: A tuple of (result, delegator, rejected_review) + """ + self.ensure_one() + # If we are already in the delegation flow, do not re-run the logic. + if self.env.context.get("in_delegation_flow"): + res = super()._rejected_tier(tiers=tiers) + return res, self.env["res.users"], self.env["tier.review"] + + user = self.env.user + delegator_to_notify = self.env["res.users"] + rejected_review = self.env["tier.review"] + res = None + + # If user is a direct reviewer, use the standard method. + if self.review_ids.filtered( + lambda r: user in r.reviewer_ids and r.status == "pending" + ): + res = super()._rejected_tier(tiers=tiers) + # Find the delegator if the current user is also a replacer for someone else + delegator_to_notify = self.review_ids.delegated_by_ids.filtered( + lambda u: u._get_final_validation_replacer() == user + ) + rejected_review = tiers or self.review_ids.filtered( + lambda r: r.status == "rejected" + ) + else: + # If not a direct reviewer, check if they are a delegate. + res, delegator_to_notify, rejected_review = self._execute_as_delegate( + "reject" + ) + if res is None: + # Fallback to the standard method if not a delegate + res = super()._rejected_tier(tiers=tiers) + + # Return all context for other modules to use + return res, delegator_to_notify, rejected_review diff --git a/base_tier_validation_delegation/readme/CONFIGURE.rst b/base_tier_validation_delegation/readme/CONFIGURE.rst new file mode 100644 index 0000000000..921364fd9d --- /dev/null +++ b/base_tier_validation_delegation/readme/CONFIGURE.rst @@ -0,0 +1,12 @@ +To configure Tier Validations Delegation, you need to: + +1. Go to **Settings > Users & Companies > Users** and select their own user profile (or click their name in the top right corner and select **My Profile**). +2. Navigate to the **Delegation** tab. +3. Check the **On Holiday** box. +4. Optionally, set the **Holiday Start/End Dates**. If no dates are set, the delegation is considered active as long as the "On Holiday" box is checked. +5. Select a **Default Replacer**. This is the user who will receive all validation requests. + +The module includes two daily automated jobs: + +- Holiday Status Update: This job automatically checks the "On Holiday" box for any user whose Holiday Start Date is today. This activation triggers the same logic as a manual change, meaning all of their existing pending reviews are automatically reassigned to their replacer. It also unchecks the box for users whose Holiday End Date has passed. +- Delegation Reminder: This job sends a reminder notification to users 3 days before their scheduled holiday if they have not yet configured a replacer. diff --git a/base_tier_validation_delegation/readme/DESCRIPTION.rst b/base_tier_validation_delegation/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..a04a79dede --- /dev/null +++ b/base_tier_validation_delegation/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module provides the core functionality for users to delegate their tier validation tasks to another user when they are out of the office. diff --git a/base_tier_validation_delegation/readme/USAGE.rst b/base_tier_validation_delegation/readme/USAGE.rst new file mode 100644 index 0000000000..253efa7234 --- /dev/null +++ b/base_tier_validation_delegation/readme/USAGE.rst @@ -0,0 +1,9 @@ +To use this module, you need to: + +* Any new tier review assigned to the original user will be automatically assigned to the final user in the delegation chain. +* Any existing pending reviews will be reassigned when the "On Holiday" status is activated. +* A message is posted in the chatter of the related document to inform that the review has been delegated. + +Delegators can track the reviews they have delegated by going to **Settings > Tier Validations > My Delegated Reviews**. + +Administrators can manage all user delegations from **Settings > Users & Companies > Delegation Management**. diff --git a/base_tier_validation_delegation/security/delegation_security.xml b/base_tier_validation_delegation/security/delegation_security.xml new file mode 100644 index 0000000000..e058dc5230 --- /dev/null +++ b/base_tier_validation_delegation/security/delegation_security.xml @@ -0,0 +1,11 @@ + + + + Delegation Administrator + + + The delegation administrator can manage the holiday/delegation settings for any user. + + diff --git a/base_tier_validation_delegation/static/description/index.html b/base_tier_validation_delegation/static/description/index.html new file mode 100644 index 0000000000..ef381cfcf0 --- /dev/null +++ b/base_tier_validation_delegation/static/description/index.html @@ -0,0 +1,445 @@ + + + + + +Base Tier Validation Delegation + + + +
+

Base Tier Validation Delegation

+ + +

Beta License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runboat

+

This module provides the core functionality for users to delegate their tier validation tasks to another user when they are out of the office.

+

Table of contents

+ +
+

Configuration

+

To configure Tier Validations Delegation, you need to:

+
    +
  1. Go to Settings > Users & Companies > Users and select their own user profile (or click their name in the top right corner and select My Profile).
  2. +
  3. Navigate to the Delegation tab.
  4. +
  5. Check the On Holiday box.
  6. +
  7. Optionally, set the Holiday Start/End Dates. If no dates are set, the delegation is considered active as long as the “On Holiday” box is checked.
  8. +
  9. Select a Default Replacer. This is the user who will receive all validation requests.
  10. +
+

The module includes two daily automated jobs:

+
    +
  • Holiday Status Update: This job automatically checks the “On Holiday” box for any user whose Holiday Start Date is today. This activation triggers the same logic as a manual change, meaning all of their existing pending reviews are automatically reassigned to their replacer. It also unchecks the box for users whose Holiday End Date has passed.
  • +
  • Delegation Reminder: This job sends a reminder notification to users 3 days before their scheduled holiday if they have not yet configured a replacer.
  • +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  • Any new tier review assigned to the original user will be automatically assigned to the final user in the delegation chain.
  • +
  • Any existing pending reviews will be reassigned when the “On Holiday” status is activated.
  • +
  • A message is posted in the chatter of the related document to inform that the review has been delegated.
  • +
+

Delegators can track the reviews they have delegated by going to Settings > Tier Validations > My Delegated Reviews.

+

Administrators can manage all user delegations from Settings > Users & Companies > Delegation Management.

+
+
+

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

+
    +
  • 360 ERP
  • +
+
+
+

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/server-ux project on GitHub.

+

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

+
+
+
+ + diff --git a/base_tier_validation_delegation/tests/__init__.py b/base_tier_validation_delegation/tests/__init__.py new file mode 100644 index 0000000000..409b1d3be2 --- /dev/null +++ b/base_tier_validation_delegation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_delegation diff --git a/base_tier_validation_delegation/tests/test_delegation.py b/base_tier_validation_delegation/tests/test_delegation.py new file mode 100644 index 0000000000..02661e39df --- /dev/null +++ b/base_tier_validation_delegation/tests/test_delegation.py @@ -0,0 +1,430 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from datetime import date, timedelta + +from odoo.exceptions import AccessError, ValidationError +from odoo.tests.common import tagged + +from odoo.addons.base_tier_validation.tests.common import CommonTierValidation + + +@tagged("post_install", "-at_install") +class TestTierValidationDelegation(CommonTierValidation): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user_delegator = cls.test_user_1 + cls.user_replacer_b = cls.env["res.users"].create( + {"name": "User B (Replacer)", "login": "user_b", "email": "b@test.com"} + ) + cls.user_replacer_c = cls.env["res.users"].create( + {"name": "User C (Final)", "login": "user_c", "email": "c@test.com"} + ) + cls.admin_user = cls.env["res.users"].create( + {"name": "Delegation Admin", "login": "deleg_admin", "email": "da@test.com"} + ) + cls.delegation_admin_group = cls.env.ref( + "base_tier_validation_delegation.group_delegation_administrator" + ) + cls.admin_user.write({"groups_id": [(4, cls.delegation_admin_group.id)]}) + + cls.test_group = cls.env["res.groups"].create({"name": "Test Review Group"}) + cls.test_user_1.write({"groups_id": [(4, cls.test_group.id)]}) + cls.test_user_2.write({"groups_id": [(4, cls.test_group.id)]}) + + def tearDown(self): + super().tearDown() + users_to_reset = ( + self.user_delegator + | self.user_replacer_b + | self.user_replacer_c + | self.test_user_2 + ) + users_to_reset.write( + {"on_holiday": False, "validation_replacer_id": False, "active": True} + ) + + def _create_record_and_request_validation(self, test_field_value=2.5): + record = self.test_model.create({"test_field": test_field_value}) + record.with_user(self.test_user_2).request_validation() + reviews = self.env["tier.review"].search([("res_id", "=", record.id)]) + self.assertTrue(reviews, "HELPER: Failed to create any tier reviews.") + return record, reviews + + def test_01_new_validation_delegation(self): + """Test that a new validation is immediately delegated.""" + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + _record, review = self._create_record_and_request_validation() + self.assertIn(self.user_replacer_b, review.reviewer_ids) + self.assertNotIn(self.user_delegator, review.reviewer_ids) + + def test_02_pending_validation_delegation(self): + """Test that a pending validation is re-assigned when a user goes on holiday.""" + _record, review = self._create_record_and_request_validation() + self.assertIn(self.user_delegator, review.reviewer_ids) + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + self.assertIn(self.user_replacer_b, review.reviewer_ids) + self.assertNotIn(self.user_delegator, review.reviewer_ids) + + def test_03_delegation_with_date_range(self): + """Test that delegation only occurs within the specified date range.""" + today = date.today() + self.user_delegator.write( + { + "on_holiday": True, + "holiday_start_date": today + timedelta(days=5), + "validation_replacer_id": self.user_replacer_b.id, + } + ) + _record, review = self._create_record_and_request_validation() + self.assertIn( + self.user_delegator, + review.reviewer_ids, + "Review should not be delegated before the holiday start date.", + ) + self.user_delegator.holiday_start_date = today - timedelta(days=1) + review._compute_reviewer_ids() + self.assertIn( + self.user_replacer_b, + review.reviewer_ids, + "Review should be delegated once within the holiday period.", + ) + + def test_04_delegation_chain(self): + """Test that a review is delegated to the end of a chain (A->B->C).""" + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + self.user_replacer_b.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_c.id} + ) + _record, review = self._create_record_and_request_validation() + self.assertIn(self.user_replacer_c, review.reviewer_ids) + self.assertNotIn(self.user_delegator, review.reviewer_ids) + self.assertNotIn(self.user_replacer_b, review.reviewer_ids) + + def test_05_group_review_delegation(self): + """Test delegation for a review assigned to a group where one member is away.""" + group_tier_def = self.env["tier.definition"].create( + { + "model_id": self.tester_model.id, + "review_type": "group", + "reviewer_group_id": self.test_group.id, + } + ) + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_c.id} + ) + _record, reviews = self._create_record_and_request_validation( + test_field_value=0.5 + ) + review = reviews.filtered(lambda r: r.definition_id == group_tier_def) + self.assertTrue(review) + self.assertIn(self.test_user_2, review.reviewer_ids) + self.assertIn(self.user_replacer_c, review.reviewer_ids) + self.assertNotIn(self.user_delegator, review.reviewer_ids) + + def test_06_user_returns_from_holiday_default(self): + """Test that pending reviews are reassigned back when a user returns.""" + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + ( + _record_while_away, + review_while_away, + ) = self._create_record_and_request_validation() + self.assertIn(self.user_replacer_b, review_while_away.reviewer_ids) + self.user_delegator.write({"on_holiday": False}) + review_while_away.invalidate_recordset(["reviewer_ids"]) + self.assertIn( + self.user_delegator, + review_while_away.reviewer_ids, + "Pending review should be reassigned back to the original user.", + ) + self.assertNotIn( + self.user_replacer_b, + review_while_away.reviewer_ids, + "Replacer should be removed after the original user returns.", + ) + ( + _record_after_return, + review_after_return, + ) = self._create_record_and_request_validation() + self.assertIn(self.user_delegator, review_after_return.reviewer_ids) + + def test_07_no_replacer_configured(self): + """Test that if 'On Holiday' is checked with no replacer, delegation does not occur.""" + self.user_delegator.write({"on_holiday": True, "validation_replacer_id": False}) + _record, review = self._create_record_and_request_validation() + self.assertIn(self.user_delegator, review.reviewer_ids) + + def test_08_self_delegation_constraint(self): + """Test that a user cannot delegate to themselves.""" + with self.assertRaises( + ValidationError, msg="Should not be able to delegate to self." + ): + self.user_delegator.write( + { + "on_holiday": True, + "validation_replacer_id": self.user_delegator.id, + } + ) + + def test_10_visual_indicator_and_menu(self): + """Test that `delegated_by_ids` is set and the menu domain works.""" + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + _record, review = self._create_record_and_request_validation() + self.assertEqual(review.delegated_by_ids, self.user_delegator) + delegated_reviews = ( + self.env["tier.review"] + .with_user(self.user_delegator) + .search([("delegated_by_ids", "in", [self.user_delegator.id])]) + ) + self.assertEqual(review, delegated_reviews) + + def test_11_admin_management(self): + """Test that an admin can edit others' settings, but a normal user cannot.""" + with self.assertRaises( + AccessError, + msg="Normal user should not be able to edit other users' delegation.", + ): + self.user_delegator.with_user(self.test_user_2).write({"on_holiday": True}) + self.user_delegator.with_user(self.user_delegator).write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + self.user_delegator.with_user(self.admin_user).write( + {"on_holiday": False, "validation_replacer_id": False} + ) + + def test_12_cron_job(self): + """Test the automatic activation/deactivation cron job.""" + today = date.today() + user_to_activate = self.user_replacer_b + user_to_deactivate = self.user_replacer_c + user_to_activate.write( + { + "on_holiday": False, + "holiday_start_date": today, + "validation_replacer_id": self.user_delegator.id, + } + ) + user_to_deactivate.write( + {"on_holiday": True, "holiday_end_date": today - timedelta(days=1)} + ) + self.env["res.users"]._cron_update_holiday_status() + self.assertTrue(user_to_activate.on_holiday) + self.assertFalse(user_to_deactivate.on_holiday) + + def test_14_circular_delegation_constraint(self): + """Test that a circular delegation (A->B->A) is prevented.""" + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + with self.assertRaises( + ValidationError, msg="Should not be able to create a delegation loop." + ): + self.user_replacer_b.write( + { + "on_holiday": True, + "validation_replacer_id": self.user_delegator.id, + } + ) + + def test_15_delegation_to_archived_user(self): + """Test that delegation falls back to the original user if the replacer is archived.""" + self.user_replacer_b.action_archive() + self.assertFalse(self.user_replacer_b.active) + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + _record, review = self._create_record_and_request_validation() + self.assertIn( + self.user_delegator, + review.reviewer_ids, + "Review should fall back to delegator if replacer is inactive.", + ) + self.assertNotIn(self.user_replacer_b, review.reviewer_ids) + + def test_16_multi_tier_delegation(self): + """Test that delegation works correctly in a multi-tier validation flow.""" + self.env["tier.definition"].create( + { + "model_id": self.tester_model.id, + "review_type": "individual", + "reviewer_id": self.user_replacer_c.id, + "sequence": 40, + } + ) + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + record, reviews = self._create_record_and_request_validation() + + tier1_review = reviews.filtered( + lambda r: r.definition_id.reviewer_id == self.user_delegator + ) + tier2_review = reviews.filtered( + lambda r: r.definition_id.reviewer_id == self.user_replacer_c + ) + + self.assertIn(self.user_replacer_b, tier1_review.reviewer_ids) + + # Validate the first tier as the replacer + record.with_user(self.user_replacer_b).validate_tier() + + # Invalidate cache to ensure we read the latest status + tier1_review.invalidate_recordset(["status"]) + tier2_review.invalidate_recordset(["status"]) + + self.assertEqual(tier1_review.status, "approved", "Tier 1 should be approved.") + self.assertEqual( + tier2_review.status, "pending", "Tier 2 should now be pending." + ) + + def test_17_delegation_by_field_reviewer(self): + """ + Test Case for the Primary Fix. + + This test ensures that a pending review is correctly delegated when the + reviewer was assigned via a dynamic field (`review_type` = 'field'). + This was the main cause of the cron job issue. + """ + self.env.user.clear_caches() # Ensure fresh reads + + # 1. Setup: Create a tier definition that uses a field to find the reviewer. + reviewer_field = self.env["ir.model.fields"].search( + [ + ("model", "=", "tier.validation.tester"), + ("name", "=", "user_id"), + ], + limit=1, + ) + self.assertTrue(reviewer_field, "Setup failed: Could not find 'user_id' field.") + + field_tier_def = self.env["tier.definition"].create( + { + "model_id": self.tester_model.id, + "review_type": "field", + "reviewer_field_id": reviewer_field.id, + "name": "Field-Based Review", + } + ) + + # Create a record where the 'user_id' is our delegator + record = self.test_model.create( + {"test_field": 1.0, "user_id": self.user_delegator.id} + ) + record.with_user(self.test_user_2).request_validation() + review = self.env["tier.review"].search( + [ + ("res_id", "=", record.id), + ("definition_id", "=", field_tier_def.id), + ] + ) + + self.assertTrue(review, "Test setup failed: Review was not created.") + self.assertIn( + self.user_delegator, + review.reviewer_ids, + "Initial reviewer should be the user from the 'user_id' field.", + ) + + # 2. Action: The user goes on holiday. This triggers the fixed `write` + # method, which in turn calls the fixed `_recompute_reviews_for_users`. + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + + # 3. Assert: The pending review should now be assigned to the replacer. + review.invalidate_recordset(["reviewer_ids"]) # Refresh the field from the DB + self.assertIn( + self.user_replacer_b, + review.reviewer_ids, + "Review should have been delegated to the replacer.", + ) + self.assertNotIn( + self.user_delegator, + review.reviewer_ids, + "Original reviewer should have been removed after delegation.", + ) + + def test_18_change_replacer_while_on_holiday(self): + """ + Test Case for the Secondary Fix. + + This test ensures that if a user is already on holiday and their + replacer is changed, their pending reviews are correctly moved from + the old replacer to the new one. + """ + # 1. Setup: User is on holiday, and a review is delegated to Replacer B. + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + record, review = self._create_record_and_request_validation() + + self.assertIn( + self.user_replacer_b, + review.reviewer_ids, + "Initial delegation to Replacer B should have occurred.", + ) + self.assertNotIn(self.user_delegator, review.reviewer_ids) + + # 2. Action: While still on holiday, the user changes their replacer to C. + # This triggers the improved `write` method in res.users. + self.user_delegator.write({"validation_replacer_id": self.user_replacer_c.id}) + + # 3. Assert: The review should be moved from B to C. + review.invalidate_recordset(["reviewer_ids"]) + self.assertIn( + self.user_replacer_c, + review.reviewer_ids, + "Review should have been re-delegated to the new replacer (C).", + ) + self.assertNotIn( + self.user_replacer_b, + review.reviewer_ids, + "The old replacer (B) should no longer be a reviewer.", + ) + + def test_19_return_from_holiday_reassigns_pending(self): + """ + Test Case for returning from holiday. + + This test verifies the new behavior introduced by the fix: when a user + returns from holiday (`on_holiday`=False), their pending reviews that + were delegated are now reassigned back to them. + """ + # 1. Setup: User is on holiday, and a review is delegated. + self.user_delegator.write( + {"on_holiday": True, "validation_replacer_id": self.user_replacer_b.id} + ) + _record, review = self._create_record_and_request_validation() + + self.assertIn( + self.user_replacer_b, + review.reviewer_ids, + "Review should be with the replacer while user is on holiday.", + ) + + # 2. Action: The user returns from holiday. + self.user_delegator.write({"on_holiday": False}) + + # 3. Assert: The pending review is reassigned back to the original user. + review.invalidate_recordset(["reviewer_ids"]) + self.assertIn( + self.user_delegator, + review.reviewer_ids, + "Pending review should be reassigned back to the original user upon their return.", + ) + self.assertNotIn( + self.user_replacer_b, + review.reviewer_ids, + "Replacer should be removed from the review once the original user returns.", + ) diff --git a/base_tier_validation_delegation/views/res_users_views.xml b/base_tier_validation_delegation/views/res_users_views.xml new file mode 100644 index 0000000000..c9e5831bc3 --- /dev/null +++ b/base_tier_validation_delegation/views/res_users_views.xml @@ -0,0 +1,113 @@ + + + + res.users + + + + + + + + + + + + + + + + res.users.form.simple.modif.delegation + res.users + + + + + + + + + + + + + + + + res.users + + + + + + + + + res.users.tree.delegation + res.users + + + + + + + + + + + Delegation Management + res.users + tree,form + + {'search_default_delegation_active': 1} + + + diff --git a/base_tier_validation_delegation/views/tier_review_views.xml b/base_tier_validation_delegation/views/tier_review_views.xml new file mode 100644 index 0000000000..9b93df5ef7 --- /dev/null +++ b/base_tier_validation_delegation/views/tier_review_views.xml @@ -0,0 +1,73 @@ + + + + + tier.review + + + + + + + + + + tier.review.form + tier.review + +
+ + + + + + + + + + + + + + + + + + +
+
+
+ + + My Delegated Reviews + tier.review + tree,form + [('delegated_by_ids', 'in', [uid])] + {'search_default_pending': 1} + +

+ No reviews have been delegated by you. +

+

+ This screen shows the reviews that you have delegated to other users. +

+
+
+ + +
diff --git a/base_tier_validation_delegation_hr/README.rst b/base_tier_validation_delegation_hr/README.rst new file mode 100644 index 0000000000..8178cac092 --- /dev/null +++ b/base_tier_validation_delegation_hr/README.rst @@ -0,0 +1,103 @@ +================================== +Base Tier Validation Delegation HR +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e847c56fb223f16d7e06ebfac173f3d08ce0dbc7f089f797c8d96fd0559bfbf8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/16.0/base_tier_validation_delegation_hr + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-16-0/server-ux-16-0-base_tier_validation_delegation_hr + :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/server-ux&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of base_tier_validation_delegation by moving the configuration interface from the User profile to the Employee profile. + +It allows HR managers or employees to manage their Tier Validation delegation settings (On Holiday status, dates, and replacer) directly from the Employee form view. The settings are automatically synchronized with the related User record. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +- Go to Employees and open an employee form. +- Ensure the employee is linked to a system User via the HR Settings tab > Related User field. +- If the user is linked, a new tab Delegation will appear. + +In the Delegation tab, you can configure: + +- On Holiday (Tier Validation): Check this box to activate delegation. +- Holiday Dates: (Optional) Set a start and end date for the delegation period. +- Tier Validation Replacer: Select the user who will validate documents in your absence. + +Usage +===== + +To use this module: + +- Navigate to the Employees app. +- Open your employee profile (or another employee's profile if you have HR rights). +- Go to the Delegation tab. +- Enable the On Holiday (Tier Validation) checkbox. +- Select a Tier Validation Replacer. +- (Optional) Define the Holiday Start Date and Holiday End Date. + +Once saved, the configuration is immediately applied to the linked User. +Any Tier Validation requests assigned to this user will be delegated according to the rules defined in base_tier_validation_delegation. + +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 +~~~~~~~ + +* 360 ERP + +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/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_tier_validation_delegation_hr/__init__.py b/base_tier_validation_delegation_hr/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/base_tier_validation_delegation_hr/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_tier_validation_delegation_hr/__manifest__.py b/base_tier_validation_delegation_hr/__manifest__.py new file mode 100644 index 0000000000..365bfc6a25 --- /dev/null +++ b/base_tier_validation_delegation_hr/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +{ + "name": "Base Tier Validation Delegation HR", + "summary": "Allows employees to delegate tier validation tasks when out of office.", + "version": "16.0.1.0.0", + "development_status": "Beta", + "category": "Tools", + "website": "https://github.com/OCA/server-ux", + "author": "360 ERP, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": [ + "hr", + "base_tier_validation_delegation", + ], + "data": [ + "views/hr_employee_views.xml", + ], +} diff --git a/base_tier_validation_delegation_hr/models/__init__.py b/base_tier_validation_delegation_hr/models/__init__.py new file mode 100644 index 0000000000..e11a62f98c --- /dev/null +++ b/base_tier_validation_delegation_hr/models/__init__.py @@ -0,0 +1 @@ +from . import hr_employee diff --git a/base_tier_validation_delegation_hr/models/hr_employee.py b/base_tier_validation_delegation_hr/models/hr_employee.py new file mode 100644 index 0000000000..3ab9031273 --- /dev/null +++ b/base_tier_validation_delegation_hr/models/hr_employee.py @@ -0,0 +1,33 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import fields, models + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + + tier_on_holiday = fields.Boolean( + string="On Holiday (Tier Validation)", + related="user_id.on_holiday", + readonly=False, + help="If checked, tier validations will be delegated to the replacer.", + ) + + tier_holiday_start_date = fields.Date( + string="Holiday Start Date", + related="user_id.holiday_start_date", + readonly=False, + ) + + tier_holiday_end_date = fields.Date( + string="Holiday End Date", related="user_id.holiday_end_date", readonly=False + ) + + tier_validation_replacer_id = fields.Many2one( + comodel_name="res.users", + string="Tier Validation Replacer", + related="user_id.validation_replacer_id", + readonly=False, + help="The user who will receive validation requests while this employee is on holiday.", + ) diff --git a/base_tier_validation_delegation_hr/readme/CONFIGURE.rst b/base_tier_validation_delegation_hr/readme/CONFIGURE.rst new file mode 100644 index 0000000000..5499c01eda --- /dev/null +++ b/base_tier_validation_delegation_hr/readme/CONFIGURE.rst @@ -0,0 +1,11 @@ +To configure this module, you need to: + +- Go to Employees and open an employee form. +- Ensure the employee is linked to a system User via the HR Settings tab > Related User field. +- If the user is linked, a new tab Delegation will appear. + +In the Delegation tab, you can configure: + +- On Holiday (Tier Validation): Check this box to activate delegation. +- Holiday Dates: (Optional) Set a start and end date for the delegation period. +- Tier Validation Replacer: Select the user who will validate documents in your absence. diff --git a/base_tier_validation_delegation_hr/readme/DESCRIPTION.rst b/base_tier_validation_delegation_hr/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..e4c8b848c8 --- /dev/null +++ b/base_tier_validation_delegation_hr/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module extends the functionality of base_tier_validation_delegation by moving the configuration interface from the User profile to the Employee profile. + +It allows HR managers or employees to manage their Tier Validation delegation settings (On Holiday status, dates, and replacer) directly from the Employee form view. The settings are automatically synchronized with the related User record. diff --git a/base_tier_validation_delegation_hr/readme/USAGE.rst b/base_tier_validation_delegation_hr/readme/USAGE.rst new file mode 100644 index 0000000000..416f396194 --- /dev/null +++ b/base_tier_validation_delegation_hr/readme/USAGE.rst @@ -0,0 +1,11 @@ +To use this module: + +- Navigate to the Employees app. +- Open your employee profile (or another employee's profile if you have HR rights). +- Go to the Delegation tab. +- Enable the On Holiday (Tier Validation) checkbox. +- Select a Tier Validation Replacer. +- (Optional) Define the Holiday Start Date and Holiday End Date. + +Once saved, the configuration is immediately applied to the linked User. +Any Tier Validation requests assigned to this user will be delegated according to the rules defined in base_tier_validation_delegation. diff --git a/base_tier_validation_delegation_hr/static/description/index.html b/base_tier_validation_delegation_hr/static/description/index.html new file mode 100644 index 0000000000..fd1d372184 --- /dev/null +++ b/base_tier_validation_delegation_hr/static/description/index.html @@ -0,0 +1,448 @@ + + + + + +Base Tier Validation Delegation HR + + + +
+

Base Tier Validation Delegation HR

+ + +

Beta License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runboat

+

This module extends the functionality of base_tier_validation_delegation by moving the configuration interface from the User profile to the Employee profile.

+

It allows HR managers or employees to manage their Tier Validation delegation settings (On Holiday status, dates, and replacer) directly from the Employee form view. The settings are automatically synchronized with the related User record.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  • Go to Employees and open an employee form.
  • +
  • Ensure the employee is linked to a system User via the HR Settings tab > Related User field.
  • +
  • If the user is linked, a new tab Delegation will appear.
  • +
+

In the Delegation tab, you can configure:

+
    +
  • On Holiday (Tier Validation): Check this box to activate delegation.
  • +
  • Holiday Dates: (Optional) Set a start and end date for the delegation period.
  • +
  • Tier Validation Replacer: Select the user who will validate documents in your absence.
  • +
+
+
+

Usage

+

To use this module:

+
    +
  • Navigate to the Employees app.
  • +
  • Open your employee profile (or another employee’s profile if you have HR rights).
  • +
  • Go to the Delegation tab.
  • +
  • Enable the On Holiday (Tier Validation) checkbox.
  • +
  • Select a Tier Validation Replacer.
  • +
  • (Optional) Define the Holiday Start Date and Holiday End Date.
  • +
+

Once saved, the configuration is immediately applied to the linked User. +Any Tier Validation requests assigned to this user will be delegated according to the rules defined in base_tier_validation_delegation.

+
+
+

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

+
    +
  • 360 ERP
  • +
+
+
+

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/server-ux project on GitHub.

+

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

+
+
+
+ + diff --git a/base_tier_validation_delegation_hr/tests/__init__.py b/base_tier_validation_delegation_hr/tests/__init__.py new file mode 100644 index 0000000000..1cec1a782d --- /dev/null +++ b/base_tier_validation_delegation_hr/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_delegation diff --git a/base_tier_validation_delegation_hr/tests/test_hr_delegation.py b/base_tier_validation_delegation_hr/tests/test_hr_delegation.py new file mode 100644 index 0000000000..5fb10bd990 --- /dev/null +++ b/base_tier_validation_delegation_hr/tests/test_hr_delegation.py @@ -0,0 +1,102 @@ +# Copyright 2025 360ERP () +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from datetime import date, timedelta + +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestHrDelegation(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.user_delegator = cls.env["res.users"].create( + { + "name": "Delegator User", + "login": "delegator_user", + "email": "delegator@test.com", + } + ) + + cls.user_replacer = cls.env["res.users"].create( + { + "name": "Replacer User", + "login": "replacer_user", + "email": "replacer@test.com", + } + ) + + cls.employee = cls.env["hr.employee"].create( + { + "name": "Delegator Employee", + "user_id": cls.user_delegator.id, + } + ) + + def test_01_sync_employee_to_user_boolean(self): + """Test that setting 'On Holiday' on Employee updates User.""" + self.assertFalse( + self.user_delegator.on_holiday, "User should start not on holiday" + ) + + self.employee.write({"tier_on_holiday": True}) + self.assertTrue( + self.user_delegator.on_holiday, + "User should be marked on holiday after Employee update", + ) + + self.employee.write({"tier_on_holiday": False}) + self.assertFalse( + self.user_delegator.on_holiday, + "User should be unmarked after Employee update", + ) + + def test_02_sync_employee_to_user_replacer(self): + """Test that setting 'Replacer' on Employee updates User.""" + self.assertFalse( + self.user_delegator.validation_replacer_id, + "User should start with no replacer", + ) + + self.employee.write({"tier_validation_replacer_id": self.user_replacer.id}) + self.assertEqual( + self.user_delegator.validation_replacer_id, + self.user_replacer, + "User's replacer should match the one set on Employee", + ) + + def test_03_sync_employee_to_user_dates(self): + """Test that setting dates on Employee updates User.""" + start_date = date.today() + end_date = date.today() + timedelta(days=5) + + self.employee.write( + { + "tier_holiday_start_date": start_date, + "tier_holiday_end_date": end_date, + } + ) + self.assertEqual(self.user_delegator.holiday_start_date, start_date) + self.assertEqual(self.user_delegator.holiday_end_date, end_date) + + def test_04_read_consistency(self): + """Test that reading from Employee reflects changes made directly on User.""" + self.user_delegator.write({"on_holiday": True}) + + self.assertTrue( + self.employee.tier_on_holiday, + "Employee field should reflect changes made to User", + ) + + def test_05_employee_without_user(self): + """Test creating an employee without a user (ensure no crash on view/read).""" + employee_no_user = self.env["hr.employee"].create( + { + "name": "Employee No User", + # user_id is False + } + ) + + self.assertFalse(employee_no_user.tier_on_holiday) + self.assertFalse(employee_no_user.tier_validation_replacer_id) diff --git a/base_tier_validation_delegation_hr/views/hr_employee_views.xml b/base_tier_validation_delegation_hr/views/hr_employee_views.xml new file mode 100644 index 0000000000..b76e93a935 --- /dev/null +++ b/base_tier_validation_delegation_hr/views/hr_employee_views.xml @@ -0,0 +1,45 @@ + + + + hr.employee + + + + + + + + + + + + + + + diff --git a/setup/base_tier_validation_delegation/odoo/addons/base_tier_validation_delegation b/setup/base_tier_validation_delegation/odoo/addons/base_tier_validation_delegation new file mode 120000 index 0000000000..16c73e3e98 --- /dev/null +++ b/setup/base_tier_validation_delegation/odoo/addons/base_tier_validation_delegation @@ -0,0 +1 @@ +../../../../base_tier_validation_delegation \ No newline at end of file diff --git a/setup/base_tier_validation_delegation/setup.py b/setup/base_tier_validation_delegation/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/base_tier_validation_delegation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/base_tier_validation_delegation_hr/odoo/addons/base_tier_validation_delegation_hr b/setup/base_tier_validation_delegation_hr/odoo/addons/base_tier_validation_delegation_hr new file mode 120000 index 0000000000..5a97561475 --- /dev/null +++ b/setup/base_tier_validation_delegation_hr/odoo/addons/base_tier_validation_delegation_hr @@ -0,0 +1 @@ +../../../../base_tier_validation_delegation_hr \ No newline at end of file diff --git a/setup/base_tier_validation_delegation_hr/setup.py b/setup/base_tier_validation_delegation_hr/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/base_tier_validation_delegation_hr/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)