From 635e4e8593b7658ff17f41fbe12a56cf9b0ddac5 Mon Sep 17 00:00:00 2001 From: baraline Date: Mon, 2 Nov 2026 16:03:51 +0100 Subject: [PATCH 1/2] Add clear_ticket_take_into_account: reset a ticket's take-into-account stamp through the legacy v1 API GLPI stamps the take-into-account date on the first write by an account allowed to take a ticket into account, and the v2 Ticket schema exposes it read-only and ignores _do_not_compute_takeintoaccount. The new method on both clients PUTs the v1 Ticket route with a null date, a zero stored duration, the do-not-compute flag and notifications disabled. It needs the v1 session and raises RuntimeError without one; a refused write raises GlpiProtocolError. Released as 0.6.8. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 17 ++++ docs/user_guide.rst | 19 +++++ glpi_python_client/__init__.py | 2 +- .../_async/clients/api/assistance/_ticket.py | 58 ++++++++++++++ .../api/assistance/tests/test_ticket.py | 80 ++++++++++++++++++- .../_sync/clients/api/assistance/_ticket.py | 58 ++++++++++++++ .../api/assistance/tests/test_ticket.py | 80 ++++++++++++++++++- pyproject.toml | 2 +- skills/glpi-asset-workflow/SKILL.md | 2 +- skills/glpi-client-setup/SKILL.md | 2 +- skills/glpi-contract-workflow/SKILL.md | 2 +- skills/glpi-document-workflow/SKILL.md | 2 +- skills/glpi-knowledge-base/SKILL.md | 2 +- skills/glpi-plugin-fields/SKILL.md | 2 +- skills/glpi-reporting-and-context/SKILL.md | 2 +- skills/glpi-team-members/SKILL.md | 2 +- skills/glpi-ticket-timeline/SKILL.md | 2 +- skills/glpi-ticket-workflow/SKILL.md | 3 +- .../glpi-user-location-provisioning/SKILL.md | 2 +- 19 files changed, 324 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e790452..4d04110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## 0.6.8 — 2026-11-02 + +A minor addition: a way to clear a ticket's take-into-account stamp, through +the legacy v1 API. GLPI stamps the date on the first write by an account +allowed to take a ticket into account, so a connector that writes to a ticket +it has just created leaves a time-to-own of nearly zero. The v2 `Ticket` +schema exposes the date as read-only and ignores `_do_not_compute_takeintoaccount`. +The helper needs `v1_base_url` / `v1_user_token` and raises `RuntimeError` +without them. Existing methods are unchanged. + +### Added + +- **`clear_ticket_take_into_account(ticket_id)`** on both clients. It sets the + ticket's take-into-account date to null and its stored duration to 0, + without recomputing them and without a "ticket updated" notification. A + refused write raises `GlpiProtocolError`. + ## 0.6.7 — 2026-10-30 A minor addition: group recursion and a user's profile/entity authorizations, diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 0f23290..9cd3f96 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -617,6 +617,25 @@ worked. rows = await client.list_user_authorizations(2568) await client.update_user_authorization(rows[0].id, entity_id=493, recursive=True) +Clearing the take-into-account stamp +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +GLPI stamps ``take_into_account_date`` on a ticket the first time an account +allowed to take it into account writes to it, which makes the time-to-own +statistic nearly zero for a ticket a connector has just created. The v2 +``Ticket`` schema exposes the date as read-only and ignores the +``_do_not_compute_takeintoaccount`` flag, so +``clear_ticket_take_into_account(ticket_id)`` goes through the legacy v1 +session (``v1_base_url`` and ``v1_user_token``); without one it raises +``RuntimeError``. It sets the date to null and the stored duration to 0, +without a "ticket updated" notification, and a refused write raises +``GlpiProtocolError``. Read the result back with ``get_ticket`` +(``take_into_account_date`` and ``take_into_account_duration``). + +.. code-block:: python + + await client.clear_ticket_take_into_account(321) + Documents linked to a followup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/glpi_python_client/__init__.py b/glpi_python_client/__init__.py index 13cfcb0..849e8f1 100644 --- a/glpi_python_client/__init__.py +++ b/glpi_python_client/__init__.py @@ -138,7 +138,7 @@ date_window, ) -__version__ = "0.6.7" +__version__ = "0.6.8" __all__ = [ "AsyncGlpiClient", diff --git a/glpi_python_client/_async/clients/api/assistance/_ticket.py b/glpi_python_client/_async/clients/api/assistance/_ticket.py index dcad77a..12f4916 100644 --- a/glpi_python_client/_async/clients/api/assistance/_ticket.py +++ b/glpi_python_client/_async/clients/api/assistance/_ticket.py @@ -9,6 +9,9 @@ from collections.abc import AsyncIterator from glpi_python_client._async.clients.commons._constants import TICKET_ENDPOINT, GlpiId +from glpi_python_client._async.clients.commons._payloads import ( + ensure_v1_write_accepted, +) from glpi_python_client._async.clients.commons._transport import TransportMixin from glpi_python_client.models.api_schema.assistance._ticket import ( DeleteTicket, @@ -17,6 +20,8 @@ PostTicket, ) +_V1_TICKET_FEATURE_LABEL = "take-into-account reset" + class TicketMixin(TransportMixin): """CRUD helpers for ``/Assistance/Ticket``. @@ -240,5 +245,58 @@ async def delete_ticket( delete_model_cls=DeleteTicket, ) + async def clear_ticket_take_into_account(self, ticket_id: GlpiId) -> None: + """Clear the take-into-account stamp of one ticket. + + GLPI stamps ``take_into_account_date`` on the first write made by an + account allowed to take a ticket into account (and when a group is + assigned), which collapses the time-to-own statistic to nearly zero + for a ticket a connector has just created. The v2 ``Ticket`` schema + exposes the date as read-only and silently ignores + ``_do_not_compute_takeintoaccount``, so the reset goes through the + legacy v1 API, which requires ``v1_base_url``/``v1_user_token`` to be + configured (pointing at the legacy ``apirest.php``). + + The update sets the date to ``null``, the stored duration to ``0`` + and asks GLPI not to recompute it. It also suppresses the "ticket + updated" notification, since only internal timer fields change; GLPI + still records the change in the ticket history. + + Parameters + ---------- + ticket_id : int | str + Identifier of the ticket. + + Raises + ------ + RuntimeError + When no legacy v1 session is configured on the client. + GlpiStatusError + When the legacy API returns a non-success status. + GlpiProtocolError + When the legacy API answers that the update was refused. + """ + + v1 = self._require_v1_session(_V1_TICKET_FEATURE_LABEL) + payload = await v1.request_json( + "PUT", + f"Ticket/{ticket_id}", + json_body={ + "input": { + "id": int(ticket_id), + "takeintoaccountdate": None, + "takeintoaccount_delay_stat": 0, + "_do_not_compute_takeintoaccount": True, + "_disablenotif": True, + } + }, + failure_message=( + f"Failed to clear the take-into-account stamp of ticket {ticket_id}" + ), + ) + ensure_v1_write_accepted( + payload, f"Clearing the take-into-account stamp of ticket {ticket_id}" + ) + __all__ = ["TicketMixin"] diff --git a/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py b/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py index 1675254..2ddcf1e 100644 --- a/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py +++ b/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py @@ -12,7 +12,7 @@ import pytest -from glpi_python_client import PatchTicket, PostTicket +from glpi_python_client import GlpiProtocolError, PatchTicket, PostTicket from glpi_python_client._async._testing import ( FailingTransportRecorder, TransportRecorder, @@ -271,3 +271,81 @@ async def fake_search( assert all(call["rsql_filter"] == "status==1" for call in calls) assert all(call["sort"] == "id:desc" for call in calls) assert all(call["fields"] == ("id", "name") for call in calls) + + +# --------------------------------------------------------------------------- +# Take-into-account reset (legacy v1) +# --------------------------------------------------------------------------- + + +class _FakeV1: + """Stand-in for the v1 session that records ``request_json`` calls.""" + + def __init__(self, *responses: object) -> None: + self.responses = list(responses) + self.calls: list[dict[str, Any]] = [] + + async def request_json( + self, + method: str, + path: str, + *, + params: dict[str, object] | None = None, + json_body: dict[str, object] | None = None, + success_statuses: tuple[int, ...] = (200, 201, 204, 206), + failure_message: str | None = None, + ) -> object: + self.calls.append( + { + "method": method, + "path": path, + "params": params, + "json_body": json_body, + "failure_message": failure_message, + } + ) + return self.responses.pop(0) + + async def close(self) -> None: + """No-op; the real session is closed with the client.""" + + +async def test_clear_ticket_take_into_account_puts_the_reset_through_v1( + client: Any, +) -> None: + """The v1 ticket route gets a null date, a zero delay and the opt-outs.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + await client.clear_ticket_take_into_account("7") + call = fake.calls[0] + assert call["method"] == "PUT" + assert call["path"] == "Ticket/7" + assert call["json_body"] == { + "input": { + "id": 7, + "takeintoaccountdate": None, + "takeintoaccount_delay_stat": 0, + "_do_not_compute_takeintoaccount": True, + "_disablenotif": True, + } + } + + +async def test_clear_ticket_take_into_account_raises_on_a_refused_answer( + client: Any, +) -> None: + """A ``false`` answer raises with the server message, never passes.""" + + client._v1 = _FakeV1([{"7": False, "message": "Right missing"}]) + with pytest.raises(GlpiProtocolError, match="Right missing"): + await client.clear_ticket_take_into_account(7) + + +async def test_clear_ticket_take_into_account_requires_the_v1_session( + client: Any, +) -> None: + """Without a v1 session the usual configuration error is raised.""" + + with pytest.raises(RuntimeError, match="take-into-account reset"): + await client.clear_ticket_take_into_account(7) diff --git a/glpi_python_client/_sync/clients/api/assistance/_ticket.py b/glpi_python_client/_sync/clients/api/assistance/_ticket.py index 0b39cc1..c9eea69 100644 --- a/glpi_python_client/_sync/clients/api/assistance/_ticket.py +++ b/glpi_python_client/_sync/clients/api/assistance/_ticket.py @@ -9,6 +9,9 @@ from collections.abc import Iterator from glpi_python_client._sync.clients.commons._constants import TICKET_ENDPOINT, GlpiId +from glpi_python_client._sync.clients.commons._payloads import ( + ensure_v1_write_accepted, +) from glpi_python_client._sync.clients.commons._transport import TransportMixin from glpi_python_client.models.api_schema.assistance._ticket import ( DeleteTicket, @@ -17,6 +20,8 @@ PostTicket, ) +_V1_TICKET_FEATURE_LABEL = "take-into-account reset" + class TicketMixin(TransportMixin): """CRUD helpers for ``/Assistance/Ticket``. @@ -240,5 +245,58 @@ def delete_ticket( delete_model_cls=DeleteTicket, ) + def clear_ticket_take_into_account(self, ticket_id: GlpiId) -> None: + """Clear the take-into-account stamp of one ticket. + + GLPI stamps ``take_into_account_date`` on the first write made by an + account allowed to take a ticket into account (and when a group is + assigned), which collapses the time-to-own statistic to nearly zero + for a ticket a connector has just created. The v2 ``Ticket`` schema + exposes the date as read-only and silently ignores + ``_do_not_compute_takeintoaccount``, so the reset goes through the + legacy v1 API, which requires ``v1_base_url``/``v1_user_token`` to be + configured (pointing at the legacy ``apirest.php``). + + The update sets the date to ``null``, the stored duration to ``0`` + and asks GLPI not to recompute it. It also suppresses the "ticket + updated" notification, since only internal timer fields change; GLPI + still records the change in the ticket history. + + Parameters + ---------- + ticket_id : int | str + Identifier of the ticket. + + Raises + ------ + RuntimeError + When no legacy v1 session is configured on the client. + GlpiStatusError + When the legacy API returns a non-success status. + GlpiProtocolError + When the legacy API answers that the update was refused. + """ + + v1 = self._require_v1_session(_V1_TICKET_FEATURE_LABEL) + payload = v1.request_json( + "PUT", + f"Ticket/{ticket_id}", + json_body={ + "input": { + "id": int(ticket_id), + "takeintoaccountdate": None, + "takeintoaccount_delay_stat": 0, + "_do_not_compute_takeintoaccount": True, + "_disablenotif": True, + } + }, + failure_message=( + f"Failed to clear the take-into-account stamp of ticket {ticket_id}" + ), + ) + ensure_v1_write_accepted( + payload, f"Clearing the take-into-account stamp of ticket {ticket_id}" + ) + __all__ = ["TicketMixin"] diff --git a/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py b/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py index ec25ebf..61ddc68 100644 --- a/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py +++ b/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py @@ -12,7 +12,7 @@ import pytest -from glpi_python_client import PatchTicket, PostTicket +from glpi_python_client import GlpiProtocolError, PatchTicket, PostTicket from glpi_python_client._sync._testing import ( FailingTransportRecorder, TransportRecorder, @@ -271,3 +271,81 @@ def fake_search( assert all(call["rsql_filter"] == "status==1" for call in calls) assert all(call["sort"] == "id:desc" for call in calls) assert all(call["fields"] == ("id", "name") for call in calls) + + +# --------------------------------------------------------------------------- +# Take-into-account reset (legacy v1) +# --------------------------------------------------------------------------- + + +class _FakeV1: + """Stand-in for the v1 session that records ``request_json`` calls.""" + + def __init__(self, *responses: object) -> None: + self.responses = list(responses) + self.calls: list[dict[str, Any]] = [] + + def request_json( + self, + method: str, + path: str, + *, + params: dict[str, object] | None = None, + json_body: dict[str, object] | None = None, + success_statuses: tuple[int, ...] = (200, 201, 204, 206), + failure_message: str | None = None, + ) -> object: + self.calls.append( + { + "method": method, + "path": path, + "params": params, + "json_body": json_body, + "failure_message": failure_message, + } + ) + return self.responses.pop(0) + + def close(self) -> None: + """No-op; the real session is closed with the client.""" + + +def test_clear_ticket_take_into_account_puts_the_reset_through_v1( + client: Any, +) -> None: + """The v1 ticket route gets a null date, a zero delay and the opt-outs.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + client.clear_ticket_take_into_account("7") + call = fake.calls[0] + assert call["method"] == "PUT" + assert call["path"] == "Ticket/7" + assert call["json_body"] == { + "input": { + "id": 7, + "takeintoaccountdate": None, + "takeintoaccount_delay_stat": 0, + "_do_not_compute_takeintoaccount": True, + "_disablenotif": True, + } + } + + +def test_clear_ticket_take_into_account_raises_on_a_refused_answer( + client: Any, +) -> None: + """A ``false`` answer raises with the server message, never passes.""" + + client._v1 = _FakeV1([{"7": False, "message": "Right missing"}]) + with pytest.raises(GlpiProtocolError, match="Right missing"): + client.clear_ticket_take_into_account(7) + + +def test_clear_ticket_take_into_account_requires_the_v1_session( + client: Any, +) -> None: + """Without a v1 session the usual configuration error is raised.""" + + with pytest.raises(RuntimeError, match="take-into-account reset"): + client.clear_ticket_take_into_account(7) diff --git a/pyproject.toml b/pyproject.toml index b5b7749..cc39cc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ exclude = [ [project] name = "glpi-python-client" -version = "0.6.7" +version = "0.6.8" description = "A typed Python client for GLPI ITSM APIs." readme = "README.md" requires-python = ">=3.11" diff --git a/skills/glpi-asset-workflow/SKILL.md b/skills/glpi-asset-workflow/SKILL.md index d0b8e23..f779b00 100644 --- a/skills/glpi-asset-workflow/SKILL.md +++ b/skills/glpi-asset-workflow/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and credentials allowed to read or write assets." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Asset Workflow diff --git a/skills/glpi-client-setup/SKILL.md b/skills/glpi-client-setup/SKILL.md index 90d75c6..d3fb4cc 100644 --- a/skills/glpi-client-setup/SKILL.md +++ b/skills/glpi-client-setup/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to a GLPI v2 API, and valid GLPI credentials." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Client Setup diff --git a/skills/glpi-contract-workflow/SKILL.md b/skills/glpi-contract-workflow/SKILL.md index 63aed62..235dcd3 100644 --- a/skills/glpi-contract-workflow/SKILL.md +++ b/skills/glpi-contract-workflow/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and credentials allowed to read or write contracts." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Contract Workflow diff --git a/skills/glpi-document-workflow/SKILL.md b/skills/glpi-document-workflow/SKILL.md index 3083c56..47584f4 100644 --- a/skills/glpi-document-workflow/SKILL.md +++ b/skills/glpi-document-workflow/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and v1 credentials configured on the client for binary uploads." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Document Workflow diff --git a/skills/glpi-knowledge-base/SKILL.md b/skills/glpi-knowledge-base/SKILL.md index 4daf65d..ebbb0fd 100644 --- a/skills/glpi-knowledge-base/SKILL.md +++ b/skills/glpi-knowledge-base/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and — for category writes only — a legacy v1 session (v1_base_url + v1_user_token)." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Knowledge Base diff --git a/skills/glpi-plugin-fields/SKILL.md b/skills/glpi-plugin-fields/SKILL.md index 04a756a..28a18d5 100644 --- a/skills/glpi-plugin-fields/SKILL.md +++ b/skills/glpi-plugin-fields/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, the GLPI Fields plugin installed server-side, and a legacy v1 session (v1_base_url + v1_user_token) — every method in this family goes over the v1 API." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Plugin Fields diff --git a/skills/glpi-reporting-and-context/SKILL.md b/skills/glpi-reporting-and-context/SKILL.md index fe7d28d..b3bab7c 100644 --- a/skills/glpi-reporting-and-context/SKILL.md +++ b/skills/glpi-reporting-and-context/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and credentials allowed to read tickets, tasks, users, entities, and timeline records." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Reporting And Context diff --git a/skills/glpi-team-members/SKILL.md b/skills/glpi-team-members/SKILL.md index 397c955..ce09af6 100644 --- a/skills/glpi-team-members/SKILL.md +++ b/skills/glpi-team-members/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and credentials allowed to manage ticket teams." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Team Members diff --git a/skills/glpi-ticket-timeline/SKILL.md b/skills/glpi-ticket-timeline/SKILL.md index 28fec8f..a761746 100644 --- a/skills/glpi-ticket-timeline/SKILL.md +++ b/skills/glpi-ticket-timeline/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, and network access to the GLPI v2 API." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Ticket Timeline diff --git a/skills/glpi-ticket-workflow/SKILL.md b/skills/glpi-ticket-workflow/SKILL.md index fb8c588..0896444 100644 --- a/skills/glpi-ticket-workflow/SKILL.md +++ b/skills/glpi-ticket-workflow/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and credentials accepted by GlpiClient." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI Ticket Workflow @@ -21,6 +21,7 @@ Use this skill for ticket reads and writes through the public client. Tickets li 4. For updates, build a `PatchTicket` containing only the fields you intend to change and call `await client.update_ticket(ticket_id, ticket)`. The method returns `None`. 5. For deletes, call `await client.delete_ticket(ticket_id, force=True|False|None)`. `force=True` permanently deletes; `False`/`None` move the record to the trash. 6. Refetch with `get_ticket()` when the task needs a populated model after a write. +7. GLPI stamps `take_into_account_date` on the first write by an account allowed to take a ticket into account, so a ticket you just created and wrote to reads as taken into account at once. v2 cannot clear it (the field is read-only and `_do_not_compute_takeintoaccount` is ignored), so call `await client.clear_ticket_take_into_account(ticket_id)`, which goes through the legacy v1 session: it needs `v1_base_url` / `v1_user_token` and raises `RuntimeError` without them, and a refused write raises `GlpiProtocolError`. ## Examples diff --git a/skills/glpi-user-location-provisioning/SKILL.md b/skills/glpi-user-location-provisioning/SKILL.md index 1657b3d..de7fca5 100644 --- a/skills/glpi-user-location-provisioning/SKILL.md +++ b/skills/glpi-user-location-provisioning/SKILL.md @@ -5,7 +5,7 @@ license: MIT compatibility: "Requires Python 3.11+, glpi-python-client, network access to the GLPI v2 API, and credentials allowed to read or write users, locations, and entities." metadata: package: glpi-python-client - version: "0.6.7" + version: "0.6.8" --- # GLPI User, Location, And Entity Provisioning From 5baec9e6554c3deaa3cc7e90bf2aac25d2b75b72 Mon Sep 17 00:00:00 2001 From: baraline Date: Mon, 2 Nov 2026 17:44:18 +0100 Subject: [PATCH 2/2] Add change_ticket_take_into_account and make clear_ticket_take_into_account a call to it, with do_not_compute and disable_notifications exposed as booleans on both change_ticket_take_into_account writes the v1 takeintoaccountdate (an aware datetime is converted to server_timezone, a naive one is taken as already on it, sent as YYYY-MM-DD HH:MM:SS) and takeintoaccount_delay_stat. The two flags default to True, as the clear sent them before. A False flag is left out of the request, not sent as false: GLPI 11 tests _disablenotif with isset() (Ticket::post_updateItem), so false would still silence the notification. The clear passes a null date and a 0 duration, and its default request body is unchanged. The v1 error label now reads "take-into-account writes". The method-invocation suite synthesizes a datetime for a datetime parameter. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 26 ++-- docs/user_guide.rst | 38 ++++-- .../_async/clients/api/assistance/_ticket.py | 124 ++++++++++++++---- .../api/assistance/tests/test_ticket.py | 85 +++++++++++- .../_sync/clients/api/assistance/_ticket.py | 124 ++++++++++++++---- .../api/assistance/tests/test_ticket.py | 85 +++++++++++- .../testing/tests/test_method_invocation.py | 2 + skills/glpi-ticket-workflow/SKILL.md | 2 +- 8 files changed, 415 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d04110..28612e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## 0.6.8 — 2026-11-02 -A minor addition: a way to clear a ticket's take-into-account stamp, through -the legacy v1 API. GLPI stamps the date on the first write by an account -allowed to take a ticket into account, so a connector that writes to a ticket -it has just created leaves a time-to-own of nearly zero. The v2 `Ticket` +A minor addition: a way to write or clear a ticket's take-into-account stamp, +through the legacy v1 API. GLPI stamps the date on the first write by an +account allowed to take a ticket into account, so a connector that writes to a +ticket it has just created leaves a time-to-own of nearly zero. The v2 `Ticket` schema exposes the date as read-only and ignores `_do_not_compute_takeintoaccount`. -The helper needs `v1_base_url` / `v1_user_token` and raises `RuntimeError` +Both helpers need `v1_base_url` / `v1_user_token` and raise `RuntimeError` without them. Existing methods are unchanged. ### Added -- **`clear_ticket_take_into_account(ticket_id)`** on both clients. It sets the - ticket's take-into-account date to null and its stored duration to 0, - without recomputing them and without a "ticket updated" notification. A - refused write raises `GlpiProtocolError`. +- **`change_ticket_take_into_account(ticket_id, *, take_into_account_date, + take_into_account_duration, do_not_compute=True, disable_notifications=True)`** + on both clients. It writes the ticket's take-into-account date (an aware + value is converted to `server_timezone`) and its duration in seconds. GLPI + writes a null date whenever the duration is 0. `do_not_compute` sends + `_do_not_compute_takeintoaccount` and `disable_notifications` sends + `_disablenotif`; a `False` flag is left out of the request, because GLPI + tests `_disablenotif` for presence. A refused write raises + `GlpiProtocolError`. +- **`clear_ticket_take_into_account(ticket_id, *, do_not_compute=True, + disable_notifications=True)`** on both clients: the change above with a null + date and a 0 duration. ## 0.6.7 — 2026-10-30 diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 9cd3f96..446520b 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -617,24 +617,44 @@ worked. rows = await client.list_user_authorizations(2568) await client.update_user_authorization(rows[0].id, entity_id=493, recursive=True) -Clearing the take-into-account stamp -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Writing the take-into-account stamp +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ GLPI stamps ``take_into_account_date`` on a ticket the first time an account allowed to take it into account writes to it, which makes the time-to-own statistic nearly zero for a ticket a connector has just created. The v2 ``Ticket`` schema exposes the date as read-only and ignores the -``_do_not_compute_takeintoaccount`` flag, so -``clear_ticket_take_into_account(ticket_id)`` goes through the legacy v1 -session (``v1_base_url`` and ``v1_user_token``); without one it raises -``RuntimeError``. It sets the date to null and the stored duration to 0, -without a "ticket updated" notification, and a refused write raises -``GlpiProtocolError``. Read the result back with ``get_ticket`` -(``take_into_account_date`` and ``take_into_account_duration``). +``_do_not_compute_takeintoaccount`` flag, so both helpers below go through the +legacy v1 session (``v1_base_url`` and ``v1_user_token``); without one they +raise ``RuntimeError``, and a refused write raises ``GlpiProtocolError``. + +* ``change_ticket_take_into_account(ticket_id, *, take_into_account_date, + take_into_account_duration, do_not_compute=True, disable_notifications=True)`` + writes the date (an aware value is converted to ``server_timezone``, a + naive one is taken as already on it) and the duration in seconds. GLPI + writes a null date whenever the duration is 0. +* ``clear_ticket_take_into_account(ticket_id, *, do_not_compute=True, + disable_notifications=True)`` is that change with a null date and a 0 + duration. + +``do_not_compute`` sends ``_do_not_compute_takeintoaccount``, which keeps GLPI +from stamping the ticket again in the same write; with it off, a clear made +by an account allowed to take the ticket into account is undone at once. +``disable_notifications`` sends ``_disablenotif``, which suppresses the +"ticket updated" notification. GLPI tests ``_disablenotif`` for presence, so a +``False`` flag is left out of the request rather than sent as ``false``. Read +the result back with ``get_ticket`` (``take_into_account_date`` and +``take_into_account_duration``). .. code-block:: python await client.clear_ticket_take_into_account(321) + await client.change_ticket_take_into_account( + 321, + take_into_account_date=datetime(2026, 11, 2, 9, 30, tzinfo=UTC), + take_into_account_duration=600, + disable_notifications=False, + ) Documents linked to a followup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/glpi_python_client/_async/clients/api/assistance/_ticket.py b/glpi_python_client/_async/clients/api/assistance/_ticket.py index 12f4916..5f961ba 100644 --- a/glpi_python_client/_async/clients/api/assistance/_ticket.py +++ b/glpi_python_client/_async/clients/api/assistance/_ticket.py @@ -7,6 +7,7 @@ from __future__ import annotations from collections.abc import AsyncIterator +from datetime import datetime from glpi_python_client._async.clients.commons._constants import TICKET_ENDPOINT, GlpiId from glpi_python_client._async.clients.commons._payloads import ( @@ -20,7 +21,7 @@ PostTicket, ) -_V1_TICKET_FEATURE_LABEL = "take-into-account reset" +_V1_TICKET_FEATURE_LABEL = "take-into-account writes" class TicketMixin(TransportMixin): @@ -245,27 +246,47 @@ async def delete_ticket( delete_model_cls=DeleteTicket, ) - async def clear_ticket_take_into_account(self, ticket_id: GlpiId) -> None: - """Clear the take-into-account stamp of one ticket. + async def change_ticket_take_into_account( + self, + ticket_id: GlpiId, + *, + take_into_account_date: datetime | None, + take_into_account_duration: int, + do_not_compute: bool = True, + disable_notifications: bool = True, + ) -> None: + """Write the take-into-account stamp of one ticket. GLPI stamps ``take_into_account_date`` on the first write made by an account allowed to take a ticket into account (and when a group is - assigned), which collapses the time-to-own statistic to nearly zero - for a ticket a connector has just created. The v2 ``Ticket`` schema - exposes the date as read-only and silently ignores - ``_do_not_compute_takeintoaccount``, so the reset goes through the - legacy v1 API, which requires ``v1_base_url``/``v1_user_token`` to be - configured (pointing at the legacy ``apirest.php``). - - The update sets the date to ``null``, the stored duration to ``0`` - and asks GLPI not to recompute it. It also suppresses the "ticket - updated" notification, since only internal timer fields change; GLPI - still records the change in the ticket history. + assigned). The v2 ``Ticket`` schema exposes the date as read-only and + silently ignores ``_do_not_compute_takeintoaccount``, so the write + goes through the legacy v1 API, which requires + ``v1_base_url``/``v1_user_token`` to be configured (pointing at the + legacy ``apirest.php``). GLPI records the change in the ticket + history either way. Parameters ---------- ticket_id : int | str Identifier of the ticket. + take_into_account_date : datetime | None + New take-into-account instant (v1 ``takeintoaccountdate``), or + ``None`` for no stamp. An aware value is converted to the + server's clock; a naive one is taken to be on it already. GLPI + writes ``null`` instead whenever the duration is ``0``. + take_into_account_duration : int + Seconds from creation to the take-into-account (v1 + ``takeintoaccount_delay_stat``). GLPI treats ``0`` as "not taken + into account". + do_not_compute : bool, optional + ``True`` (the default) sends ``_do_not_compute_takeintoaccount`` + so GLPI keeps the values written. With ``False`` and a zero + duration, an account allowed to take the ticket into account + stamps it again with the current time in the same write. + disable_notifications : bool, optional + ``True`` (the default) sends ``_disablenotif``, which suppresses + the "ticket updated" notification. Raises ------ @@ -278,24 +299,77 @@ async def clear_ticket_take_into_account(self, ticket_id: GlpiId) -> None: """ v1 = self._require_v1_session(_V1_TICKET_FEATURE_LABEL) + stamp: str | None = None + if take_into_account_date is not None: + if take_into_account_date.tzinfo is not None: + take_into_account_date = take_into_account_date.astimezone( + self.server_timezone + ) + stamp = take_into_account_date.strftime("%Y-%m-%d %H:%M:%S") + update: dict[str, object] = { + "id": int(ticket_id), + "takeintoaccountdate": stamp, + "takeintoaccount_delay_stat": take_into_account_duration, + } + # GLPI reads ``_disablenotif`` with isset(), so a False flag must be absent. + if do_not_compute: + update["_do_not_compute_takeintoaccount"] = True + if disable_notifications: + update["_disablenotif"] = True payload = await v1.request_json( "PUT", f"Ticket/{ticket_id}", - json_body={ - "input": { - "id": int(ticket_id), - "takeintoaccountdate": None, - "takeintoaccount_delay_stat": 0, - "_do_not_compute_takeintoaccount": True, - "_disablenotif": True, - } - }, + json_body={"input": update}, failure_message=( - f"Failed to clear the take-into-account stamp of ticket {ticket_id}" + f"Failed to write the take-into-account stamp of ticket {ticket_id}" ), ) ensure_v1_write_accepted( - payload, f"Clearing the take-into-account stamp of ticket {ticket_id}" + payload, f"Writing the take-into-account stamp of ticket {ticket_id}" + ) + + async def clear_ticket_take_into_account( + self, + ticket_id: GlpiId, + *, + do_not_compute: bool = True, + disable_notifications: bool = True, + ) -> None: + """Clear the take-into-account stamp of one ticket. + + The stamp collapses the time-to-own statistic to nearly zero for a + ticket a connector has just created and written to. This is + :meth:`change_ticket_take_into_account` with a ``None`` date and a + ``0`` duration, so it has the same v1 requirement. + + Parameters + ---------- + ticket_id : int | str + Identifier of the ticket. + do_not_compute : bool, optional + ``True`` (the default) keeps GLPI from stamping the ticket again + in the same write. ``False`` lets an account allowed to take the + ticket into account undo the clear at once. + disable_notifications : bool, optional + ``True`` (the default) suppresses the "ticket updated" + notification. + + Raises + ------ + RuntimeError + When no legacy v1 session is configured on the client. + GlpiStatusError + When the legacy API returns a non-success status. + GlpiProtocolError + When the legacy API answers that the update was refused. + """ + + await self.change_ticket_take_into_account( + ticket_id, + take_into_account_date=None, + take_into_account_duration=0, + do_not_compute=do_not_compute, + disable_notifications=disable_notifications, ) diff --git a/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py b/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py index 2ddcf1e..26e8f22 100644 --- a/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py +++ b/glpi_python_client/_async/clients/api/assistance/tests/test_ticket.py @@ -8,6 +8,7 @@ from __future__ import annotations from collections.abc import Callable +from datetime import UTC, datetime from typing import Any import pytest @@ -310,6 +311,88 @@ async def close(self) -> None: """No-op; the real session is closed with the client.""" +@pytest.mark.parametrize( + ("when", "written"), + [ + (datetime(2026, 11, 2, 9, 30, tzinfo=UTC), "2026-11-02 10:30:00"), + (datetime(2026, 11, 2, 9, 30), "2026-11-02 09:30:00"), + ], + ids=["aware-converted-to-server-clock", "naive-kept-as-server-clock"], +) +async def test_change_ticket_take_into_account_writes_the_date_and_duration( + client: Any, when: datetime, written: str +) -> None: + """The date goes out on the server's clock, beside the duration and flags.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + await client.change_ticket_take_into_account( + 7, take_into_account_date=when, take_into_account_duration=600 + ) + call = fake.calls[0] + assert call["method"] == "PUT" + assert call["path"] == "Ticket/7" + assert call["json_body"] == { + "input": { + "id": 7, + "takeintoaccountdate": written, + "takeintoaccount_delay_stat": 600, + "_do_not_compute_takeintoaccount": True, + "_disablenotif": True, + } + } + + +async def test_change_ticket_take_into_account_omits_a_flag_set_to_false( + client: Any, +) -> None: + """A ``False`` flag is left out: GLPI disables notifications on ``isset``.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + await client.change_ticket_take_into_account( + 7, + take_into_account_date=None, + take_into_account_duration=0, + do_not_compute=False, + disable_notifications=False, + ) + assert fake.calls[0]["json_body"] == { + "input": { + "id": 7, + "takeintoaccountdate": None, + "takeintoaccount_delay_stat": 0, + } + } + + +async def test_change_ticket_take_into_account_raises_on_a_refused_answer( + client: Any, +) -> None: + """A ``false`` answer raises with the server message, never passes.""" + + client._v1 = _FakeV1([{"7": False, "message": "Right missing"}]) + with pytest.raises(GlpiProtocolError, match="Right missing"): + await client.change_ticket_take_into_account( + 7, take_into_account_date=None, take_into_account_duration=0 + ) + + +async def test_clear_ticket_take_into_account_forwards_the_flags( + client: Any, +) -> None: + """The clear passes both flags through to the change it delegates to.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + await client.clear_ticket_take_into_account( + 7, do_not_compute=False, disable_notifications=False + ) + assert fake.calls[0]["json_body"] == { + "input": {"id": 7, "takeintoaccountdate": None, "takeintoaccount_delay_stat": 0} + } + + async def test_clear_ticket_take_into_account_puts_the_reset_through_v1( client: Any, ) -> None: @@ -347,5 +430,5 @@ async def test_clear_ticket_take_into_account_requires_the_v1_session( ) -> None: """Without a v1 session the usual configuration error is raised.""" - with pytest.raises(RuntimeError, match="take-into-account reset"): + with pytest.raises(RuntimeError, match="take-into-account writes"): await client.clear_ticket_take_into_account(7) diff --git a/glpi_python_client/_sync/clients/api/assistance/_ticket.py b/glpi_python_client/_sync/clients/api/assistance/_ticket.py index c9eea69..9816c64 100644 --- a/glpi_python_client/_sync/clients/api/assistance/_ticket.py +++ b/glpi_python_client/_sync/clients/api/assistance/_ticket.py @@ -7,6 +7,7 @@ from __future__ import annotations from collections.abc import Iterator +from datetime import datetime from glpi_python_client._sync.clients.commons._constants import TICKET_ENDPOINT, GlpiId from glpi_python_client._sync.clients.commons._payloads import ( @@ -20,7 +21,7 @@ PostTicket, ) -_V1_TICKET_FEATURE_LABEL = "take-into-account reset" +_V1_TICKET_FEATURE_LABEL = "take-into-account writes" class TicketMixin(TransportMixin): @@ -245,27 +246,47 @@ def delete_ticket( delete_model_cls=DeleteTicket, ) - def clear_ticket_take_into_account(self, ticket_id: GlpiId) -> None: - """Clear the take-into-account stamp of one ticket. + def change_ticket_take_into_account( + self, + ticket_id: GlpiId, + *, + take_into_account_date: datetime | None, + take_into_account_duration: int, + do_not_compute: bool = True, + disable_notifications: bool = True, + ) -> None: + """Write the take-into-account stamp of one ticket. GLPI stamps ``take_into_account_date`` on the first write made by an account allowed to take a ticket into account (and when a group is - assigned), which collapses the time-to-own statistic to nearly zero - for a ticket a connector has just created. The v2 ``Ticket`` schema - exposes the date as read-only and silently ignores - ``_do_not_compute_takeintoaccount``, so the reset goes through the - legacy v1 API, which requires ``v1_base_url``/``v1_user_token`` to be - configured (pointing at the legacy ``apirest.php``). - - The update sets the date to ``null``, the stored duration to ``0`` - and asks GLPI not to recompute it. It also suppresses the "ticket - updated" notification, since only internal timer fields change; GLPI - still records the change in the ticket history. + assigned). The v2 ``Ticket`` schema exposes the date as read-only and + silently ignores ``_do_not_compute_takeintoaccount``, so the write + goes through the legacy v1 API, which requires + ``v1_base_url``/``v1_user_token`` to be configured (pointing at the + legacy ``apirest.php``). GLPI records the change in the ticket + history either way. Parameters ---------- ticket_id : int | str Identifier of the ticket. + take_into_account_date : datetime | None + New take-into-account instant (v1 ``takeintoaccountdate``), or + ``None`` for no stamp. An aware value is converted to the + server's clock; a naive one is taken to be on it already. GLPI + writes ``null`` instead whenever the duration is ``0``. + take_into_account_duration : int + Seconds from creation to the take-into-account (v1 + ``takeintoaccount_delay_stat``). GLPI treats ``0`` as "not taken + into account". + do_not_compute : bool, optional + ``True`` (the default) sends ``_do_not_compute_takeintoaccount`` + so GLPI keeps the values written. With ``False`` and a zero + duration, an account allowed to take the ticket into account + stamps it again with the current time in the same write. + disable_notifications : bool, optional + ``True`` (the default) sends ``_disablenotif``, which suppresses + the "ticket updated" notification. Raises ------ @@ -278,24 +299,77 @@ def clear_ticket_take_into_account(self, ticket_id: GlpiId) -> None: """ v1 = self._require_v1_session(_V1_TICKET_FEATURE_LABEL) + stamp: str | None = None + if take_into_account_date is not None: + if take_into_account_date.tzinfo is not None: + take_into_account_date = take_into_account_date.astimezone( + self.server_timezone + ) + stamp = take_into_account_date.strftime("%Y-%m-%d %H:%M:%S") + update: dict[str, object] = { + "id": int(ticket_id), + "takeintoaccountdate": stamp, + "takeintoaccount_delay_stat": take_into_account_duration, + } + # GLPI reads ``_disablenotif`` with isset(), so a False flag must be absent. + if do_not_compute: + update["_do_not_compute_takeintoaccount"] = True + if disable_notifications: + update["_disablenotif"] = True payload = v1.request_json( "PUT", f"Ticket/{ticket_id}", - json_body={ - "input": { - "id": int(ticket_id), - "takeintoaccountdate": None, - "takeintoaccount_delay_stat": 0, - "_do_not_compute_takeintoaccount": True, - "_disablenotif": True, - } - }, + json_body={"input": update}, failure_message=( - f"Failed to clear the take-into-account stamp of ticket {ticket_id}" + f"Failed to write the take-into-account stamp of ticket {ticket_id}" ), ) ensure_v1_write_accepted( - payload, f"Clearing the take-into-account stamp of ticket {ticket_id}" + payload, f"Writing the take-into-account stamp of ticket {ticket_id}" + ) + + def clear_ticket_take_into_account( + self, + ticket_id: GlpiId, + *, + do_not_compute: bool = True, + disable_notifications: bool = True, + ) -> None: + """Clear the take-into-account stamp of one ticket. + + The stamp collapses the time-to-own statistic to nearly zero for a + ticket a connector has just created and written to. This is + :meth:`change_ticket_take_into_account` with a ``None`` date and a + ``0`` duration, so it has the same v1 requirement. + + Parameters + ---------- + ticket_id : int | str + Identifier of the ticket. + do_not_compute : bool, optional + ``True`` (the default) keeps GLPI from stamping the ticket again + in the same write. ``False`` lets an account allowed to take the + ticket into account undo the clear at once. + disable_notifications : bool, optional + ``True`` (the default) suppresses the "ticket updated" + notification. + + Raises + ------ + RuntimeError + When no legacy v1 session is configured on the client. + GlpiStatusError + When the legacy API returns a non-success status. + GlpiProtocolError + When the legacy API answers that the update was refused. + """ + + self.change_ticket_take_into_account( + ticket_id, + take_into_account_date=None, + take_into_account_duration=0, + do_not_compute=do_not_compute, + disable_notifications=disable_notifications, ) diff --git a/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py b/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py index 61ddc68..8e6ac67 100644 --- a/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py +++ b/glpi_python_client/_sync/clients/api/assistance/tests/test_ticket.py @@ -8,6 +8,7 @@ from __future__ import annotations from collections.abc import Callable +from datetime import UTC, datetime from typing import Any import pytest @@ -310,6 +311,88 @@ def close(self) -> None: """No-op; the real session is closed with the client.""" +@pytest.mark.parametrize( + ("when", "written"), + [ + (datetime(2026, 11, 2, 9, 30, tzinfo=UTC), "2026-11-02 10:30:00"), + (datetime(2026, 11, 2, 9, 30), "2026-11-02 09:30:00"), + ], + ids=["aware-converted-to-server-clock", "naive-kept-as-server-clock"], +) +def test_change_ticket_take_into_account_writes_the_date_and_duration( + client: Any, when: datetime, written: str +) -> None: + """The date goes out on the server's clock, beside the duration and flags.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + client.change_ticket_take_into_account( + 7, take_into_account_date=when, take_into_account_duration=600 + ) + call = fake.calls[0] + assert call["method"] == "PUT" + assert call["path"] == "Ticket/7" + assert call["json_body"] == { + "input": { + "id": 7, + "takeintoaccountdate": written, + "takeintoaccount_delay_stat": 600, + "_do_not_compute_takeintoaccount": True, + "_disablenotif": True, + } + } + + +def test_change_ticket_take_into_account_omits_a_flag_set_to_false( + client: Any, +) -> None: + """A ``False`` flag is left out: GLPI disables notifications on ``isset``.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + client.change_ticket_take_into_account( + 7, + take_into_account_date=None, + take_into_account_duration=0, + do_not_compute=False, + disable_notifications=False, + ) + assert fake.calls[0]["json_body"] == { + "input": { + "id": 7, + "takeintoaccountdate": None, + "takeintoaccount_delay_stat": 0, + } + } + + +def test_change_ticket_take_into_account_raises_on_a_refused_answer( + client: Any, +) -> None: + """A ``false`` answer raises with the server message, never passes.""" + + client._v1 = _FakeV1([{"7": False, "message": "Right missing"}]) + with pytest.raises(GlpiProtocolError, match="Right missing"): + client.change_ticket_take_into_account( + 7, take_into_account_date=None, take_into_account_duration=0 + ) + + +def test_clear_ticket_take_into_account_forwards_the_flags( + client: Any, +) -> None: + """The clear passes both flags through to the change it delegates to.""" + + fake = _FakeV1([{"7": True, "message": ""}]) + client._v1 = fake + client.clear_ticket_take_into_account( + 7, do_not_compute=False, disable_notifications=False + ) + assert fake.calls[0]["json_body"] == { + "input": {"id": 7, "takeintoaccountdate": None, "takeintoaccount_delay_stat": 0} + } + + def test_clear_ticket_take_into_account_puts_the_reset_through_v1( client: Any, ) -> None: @@ -347,5 +430,5 @@ def test_clear_ticket_take_into_account_requires_the_v1_session( ) -> None: """Without a v1 session the usual configuration error is raised.""" - with pytest.raises(RuntimeError, match="take-into-account reset"): + with pytest.raises(RuntimeError, match="take-into-account writes"): client.clear_ticket_take_into_account(7) diff --git a/glpi_python_client/testing/tests/test_method_invocation.py b/glpi_python_client/testing/tests/test_method_invocation.py index eca6749..e67d4f1 100644 --- a/glpi_python_client/testing/tests/test_method_invocation.py +++ b/glpi_python_client/testing/tests/test_method_invocation.py @@ -245,6 +245,8 @@ def _argument_for(name: str, annotation: Any) -> Any: return [1] if "int" in text else ["stub"] if "dict" in text or "Mapping" in text: return {"stub": "stub"} + if "datetime" in text: + return datetime(2026, 1, 1, tzinfo=UTC) if "bool" in text: return False if "str" in text: diff --git a/skills/glpi-ticket-workflow/SKILL.md b/skills/glpi-ticket-workflow/SKILL.md index 0896444..29d240e 100644 --- a/skills/glpi-ticket-workflow/SKILL.md +++ b/skills/glpi-ticket-workflow/SKILL.md @@ -21,7 +21,7 @@ Use this skill for ticket reads and writes through the public client. Tickets li 4. For updates, build a `PatchTicket` containing only the fields you intend to change and call `await client.update_ticket(ticket_id, ticket)`. The method returns `None`. 5. For deletes, call `await client.delete_ticket(ticket_id, force=True|False|None)`. `force=True` permanently deletes; `False`/`None` move the record to the trash. 6. Refetch with `get_ticket()` when the task needs a populated model after a write. -7. GLPI stamps `take_into_account_date` on the first write by an account allowed to take a ticket into account, so a ticket you just created and wrote to reads as taken into account at once. v2 cannot clear it (the field is read-only and `_do_not_compute_takeintoaccount` is ignored), so call `await client.clear_ticket_take_into_account(ticket_id)`, which goes through the legacy v1 session: it needs `v1_base_url` / `v1_user_token` and raises `RuntimeError` without them, and a refused write raises `GlpiProtocolError`. +7. GLPI stamps `take_into_account_date` on the first write by an account allowed to take a ticket into account, so a ticket you just created and wrote to reads as taken into account at once. v2 cannot clear it (the field is read-only and `_do_not_compute_takeintoaccount` is ignored), so call `await client.clear_ticket_take_into_account(ticket_id)`, or `await client.change_ticket_take_into_account(ticket_id, take_into_account_date=..., take_into_account_duration=...)` to write a stamp (an aware date is converted to `server_timezone`; a 0 duration makes GLPI write a null date). Both go through the legacy v1 session: they need `v1_base_url` / `v1_user_token` and raise `RuntimeError` without them, and a refused write raises `GlpiProtocolError`. Both take `do_not_compute=True` (sends `_do_not_compute_takeintoaccount`, so GLPI does not stamp the ticket again in the same write) and `disable_notifications=True` (sends `_disablenotif`, no "ticket updated" notification); a `False` flag is left out of the request, because GLPI tests `_disablenotif` for presence. ## Examples