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:
@@ -0,0 +1,76 @@
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from app.models.special_assignment import AssignmentMode
|
||||
|
||||
|
||||
class SpecialAssignmentCreate(BaseModel):
|
||||
date_from: date
|
||||
date_to: date
|
||||
factor: Decimal = Field(gt=0, le=10, decimal_places=3)
|
||||
mode: AssignmentMode = AssignmentMode.both
|
||||
description: str | None = None
|
||||
label: str | None = Field(None, max_length=100)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def dates_valid(self) -> "SpecialAssignmentCreate":
|
||||
if self.date_from > self.date_to:
|
||||
raise ValueError("date_from darf nicht nach date_to liegen")
|
||||
return self
|
||||
|
||||
|
||||
class SpecialAssignmentUpdate(BaseModel):
|
||||
date_from: date | None = None
|
||||
date_to: date | None = None
|
||||
factor: Decimal | None = Field(None, gt=0, le=10)
|
||||
mode: AssignmentMode | None = None
|
||||
description: str | None = None
|
||||
label: str | None = Field(None, max_length=100)
|
||||
|
||||
|
||||
class SpecialAssignmentOut(BaseModel):
|
||||
id: UUID
|
||||
user_id: UUID
|
||||
company_id: UUID
|
||||
date_from: date
|
||||
date_to: date
|
||||
factor: Decimal
|
||||
mode: AssignmentMode
|
||||
description: str | None = None
|
||||
label: str | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── Payroll-Report-Schemas ────────────────────────────────────────────────────
|
||||
|
||||
class PayrollAssignmentEntry(BaseModel):
|
||||
"""Einzelner Zeitraum mit Faktor für den Payroll-Report."""
|
||||
assignment_id: UUID
|
||||
label: str | None
|
||||
date_from: date
|
||||
date_to: date
|
||||
factor: Decimal
|
||||
normal_hours: float # Stunden ohne Faktor
|
||||
factor_hours: float # Stunden * Faktor (effektiv für Abrechnung)
|
||||
extra_hours: float # factor_hours - normal_hours (Mehrwert)
|
||||
|
||||
|
||||
class PayrollAssignmentRow(BaseModel):
|
||||
"""Zusammenfassung pro Mitarbeiter."""
|
||||
user_id: UUID
|
||||
user_name: str
|
||||
personnel_number: str | None
|
||||
assignments: list[PayrollAssignmentEntry]
|
||||
total_normal_hours: float
|
||||
total_factor_hours: float
|
||||
total_extra_hours: float
|
||||
|
||||
|
||||
class PayrollAssignmentReport(BaseModel):
|
||||
year: int
|
||||
month: int
|
||||
rows: list[PayrollAssignmentRow]
|
||||
Reference in New Issue
Block a user