d60349df67
- 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>
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""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")
|