Skip to content

GLPI Report

SLA attainment rollups, scheduled per-entity reports and quarterly business review packs, for GLPI 11.

Aggregates what core only stores per ticket, renders it as a branded document, archives exactly what was shown, and waits for a person to press Send on anything with an argument in it.

Generated monthly service review: branded masthead, draft banner, volume figures, SLA meters, satisfaction, asset census, top categories, and a closing list of what the report omits

One self-contained page, no external request in it, and a footer stating which sections were left out and why.

Statistics (core) Dashboards (core) This plugin
Audience Technicians Technicians The people being reported to
Where it lives A GLPI page A GLPI page A document, an email, a PDF
Branding GLPI’s GLPI’s Yours, or none — never GLPI’s
Aggregates Computed live, per view Computed live, per view Precomputed nightly, stored
SLA attainment Not aggregated Breach counts, current Met/breached per period, per dimension
History Recomputed from tickets, so it changes Same Frozen per period
Scheduling None None Monthly and quarterly, with delivery
Archive None None Every generated report, kept verbatim

The last two rows matter most: a recipient asking what they were told in March should get the document sent in March, not what regenerating March produces today after two tickets were reclassified.

  • Nightly rollups per entity, per month, per dimension: tickets opened, resolved, closed, backlog at period end; response and resolution attainment; mean time to respond and resolve; satisfaction; volume by category, requester and priority; an asset census. Recomputing a month replaces it.
  • An attainment page that reads only rollups. It never scans glpi_tickets, so it opens instantly on an instance with four years of history.
  • Report definitions — name, entity, period (monthly or quarterly), an ordered set of sections, a recipient list.
  • A renderer emitting one self-contained HTML document: inline CSS, inline logo, @page print rules. No external request, so it survives being emailed.
  • An archive. Every generated report is stored verbatim and written to a GLPI Document attached to the entity.
  • Scheduled delivery. Monthly definitions can send themselves if somebody ticked the box. Quarterly reviews never do.
  • sla_attainment read-only tool for glpiai, reading the rollup rather than the tickets, so the figures a model quotes are the ones the entity’s PDF shows.

Attainment page for one entity and month: volume, response and resolution meters, mean times, comparison against the previous month, breakdowns by category, requester and asset type

Every figure comes from the rollup table. Add a ticket inside the window without recomputing and the page does not move — asserted in tests/browser/report-check.js.

Attainment compares two stored dates — solvedate against time_to_resolve — rather than elapsed time against a target duration.

GLPI already excludes time spent waiting on the requester: solve_delay_stat has waiting_duration subtracted from it, and core pushes time_to_resolve forward by sla_waiting_duration every time a ticket leaves a pending status. Doing the subtraction again turns real breaches into passes. The stored deadline also already accounts for the SLA’s calendar and its end_of_working_day setting, which no duration arithmetic reproduces.

The formula matches CommonITILObject::generateSLAOLAComputation(), the expression behind GLPI’s own dashboards, so this plugin and core never disagree in front of the people being reported to.

A ticket with no SLA counts as neither met nor breached, and the report says how many there were. Attainment is null rather than 100% when nothing was measurable, and an unfinished period is not a result — both carried verbatim into the AI tool’s answer. tests/attainment.php pins every case.

Terminal window
# from the GLPI root
git clone https://github.com/bijstaan/glpi-report.git plugins/glpireport
php bin/console plugin:install -u glpi glpireport
php bin/console plugin:activate glpireport

Then, in order:

  1. Setup → Plugins → GLPI Report — check the status card; it will say the rollups have never run.
  2. Tools → Report definitions → the chart icon — rebuild a few months of history. The nightly job only touches the current month and the one before.
  3. Create a definition, generate it, read it, then consider delivery.
Task Frequency What
rollup daily Rebuilds the current and previous month for every entity with tickets or assets
reports hourly Generates definitions whose period has ended, sends the ones marked to send
Right Grants
plugin_glpireport_report The attainment page, definitions, the archive, the Send button
plugin_glpireport_config The settings page, which is where the ERPNext credential lives

Whoever owns the relationship sends the report; they do not hold the billing system’s API secret. Both are granted at install to profiles holding core config UPDATE.

Setup → Plugins → GLPI Report.

Setting Default Notes
Backfill ceiling 12 months A bound, not a preference: a rebuild runs in the request
Members per dimension 15 Categories and requesters kept per period, ranked by volume
Census asset types Computer, Monitor, NetworkEquipment, Printer, Phone, Peripheral Filtered against $CFG_GLPI['asset_types']
Public URL base GLPI’s own Set it when GLPI’s URL is an internal name a recipient cannot resolve
Footer text empty Appended to every report
Let this plugin send email off Master switch. Reports still generate and archive with it off
Wait after a period ends 3 days Tickets close late and surveys come back late
AI executive summaries off Requires glpiai, whose own entity policy still decides
Model tier quality A few hundred words a month that somebody outside reads
ERPNext URL / key / secret empty The only thing that leaves your estate. Secret encrypted with GLPI’s key
ERPNext timeout 8s A slow billing system should cost one section, not the run

Settings page: status card, then one card per group

  • Quarterly (QBR) definitions always generate as drafts. The auto-send checkbox is refused for them in the form and again on save; switching a monthly definition to quarterly disarms it in the same save.
  • Monthly definitions may auto-send, off by default.
  • Sending needs the master switch. With delivery off the Send button refuses and says why.
  • A sent report can never be resent or regenerated.
  • Send is a POST with a confirmation, never a link. A GET that emails a recipient is one a prefetching mail client can fire on somebody’s behalf.

Report definition: name, cadence, sub-entity scope, recipients, the auto-send checkbox with its warning, and the ordered section list

Optional sections carry the reason their dependency is unavailable next to the checkbox, rather than in an archive note a month later.

Mail goes into glpi_queuednotifications, so retries, the per-entity send delay and the sent trail are GLPI’s.

A short covering note — name, period, headline figures and a button — with the report attached and reachable at its own URL. The note is a real GLPI notification template, seeded at install and editable under Setup → Notifications → Templates.

The report is not the message body. Renderer::render() produces a complete <!DOCTYPE html> document with its own stylesheet, and GLPI’s template pipeline wraps whatever it is given in its own <html><head>…<body> — nesting one document inside another, which mail clients strip and Outlook renders blank.

Delivery did not move: a report’s recipients are the contacts on the report, chosen per definition and often per entity. A Notification record’s target list would be a second, contradictory answer, and getting it wrong sends one entity’s figures to another’s mailbox. The seeded template is therefore listed under Notifications with no Notification using it.

Core attaches a document only to a message whose recipient it can resolve to a GLPI user, unless Add documents into ticket notifications is on for anonymous recipients — and a recipient’s operations mailbox is not a GLPI user. The send result says how many recipients will not get the attachment and names the setting that would change it. The note carries the report’s URL either way.

The archived document is linked to the report, not only the entity, which is what makes attaching it possible without sending the recipient every document ever filed against them.

Reports never say GLPI. Three states:

  • glpiwhitelabel configured — its name and logo, read from that plugin. The logo is inlined as a data: URI so it survives print and email.
  • glpiwhitelabel absent or empty — neutral: the entity’s own name, no logo, no product name.
  • Either way the document names GLPI nowhere. tests/render.php asserts it.

Core prefixes every notification subject with the entity’s notification_subject_tag and falls back to the literal string “GLPI” when no entity has set one. The note’s notification target keeps a configured tag and drops the fallback.

Both guarded, both degrading to a section left out plus a note, on the report and in the archive.

glpiai drafts the executive summary from the report’s own figures — a compact JSON of counts, percentages, means and category names. No ticket titles, no content, no requester names. Its per-entity policy still decides whether anything may be sent, and this plugin cannot override it. Any report containing drafted prose is marked as such in the archive, the search list and the document.

ERPNext / glpi_billing supplies contract hours. It reads both the response shape this plugin was specified against and the one the deployed endpoint returns, and never derives a consumption figure it was not given.

Archive list: three generated reports with period, state, generation time and whether any prose was AI-drafted

One archived report: draft badge, frozen recipient list, notes about unavailable sections, action row

Send and Regenerate disappear once a report has been sent.

  • No charts. Bars drawn in CSS, tables for everything else. A chart library is either a JavaScript payload landing in a recipient’s mailbox or a server-side rasteriser this plugin would have to own.
  • PDF is your browser’s print dialogue. The @page rules and break-inside: avoid are the whole mechanism.
  • Tickets only. Changes and problems are not counted.
  • No OLA attainment. Internal targets are not what a recipient is shown.
  • Backfill runs in the request. Rebuilding a year across every entity on a large instance may hit the web server timeout. Do a few months at a time.
  • Asset retirements are not dated by GLPI, so the census shows a net change between two snapshots rather than a removal count, and says so.
  • Rollups are keyed to the exact entity. A parent’s report sums its descendants at read time, and only when the definition says to.
  • The archive keeps the HTML twice — in this plugin’s table and as a GLPI Document. The document is the audit artefact; the stored copy is what preview, print and the email body read.
Terminal window
sh tests/run.sh

The pure suites: the SLA formula against synthetic ticket rows, the renderer against a hand-built dataset, and the ERPNext response normaliser. No database, no session, no configuration. 141 assertions.

Three more need a live instance and run.sh does not run them:

Terminal window
docker exec <container> php plugins/glpireport/tests/covering-note.php
docker exec <container> php plugins/glpireport/tests/rollup-db.php --yes-i-mean-it
docker exec <container> php plugins/glpireport/tests/delivery-db.php --yes-i-mean-it

covering-note.php only reads: it renders the note through GLPI’s template engine and asserts no surviving ##tag##, prose rather than the JSON the notes are stored as, no heading over an empty section, no [GLPI] in the subject, and exactly one <!DOCTYPE — a second would mean the report document got nested back inside the message.

The other two write. Both refuse to start without the flag and clean up everything they made — the report row, the definition, the queued messages and every document archived — in a finally. Nothing in tests/ reads or writes plugin configuration.

Terminal window
cd tests/browser && SHOT_DIR=../../docs/screenshots node report-check.js

Asserts the attainment page shows the rollup’s numbers, a ticket added without a recompute does not move it, section checkboxes survive a save, a posted auto_send=1 is disarmed by the save rather than the disabled attribute, and the rendered document is self-contained, print-ruled and served under default-src 'none'.

GPL-3.0-or-later, the same licence as GLPI. The plugin is loaded into GLPI’s process and extends its classes, so it is a derivative work. See LICENSE.