feat: Sondervertretungs-Faktoren (special_assignments)
- Neues Model SpecialAssignment mit AssignmentMode (fza|payroll|both)
- CRUD-Endpunkte unter /users/{id}/special-assignments
- Payroll-Report: GET /reports/special-assignments/payroll?year=&month=
- Migration 0029: special_assignments Tabelle + btree_gist Overlap-Constraint
- _recalculate_overtime_balance berücksichtigt FZA-Faktoren
- Frontend: Sondervertretungs-Zeiträume im UsersPage Edit-Modal
- Frontend: ReportsPage neuer Tab 'Sondervertretungen' mit Payroll-Tabelle + CSV-Export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -181,7 +181,14 @@ async def _get_or_create_overtime_balance(user: User, db: AsyncSession) -> Overt
|
||||
async def _recalculate_overtime_balance(
|
||||
user: User, schedule: WorkSchedule | None, db: AsyncSession
|
||||
) -> OvertimeBalance:
|
||||
"""Berechnet das Überstundenguthaben neu aus allen genehmigten Zeiteinträgen."""
|
||||
"""Berechnet das Überstundenguthaben neu aus allen genehmigten Zeiteinträgen.
|
||||
|
||||
Sondervertretungs-Faktoren (mode=fza|both) werden berücksichtigt:
|
||||
Für jeden Zeiteintrag wird geprüft ob der Tag in einem aktiven Sondervertretungs-
|
||||
Zeitraum liegt; falls ja wird worked_hours mit dem Faktor multipliziert.
|
||||
"""
|
||||
from app.models.special_assignment import AssignmentMode, SpecialAssignment
|
||||
|
||||
entries = list(await db.scalars(
|
||||
select(TimeEntry).where(
|
||||
TimeEntry.user_id == user.id,
|
||||
@@ -197,10 +204,27 @@ async def _recalculate_overtime_balance(
|
||||
bal.last_calculated = datetime.utcnow()
|
||||
return bal
|
||||
|
||||
date_from = min(e.date for e in entries)
|
||||
date_to = max(e.date for e in entries)
|
||||
expected = _expected_hours(schedule, date_from, date_to)
|
||||
worked = sum(e.worked_hours or 0.0 for e in entries)
|
||||
# Sondervertretungs-Zuweisungen laden (nur FZA-relevante)
|
||||
date_from_all = min(e.date for e in entries)
|
||||
date_to_all = max(e.date for e in entries)
|
||||
special_assignments = list(await db.scalars(
|
||||
select(SpecialAssignment).where(
|
||||
SpecialAssignment.user_id == user.id,
|
||||
SpecialAssignment.mode.in_([AssignmentMode.fza, AssignmentMode.both]),
|
||||
SpecialAssignment.date_from <= date_to_all,
|
||||
SpecialAssignment.date_to >= date_from_all,
|
||||
)
|
||||
))
|
||||
|
||||
def _fza_factor(entry_date: date) -> float:
|
||||
"""Gibt den Faktor für einen Tag zurück (1.0 wenn keine Zuweisung aktiv)."""
|
||||
for sa in special_assignments:
|
||||
if sa.date_from <= entry_date <= sa.date_to:
|
||||
return float(sa.factor)
|
||||
return 1.0
|
||||
|
||||
expected = _expected_hours(schedule, date_from_all, date_to_all)
|
||||
worked = sum((e.worked_hours or 0.0) * _fza_factor(e.date) for e in entries)
|
||||
overtime = max(0.0, worked - expected)
|
||||
|
||||
bal.total_hours = Decimal(str(round(overtime, 2)))
|
||||
|
||||
Reference in New Issue
Block a user