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,61 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user