Skip to content

Cost Centres Developer Guide

Ed Mozley edited this page Oct 4, 2026 · 1 revision

Cost centres β€” Developer Guide

Since 3.1.0 Β· #160 stage 1 Β· User guide: Cost centres Β· API: REST API: Cost centres


Files

File Role
includes/services/cost_centres.php CostCentresService β€” every rule, once: create, update, delete, list, sync()
api/system/cost_centres.php The screen's endpoint (list, save, delete, import preview/apply, export)
api/v1/resources/cost_centres.php REST: reads + serialiser here, every write through the service
includes/spreadsheet.php Pure-PHP .xlsx read/write and CSV β€” reusable
system/cost-centres/index.php, system/help/cost-centres.php The screen and its help
cost_centres tenant_id NOT NULL, code, name, description, parent_id, is_active, is_demo
database/demo-data/cost-centres.json 24 demo codes for the Default company
tests/cost-centres.php 38 checks, rolled back

The rules

  • Always one company's. tenant_id is NOT NULL β€” there's no "install-wide" cost centre. Every by-id method starts with load(), which 404s one outside the caller's companies (never 403, so ids can't be probed).
  • The code is text. Trimmed and nothing else β€” never cast, padded or upper-cased. Unique per company ignoring case: the column's collation does that, and so does every comparison in the service.
  • The code is the sync key. sync() matches on lower-cased code, so the same file or API call can be sent every night without duplicating anything.
  • Inactive means "not for new assignments". Whatever is already charged keeps it (stage 2). Delete is refused while a cost centre has children.
  • A parent is in the same company, and is never the cost centre itself or anything below it β€” sync() and update() both refuse a loop.

sync() β€” the import and the API share it

CostCentresService::sync($conn, $actor, $tenantId, $rows, ['dry_run' => …, 'deactivate_missing' => …])

  1. Load the company's current list, keyed by lower-cased code.
  2. Validate every row first (codes, lengths, Active values, parents that exist in the file or the list, loops), collecting every error with its line.
  3. Any error β†’ return the errors and write nothing.
  4. Otherwise plan create / update / unchanged (and deactivate for codes missing from the list, if asked), then apply in one transaction unless dry_run: every row is written first, then the parent links in a second pass, so a child can name a parent that comes later in the file.

The screen's Preview is dry_run; the REST /cost-centres/sync is the same call with JSON rows.

includes/spreadsheet.php

The Docker image has no zip extension, so .xlsx (a zip of XML) is read and written in pure PHP: a minimal zip reader/writer and just enough SpreadsheetML. CSV detection handles commas, semicolons (Excel in much of Europe) and tabs. Codes are written to the .xlsx as text cells, so a round trip through Excel keeps leading zeros; a numeric cell with a 0000 format is read the way it's shown. Reuse this for any future import or export rather than adding a library.

Traps

  • api/v1/lib/openapi_schemas.php has hand-written comments. Never round-trip it through var_export (api/v1/dev/openapi_fix.php does exactly that on write) β€” add schemas by text insertion. The same for api/v1/spec.json: a JSON re-encode isn't byte-identical.
  • Excel turns 0010 into 10 in a CSV before FreeITSM sees it, and it can't be detected afterwards. The help steers people to the .xlsx export.

Stage 2 (not built)

Charging assets and service bookings to a cost centre; order cost, order date and depreciation on assets. Then the picker offers active cost centres of the same company only, existing assignments keep an inactive one, and delete must also be refused while anything references a cost centre. Domains and contracts already have a free-text cost_centre column β€” whether to link those to this table is a decision still to make.


See also: Cost centres Β· REST API: Cost centres Β· Demo Data β€” Developer Guide

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally