# Copyright 2026 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, models # The context of an RPC call is controlled by the client: odoo.api.call_kw # rebuilds the environment with whatever dictionary arrives in the request, so # a flag carrying a JSON value could be forged by any authenticated user and # the cascade would delete a sheet without the confirmation wizard. This # sentinel is compared by identity and cannot be produced by a deserializer, # so it only ever reaches the consumer from inside this process. _SHEET_DELETE_CASCADE = object() class AccountAnalyticLine(models.Model): _inherit = "account.analytic.line" def _sheet_delete_cascade_token(self): """Hook for extensions""" return _SHEET_DELETE_CASCADE def _in_sheet_delete_cascade(self): return ( self.env.context.get("sheet_delete_cascade") is self._sheet_delete_cascade_token() ) def _sheet_leave_policy(self): """Hook for extensions""" companies = self.sheet_id.company_id or self.company_id or self.env.company policies = companies.mapped("timesheet_sheet_delete_leave_policy") return "cancel" if "cancel" in policies else "detach" def _in_deliberate_delete_cascade(self): """Hook for extensions The sentinel alone is not enough: the elevated privileges that only the cancellation branch of the sheet grants are required as a second barrier, so an in-process caller that merely carries the cascade context cannot delete a time off entry on its way. """ return self._in_sheet_delete_cascade() and self.env.su def _detach_leave_lines_on_unlink(self): if self._in_deliberate_delete_cascade(): return self._sheet_leave_policy() != "cancel" return super()._detach_leave_lines_on_unlink() @api.ondelete(at_uninstall=False) def _unlink_except_linked_leave(self): # Defence in depth. The cancellation branch of the cascade never gets # here with a time off entry: project_timesheet_holidays clears # holiday_id before unlinking the entries of a refused request # (models/hr_holidays.py:127-134), so by then they are ordinary lines. # This keeps the contract true for any other caller that unlinks a time # off entry from inside a deliberate cascade. if ( self._in_deliberate_delete_cascade() and self._sheet_leave_policy() == "cancel" ): return return super()._unlink_except_linked_leave()