Files
timemaster/backend/migrations/versions/0029_special_assignments.py
T
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

62 lines
2.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Sondervertretungs-Faktoren: special_assignments Tabelle
Revision ID: 0029
Revises: 0028
Create Date: 2026-05-25
Neue Tabelle special_assignments:
- user_id + company_id (ForeignKeys mit CASCADE)
- date_from / date_to
- factor NUMERIC(5,3) Multiplikator (z.B. 1.5)
- mode ENUM(fza|payroll|both)
- label / description (optional)
- Overlap-Check per Constraint (date_from <= date_to) + App-seitige Prüfung
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision = "0029"
down_revision = "0028"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("CREATE EXTENSION IF NOT EXISTS btree_gist")
# Enum erzeugen
op.execute("CREATE TYPE assignment_mode AS ENUM ('fza', 'payroll', 'both')")
op.create_table(
"special_assignments",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("company_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True),
sa.Column("date_from", sa.Date, nullable=False),
sa.Column("date_to", sa.Date, nullable=False),
sa.Column("factor", sa.Numeric(5, 3), nullable=False),
sa.Column("mode", sa.Enum("fza", "payroll", "both", name="assignment_mode", create_type=False), nullable=False, server_default="both"),
sa.Column("label", sa.String(100), nullable=True),
sa.Column("description", sa.Text, nullable=True),
sa.CheckConstraint("factor > 0 AND factor <= 10", name="ck_special_assignment_factor"),
sa.CheckConstraint("date_from <= date_to", name="ck_special_assignment_dates"),
)
# Exclusion Constraint: kein überlappender Zeitraum pro User
op.execute(
"""
ALTER TABLE special_assignments
ADD CONSTRAINT special_assignments_no_overlap
EXCLUDE USING gist (
user_id WITH =,
daterange(date_from, date_to, '[]') WITH &&
)
"""
)
def downgrade() -> None:
op.drop_table("special_assignments")
op.execute("DROP TYPE IF EXISTS assignment_mode")