|
| 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