Skip to content

payroll_tool

Component for the Global Payroll Tool — snapshots, payroll events, payroll changes, and the template-driven renderer.

What payroll templates are

A PayrollTemplate is a saved layout for the payroll-changes export an admin sees in the dashboard (and for the corresponding CSV download). It defines:

  • Which rows show up — via granularity (one row per change, per member, …) and optional filtering (e.g. exclude no_change rows).
  • Which columns appear, in what order — via the ordered list of PayrollColumnDefinitions. Each column has a data_field (what raw data to pull from the data layer), an optional aggregation (how rows collapse together), and formatting (how Layer B renders the value).
  • Where the template applies — via the scope tuple (country, sector, account_id, admin_user_id). More specific scopes win over less specific ones; admins customizing a template fork it at admin-level.

We ship country/sector/account-level defaults so admins start with a sensible layout. Admins can customize their view (PAY-1824 duplicate-on-write); customizations live as separate rows at admin-level and never collide with defaults.

Default templates: how to seed, update, and remove

Default PayrollTemplate rows are managed through two Flask CLI commands. Defaults are scoped (country / sector / account) — never admin-level.

Where defaults are defined

internal/templates/defaults_registry.py exports a single list:

ALL_DEFAULTS: list[tuple[DefaultScope, TemplateDefinition]]

Each entry pairs a DefaultScope (where the default applies) with a TemplateDefinition (what to seed at that scope). admin_user_id is intentionally absent from the scope — defaults are never admin-level.

Seed defaults for a given scope

flask payroll_tool seed_default_templates [--country be] [--sector public] [--account-id <uuid>] [--execute]

Filters ALL_DEFAULTS to entries whose scope equals the input. Dry-run by default — prints what would be inserted vs updated. Pass --execute to actually upsert via seed_template; the command then prints the resulting row details (id, display_name, granularity, column count). Existing rows at the same scope are updated; their column lists are replaced.

Examples:

Command Effect
flask payroll_tool seed_default_templates Dry-run preview of the global default(s) (scope = all-None)
flask payroll_tool seed_default_templates --country be Dry-run preview of the 3 BE country-level defaults
flask payroll_tool seed_default_templates --country be --execute Actually seeds the 3 BE defaults
flask payroll_tool seed_default_templates --country fr "No defaults registered for fr" (none exist yet)

If no defaults match the input, the command prints a clear message and exits 0.

Add a new default

  1. Open internal/templates/defaults_registry.py.
  2. Append a (DefaultScope(...), TemplateDefinition(...)) tuple to ALL_DEFAULTS.
  3. Run the seed command for the matching scope to insert the row.

Update an existing default

  1. Edit the TemplateDefinition in defaults_registry.py (change display_name, columns, granularity, etc.).
  2. Re-run the seed command for the matching scope. The seed_template action upserts by scope tuple — same row, updated fields, columns replaced.

Remove a default

flask payroll_tool remove_default_template --key <key> [--country be] [--sector public] [--account-id <uuid>] [--execute]

Dry-run by default — shows what would be deleted. Add --execute to actually commit.

--key is required. Other flags scope the match. The command never deletes admin-level templates (admin_user_id IS NULL is always enforced).

If no row matches, the command prints "No template found" and exits 0.

Notes

  • All defaults use lowercase country codes (be, fr, …) via the canonical Country enum.
  • Scope uniqueness is enforced at the application layer (PAY-1824), not at the DB level. The seed command relies on seed_template reading the existing row and updating it — no two defaults at the same scope can ever co-exist as long as you go through the command.
  • Re-seeding replaces the column list. Manual edits to seeded templates via Flask admin will be wiped on the next seed run if the registry says otherwise.