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.

One self-contained page, no external request in it, and a footer stating which sections were left out and why.
Compared to core
Section titled “Compared to core”| 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.
Features
Section titled “Features”- 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,
@pageprint rules. No external request, so it survives being emailed. - An archive. Every generated report is stored verbatim and written to a GLPI
Documentattached to the entity. - Scheduled delivery. Monthly definitions can send themselves if somebody ticked the box. Quarterly reviews never do.
sla_attainmentread-only tool forglpiai, reading the rollup rather than the tickets, so the figures a model quotes are the ones the entity’s PDF shows.

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.
How SLA attainment is computed
Section titled “How SLA attainment is computed”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.
Install
Section titled “Install”# from the GLPI rootgit clone https://github.com/bijstaan/glpi-report.git plugins/glpireportphp bin/console plugin:install -u glpi glpireportphp bin/console plugin:activate glpireportThen, in order:
- Setup → Plugins → GLPI Report — check the status card; it will say the rollups have never run.
- Tools → Report definitions → the chart icon — rebuild a few months of history. The nightly job only touches the current month and the one before.
- 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 |
Rights
Section titled “Rights”| 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.
Settings
Section titled “Settings”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 |

Sending
Section titled “Sending”- 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.

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.
What the recipient receives
Section titled “What the recipient receives”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.
Branding
Section titled “Branding”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.phpasserts 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.
Optional integrations
Section titled “Optional integrations”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.
The archive
Section titled “The archive”

Send and Regenerate disappear once a report has been sent.
Limitations
Section titled “Limitations”- 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
@pagerules andbreak-inside: avoidare 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.
sh tests/run.shThe 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:
docker exec <container> php plugins/glpireport/tests/covering-note.phpdocker exec <container> php plugins/glpireport/tests/rollup-db.php --yes-i-mean-itdocker exec <container> php plugins/glpireport/tests/delivery-db.php --yes-i-mean-itcovering-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.
cd tests/browser && SHOT_DIR=../../docs/screenshots node report-check.jsAsserts 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'.
Licence
Section titled “Licence”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.