diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ff56e3..52794961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fix infinite loop / Gateway Timeout when a ticket's assigned technician and assigned group are changed simultaneously, caused by a synchronous actor removal during `pre_item_update()` - Fixed group reassignment to remove previously assigned groups only when using the Escalade reassignment action +- Fixed `_plugin_escalade_rules_only` being ignored on group assignments, so callers could not opt out of escalade's automatic processing ## [2.10.6] - 2026-07-31 diff --git a/hook.php b/hook.php index 4655b2e2..752848fe 100644 --- a/hook.php +++ b/hook.php @@ -523,6 +523,7 @@ function plugin_escalade_pre_item_add_group_ticket($item) if ( $item instanceof Group_Ticket && $item->input['type'] == CommonITILActor::ASSIGN + && empty($item->input['_plugin_escalade_rules_only']) ) { if (!isset($_SESSION['plugin_escalade']['current_group_assignment'])) { $_SESSION['plugin_escalade']['current_group_assignment'] = []; diff --git a/inc/ticket.class.php b/inc/ticket.class.php index 55bdfbc0..f5ea3187 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -532,6 +532,14 @@ public static function addHistoryOnAddGroup(CommonDBTM $item) public static function processAfterAddGroup(Group_Ticket $item) { + // Explicit opt-out for callers that assign a technician group on their own + // (other plugins, scripts). Same meaning as in pre_item_update(): escalade + // skips its logic entirely, so no group cleanup, no technician unassignment, + // no history entry and no automatic status change for this assignment. + if (!empty($item->input['_plugin_escalade_rules_only'])) { + return; + } + $tickets_id = $item->fields['tickets_id']; $groups_id = $item->fields['groups_id']; diff --git a/tests/Units/GroupEscalationTest.php b/tests/Units/GroupEscalationTest.php index c3eab8b9..3fcdd92a 100644 --- a/tests/Units/GroupEscalationTest.php +++ b/tests/Units/GroupEscalationTest.php @@ -97,6 +97,107 @@ public function testStandardGroupAssignmentKeepsExistingGroups(): void ])); } + /** + * Without the opt-out flag, adding a technician group triggers the escalade + * processing: the previously assigned technician is unassigned. + */ + public function testGroupAssignmentWithoutOptOutRemovesTechnician(): void + { + $this->initConfig([ + 'remove_tech' => 1, + 'show_history' => 1, + ]); + + $tech = getItemByTypeName(User::class, 'tech'); + $group = $this->createGroup('no_opt_out_group_' . uniqid()); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Group assignment without opt-out', + 'content' => '', + '_actors' => [ + 'assign' => [ + [ + 'items_id' => $tech->getID(), + 'itemtype' => 'User', + ], + ], + ], + ]); + + $group_ticket = new Group_Ticket(); + $this->assertNotFalse($group_ticket->add([ + 'tickets_id' => $ticket->getID(), + 'groups_id' => $group->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + $this->assertEquals(0, countElementsInTable(Ticket_User::getTable(), [ + 'tickets_id' => $ticket->getID(), + 'users_id' => $tech->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + $this->assertEquals(1, countElementsInTable('glpi_plugin_escalade_histories', [ + 'tickets_id' => $ticket->getID(), + ])); + } + + /** + * A caller that assigns a technician group on its own can opt out of the + * escalade processing with _plugin_escalade_rules_only: the technician stays + * assigned and no escalation history entry is created. + */ + public function testGroupAssignmentWithOptOutKeepsTechnician(): void + { + $this->initConfig([ + 'remove_tech' => 1, + 'show_history' => 1, + ]); + + $tech = getItemByTypeName(User::class, 'tech'); + $group = $this->createGroup('opt_out_group_' . uniqid()); + + $ticket = $this->createItem(Ticket::class, [ + 'name' => 'Group assignment with opt-out', + 'content' => '', + '_actors' => [ + 'assign' => [ + [ + 'items_id' => $tech->getID(), + 'itemtype' => 'User', + ], + ], + ], + ]); + + $group_ticket = new Group_Ticket(); + $this->assertNotFalse($group_ticket->add([ + 'tickets_id' => $ticket->getID(), + 'groups_id' => $group->getID(), + 'type' => CommonITILActor::ASSIGN, + '_plugin_escalade_rules_only' => true, + ])); + + // The group is still linked to the ticket... + $this->assertEquals(1, countElementsInTable(Group_Ticket::getTable(), [ + 'tickets_id' => $ticket->getID(), + 'groups_id' => $group->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + // ... but escalade did not unassign the technician. + $this->assertEquals(1, countElementsInTable(Ticket_User::getTable(), [ + 'tickets_id' => $ticket->getID(), + 'users_id' => $tech->getID(), + 'type' => CommonITILActor::ASSIGN, + ])); + + // ... and did not record an escalation. + $this->assertEquals(0, countElementsInTable('glpi_plugin_escalade_histories', [ + 'tickets_id' => $ticket->getID(), + ])); + } + /** * A real Escalade reassignment must remove old groups while preserving * every previously assigned group in the visual assignment history.