GLPI Project
Delivery management on top of GLPI 11’s own Project object: a schedule with
dependencies and a critical path, allocation and capacity, and a budget that
costs the hours GLPI already records.

There is no PluginGlpiprojectProject. Everything hangs off core’s Project and
ProjectTask by id, so entity scoping, search, saved searches, the Kanban, the
HL API and the ticket/asset links keep working as the rest of GLPI expects.
Dependencies go in core’s own glpi_projecttasklinks, not a copy of it, so this
plugin and Teclib’s gantt plugin read the same graph and uninstalling this one
does not delete a project’s dependencies.
What core has, and does not
Section titled “What core has, and does not”Core models what a project contains — a hierarchical task tree, teams, costs, milestones, states, and links out to tickets, changes and CMDB assets — and is better than any dedicated PM tool at the CMDB half. It models none of the mechanics that turn a task list into a plan:
- No schedule.
glpi_projecttasklinkshas carried a complete predecessor model (FS, SS, FF, SF, with lag and lead) since GLPI 9.5.4, and no core code has ever written a row to it. The only two references insrc/are a purge cleanup and a@used-by gantt plugindocblock. - No allocation.
Project::getTeamRoles()returns exactly[ROLE_OWNER, ROLE_MEMBER]. A team is a list of names with no percentage, rate or availability, and nothing aggregates a person across projects. - No money on the project.
glpi_projectshas no budget column,glpi_projectcostsis a hand-typed ledger, and labour is never costed even though the hours sit ineffective_duration.
| Core GLPI | This plugin | |
|---|---|---|
| Dependencies | A table nothing writes | Drawn on the task, written to that same table |
| Schedule | Independent typed dates | Forward and backward pass, critical path, float |
| Working time | Raw seconds; a 3-day task from Friday ends Sunday | Core Calendar, the same one SLAs use |
| Constraints | — | Start-no-earlier-than, finish-no-later-than |
| Roles | Manager, Member | Six roles, plus a percentage of somebody’s week |
| Capacity | — | Hours a week per person, dated |
| Over-booking | — | A heatmap across every project, per week |
| Budget | Cost rows with nothing to compare them to | A budget, labour costed from recorded hours, forecast at completion |
| Baselines | One plan, overwritten on re-plan | Named snapshots and a variance report |
| Risks and issues | — | A RAID register with owners, scores and review dates |
| Alerts | new, update, delete |
Six conditions a cron goes looking for |
| Reporting | No project widget in the dashboard | A RAG portfolio board and five dashboard cards |
| Discussion | A description and a comment box | A thread on every project and task, with mentions |
| Approvals | A dropdown somebody changed | Stage gates with named approvers and a recorded decision |
| Templates | Field defaults for one task; clone carries absolute dates | Methodologies with relative offsets and role placeholders |
| Numbering | code on a project, nothing on a task |
WBS numbers down the whole tree |
| Custom fields | Asset definitions only | Typed fields on projects and tasks |
| Client visibility | Central interface only | A published, redacted view in the self-service portal |
Schedule
Section titled “Schedule”schedule_autoapply ships off. With it off the chart draws the plan somebody
typed, the plugin computes what the dependencies imply, and the difference is a
list with one button:
The schedule disagrees with these dates Network and circuits — planned 2026-09-07 → computed 2026-09-12
The critical path highlights scheduling conflicts as advisory warnings before you choose to enforce dates.
Allocation and capacity
Section titled “Allocation and capacity”
A person allocated at 60% consumes 60% of their week for as long as the work
runs; utilisation is the sum across every project they are on. That is the
allocation model rather than the effort model: the effort model is more precise
and needs an estimate on every task, and on a real GLPI instance most project
tasks have planned_duration = 0, so it would report that everybody is free.
Allocations are keyed to the core team row, not a (project, user) pair. Adding somebody through core’s own Project team tab creates the row and this plugin fills in behind it; removing them there takes the allocation with it.

Labour is costed from hours that already exist — effective_duration on each
task plus the actiontime of linked tickets — at a rate resolved in order:
- A per-task override
- A rate card for that person, valid on the date
- The house rate
- The plugin default
Steps 2 and 3 go through the glpiproject_rates hook first, so a bridge to
glpi_billing — which already holds tech_cost_rate and uncontracted_rate —
is authoritative over the local cards.
Every rate defaults to 0.00, making every computed labour cost zero and saying so on the panel, rather than inventing a plausible number that gets quoted in a QBR six months later.
Hours are attributed to a task’s team members, split evenly. Neither
effective_duration nor a ticket’s actiontime records who, so an even split
is the only honest arithmetic available, and the panel says so.
Baselines
Section titled “Baselines”
glpi_projects and glpi_projecttasks each hold one planned start and one
planned end. Re-plan and the original is gone, leaving only a field-change row in
the Log tab that nothing aggregates.
A baseline copies the task tree’s dates at a chosen moment. Copies, not references: a task since renamed, re-planned or deleted still appears as it stood. Two distinctions the variance report makes:
- A late start is not a late finish. A task starting three days late and finishing on time absorbed the slip; one starting on time and finishing three days late created it. Both columns are shown.
- Scope change is not slip. Tasks added and removed since the baseline are counted separately.
The RAID register
Section titled “The RAID register”
One itemtype, four kinds, read as one list — “what is outstanding on this
project” does not sort by kind. Risks score probability × impact and band at the
same thirds glpichange uses, so an estate learns one vocabulary.
An issue can become a real Ticket linked through core’s Itil_Project. The entry
stays and records the ticket, so the governance trail survives and the work moves
to where work lives. Overdue reviews lead the tab.
Alerts
Section titled “Alerts”Core offers new, update and delete for Project and ProjectTask. Nothing
fires when a task becomes due, goes overdue, gets blocked, or when a milestone or
project slips past its baseline — each becomes true because time passed, which a
save hook cannot see.
Six events, added to core’s own notification targets so recipient resolution stays core’s job. Each is sent once and not again until the condition clears.
Templates are installed; no Notification is.
The portfolio
Section titled “The portfolio”
grep -c "Project" src/Glpi/Dashboard/Provider.php returns 0.
Each row takes the worst of four independent things: slip against baseline, budget forecast, open high risks, and whether anybody on it is over-booked. A project on time, on budget and staffed by one person at 160% is not green. The reasons travel with the colour.
Five dashboard cards come with it, including a completion trend drawn from daily snapshots the cron records. No back-fill: a plugin installed last week has one week of history and the chart says so.
Discussion
Section titled “Discussion”
Project::defineTabs() registers none of GLPI’s timeline, so a conversation
about a task happens in email or some ticket and is unfindable from the project.
One thread per project and per task, oldest first, with @name mentions parsed
by core’s own UserMention. What is not reused is core’s
handleUserMentions(): it adds mentioned people to the item as observers, and
putting somebody on a project team because their name appeared in a comment is a
side effect nobody asked for.
Comments can be deleted but not edited.
Stage gates
Section titled “Stage gates”
This is not built on CommonITILValidation, and cannot be. That class is
abstract over CommonITILObject: getItilObjectItemInstance() throws a
LogicException for anything else, and it is threaded through timeline
positions, computeValidationStatus(CommonITILObject $itil) and
alertValidation(CommonITILObject $item). Project extends CommonDBTM, so a
subclass fatals on first use. Opening a throwaway Ticket per gate to borrow its
validation would put governance records in the queue technicians work.
Gates are their own model with their own My project approvals page:
- Any rejection rejects the gate. A gate that passed because three of four approvers agreed is one the fourth objection was dropped from.
- A group is one answer, satisfied by anybody in it, with the row recording who answered. Expanding a group into one row per member turns “ask the change board” into “block the project until all eleven log in”.
- Approving takes a baseline — the moment “what we committed to” is worth recording, and the only one anybody reliably remembers.
- A gate informs by default.
gate_block_statemakes it block the state it guards, and ships off.
Methodologies
Section titled “Methodologies”
ProjectTaskTemplate templates one task’s field defaults, and cloning a project
carries absolute dates, so every instantiation is followed by somebody retyping
38 of them.
A methodology stores “starts on day 5, runs 3 days” in working days and names roles rather than people. Dependencies come with it, so the instantiated project has a critical path rather than a flat list with dates on it.
Existing successful projects can also be captured directly into methodologies: after completing a project structure once, you can save it as a reusable methodology template with dependencies, roles, gates, and baselines intact.
Smaller things
Section titled “Smaller things”- WBS numbering — 1, 1.1, 1.2 down the tree, on the Gantt. Computed, never stored: a stored number is wrong the moment somebody re-parents a task.
- Custom fields on projects and tasks, five types, scoped by project type.
GLPI 11’s
CustomFieldDefinitionis asset-definitions only. The inputs render inside core’s own form and post with its ordinary Save. - A Kanban filter that tells projects from tasks. The global board stacks
both in one column because
glpi_projectstatesis a single shared dropdown, and the column sets, WIP limits and swimlanes all live in core’sGlpi\Features\Kanbanwith no seam to reach them, sokind:projectis the honest fix.
The client view
Section titled “The client view”
Project::canView() checks a central right and the self-service portal has no
project view at all.
A project is invisible to clients until somebody publishes it. Not “visible unless hidden”, not “visible when the entity matches” — an entity is just where a project is filed, so an entity-only rule would silently publish every internal project filed against one, and a flag added afterwards cannot un-publish what somebody already read.
Visibility is published AND in the session’s entities. Both, always.
They get progress, a summary written for them, key dates, the next few steps, and comments not marked internal. They never get costs, budgets, forecasts, rates, allocation, utilisation, float, the critical path, baselines, slip, the register, gates, internal comments, or the project’s own description.
That list is an allow-list in a pure class: the payload is built field by field,
so a column added to glpi_projects next year cannot appear on the portal by
default. tests/clientview.php hands it a project row stuffed with every
internal figure the plugin computes and asserts the payload’s keys exactly.

It uses Tabler cards, list groups and progress bars in the same shapes GLPI’s
helpdesk uses, so glpiwhitelabel restyles it for free.
Two constraints:
- There is no client-side plugin right, and there cannot be.
Profile::cleanProfile()unsets every field of a helpdesk profile not onProfile::$helpdesk_rights— a hard-coded core list a plugin cannot join — and the stripped profile is what gets cached into the session at login. Access is decided by publishing and entity. - An unpublished or out-of-entity project is a 404, not a 403. An id that answers differently for two clients is an enumeration oracle.
Rights
Section titled “Rights”| Right | Covers |
|---|---|
plugin_glpiproject_schedule |
Dependencies, constraints, applying a schedule, the workload heatmap |
plugin_glpiproject_finance |
Budgets, rates, the money panel |
plugin_glpiproject_config |
Settings, capacity records, rate cards |
Money is separate because a project’s margin is not something everybody who works the project should see, and rate cards are salary information by another name.
Central interface only.
Not built
Section titled “Not built”- A new Kanban. Core’s board is good; what is wrong with it belongs in
Glpi\Features\Kanban, not a second board. - A timesheet UI. Ticket task time entry exists and rolls up already.
- Letting the client reply. The portal is read-only. “Raise a ticket” is already the answer.
- Kanban WIP limits, swimlanes, and inline editing on core’s task tab. All live in core with no plugin seam.
- Improvement tracking, change scheduling, PDF and mail.
glpiimprove,glpichange,glpipdfandglpimailown those.
docker compose -p glpi exec glpi sh -c 'cd /var/www/glpi/plugins/glpiproject && tests/run.sh'Seven suites needing nothing but PHP: the critical path, earned value,
utilisation, baseline variance, WBS numbering, chart geometry, and what a client
is allowed to see. All live in classes taking plain arrays, so every expected
value can be worked out on paper. --with-db adds a suite that creates a project
on a live instance and removes it again.
A demo project with a branching schedule and somebody genuinely over-booked:
php tests/browser/project-setup.php # createphp tests/browser/project-setup.php --remove # and take it away againInstall
Section titled “Install”# from the GLPI rootgit clone https://github.com/bijstaan/glpi-project.git plugins/glpiprojectphp bin/console plugin:install -u glpi glpiprojectphp bin/console plugin:activate glpiprojectGLPI 11.0 or later. No other plugin is required; glpiai, glpipdf, glpimail
and a glpi_billing bridge are used when present and never depended on.
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.