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 optionalfiltering(e.g. excludeno_changerows). - Which columns appear, in what order — via the ordered list of
PayrollColumnDefinitions. Each column has adata_field(what raw data to pull from the data layer), an optionalaggregation(how rows collapse together), andformatting(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:
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¶
- Open
internal/templates/defaults_registry.py. - Append a
(DefaultScope(...), TemplateDefinition(...))tuple toALL_DEFAULTS. - Run the seed command for the matching scope to insert the row.
Update an existing default¶
- Edit the
TemplateDefinitionindefaults_registry.py(changedisplay_name,columns,granularity, etc.). - Re-run the seed command for the matching scope. The
seed_templateaction 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 canonicalCountryenum. - Scope uniqueness is enforced at the application layer (PAY-1824), not at the DB level. The seed command relies on
seed_templatereading 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.