Cost Centres Developer Guide - edmozley/freeitsm GitHub Wiki
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_idis NOT NULL โ there's no "install-wide" cost centre. Every by-id method starts withload(), 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()andupdate()both refuse a loop.
sync() โ the import and the API share it
CostCentresService::sync($conn, $actor, $tenantId, $rows, ['dry_run' => โฆ, 'deactivate_missing' => โฆ])
- Load the company's current list, keyed by lower-cased code.
- Validate every row first (codes, lengths,
Activevalues, parents that exist in the file or the list, loops), collecting every error with its line. - Any error โ return the errors and write nothing.
- Otherwise plan
create/update/unchanged(anddeactivatefor codes missing from the list, if asked), then apply in one transaction unlessdry_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.phphas hand-written comments. Never round-trip it throughvar_export(api/v1/dev/openapi_fix.phpdoes exactly that on write) โ add schemas by text insertion. The same forapi/v1/spec.json: a JSON re-encode isn't byte-identical.- Excel turns
0010into10in a CSV before FreeITSM sees it, and it can't be detected afterwards. The help steers people to the .xlsx export.
Stage 2 โ charging things to a cost centre (3.5.0)
assets, contracts and users each have cost_centre_id (FK ON DELETE SET NULL as a backstop, added by Database Verification). All the rules are in includes/cost_centre_assign.php, and every path goes through it:
| Function | Use |
|---|---|
costCentreCheck($conn, $tenantId, $value, $currentId) |
The save rule: an active cost centre of the record's company, or the one it already holds (so an inactive one is kept, and a save that doesn't touch it still works). Throws ServiceError. Another company's id reads as unknown. |
costCentreOptions($conn, $tenantId, $keepId) |
The picker list - active ones, plus $keepId when it's the record's current one. Served by api/cost-centres/options.php?company_id=&keep= (any of Assets / Contracts / Tickets, company must be reachable). |
costCentreRecordTenant($conn, $tenantId) |
A record's company for this purpose: NULL โ Default. cost_centres.tenant_id is never NULL, so always compare against the effective id. |
costCentreByCode($conn, $tenantId, $code) |
By code (the column's collation ignores case). The asset import and the contracts API use it โ never ctype_digit โ id: codes like 0010 are digits too. |
costCentreSelectSql($conn, $table, $alias) |
, a.cost_centre_id, (โฆ) AS cost_centre_label for any SELECT โ NULLs before Verification. |
costCentreUsage($conn, $id) |
Counts per kind (assets, contracts, people, project budget lines). CostCentresService::delete() refuses while it's non-empty. |
Where it is wired:
- Assets โ
AssetsService::fieldMap()kindcost_centre, checked inupdateFields()andcreateAsset()(where the company is known โvalidateField()has no context).update_asset_field.phpallows it;get_assets.phpaddscost_centre_id+cost_centre_label(and to the GROUP BY); Assets โ Table columncost_centre_label; the import'sCORE_TARGETS+resolveLookups(); RESTlifecycle.cost_centreand?cost_centre_id=. - Contracts โ
ContractsService::costCentreFor()returns[id, legacy text]:cost_centre_id(checked), orcost_centreas a code (the API's old field), or neither (unchanged โ but moving company with the old company's cost centre is refused). The free-text column stays for text that matched nothing; choosing a cost centre clears it. The first Verification links text that equals a code in the contract's company ($contractsCcColWasMissing). REST keepscost_centrea string (the code, or the old text) and addscost_centre_id/cost_centre_name, so existing integrations don't break. - People โ
api/tickets/save_user.php(the shared person editor,includes/person_editor.php, refills the box when the company changes),get_person.php, the People page (includes/people.php), RESTcost_centre+cost_centre_id(apiUserCostCentre()inapi/v1/resources/users.php).
tests/cost-centre-assign.php covers the rules on every service path with positive controls, in a rolled-back transaction.
Still not built (Daniel's original list): order cost, order date and depreciation on assets; service bookings. Domains keep their free-text cost_centre.
See also: Cost centres ยท REST API: Cost centres ยท Demo Data โ Developer Guide