Files
timemaster/backend/app/schemas/company.py
T
patrickandClaude Opus 4.8 eab41ede69 feat: agent-12 – Zwei-Stufen-Genehmigung für Abwesenheiten
Optionale zweistufige Freigabe (Feature-Parität mit Urlaubsverwaltung,
Second-Stage-Authority), ohne SSO:

- Firmen-Opt-in companies.two_stage_approval_enabled + two_stage_min_days
  (nur Anträge ab X Arbeitstagen brauchen Stufe 2; 0 = alle).
- Ablauf PENDING → FIRST_APPROVED → APPROVED: erste Stufe durch Manager-Rollen,
  finale Stufe nur HR/Admin und zwingend eine ANDERE Person als Stufe 1.
- Urlaubs-/FZA-Abzug, CalDAV-Sync und Vertreter-Mail erst bei finaler Genehmigung.
  Ablehnen in beiden Stufen möglich; Eigentümer darf FIRST_APPROVED noch stornieren.
- pending_days, Kalender und Reminder-Digest berücksichtigen FIRST_APPROVED.
- Neuer Status-Wert + absences.first_approved_by; System-Kommentar bei Stufe 1.

Frontend: CompanySettingsPage (Toggle + Schwellwert), AbsencesPage
("Endgültig genehmigen"/Ablehnen für HR/Admin ≠ Erstgenehmiger, Status-Badge).

Migration 0038. 190/190 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:17:32 +02:00

114 lines
3.9 KiB
Python
Raw 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.
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
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)
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