update
This commit is contained in:
106
backup 24.2.26 - Kopie/workforce_planner/absences/rules.py
Normal file
106
backup 24.2.26 - Kopie/workforce_planner/absences/rules.py
Normal 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)
|
||||
Reference in New Issue
Block a user