# Copyright 2026 Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from markupsafe import Markup from odoo import _, api, fields, models from ..models.res_company import LEAVE_POLICY_SELECTION class HrTimesheetSheetDeleteWizard(models.TransientModel): _name = "hr_timesheet.sheet.delete.wizard" _description = "Delete Timesheet Sheets" sheet_ids = fields.Many2many( comodel_name="hr_timesheet.sheet", string="Timesheet Sheets", ) sheet_count = fields.Integer( string="Sheets", compute="_compute_delete_cascade_data", ) timesheet_count = fields.Integer( string="Timesheet Entries", compute="_compute_delete_cascade_data", ) timesheet_hours = fields.Float( string="Hours", compute="_compute_delete_cascade_data", ) invoiced_count = fields.Integer( string="Invoiced Entries", compute="_compute_delete_cascade_data", ) leave_policy = fields.Selection( selection=LEAVE_POLICY_SELECTION, string="Time Off Policy", compute="_compute_delete_cascade_data", ) leave_ids = fields.Many2many( comodel_name="hr.leave", relation="hr_timesheet_sheet_delete_wizard_leave_rel", string="Time Off To Cancel", compute="_compute_delete_cascade_data", ) leave_count = fields.Integer( string="Time Off Entries", compute="_compute_delete_cascade_data", ) leave_days = fields.Float( string="Days Restored", compute="_compute_delete_cascade_data", ) partial_leave_ids = fields.Many2many( comodel_name="hr.leave", relation="hr_timesheet_sheet_delete_wizard_partial_leave_rel", string="Time Off Beyond The Sheet Period", compute="_compute_delete_cascade_data", ) blocking_reason = fields.Text( string="Blocking Reason", compute="_compute_delete_cascade_data", ) summary_html = fields.Html( string="Summary", compute="_compute_delete_cascade_data", sanitize=False, ) @api.model def default_get(self, fields_list): res = super().default_get(fields_list) if self.env.context.get("active_model") == "hr_timesheet.sheet": res["sheet_ids"] = [(6, 0, self.env.context.get("active_ids") or [])] return res @api.depends("sheet_ids") def _compute_delete_cascade_data(self): for wizard in self: sheets = wizard.sheet_ids data = sheets._delete_cascade_prepare() wizard.sheet_count = data["sheet_count"] wizard.timesheet_count = data["timesheet_count"] wizard.timesheet_hours = data["timesheet_hours"] wizard.invoiced_count = data["invoiced_count"] wizard.leave_policy = data["leave_policy"] wizard.leave_count = data["leave_count"] wizard.leave_days = data["leave_days"] cancel_sheets = sheets._delete_cascade_cancel_sheets() wizard.leave_ids = cancel_sheets._sheet_leaves_to_cancel().ids wizard.partial_leave_ids = cancel_sheets._sheet_leaves_beyond_period().ids wizard.blocking_reason = "\n".join(data["blocking"]) or False wizard.summary_html = wizard._build_summary_html(data) def _build_summary_html(self, data): self.ensure_one() if not data["sheet_count"]: return Markup("

%s

") % _("No timesheet sheet was selected.") parts = [] for section in data["sections"]: parts.append(Markup("

%s

") % section["title"]) parts.append( Markup("") % Markup("").join( Markup("
  • %s
  • ") % line for line in section["lines"] ) ) return Markup("").join(parts) def _delete_cascade_list_action_xml_id(self): """Hook for extensions The list the user plausibly comes from. Deleting the sheet of another employee requires *Timesheets / Administrator*, and that sheet is only listed by *All Timesheet Sheets*; every other case is a user looking at his own sheets, and *My Timesheet Sheets* is the only list of sheets an employee outside that group has a menu for. """ self.ensure_one() own_sheets = self.sheet_ids.user_id == self.env.user is_manager = self.env.user.has_group("hr_timesheet.group_timesheet_manager") if own_sheets or not is_manager: return "hr_timesheet_sheet.act_hr_timesheet_sheet_my_timesheets" return "hr_timesheet_sheet.act_hr_timesheet_sheet_all_timesheets" def _delete_cascade_next_action(self): """Hook for extensions Called before the deletion, while the sheets can still be read. Opened from a list, the caller is left on that list: closing the dialog reloads it and keeps its filters, its grouping and its selection, and there is no dead record on screen. Opened from the form of a sheet that no longer exists, closing the dialog would leave that form on screen with the values of a deleted record: return the list of sheets instead, with ``target="main"`` so the deleted form is dropped from the breadcrumb rather than kept one step behind. """ self.ensure_one() if not self.env.context.get("sheet_delete_cascade_from_form"): return {"type": "ir.actions.act_window_close"} action = self.env["ir.actions.act_window"]._for_xml_id( self._delete_cascade_list_action_xml_id() ) action["target"] = "main" return action def action_confirm_delete(self): self.ensure_one() next_action = self._delete_cascade_next_action() self.sheet_ids._with_delete_cascade().unlink() return next_action