Leantime/leantime · error · Exception
Task has timesheets attached, delete all timesheets first or
Error message
Task has timesheets attached, delete all timesheets first or consider archiving the task
What it means
The second guard in Tickets::canDelete($id): the ticket exists, but timesheetsRepo->getTimesheetsByTicket($id) returns rows, so deleting would orphan tracked time in zp_timesheets. Leantime blocks the delete and instructs: remove all logged hours first, or archive the task instead. This protects reporting/invoicing integrity - archived tasks keep their history, deleted ones do not.
Source
Thrown at app/Domain/Tickets/Services/Tickets.php:3717
return true;
}
return false;
}
public function canDelete($id)
{
$ticket = $this->getTicket($id);
if (empty($ticket)) {
throw new \Exception('Task does not exist');
}
$hasLoggedHours = $this->timesheetsRepo->getTimesheetsByTicket($id);
if ($hasLoggedHours) {
throw new \Exception('Task has timesheets attached, delete all timesheets first or consider archiving the task');
}
return true;
}
/**
* @return bool|string[]
*
* @throws BindingResolutionException
*
* @api
*/
#[RequiresPermission(TicketsPermissions::DELETE, entityScoped: true)]
public function deleteMilestone($id): array|bool
{
$ticket = $this->getTicket($id);View on GitHub (pinned to 9a9f49f100)
Solutions
- Archive the task instead of deleting it - this is the intended path and preserves the time history.
- Delete the timesheet entries first (Timesheets UI / the row set in zp_timesheets WHERE ticket_id = {id}), then retry the delete.
- If the hours are simply wrong, edit/correct them via the timesheet functionality rather than destroying the task.
- As a last resort with a DB backup in hand: DELETE FROM zp_timesheets WHERE ticket_id = {id}, then delete the task.
Example fix
// before: plain delete attempt on a task with logged time
$this->ticketService->canDelete($id); // throws 'Task has timesheets attached...'
$this->ticketService->delete($id);
// after: archive when time is logged, delete only when clean
if (! empty($this->timesheetsRepo->getTimesheetsByTicket($id))) {
$this->ticketService->updateTicket(['id' => $id, 'tags' => 'archived']); // or set an archived-type status
return redirect()->back()->with('message', 'Task archived because timesheets exist');
}
$this->ticketService->canDelete($id);
$this->ticketService->delete($id); Defensive patterns
Strategy: validation
Validate before calling
if (! empty($timesheetsRepo->getTimesheetsByTicket($ticketId))) {
// time is booked: archive instead of delete, preserving reporting history
$ticketService->updateTicket(['id' => $ticketId, 'editorType' => 'task', 'tags' => 'archived']);
return redirect()->back()->with('message', 'Task archived because timesheets are attached');
}
$ticketService->canDelete($ticketId); Type guard
/** True when the ticket can be safely deleted (exists and has no logged hours). */
function isTicketDeletable(\Leantime\Domain\Tickets\Services\Tickets $tickets, mixed $id): bool
{
try {
return $tickets->canDelete($id) === true;
} catch (\Exception) {
return false;
}
} Try / catch
try {
$ticketService->canDelete($id);
$ticketService->delete($id);
} catch (\Exception $e) {
if (str_contains($e->getMessage(), 'timesheets attached')) {
return back()->with('message', 'This task has logged hours - it was archived instead of deleted.');
// archive flow: $ticketService->updateTicket(['id' => $id, ...archived status...]);
}
throw $e;
} Prevention
- Train users to archive (not delete) tasks that carry tracked time.
- Check getTimesheetsByTicket($id) before offering the delete action in custom UIs.
- Correct wrongly logged hours via the timesheet editor instead of deleting the task.
- Before bulk deletes, pre-filter with a query for tickets without zp_timesheets rows.
When it happens
Trigger: Deleting any task with at least one timesheet row via /tickets/delTicket/{id} or an RPC flow that calls canDelete first: cleaning up subtasks that received tracked work, deleting finished tasks at project close, bulk-cleanup scripts that assume empty tasks.
Common situations: End-of-project cleanup where tasks carry historical logged hours; users unaware that archive is the intended path for worked-on tasks; mistakenly logged hours on a task someone then wants gone.
Related errors
AI-assisted analysis of Leantime/leantime@9a9f49f100 (2026-08-21).
Data as JSON: /api/errors/d5c18a7fff553327.
Report an issue: GitHub.