Finance / Payroll

The Finance app is the EUSRR adapter around the project-independent payroll_core package. The dependency points in one direction:

Django models, permissions, workflow and API
                    |
                    v
         immutable payroll_core DTOs
                    |
                    v
         deterministic calculation result

The core does not know about Django, users, databases, Excel or EUSRR. The adapter resolves approved inputs, persists snapshots and implements the company workflow.

Implemented slice

  • effective-dated, revisioned employee rates;
  • period roster, point facts and legacy Excel control totals;
  • typed bonuses, corrections, one-time accruals, deductions and advances;
  • maker-checker approval for rates and input records;
  • deterministic period calculation with input, ruleset and result hashes;
  • immutable run revisions and employee statements;
  • review, approval, publication, correction return and republication;
  • native payroll workspace API for periods, rates, work records, input lines and run workflow actions;
  • employee-only statement API and idempotent receipt acknowledgement;
  • payroll audit events and dedicated Django permissions.

The June 2026 example is covered by an integration test:

base 80,000 + bonus 15,000 + correction 20,000 = payable 115,000 RUB
target points 110, actual points 110, point amount 0

This proves the photographed row only. It does not prove the formula for employees above or below the point target.

Spreadsheet column Finance source
Оклад approved EmployeePayRate
Норма баллов / баллы approved PayrollWorkRecord
Отпускные / премия / коррекция / разовая typed PayrollInputLine
Аванс payment input line, reducing payable
Итого / выработка / перерасчёт / к выплате reconciliation checkpoints

Published statements persist a signed carryover balance. Each input column has an independent setting that decides whether its approved manual lines participate. Debit-like lines (negative adjustments, deductions and payments) increase the balance; credit-like lines (earnings and positive adjustments) decrease it. Point-based recalculation never creates a carryover. The next period uses the last published closing balance as its opening balance; input cell values themselves are not copied into the next period. Historical statements from before this feature start with a zero closing balance and are not backfilled.

Checkpoint columns can block review on mismatch but never feed money back into the calculation.

Workflow

During the pilot, PAYROLL_SIMPLE_ADMIN_ACCESS=true is the default. Active staff and superuser accounts receive every payroll operation without extra Finance roles, may edit any draft, and may approve records and runs they created. The legacy self_approval_overridden value remains internal to satisfy existing database constraints; it is not exposed by the payroll API or shown in the normal payroll workspace and Django model forms. With the pilot mode disabled, privileged auditors can still inspect the preserved approval metadata in the immutable audit log.

Set PAYROLL_SIMPLE_ADMIN_ACCESS=false to restore the preserved granular policy: input records and rates are created as drafts and normally approved by another actor; an approver cannot approve their own record or edit another user's draft without the dedicated override policy.

TODO(payroll-access-hardening): disable the temporary full-access mode only after the Finance role matrix and maker-checker workflow have passed a full pilot with representative payroll periods. In the same deployment, remove the temporary staff shortcut from the frontend navigation so it follows the granular Finance permissions again.

The approval screen captures the exact draft version the checker reviewed; any concurrent maker edit increments that version and makes the approval fail closed until the checker reviews it again.

inputs: draft -> approved

run: calculated -> review -> approved -> published
                       |          |
                       +-> returned <-+
                              |
                    calculate new revision

Every revision after the first requires a reason. Publishing a new revision supersedes the previous published run without changing its snapshots. A legacy Excel mismatch blocks submission for review.

Permissions

In the current pilot, active staff and superuser accounts receive all of the following permissions automatically. When simple access is disabled, use the custom Finance permissions rather than ordinary Django model access:

  • finance.manage_payroll_inputs — create and maintain own drafts;
  • finance.approve_payroll_inputs — approve another actor's inputs;
  • finance.calculate_payroll — calculate, submit or withdraw a calculated run;
  • finance.approve_payroll — return, review and approve runs;
  • finance.override_payroll_approval — additionally allow an input or run approver to approve an object they created themselves; this permission grants no workspace access and never replaces either ordinary approval permission;
  • finance.publish_payroll — publish employee statements;
  • finance.view_all_payroll — see payroll data for all employees;
  • finance.audit_payroll — inspect the audit journal.

Ordinary employees need none of these. The self-service API always filters by the authenticated employee and by the currently published run:

GET  /api/v1/finance/payroll/me/statements/
GET  /api/v1/finance/payroll/me/statements/<public_id>/
POST /api/v1/finance/payroll/me/statements/<public_id>/acknowledge/

Payroll operators use the permission-gated native workspace under /api/v1/finance/payroll/admin/. It exposes explicit commands rather than generic model CRUD: draft edits require the version that the operator opened, the granular fallback can require a different actor, and run transitions call the payroll services. The portal entry point is /finances/payroll/manage; Django Admin is not required for the normal payroll workflow.

Responses contain no internal source references and are marked Cache-Control: private, no-store.

Payments outside payroll periods

The management workspace includes a separate actual-payment register. Operators with manage_payroll_inputs can record a payment to an existing (including inactive) employee or a manually named recipient. Viewing requires management access plus manage_payroll_inputs or view_all_payroll.

Each record stores the cash date, amount, currency, reason, author and optional closed source period. For a linked employee and source period, it references the existing statement and checks the remaining payable under row locks. Partial payments reduce this live settlement balance without changing the historical statement, salary inputs, published calculations or carryover formulas. The old whole-statement payment confirmation still means the entire payable was issued. For a manual recipient or an unlinked payment the system records cash only; it cannot infer an outstanding debt from a name.

The request UUID makes retries idempotent. Corrections are performed by voiding with a reason and recording a replacement; both operations are audited. These records never create statements or trigger payroll publication/notifications.

Period tables show cash records separately and add them once to the recorded payment total by cash date. People analytics show them separately from salary; company cash outflow includes them without expensing the same salary again. Deploy migration 0039_payrolloffcyclepayment before enabling the updated UI.

Configuration

Rules live in FINANCE_PAYROLL and can be supplied through the PAYROLL_* environment variables documented in .env.example. Increment PAYROLL_RULESET_VERSION whenever calculation semantics change.

The point policy is disabled by default. excess_only is available for a controlled shadow run and computes max(actual - target, 0) * point_rate, but must not be enabled for publication until the company confirms it on a representative workbook.

The adapter intentionally accepts only a 0.01 money quantum and nonnegative payable amounts because the persisted schema stores two decimal places.

Attendance work import

The EUSRR host adapter can prepare PayrollWorkRecord drafts from stored attendance through the period-scoped preview/apply command. This integration does not live in payroll_core: it translates verified portal attendance into the versioned payroll input contract.

Policy attendance_to_daily_points_v2 keeps attendance hours and payroll points in separate units. Every verified workday contributes the configured daily point target; actual points for that day are daily target × worked hours ÷ expected hours. A complete shift therefore earns the daily target, a partial shift earns its fraction, and overtime may exceed it. Missing days, technical issues, open shifts, unverified remote work and work outside the effective schedule block the affected employee. The apply command never auto-approves: it creates a draft, updates an administrator-accessible draft, or creates a draft revision that leaves the approved record intact until the record is explicitly approved.

Operational boundaries

  • PostgreSQL is required in production. SQLite is suitable only for single-process development/tests because it does not provide the row locks used by the payroll workflow.
  • An acknowledgement means the employee confirmed receipt of the statement; it is not evidence that a bank transfer completed.
  • Model/admin protections are defense in depth for normal application paths. Database administrators and direct QuerySet.update() calls can bypass model validation, so production DB access must remain restricted.
  • Successful workflow changes and detailed employee views are audited. Failed command attempts are not yet written to a separate append-only command log; add that external audit sink before treating this module as the sole compliance record.
  • Overlapping periods are rejected by application validation. A PostgreSQL exclusion constraint should be added before periods can be written by more than one integration.
  • Linked reversal records are intentionally disabled in this slice. Use an explicit debit/credit correction with a reason until reversal semantics are approved and implemented atomically.
  • Taxes, statutory deductions, bank files, 1C export/import and disputes are deliberately outside this first slice.

Rollout

  1. Import rates and several representative spreadsheet periods as drafts.
  2. Reconcile every employee in shadow mode; do not publish mismatched runs.
  3. Confirm point, correction, deduction and rounding rules with accounting.
  4. Freeze and version the approved ruleset.
  5. Grant separated operator, checker, publisher and auditor roles.
  6. Run one parallel payroll cycle before making the portal authoritative.