diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb036dc..ff68c4b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [unreleased]
+### Fixed
+
+- Handle mandatory template fields in history button escalation
+
## [2.9.18] - 2025-30-09
### Fixed
diff --git a/inc/ticket.class.php b/inc/ticket.class.php
index 0aac77e..2c8e96c 100644
--- a/inc/ticket.class.php
+++ b/inc/ticket.class.php
@@ -605,24 +605,72 @@ public static function climb_group($tickets_id, $groups_id, $no_redirect = false
'type' => CommonITILActor::ASSIGN,
];
if (!$group_ticket->find($condition)) {
- $ticket_group = new Group_Ticket();
- PluginEscaladeTask_Manager::setTicketTask([
- 'tickets_id' => $tickets_id,
- 'is_private' => true,
- 'state' => Planning::INFO,
- // Sanitize before merging with $_POST['comment'] which is already sanitized
- 'content' => Sanitizer::sanitize(
- '
' . sprintf(__('Escalation to the group %s.', 'escalade'), Sanitizer::unsanitize($group->getName())) . '
',
- ),
- ]);
+ // Get ticket to retrieve existing data for template validation
$ticket = new Ticket();
- $ticket->update([
+ $ticket->getFromDB($tickets_id);
+
+ // Prepare minimal update with only the fields needed to pass template validation
+ // This ensures mandatory fields from templates are satisfied while only changing the assigned group
+ $update_data = [
'id' => $tickets_id,
'_itil_assign' => [
'groups_id' => $groups_id,
'_type' => 'group',
],
+ // Also use _groups_id_assign to satisfy mandatory field validation
+ '_groups_id_assign' => [$groups_id],
+ ];
+
+ // Add mandatory fields that are commonly required by templates
+ // to prevent "Mandatory fields are not filled" errors
+ $required_fields = ['name', 'content', 'itilcategories_id', 'urgency', 'entities_id', 'type'];
+ foreach ($required_fields as $field) {
+ if (isset($ticket->fields[$field]) && !empty($ticket->fields[$field])) {
+ $update_data[$field] = $ticket->fields[$field];
+ }
+ }
+
+ // Add existing actors to satisfy mandatory actor fields from template
+ $ticket_user = new \Ticket_User();
+ $ticket_group_model = new \Group_Ticket();
+
+ // Get existing requesters
+ $requesters = $ticket_user->find([
+ 'tickets_id' => $tickets_id,
+ 'type' => CommonITILActor::REQUESTER,
]);
+ if (!empty($requesters)) {
+ $update_data['_users_id_requester'] = array_column($requesters, 'users_id');
+ }
+
+ // Get existing observers
+ $observers = $ticket_user->find([
+ 'tickets_id' => $tickets_id,
+ 'type' => CommonITILActor::OBSERVER,
+ ]);
+ if (!empty($observers)) {
+ $update_data['_users_id_observer'] = array_column($observers, 'users_id');
+ }
+
+ // Get existing requester groups
+ $requester_groups = $ticket_group_model->find([
+ 'tickets_id' => $tickets_id,
+ 'type' => CommonITILActor::REQUESTER,
+ ]);
+ if (!empty($requester_groups)) {
+ $update_data['_groups_id_requester'] = array_column($requester_groups, 'groups_id');
+ }
+
+ // Get existing observer groups
+ $observer_groups = $ticket_group_model->find([
+ 'tickets_id' => $tickets_id,
+ 'type' => CommonITILActor::OBSERVER,
+ ]);
+ if (!empty($observer_groups)) {
+ $update_data['_groups_id_observer'] = array_column($observer_groups, 'groups_id');
+ }
+
+ $ticket->update($update_data);
}
if (!$no_redirect) {
diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php
index 7b1e702..6884918 100644
--- a/tests/Units/TicketTest.php
+++ b/tests/Units/TicketTest.php
@@ -1382,4 +1382,147 @@ public function testHistoryButtonEscalationWithMandatoryTemplateFields()
$group1->delete(['id' => $group1_id], true);
$group2->delete(['id' => $group2_id], true);
}
+
+ /**
+ * Test that using the History button escalation works correctly with mandatory "Assigned Group" field
+ */
+ public function testHistoryButtonEscalationWithMandatoryAssignedGroupField()
+ {
+ $this->login();
+
+ // Load Escalade plugin configuration
+ $config = new PluginEscaladeConfig();
+ $conf = $config->find();
+ $conf = reset($conf);
+ $config->getFromDB($conf['id']);
+ $this->assertGreaterThan(0, $conf['id']);
+ PluginEscaladeConfig::loadInSession();
+
+ // Create a ticket template with mandatory "Assigned Group" field (field num 8)
+ $template = $this->createItem(\TicketTemplate::class, [
+ 'name' => 'Test template with mandatory assigned group',
+ 'entities_id' => 0,
+ 'is_recursive' => 1,
+ ]);
+
+ // Add mandatory field for "Groupe de techniciens" (Assigned Group)
+ $mandatory_field = $this->createItem(\TicketTemplateMandatoryField::class, [
+ 'tickettemplates_id' => $template->getID(),
+ 'num' => 8, // _groups_id_assign field number
+ ]);
+
+ // Also add mandatory requester field to match real-world scenarios
+ $mandatory_field2 = $this->createItem(\TicketTemplateMandatoryField::class, [
+ 'tickettemplates_id' => $template->getID(),
+ 'num' => 4, // _users_id_requester field number
+ ]);
+
+ // Create a category linked to this template
+ $category = $this->createItem(\ITILCategory::class, [
+ 'name' => 'Test category with mandatory assigned group',
+ 'tickettemplates_id_incident' => $template->getID(),
+ 'is_incident' => 1,
+ 'entities_id' => 0,
+ 'is_recursive' => 1,
+ ]);
+
+ // Create a requester user
+ $requester = $this->createItem(\User::class, [
+ 'name' => 'requester_assigned_group_test',
+ 'realname' => 'AssignedGroupTest',
+ 'firstname' => 'Requester',
+ ]);
+
+ // Create first escalation group
+ $group1 = $this->createItem(\Group::class, [
+ 'name' => 'First assigned group for escalation',
+ 'entities_id' => 0,
+ 'is_recursive' => 1,
+ 'is_assign' => 1,
+ ]);
+
+ // Create second escalation group
+ $group2 = $this->createItem(\Group::class, [
+ 'name' => 'Second assigned group for escalation',
+ 'entities_id' => 0,
+ 'is_recursive' => 1,
+ 'is_assign' => 1,
+ ]);
+
+ // Create third group for history button test
+ $group3 = $this->createItem(\Group::class, [
+ 'name' => 'Third assigned group for history',
+ 'entities_id' => 0,
+ 'is_recursive' => 1,
+ 'is_assign' => 1,
+ ]);
+
+ // Create a ticket with the template, mandatory requester and mandatory assigned group filled
+ $ticket = $this->createItem(\Ticket::class, [
+ 'name' => 'Test ticket with mandatory assigned group',
+ 'content' => 'Content for testing mandatory assigned group in history button',
+ 'itilcategories_id' => $category->getID(),
+ '_users_id_requester' => [$requester->getID()],
+ '_groups_id_assign' => [$group1->getID()],
+ ]);
+
+ // Verify initial group assignment
+ $group_ticket = new \Group_Ticket();
+ $initial_groups = $group_ticket->find([
+ 'tickets_id' => $ticket->getID(),
+ 'type' => CommonITILActor::ASSIGN,
+ ]);
+ $this->assertEquals(1, count($initial_groups));
+
+ // Escalate to second group
+ $this->createItem(\Group_Ticket::class, [
+ 'tickets_id' => $ticket->getID(),
+ 'groups_id' => $group2->getID(),
+ 'type' => CommonITILActor::ASSIGN,
+ ]);
+
+ // Escalate to third group to create more history
+ $this->createItem(\Group_Ticket::class, [
+ 'tickets_id' => $ticket->getID(),
+ 'groups_id' => $group3->getID(),
+ 'type' => CommonITILActor::ASSIGN,
+ ]);
+
+ // Now test the history button escalation using climb_group
+ // This reproduces issue #381 where mandatory "Groupe de techniciens" field causes an error
+ PluginEscaladeTicket::climb_group($ticket->getID(), $group1->getID(), true);
+
+ // Verify that no error occurred during the climb_group operation
+ // by checking that group1 is now assigned
+ $group1_assigned = $group_ticket->find([
+ 'tickets_id' => $ticket->getID(),
+ 'groups_id' => $group1->getID(),
+ 'type' => CommonITILActor::ASSIGN,
+ ]);
+ $this->assertGreaterThan(0, count($group1_assigned), 'Group 1 should be assigned after climb_group');
+
+ // Test climbing to group2 (another history group)
+ PluginEscaladeTicket::climb_group($ticket->getID(), $group2->getID(), true);
+
+ $group2_assigned = $group_ticket->find([
+ 'tickets_id' => $ticket->getID(),
+ 'groups_id' => $group2->getID(),
+ 'type' => CommonITILActor::ASSIGN,
+ ]);
+ $this->assertGreaterThan(0, count($group2_assigned), 'Group 2 should be assigned after second climb_group');
+
+ // Verify that the requester is still properly assigned
+ $ticket_user = new \Ticket_User();
+ $requesters_after = $ticket_user->find([
+ 'tickets_id' => $ticket->getID(),
+ 'type' => CommonITILActor::REQUESTER,
+ ]);
+ $this->assertEquals(1, count($requesters_after));
+ $requester_after = reset($requesters_after);
+ $this->assertEquals($requester->getID(), $requester_after['users_id']);
+
+ // Verify that the ticket still has the correct category with template
+ $ticket->getFromDB($ticket->getID());
+ $this->assertEquals($category->getID(), $ticket->fields['itilcategories_id']);
+ }
}