Files
aza/AzA march 2026 - Kopie (3)/workforce_planner/absences/service.py

128 lines
4.5 KiB
Python
Raw Normal View History

2026-03-25 13:42:48 +01:00
# -*- coding: utf-8 -*-
"""Service Layer Orchestriert Business-Logik für Abwesenheiten."""
import datetime
from typing import Optional
from sqlalchemy.orm import Session
from ..core.models import Absence
from ..core.enums import AbsenceStatus, ABSENCE_META
from ..core.schemas import (
AbsenceCreate, AbsenceUpdate, AbsenceRead,
BalanceRead, BalanceAdjust, ApprovalDecision,
)
from .repository import AbsenceRepository, BalanceRepository
from .rules import validate_absence, _business_days, RuleViolation
from ..employees.repository import EmployeeRepository
class AbsenceService:
"""Zentrale Geschäftslogik wird von API UND Desktop-Client genutzt."""
def __init__(self, db: Session):
self.db = db
self.abs_repo = AbsenceRepository(db)
self.bal_repo = BalanceRepository(db)
self.emp_repo = EmployeeRepository(db)
def create_absence(self, data: AbsenceCreate) -> Absence:
emp = self.emp_repo.get_by_id(data.employee_id)
if not emp:
raise ValueError(f"Mitarbeiter {data.employee_id} nicht gefunden")
validate_absence(self.db, data)
bd = _business_days(data.start_date, data.end_date)
meta = ABSENCE_META.get(data.category, {})
absence = self.abs_repo.create(data, business_days=bd)
if not meta.get("requires_approval"):
absence.status = AbsenceStatus.APPROVED
self.db.commit()
return absence
def update_absence(self, absence_id: str, data: AbsenceUpdate) -> Optional[Absence]:
absence = self.abs_repo.get_by_id(absence_id)
if not absence:
return None
if data.start_date or data.end_date:
start = data.start_date or absence.start_date
end = data.end_date or absence.end_date
check_data = AbsenceCreate(
employee_id=absence.employee_id,
category=data.category or absence.category,
start_date=start,
end_date=end,
reason=data.reason or absence.reason,
)
validate_absence(self.db, check_data, skip_rules=[])
absence.business_days = _business_days(start, end)
result = self.abs_repo.update(absence_id, data)
self.db.commit()
return result
def delete_absence(self, absence_id: str) -> bool:
ok = self.abs_repo.delete(absence_id)
if ok:
self.db.commit()
return ok
def approve(self, decision: ApprovalDecision) -> Absence:
absence = self.abs_repo.get_by_id(decision.absence_id)
if not absence:
raise ValueError("Abwesenheit nicht gefunden")
if absence.status != AbsenceStatus.PENDING:
raise ValueError(f"Status ist bereits {absence.status.value}")
absence.approver_id = decision.approver_id
absence.approved_at = datetime.datetime.utcnow()
absence.status = AbsenceStatus.APPROVED if decision.approved else AbsenceStatus.REJECTED
self.db.commit()
return absence
def get_absences_for_employee(self, employee_id: str, year: Optional[int] = None) -> list[Absence]:
return self.abs_repo.list_for_employee(employee_id, year)
def get_absences_for_period(
self, start: datetime.date, end: datetime.date
) -> list[Absence]:
return self.abs_repo.list_for_period(start, end)
def get_all_absences(self, year: Optional[int] = None) -> list[Absence]:
return self.abs_repo.list_all(year)
def get_balance(self, employee_id: str, year: int) -> BalanceRead:
emp = self.emp_repo.get_by_id(employee_id)
if not emp:
raise ValueError(f"Mitarbeiter {employee_id} nicht gefunden")
ba = self.bal_repo.get_or_create(employee_id, year)
used = self.abs_repo.used_days(employee_id, year)
return BalanceRead(
employee_id=employee_id,
employee_name=emp.name,
year=year,
yearly_quota=ba.yearly_quota,
carry_over=ba.carry_over,
manual_adjustment=ba.manual_adjustment,
total_available=ba.total_available,
used_days=used,
remaining=ba.total_available - used,
)
def adjust_balance(self, employee_id: str, year: int, data: BalanceAdjust) -> BalanceRead:
self.bal_repo.update(
employee_id, year,
carry_over=data.carry_over,
manual_adjustment=data.manual_adjustment,
yearly_quota=data.yearly_quota,
)
self.db.commit()
return self.get_balance(employee_id, year)