Skip to content

Commit b9e697e

Browse files
committed
[ADD] base_tier_validation_delegation
1 parent 2418ec8 commit b9e697e

File tree

19 files changed

+1525
-0
lines changed

19 files changed

+1525
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
===============================
2+
Base Tier Validation Delegation
3+
===============================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:80e0eb6d81b34912b48ec36fcb243ad8154a470253cd6509da92e245dbea4f91
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github
20+
:target: https://github.com/OCA/server-ux/tree/16.0/base_tier_validation_delegation
21+
:alt: OCA/server-ux
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/server-ux-16-0/server-ux-16-0-base_tier_validation_delegation
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-ux&target_branch=16.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module provides the core functionality for users to delegate their tier validation tasks to another user when they are out of the office.
32+
33+
**Table of contents**
34+
35+
.. contents::
36+
:local:
37+
38+
Configuration
39+
=============
40+
41+
To configure Tier Validations Delegation, you need to:
42+
43+
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**).
44+
2. Navigate to the **Delegation** tab.
45+
3. Check the **On Holiday** box.
46+
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.
47+
5. Select a **Default Replacer**. This is the user who will receive all validation requests.
48+
49+
Usage
50+
=====
51+
52+
To use this module, you need to:
53+
54+
* Any new tier review assigned to the original user will be automatically assigned to the final user in the delegation chain.
55+
* Any existing pending reviews will be reassigned when the "On Holiday" status is activated.
56+
* A message is posted in the chatter of the related document to inform that the review has been delegated.
57+
58+
Delegators can track the reviews they have delegated by going to **Settings > Tier Validations > My Delegated Reviews**.
59+
60+
Administrators can manage all user delegations from **Settings > Users & Companies > Delegation Management**.
61+
62+
Bug Tracker
63+
===========
64+
65+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-ux/issues>`_.
66+
In case of trouble, please check there if your issue has already been reported.
67+
If you spotted it first, help us to smash it by providing a detailed and welcomed
68+
`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**>`_.
69+
70+
Do not contact contributors directly about support or help with technical issues.
71+
72+
Credits
73+
=======
74+
75+
Authors
76+
~~~~~~~
77+
78+
* 360 ERP
79+
80+
Maintainers
81+
~~~~~~~~~~~
82+
83+
This module is maintained by the OCA.
84+
85+
.. image:: https://odoo-community.org/logo.png
86+
:alt: Odoo Community Association
87+
:target: https://odoo-community.org
88+
89+
OCA, or the Odoo Community Association, is a nonprofit organization whose
90+
mission is to support the collaborative development of Odoo features and
91+
promote its widespread use.
92+
93+
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.
94+
95+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2025 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
{
5+
"name": "Base Tier Validation Delegation",
6+
"summary": "Allows users to delegate tier validation tasks when out of office.",
7+
"version": "16.0.1.0.0",
8+
"development_status": "Beta",
9+
"category": "Tools",
10+
"website": "https://github.com/OCA/server-ux",
11+
"author": "360 ERP, Odoo Community Association (OCA)",
12+
"license": "AGPL-3",
13+
"depends": ["base_tier_validation"],
14+
"data": [
15+
"security/delegation_security.xml",
16+
"data/cron.xml",
17+
"views/res_users_views.xml",
18+
"views/tier_review_views.xml",
19+
],
20+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<odoo noupdate="1">
3+
<record id="ir_cron_update_holiday_status" model="ir.cron">
4+
<field name="name">Delegation: Update Holiday Status</field>
5+
<field name="model_id" ref="base.model_res_users" />
6+
<field name="state">code</field>
7+
<field name="code">model._cron_update_holiday_status()</field>
8+
<field name="user_id" ref="base.user_root" />
9+
<field name="interval_number">1</field>
10+
<field name="interval_type">days</field>
11+
<field name="numbercall">-1</field>
12+
<field
13+
name="nextcall"
14+
eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 01:00:00')"
15+
/>
16+
<field
17+
name="help"
18+
>Automatically activate or deactivate a user's 'On Holiday' status based on their configured start and end dates.</field>
19+
</record>
20+
21+
<record id="ir_cron_send_delegation_reminder" model="ir.cron">
22+
<field name="name">Delegation: Send Holiday Reminder</field>
23+
<field name="model_id" ref="base.model_res_users" />
24+
<field name="state">code</field>
25+
<field name="code">model._cron_send_delegation_reminder()</field>
26+
<field name="user_id" ref="base.user_root" />
27+
<field name="interval_number">1</field>
28+
<field name="interval_type">days</field>
29+
<field name="numbercall">-1</field>
30+
<field
31+
name="nextcall"
32+
eval="(datetime.now() + timedelta(days=1)).strftime('%Y-%m-%d 08:00:00')"
33+
/>
34+
<field
35+
name="help"
36+
>Notifies users 3 days before their scheduled holiday if they have not yet configured a replacer.</field>
37+
</record>
38+
</odoo>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import res_users
2+
from . import tier_review
3+
from . import tier_validation
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Copyright 2025 360ERP (<https://www.360erp.com>)
2+
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
3+
4+
import logging
5+
from datetime import timedelta
6+
7+
from odoo import _, api, fields, models
8+
from odoo.exceptions import ValidationError
9+
10+
_logger = logging.getLogger(__name__)
11+
12+
13+
class ResUsers(models.Model):
14+
_inherit = "res.users"
15+
16+
on_holiday = fields.Boolean(
17+
help="Check this box if you are out of office and want to delegate your "
18+
"validation tasks.",
19+
)
20+
holiday_start_date = fields.Date()
21+
holiday_end_date = fields.Date()
22+
validation_replacer_id = fields.Many2one(
23+
"res.users",
24+
string="Default Replacer",
25+
help="This user will receive your validation requests while you are on holiday.",
26+
)
27+
28+
@api.constrains("on_holiday", "holiday_start_date", "holiday_end_date")
29+
def _check_holiday_dates(self):
30+
"""Ensure end date is not before start date."""
31+
for user in self:
32+
if (
33+
user.on_holiday
34+
and user.holiday_start_date
35+
and user.holiday_end_date
36+
and user.holiday_start_date > user.holiday_end_date
37+
):
38+
raise ValidationError(
39+
_("Holiday End Date cannot be before the Start Date.")
40+
)
41+
42+
@api.constrains("on_holiday", "validation_replacer_id")
43+
def _check_validation_replacer(self):
44+
"""Ensures a user does not delegate to themselves or create a circular loop."""
45+
for user in self:
46+
if not user.on_holiday or not user.validation_replacer_id:
47+
continue
48+
if user.validation_replacer_id == user:
49+
raise ValidationError(
50+
_("You cannot delegate validation tasks to yourself.")
51+
)
52+
# Check for circular delegation (e.g., A->B->C->A)
53+
next_replacer = user.validation_replacer_id
54+
path = {user}
55+
while next_replacer:
56+
if next_replacer in path:
57+
raise ValidationError(
58+
_("You cannot create a circular delegation path.")
59+
)
60+
path.add(next_replacer)
61+
next_replacer = next_replacer.validation_replacer_id
62+
63+
def _is_currently_on_holiday(self, today=None):
64+
"""
65+
Checks if a user is considered on holiday right now, respecting date ranges.
66+
"""
67+
self.ensure_one()
68+
if not today:
69+
today = fields.Date.context_today(self)
70+
return (
71+
self.on_holiday
72+
and self.validation_replacer_id
73+
and (not self.holiday_start_date or self.holiday_start_date <= today)
74+
and (not self.holiday_end_date or self.holiday_end_date >= today)
75+
)
76+
77+
def _get_final_validation_replacer(self):
78+
"""
79+
Recursively finds the final active user in a delegation chain.
80+
"""
81+
self.ensure_one()
82+
delegation_path = {self}
83+
current_user = self
84+
today = fields.Date.context_today(self)
85+
86+
while current_user._is_currently_on_holiday(today=today):
87+
next_user_candidate = current_user.validation_replacer_id
88+
89+
if not next_user_candidate or not next_user_candidate.active:
90+
_logger.debug(
91+
"Delegation chain broken, falling back to '%s'.", current_user.login
92+
)
93+
return current_user
94+
95+
if next_user_candidate in delegation_path:
96+
_logger.warning(
97+
"Circular delegation detected, falling back to '%s'.",
98+
current_user.login,
99+
)
100+
return current_user
101+
102+
delegation_path.add(next_user_candidate)
103+
current_user = next_user_candidate
104+
return current_user
105+
106+
def write(self, vals):
107+
"""
108+
If a user's holiday status or replacer changes, find all their pending
109+
reviews and trigger a re-computation of the reviewers.
110+
"""
111+
holiday_fields = [
112+
"on_holiday",
113+
"holiday_start_date",
114+
"holiday_end_date",
115+
"validation_replacer_id",
116+
]
117+
if not any(field in holiday_fields for field in vals):
118+
return super().write(vals)
119+
120+
users_to_recompute = self.env["res.users"]
121+
if vals.get("on_holiday") is True:
122+
users_to_recompute = self.filtered(lambda u: not u.on_holiday)
123+
124+
res = super().write(vals)
125+
126+
if users_to_recompute:
127+
self.env["tier.review"]._recompute_reviews_for_users(users_to_recompute)
128+
return res
129+
130+
@api.model
131+
def _cron_update_holiday_status(self):
132+
"""
133+
A daily cron job to automatically activate or deactivate a user's
134+
holiday status based on the configured start and end dates.
135+
"""
136+
_logger.info("CRON: Running automatic holiday status update.")
137+
today = fields.Date.context_today(self)
138+
users_to_activate = self.search(
139+
[("on_holiday", "=", False), ("holiday_start_date", "=", today)]
140+
)
141+
if users_to_activate:
142+
users_to_activate.write({"on_holiday": True})
143+
users_to_deactivate = self.search(
144+
[("on_holiday", "=", True), ("holiday_end_date", "<", today)]
145+
)
146+
if users_to_deactivate:
147+
users_to_deactivate.write({"on_holiday": False})
148+
_logger.info("CRON: Finished holiday status update.")
149+
150+
@api.model
151+
def _cron_send_delegation_reminder(self):
152+
"""
153+
Sends a reminder to users whose holiday is starting soon but have not
154+
configured a replacer.
155+
"""
156+
_logger.info("CRON: Running delegation reminder check.")
157+
reminder_date = fields.Date.context_today(self) + timedelta(days=3)
158+
users_to_remind = self.search(
159+
[
160+
("holiday_start_date", "=", reminder_date),
161+
("on_holiday", "=", False),
162+
("validation_replacer_id", "=", False),
163+
]
164+
)
165+
for user in users_to_remind:
166+
user.partner_id.message_post(
167+
body=_(
168+
"Your holiday is scheduled to start on %s. Please remember to "
169+
"configure a validation replacer in your preferences to avoid "
170+
"blocking any documents."
171+
)
172+
% user.holiday_start_date,
173+
message_type="notification",
174+
subtype_xmlid="mail.mt_comment",
175+
)
176+
_logger.info("CRON: Finished delegation reminder check.")

0 commit comments

Comments
 (0)