Corp/backend/finance/README.md

229 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Finance / Payroll
The Finance app is the EUSRR adapter around the project-independent
`payroll_core` package. The dependency points in one direction:
```text
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:
```text
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.
```text
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:
```text
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.