diff --git a/admin/database_updates/2.5.4.php b/admin/database_updates/2.5.4.php
new file mode 100644
index 00000000..a5e56415
--- /dev/null
+++ b/admin/database_updates/2.5.4.php
@@ -0,0 +1,16 @@
+
+
+
+
-
+
@@ -306,6 +349,35 @@ ob_start();
+
+
+
0) {
- while ($row = mysqli_fetch_assoc($sql_task_templates)) {
- $task_order = intval($row['task_template_order']);
- $task_name = escapeSql($row['task_template_name']);
- $task_completion_estimate = intval($row['task_template_completion_estimate']);
-
- mysqli_query($mysqli,"INSERT INTO tasks SET task_name = '$task_name', task_order = $task_order, task_completion_estimate = $task_completion_estimate, task_ticket_id = $ticket_id");
- }
- }
- }
+ addTasksFromTicketTemplate($ticket_id, $ticket_template_id);
// Add Watchers
if (isset($_POST['watchers'])) {
@@ -1705,10 +1692,6 @@ if (isset($_POST['bulk_add_asset_ticket'])) {
$subject = escapeSql($row['ticket_template_subject']);
}
$details = mysqli_escape_string($mysqli, $row['ticket_template_details']);
-
- // Get Associated Tasks from the ticket template
- $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id");
-
}
// Create ticket for each selected asset
@@ -1770,16 +1753,7 @@ if (isset($_POST['bulk_add_asset_ticket'])) {
}
// Add Tasks from Template if Template was selected
- if($ticket_template_id) {
- if (mysqli_num_rows($sql_task_templates) > 0) {
- while ($row = mysqli_fetch_assoc($sql_task_templates)) {
- $task_order = intval($row['task_template_order']);
- $task_name = escapeSql($row['task_template_name']);
-
- mysqli_query($mysqli,"INSERT INTO tasks SET task_name = '$task_name', task_order = $task_order, task_ticket_id = $ticket_id");
- }
- }
- }
+ addTasksFromTicketTemplate($ticket_id, $ticket_template_id);
// Custom action/notif handler
triggerCustomAction('ticket_create', $ticket_id);
diff --git a/cron/cron.php b/cron/cron.php
index 2dde102c..d7a1c62c 100644
--- a/cron/cron.php
+++ b/cron/cron.php
@@ -317,6 +317,7 @@ if (mysqli_num_rows($sql_recurring_tickets) > 0) {
$contact_id = intval($row['recurring_ticket_contact_id']);
$asset_id = intval($row['recurring_ticket_asset_id']);
$category = intval($row['recurring_ticket_category']);
+ $ticket_template_id = intval($row['recurring_ticket_ticket_template_id']);
$ticket_status = 1; // Default
if ($assigned_id > 0) {
@@ -351,6 +352,9 @@ if (mysqli_num_rows($sql_recurring_tickets) > 0) {
FROM recurring_ticket_assets
WHERE recurring_ticket_id = $recurring_ticket_id");
+ // Copy Tasks from the linked ticket template, if one is set
+ addTasksFromTicketTemplate($id, $ticket_template_id);
+
// Logging
logAudit("Ticket", "Create", "Cron created recurring scheduled $frequency ticket - $subject", $client_id, $id);
diff --git a/db.sql b/db.sql
index f47c68b6..89b7e2af 100644
--- a/db.sql
+++ b/db.sql
@@ -1937,6 +1937,7 @@ CREATE TABLE `recurring_tickets` (
`recurring_ticket_client_id` int(11) NOT NULL DEFAULT 0,
`recurring_ticket_contact_id` int(11) NOT NULL DEFAULT 0,
`recurring_ticket_asset_id` int(11) NOT NULL DEFAULT 0,
+ `recurring_ticket_ticket_template_id` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`recurring_ticket_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
@@ -3066,4 +3067,4 @@ CREATE TABLE `vendors` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
--- Dump completed on 2026-07-29 14:15:02
+-- Dump completed on 2026-07-29 15:54:34
diff --git a/functions/app.php b/functions/app.php
index a3eb99a5..11a6a66a 100644
--- a/functions/app.php
+++ b/functions/app.php
@@ -71,6 +71,48 @@ function getTicketStatusName($ticket_status) {
}
+/**
+ * Copies a ticket template's tasks onto a ticket.
+ *
+ * Called anywhere a ticket is raised from a template - the ticket add modals,
+ * the bulk asset add, and every recurring ticket run (cron, force, bulk force).
+ * Tasks are a snapshot: editing the template later does not touch tickets that
+ * have already been raised from it.
+ *
+ * @param int $ticket_id The ticket to attach the tasks to.
+ * @param int $ticket_template_id The template to copy tasks from. 0 = no-op.
+ *
+ * @return int The number of tasks created.
+ */
+function addTasksFromTicketTemplate($ticket_id, $ticket_template_id) {
+
+ global $mysqli;
+
+ $ticket_id = intval($ticket_id);
+ $ticket_template_id = intval($ticket_template_id);
+
+ if (!$ticket_id || !$ticket_template_id) {
+ return 0;
+ }
+
+ $sql_task_templates = mysqli_query($mysqli, "SELECT * FROM task_templates WHERE task_template_ticket_template_id = $ticket_template_id ORDER BY task_template_order ASC");
+
+ $tasks_added = 0;
+
+ while ($row = mysqli_fetch_assoc($sql_task_templates)) {
+ $task_order = intval($row['task_template_order']);
+ $task_name = escapeSql($row['task_template_name']);
+ $task_completion_estimate = intval($row['task_template_completion_estimate']);
+
+ mysqli_query($mysqli, "INSERT INTO tasks SET task_name = '$task_name', task_order = $task_order, task_completion_estimate = $task_completion_estimate, task_ticket_id = $ticket_id");
+
+ $tasks_added++;
+ }
+
+ return $tasks_added;
+
+}
+
/**
* Retrieves a specified field's value from a table based on the record's id.
* It validates the table and field names, automatically determines the primary key (or uses the first column as fallback),