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:
@@ -39,18 +39,8 @@ router = APIRouter(tags=["Abwesenheiten"])
|
||||
|
||||
|
||||
def _carryover_expiry(company: Company, year: int) -> tuple[date | None, bool]:
|
||||
"""Verfallsdatum für Resturlaub berechnen.
|
||||
Gibt (expires_at, is_expired) zurück. None wenn kein Verfall konfiguriert."""
|
||||
s = company.settings or {}
|
||||
month = s.get("carryover_expires_month")
|
||||
day = s.get("carryover_expires_day")
|
||||
if not month or not day:
|
||||
return None, False
|
||||
try:
|
||||
expires_at = date(year, int(month), int(day))
|
||||
return expires_at, date.today() > expires_at
|
||||
except ValueError:
|
||||
return None, False
|
||||
"""Verfallsdatum für Resturlaub. Delegiert an den Service (eine Quelle der Wahrheit)."""
|
||||
return absence_service._carryover_expired(company, year)
|
||||
|
||||
_admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
|
||||
_manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
|
||||
@@ -142,6 +132,7 @@ async def get_own_balance(
|
||||
expires_at, expired = _carryover_expiry(company, year) if company else (None, False)
|
||||
return VacationBalanceOut.model_validate(balance).model_copy(update={
|
||||
"pending_days": pending,
|
||||
"available_days": absence_service.effective_available(balance, expired),
|
||||
"carried_over_expires_at": expires_at,
|
||||
"carried_over_expired": expired,
|
||||
})
|
||||
@@ -165,6 +156,7 @@ async def get_balance_for_user(
|
||||
expires_at, expired = _carryover_expiry(company, year) if company else (None, False)
|
||||
return VacationBalanceOut.model_validate(balance).model_copy(update={
|
||||
"pending_days": pending,
|
||||
"available_days": absence_service.effective_available(balance, expired),
|
||||
"carried_over_expires_at": expires_at,
|
||||
"carried_over_expired": expired,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user