Skip to content

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.

Schedule: critical path in red, dependency arrows, and the tasks whose stored dates disagree with it

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.

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_projecttasklinks has 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 in src/ are a purge cleanup and a @used-by gantt plugin docblock.
  • 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_projects has no budget column, glpi_projectcosts is a hand-typed ledger, and labour is never costed even though the hours sit in effective_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_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.

Team workload heatmap, with two people over-booked in the same fortnight

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.

Budget panel: earned value, cost performance, forecast, and the hours behind them

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:

  1. A per-task override
  2. A rate card for that person, valid on the date
  3. The house rate
  4. 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.

Variance report: what the plan said, what it says now, which task moved

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.

Risks, issues, decisions and dependencies in one list, worst first

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.

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.

RAG board: every project, worst first, with the reason on each row

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.

A project thread, with an internal note marked as one

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.

A gate, its approvers, and what each decided

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_state makes it block the state it guards, and ships off.

Instantiating a captured methodology against a start date, mapping roles to people

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.

  • 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 CustomFieldDefinition is 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_projectstates is a single shared dropdown, and the column sets, WIP limits and swimlanes all live in core’s Glpi\Features\Kanban with no seam to reach them, so kind:project is the honest fix.

Client’s view of a project: progress, a summary written for them, key dates, what happens next

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.

The card on the helpdesk home, beside core’s own

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 on Profile::$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.
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.

  • 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, glpipdf and glpimail own those.
Terminal window
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:

Terminal window
php tests/browser/project-setup.php # create
php tests/browser/project-setup.php --remove # and take it away again
Terminal window
# from the GLPI root
git clone https://github.com/bijstaan/glpi-project.git plugins/glpiproject
php bin/console plugin:install -u glpi glpiproject
php bin/console plugin:activate glpiproject

GLPI 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.

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.