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>
116 lines
4.0 KiB
Python
116 lines
4.0 KiB
Python
import uuid
|
||
from datetime import datetime
|
||
from typing import Literal
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
|
||
PersonnelNumberModeT = Literal["manual", "auto"]
|
||
|
||
|
||
class CompanySettingsUpdate(BaseModel):
|
||
"""Validiertes Sub-Schema für das company.settings JSONB-Feld.
|
||
|
||
Nur bekannte Top-Level-Keys sind erlaubt (extra="forbid").
|
||
Derzeit genutzte Keys: carryover_expires_month, carryover_expires_day
|
||
(gelesen in absences.py für Resturlaub-Verfall-Berechnung).
|
||
"""
|
||
model_config = ConfigDict(extra="forbid")
|
||
|
||
carryover_expires_month: int | None = Field(None, ge=1, le=12)
|
||
carryover_expires_day: int | None = Field(None, ge=1, le=31)
|
||
|
||
|
||
class CompanyOut(BaseModel):
|
||
model_config = {"from_attributes": True}
|
||
|
||
id: uuid.UUID
|
||
name: str
|
||
slug: str
|
||
plan: str
|
||
logo_url: str | None
|
||
country: str
|
||
state: str | None
|
||
settings: dict
|
||
personnel_number_required: bool = False
|
||
personnel_number_mode: PersonnelNumberModeT = "manual"
|
||
personnel_number_next: int = 1
|
||
mobile_stamping_enabled: bool = True
|
||
overtime_overdraft_allowed: bool = True
|
||
overtime_warning_threshold_hours: int = 0
|
||
overtime_cap_hours: int | None = None
|
||
overtime_expiry_enabled: bool = False
|
||
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
|
||
public_stamp_enabled: bool = False
|
||
vacation_default_days: int = 30
|
||
vacation_prorate_first_year: bool = True
|
||
vacation_from_schedule: bool = False
|
||
two_stage_approval_enabled: bool = False
|
||
two_stage_min_days: int = 0
|
||
|
||
|
||
class PublicStampTokenStatus(BaseModel):
|
||
"""Status des öffentlichen QR-Stempel-Tokens (kein Klartext)."""
|
||
enabled: bool
|
||
configured: bool
|
||
created_at: datetime | None = None
|
||
|
||
|
||
class PublicStampTokenRotated(BaseModel):
|
||
"""Antwort beim Rotieren – enthält Klartext-Token + fertige URL (nur einmalig)."""
|
||
token: str = Field(..., description="Klartext-Token, wird nur einmal angezeigt.")
|
||
public_url: str
|
||
created_at: datetime
|
||
|
||
|
||
class CompanyUpdate(BaseModel):
|
||
name: str | None = Field(None, min_length=2, max_length=255)
|
||
state: str | None = Field(None, max_length=10)
|
||
settings: CompanySettingsUpdate | None = None
|
||
personnel_number_required: bool | None = None
|
||
personnel_number_mode: PersonnelNumberModeT | None = None
|
||
personnel_number_next: int | None = Field(None, ge=1)
|
||
mobile_stamping_enabled: bool | None = None
|
||
overtime_overdraft_allowed: bool | None = None
|
||
overtime_warning_threshold_hours: int | None = Field(None, ge=0)
|
||
overtime_cap_hours: int | None = Field(None, ge=1, le=9999)
|
||
overtime_expiry_enabled: bool | None = None
|
||
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)
|
||
public_stamp_enabled: bool | None = None
|
||
vacation_default_days: int | None = Field(None, ge=0, le=365)
|
||
vacation_prorate_first_year: bool | None = None
|
||
vacation_from_schedule: bool | None = None
|
||
two_stage_approval_enabled: bool | None = None
|
||
two_stage_min_days: int | None = Field(None, ge=0, le=365)
|
||
|
||
|
||
class DepartmentOut(BaseModel):
|
||
model_config = {"from_attributes": True}
|
||
|
||
id: uuid.UUID
|
||
company_id: uuid.UUID
|
||
name: str
|
||
manager_id: uuid.UUID | None
|
||
|
||
|
||
class DepartmentCreate(BaseModel):
|
||
name: str = Field(min_length=1, max_length=255)
|
||
manager_id: uuid.UUID | None = None
|
||
|
||
|
||
class DepartmentUpdate(BaseModel):
|
||
name: str | None = Field(None, min_length=1, max_length=255)
|
||
manager_id: uuid.UUID | None = None
|