This commit is contained in:
2026-03-25 22:03:39 +01:00
parent a0073b4fb1
commit faf4ca10c9
5603 changed files with 1030866 additions and 79 deletions

View File

@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
"""Business Rules Validierungsregeln für Abwesenheiten.
Jede Regel ist eine Funktion die (db, AbsenceCreate) nimmt und
bei Verstoss eine RuleViolation raised.
"""
import datetime
from sqlalchemy.orm import Session
from ..core.schemas import AbsenceCreate
from ..core.enums import ABSENCE_META
from ..employees.repository import EmployeeRepository
from .repository import AbsenceRepository, BalanceRepository
from ..config import MIN_STAFF_COUNT
class RuleViolation(Exception):
"""Wird geworfen wenn eine Business Rule verletzt ist."""
def __init__(self, rule: str, message: str):
self.rule = rule
self.message = message
super().__init__(f"[{rule}] {message}")
def _business_days(start: datetime.date, end: datetime.date) -> float:
count = 0
d = start
while d <= end:
if d.weekday() < 5:
count += 1
d += datetime.timedelta(days=1)
return count
def check_no_overlap(db: Session, data: AbsenceCreate):
"""Keine überlappenden Abwesenheiten für denselben Mitarbeiter."""
repo = AbsenceRepository(db)
overlaps = repo.find_overlapping(data.employee_id, data.start_date, data.end_date)
if overlaps:
first = overlaps[0]
raise RuleViolation(
"NO_OVERLAP",
f"Überschneidung mit bestehender Abwesenheit "
f"({first.start_date} {first.end_date}, {first.category.value})"
)
def check_balance(db: Session, data: AbsenceCreate):
"""Ferientage-Kontingent prüfen (nur für Kategorien mit deducts_balance)."""
meta = ABSENCE_META.get(data.category, {})
if not meta.get("deducts_balance"):
return
year = data.start_date.year
abs_repo = AbsenceRepository(db)
bal_repo = BalanceRepository(db)
balance = bal_repo.get_or_create(data.employee_id, year)
used = abs_repo.used_days(data.employee_id, year)
needed = _business_days(data.start_date, data.end_date)
remaining = balance.total_available - used
if needed > remaining:
raise RuleViolation(
"INSUFFICIENT_BALANCE",
f"Nicht genügend Ferientage. Benötigt: {needed:.0f}, "
f"Verfügbar: {remaining:.0f} (Anspruch: {balance.total_available:.0f}, "
f"Genommen: {used:.0f})"
)
def check_min_staffing(db: Session, data: AbsenceCreate):
"""Mindestbesetzung prüfen nicht alle gleichzeitig abwesend."""
emp_repo = EmployeeRepository(db)
abs_repo = AbsenceRepository(db)
total_active = emp_repo.count_active()
if total_active <= MIN_STAFF_COUNT:
return
d = data.start_date
while d <= data.end_date:
if d.weekday() < 5:
absent_count = abs_repo.count_absent_on_date(d)
present = total_active - absent_count - 1
if present < MIN_STAFF_COUNT:
raise RuleViolation(
"MIN_STAFFING",
f"Am {d.strftime('%d.%m.%Y')} wären nur noch {present} "
f"Mitarbeiter anwesend (Minimum: {MIN_STAFF_COUNT})"
)
d += datetime.timedelta(days=1)
ALL_RULES = [check_no_overlap, check_balance, check_min_staffing]
def validate_absence(db: Session, data: AbsenceCreate, skip_rules: list[str] | None = None):
"""Alle Regeln durchlaufen. Wirft RuleViolation beim ersten Verstoss."""
skip = set(skip_rules or [])
for rule_fn in ALL_RULES:
if rule_fn.__name__ in skip:
continue
rule_fn(db, data)