Private staff cost configuration
The margin model costs recorded work using an employee's salary-only hourly rate or a contractor's hourly estimate. Maintain the inputs in a private JSON file, not in CRM or hardcoded in Python. The report audience must be authorised to see staff costs: hours and labour cost can reveal someone's hourly rate.
The calculation approach explains the formulas and the limits of the payroll comparison. The model reference lists the imported fields and DAX measures.
Maintain the file
Use config/staff-costs.example.json as a fictional format example. Copy it
to a private location outside the image and Git, replace its example records,
and set file permissions to 600 and the containing directory to 700.
Do not use the example values as production salaries.
| Field | Meaning |
|---|---|
schema_version, currency, cost_basis |
Required values: 1, AUD, salary_only. This config version differs from report snapshot version 2. |
employee_id |
Stable private identifier for the person. |
name |
Display name in the restricted report. |
projects_user_ids |
Verified Projects API user IDs as strings, matched to timelog owner_zpuid; not the short ID displayed in the user list. Multiple IDs may refer to one person. |
zoho_zuid, crm_user_id |
Optional identity evidence; not used to resolve the cost rate. |
employment_start, employment_end |
ISO dates, inclusive; null means no boundary is known. |
cost_periods |
Non-overlapping dated rates. effective_from is required; effective_to is inclusive or null for ongoing. |
| Annual salary period | basis: annual_salary, actual annual_salary, and paid_hours_per_fortnight. Part-time salaries are already actual annual amounts. |
| Optional costing assumption | costing_annual_salary with a nonempty costing_reason on a salary period. Changes the service hourly rate only; annual_salary still drives the salary budget and salary attributed to time. |
| Contractor period | basis: hourly_contractor and hourly_rate. No employee salary budget is calculated. |
To change a salary, end the previous period on the preceding date and append a new period with the effective date, salary and contracted hours. Do not replace an old rate unless intentionally correcting history. Retain former employees and close their employment/rate dates. Employees terminated before the report window with no recorded time produce no July onward budget.
Rates must be positive and Projects IDs unique across people. Missing identity or date coverage uses the explicit $80 fallback and is flagged for review. An absent file setting retains the old flat-rate model; an explicitly configured file that is missing, invalid or readable by group/others blocks the report with HTTP 503. Config changes invalidate the snapshot cache on the next request.
Configure the server
Set COMMERCIAL_MARGIN_STAFF_COST_FILE to the absolute private file path for a
non-container process. The backend reads it locally; no salary inputs are
written to CRM or Xero. Do not put it in a public directory or docs site.
For Docker Compose, the optional override mounts it read-only:
export COMMERCIAL_MARGIN_STAFF_COST_FILE=/absolute/private/staff-costs.json
docker compose -f docker-compose.yml -f docker-compose.staff-costs.yml up -d --build app
This is a deployment command to run on the host with the private file. The
container sees /run/secrets/staff-costs.json; the original host file must be
readable by the app process and have mode 600. The mount refuses to create a
missing host path. .local/ is excluded from Git and Docker build context.
Back up the private file in restricted storage separately from the repository.
Deploy the backend before refreshing the schema-2 Power BI project. With
SnapshotPath blank, Power BI uses the server, so changing a local JSON file
alone will not change the report. An authorised reviewer can instead export a
private snapshot and set SnapshotPath to that file on the Windows machine.
Financial snapshots and embedded offline HTML contain staff cost information;
keep them private too. The report exporter writes snapshots with mode 600.
Check a refresh
Open Staff cost reconciliation, select a complete month, and inspect the fallback hours and per-person status. Configured staff without recorded time remain visible. Confirm the monthly salary budget and the division between client, central and unassigned hours. Changes to staff rates change service contribution but must leave Xero profit and the business bridge tie-out unchanged.
Salary budget and nominal paid hours are monthly equivalents, calendar-day prorated for partial periods. This report does not read payslips, leave balances or payroll paid hours. The Xero payroll difference includes employer costs, posting timing and coverage; it is not an employee-by-employee payroll audit.