feat: agent-11 PR2 – Urlaubsanspruch (Verfall scharf, Pro-rata, Teilzeit)

Korrektheit der Urlaubskonten (Feature-Parität mit Urlaubsverwaltung):

- Verfall scharfgeschaltet: neuer effektiv verfügbarer Saldo (available_days)
  schließt verfallenen, noch nicht verbrauchten Resturlaub aus; Konto-Warnung
  beim Antrag nutzt jetzt available statt remaining. (Verfallsdatum bleibt in
  company.settings, UI bereits vorhanden.)
- Anteilige Berechnung (Zwölftel) im Ein-/Austrittsjahr anhand neuer Felder
  users.entry_date / exit_date; opt-in pro Firma (vacation_prorate_first_year).
- Teilzeit: Anspruch optional aus Arbeitstagen/Woche des WorkSchedule abgeleitet
  (vacation_from_schedule), Basis vacation_default_days.
- _get_or_create_balance berechnet den Grundanspruch jetzt frisch
  (_compute_entitlement); _carryover_expired/effective_available als Service-API,
  Router delegiert.

Frontend: CompanySettingsPage (Jahresurlaub + Pro-rata- und Teilzeit-Toggles),
UsersPage (Ein-/Austrittsdatum im Edit-Modal), AbsencesPage ("Verfügbar" + Hinweis
bei verfallenem Resturlaub).

Migration 0036. 183/183 Tests grün. Deployed auf 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 12:03:42 +02:00
co-authored by Claude Opus 4.8
parent 6fa66b8c13
commit e8bed43570
13 changed files with 277 additions and 20 deletions
+2
View File
@@ -188,6 +188,8 @@ class VacationBalanceOut(BaseModel):
used_days: int
total_days: int
remaining_days: int
# Effektiv verfügbar: berücksichtigt verfallenen Resturlaub (zur Laufzeit befüllt).
available_days: int = 0
pending_days: float = 0
# Resturlaub-Verfall (wird zur Laufzeit befüllt, nicht in DB)
carried_over_expires_at: date | None = None
+6
View File
@@ -47,6 +47,9 @@ class CompanyOut(BaseModel):
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
class PublicStampTokenStatus(BaseModel):
@@ -82,6 +85,9 @@ class CompanyUpdate(BaseModel):
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
class DepartmentOut(BaseModel):
+6 -1
View File
@@ -1,5 +1,5 @@
import uuid
from datetime import datetime
from datetime import datetime, date
from pydantic import BaseModel, EmailStr, Field, model_validator
@@ -27,6 +27,9 @@ class UserOut(BaseModel):
kuerzel: str | None = None
personnel_number: str | None = None
can_manual_time_entry: bool = False
work_schedule_id: uuid.UUID | None = None
entry_date: date | None = None
exit_date: date | None = None
class UserUpdate(BaseModel):
@@ -39,6 +42,8 @@ class UserUpdate(BaseModel):
personnel_number: str | None = Field(None, max_length=50, pattern=PERSONNEL_NUMBER_PATTERN)
can_manual_time_entry: bool | None = None
is_active: bool | None = None
entry_date: date | None = None
exit_date: date | None = None
class InviteRequest(BaseModel):