feat(overtime): Auszahlungs-Anträge durch Mitarbeiter (opt-in pro Firma)
Security Audit / Node.js Dependency Audit (push) Has been cancelled
Security Audit / Python Dependency Audit (push) Has been cancelled

Mitarbeiter beantragt Überstunden-Auszahlung, HR genehmigt/lehnt ab.
Firmen-Opt-in payout_request_enabled (Default aus - nur HR-Direktbuchung).
Saldo-Abzug erst bei Genehmigung; Reject/Cancel bucht nichts.

- Migration 0042: hours_payouts.status/decided_by/decided_at/rejection_reason
  + companies.payout_request_enabled (nur Spalten, RLS unveraendert)
- Router: POST /hr/payouts/request|{id}/approve|reject|cancel; list status-Filter;
  HR-Direktbuchung bleibt (status approved)
- Frontend: PayoutRequestCard (Selbstbedienung in AbsencesPage), HR-Page
  Status-Spalte + Genehmigen/Ablehnen, CompanySettings-Toggle
- 4 pytest-Cases

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 22:43:26 +02:00
co-authored by Claude Opus 4.8
parent 2c57c36abd
commit 65596e80a4
15 changed files with 1134 additions and 96 deletions
+2
View File
@@ -43,6 +43,7 @@ class CompanyOut(BaseModel):
overtime_expiry_month: int = 3
overtime_expiry_day: int = 31
overtime_max_carryover_hours: int | None = None
payout_request_enabled: bool = False
kiosk_require_approval: bool = True
kiosk_track_current_user: bool = True
kiosk_heartbeat_interval_sec: int = 30
@@ -83,6 +84,7 @@ class CompanyUpdate(BaseModel):
overtime_expiry_month: int | None = Field(None, ge=1, le=12)
overtime_expiry_day: int | None = Field(None, ge=1, le=31)
overtime_max_carryover_hours: int | None = Field(None, ge=0, le=9999)
payout_request_enabled: bool | None = None
kiosk_require_approval: bool | None = None
kiosk_track_current_user: bool | None = None
kiosk_heartbeat_interval_sec: int | None = Field(None, ge=10, le=120)
+17
View File
@@ -12,6 +12,18 @@ class HoursPayoutCreate(BaseModel):
note: str | None = Field(None, max_length=500)
class HoursPayoutRequest(BaseModel):
"""Mitarbeiter beantragt Auszahlung eigener Überstunden (kein user_id immer self)."""
hours: Decimal = Field(gt=0, le=999.99, decimal_places=2)
period_year: int | None = Field(None, ge=2000, le=2100)
period_month: int | None = Field(None, ge=1, le=12)
note: str | None = Field(None, max_length=500)
class HoursPayoutReject(BaseModel):
rejection_reason: str | None = Field(None, max_length=500)
class HoursPayoutOut(BaseModel):
model_config = {"from_attributes": True}
@@ -23,8 +35,13 @@ class HoursPayoutOut(BaseModel):
period_year: int | None
period_month: int | None
note: str | None
status: str = "approved"
rejection_reason: str | None = None
created_by: uuid.UUID
created_by_name: str = "" # Computed im Router
decided_by: uuid.UUID | None = None
decided_by_name: str = "" # Computed im Router
decided_at: datetime | None = None
created_at: datetime