Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions base_tier_validation_delegation/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
===============================
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.

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 <https://github.com/OCA/server-ux/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 <https://github.com/OCA/server-ux/issues/new?body=module:%20base_tier_validation_delegation%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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 <https://github.com/OCA/server-ux/tree/16.0/base_tier_validation_delegation>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions base_tier_validation_delegation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
20 changes: 20 additions & 0 deletions base_tier_validation_delegation/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2025 360ERP (<https://www.360erp.com>)
# 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",
],
}
38 changes: 38 additions & 0 deletions base_tier_validation_delegation/data/cron.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="ir_cron_update_holiday_status" model="ir.cron">
<field name="name">Delegation: Update Holiday Status</field>
<field name="model_id" ref="base.model_res_users" />
<field name="state">code</field>
<field name="code">model._cron_update_holiday_status()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field
name="nextcall"
eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 01:00:00')"
/>
<field
name="help"
>Automatically activate or deactivate a user's 'On Holiday' status based on their configured start and end dates.</field>
</record>

<record id="ir_cron_send_delegation_reminder" model="ir.cron">
<field name="name">Delegation: Send Holiday Reminder</field>
<field name="model_id" ref="base.model_res_users" />
<field name="state">code</field>
<field name="code">model._cron_send_delegation_reminder()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field
name="nextcall"
eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 08:00:00')"
/>
<field
name="help"
>Notifies users 3 days before their scheduled holiday if they have not yet configured a replacer.</field>
</record>
</odoo>
3 changes: 3 additions & 0 deletions base_tier_validation_delegation/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import res_users
from . import tier_review
from . import tier_validation
176 changes: 176 additions & 0 deletions base_tier_validation_delegation/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Copyright 2025 360ERP (<https://www.360erp.com>)
# 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.env["res.users"]
if vals.get("on_holiday") is True:
users_to_recompute = self.filtered(lambda u: not u.on_holiday)

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.")
Loading