Files
patrick d60349df67 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>
2026-05-25 00:55:47 +02:00

39 lines
1.8 KiB
Python

"""Sondervertretungs-Faktoren: Per-User Zeitraum-Zuweisung mit Multiplikator."""
import enum
from datetime import date
from decimal import Decimal
from uuid import UUID, uuid4
from sqlalchemy import CheckConstraint, Date, Enum, ForeignKey, Numeric, String, Text
from sqlalchemy.dialects.postgresql import UUID as PG_UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base
class AssignmentMode(str, enum.Enum):
fza = "fza" # Nur FZA-Stunden-Anrechnung
payroll = "payroll" # Nur Gehaltsabrechnung (Bericht)
both = "both" # Beides
class SpecialAssignment(Base):
"""Sondervertretungs-Zeitraum mit Faktor für einen Mitarbeiter."""
__tablename__ = "special_assignments"
__table_args__ = (
CheckConstraint("factor > 0 AND factor <= 10", name="ck_special_assignment_factor"),
CheckConstraint("date_from <= date_to", name="ck_special_assignment_dates"),
)
id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4)
user_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
company_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True)
date_from: Mapped[date] = mapped_column(Date, nullable=False)
date_to: Mapped[date] = mapped_column(Date, nullable=False)
factor: Mapped[Decimal] = mapped_column(Numeric(5, 3), nullable=False)
mode: Mapped[AssignmentMode] = mapped_column(Enum(AssignmentMode, name="assignment_mode"), nullable=False, default=AssignmentMode.both)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
label: Mapped[str | None] = mapped_column(String(100), nullable=True) # z.B. "Schichtleiter Vertretung"