Files
sysops 1fedd683e0 Initial commit – TimeMaster Zeiterfassung & HR-Tool
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer),
Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst.
Migrations 0001–0023 deployed auf 192.168.1.137 + .164.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:27 +02:00

152 lines
7.9 KiB
Python
Raw Permalink 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.
"""Work schedule linkage, absence extensions, overtime balance, CalDAV configs
Revision ID: 0005_extensions
Revises: 0004_ldap
Create Date: 2026-03-27
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "0005_extensions"
down_revision = "0004_ldap"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ── 1. AbsenceCategory enum ───────────────────────────────────────────────
absence_category = postgresql.ENUM(
"vacation", "sick", "overtime_comp", "training", "business_trip", "other",
name="absencecategory", create_type=False,
)
absence_category.create(op.get_bind(), checkfirst=True)
# ── 2. absence_types neue Spalten ───────────────────────────────────────
op.add_column("absence_types", sa.Column(
"category",
sa.Enum("vacation", "sick", "overtime_comp", "training", "business_trip", "other",
name="absencecategory"),
nullable=False, server_default="other",
))
op.add_column("absence_types", sa.Column(
"affects_overtime_balance", sa.Boolean(), nullable=False, server_default="false"
))
op.add_column("absence_types", sa.Column(
"requires_certificate", sa.Boolean(), nullable=False, server_default="false"
))
op.add_column("absence_types", sa.Column(
"certificate_after_days", sa.Integer(), nullable=False, server_default="3"
))
# Bestehende Typen klassifizieren
op.execute("""
UPDATE absence_types SET category = 'vacation'
WHERE lower(name) IN ('urlaub', 'sonderurlaub')
""")
op.execute("""
UPDATE absence_types SET category = 'sick', requires_certificate = true
WHERE lower(name) = 'krankheit'
""")
op.execute("""
UPDATE absence_types SET category = 'business_trip'
WHERE lower(name) = 'dienstreise'
""")
op.execute("""
UPDATE absence_types SET category = 'training'
WHERE lower(name) IN ('weiterbildung', 'bildungsurlaub')
""")
# ── 3. absences CalDAV + Meta + Zertifikat ──────────────────────────────
op.add_column("absences", sa.Column("meta", postgresql.JSONB(), nullable=True))
op.add_column("absences", sa.Column("certificate_required_by", sa.Date(), nullable=True))
op.add_column("absences", sa.Column("certificate_received_at", sa.Date(), nullable=True))
op.add_column("absences", sa.Column("caldav_uid", sa.String(255), nullable=True))
op.add_column("absences", sa.Column("caldav_user_etag", sa.Text(), nullable=True))
op.add_column("absences", sa.Column("caldav_company_etag", sa.Text(), nullable=True))
op.add_column("absences", sa.Column("caldav_last_error", sa.Text(), nullable=True))
op.add_column("absences", sa.Column(
"caldav_synced_at", sa.DateTime(timezone=True), nullable=True
))
# ── 4. users work_schedule_id ───────────────────────────────────────────
op.add_column("users", sa.Column(
"work_schedule_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("work_schedules.id", ondelete="SET NULL"),
nullable=True,
))
# ── 5. overtime_balances ──────────────────────────────────────────────────
op.create_table(
"overtime_balances",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True),
sa.Column("company_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("companies.id", ondelete="CASCADE"), nullable=False),
sa.Column("total_hours", sa.Numeric(8, 2), nullable=False, server_default="0"),
sa.Column("taken_hours", sa.Numeric(8, 2), nullable=False, server_default="0"),
sa.Column("last_calculated", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_overtime_balances_user_id", "overtime_balances", ["user_id"])
op.create_index("ix_overtime_balances_company_id", "overtime_balances", ["company_id"])
# ── 6. caldav_company_configs ─────────────────────────────────────────────
op.create_table(
"caldav_company_configs",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("company_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("companies.id", ondelete="CASCADE"),
nullable=False, unique=True),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("principal_url", sa.Text(), nullable=False),
sa.Column("calendar_url", sa.Text(), nullable=True),
sa.Column("username", sa.String(255), nullable=False),
sa.Column("password_encrypted", sa.Text(), nullable=False),
sa.Column("calendar_display_name", sa.String(255), nullable=False, server_default=""),
sa.Column("verify_ssl", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("last_error", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_caldav_company_configs_company_id", "caldav_company_configs", ["company_id"])
# ── 7. caldav_user_configs ────────────────────────────────────────────────
op.create_table(
"caldav_user_configs",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column("user_id", postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="CASCADE"),
nullable=False, unique=True),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default="false"),
sa.Column("principal_url", sa.Text(), nullable=False),
sa.Column("calendar_url", sa.Text(), nullable=True),
sa.Column("username", sa.String(255), nullable=False),
sa.Column("password_encrypted", sa.Text(), nullable=False),
sa.Column("calendar_display_name", sa.String(255), nullable=False, server_default=""),
sa.Column("verify_ssl", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("last_error", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
)
op.create_index("ix_caldav_user_configs_user_id", "caldav_user_configs", ["user_id"])
def downgrade() -> None:
op.drop_table("caldav_user_configs")
op.drop_table("caldav_company_configs")
op.drop_table("overtime_balances")
op.drop_column("users", "work_schedule_id")
for col in ["caldav_synced_at", "caldav_last_error", "caldav_company_etag",
"caldav_user_etag", "caldav_uid", "certificate_received_at",
"certificate_required_by", "meta"]:
op.drop_column("absences", col)
for col in ["certificate_after_days", "requires_certificate",
"affects_overtime_balance", "category"]:
op.drop_column("absence_types", col)
op.execute("DROP TYPE IF EXISTS absencecategory")