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>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""initial schema - auth tables
|
||||
|
||||
Revision ID: 0001_initial
|
||||
Revises:
|
||||
Create Date: 2026-03-25
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = "0001_initial"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── companies ─────────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"companies",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("slug", sa.String(100), nullable=False, unique=True),
|
||||
sa.Column("plan", sa.String(50), server_default="trial"),
|
||||
sa.Column("logo_url", sa.Text),
|
||||
sa.Column("country", sa.String(10), server_default="DE"),
|
||||
sa.Column("state", sa.String(10)),
|
||||
sa.Column("settings", postgresql.JSONB, server_default="{}"),
|
||||
)
|
||||
|
||||
# ── departments (before users – FK target) ─────────────────────────────────
|
||||
op.create_table(
|
||||
"departments",
|
||||
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),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("manager_id", postgresql.UUID(as_uuid=True)), # FK added after users
|
||||
)
|
||||
|
||||
# ── users ─────────────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"users",
|
||||
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")),
|
||||
sa.Column("department_id", postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey("departments.id", ondelete="SET NULL")),
|
||||
sa.Column("email", sa.String(255), nullable=False, unique=True),
|
||||
sa.Column("password_hash", sa.Text, nullable=False),
|
||||
sa.Column("first_name", sa.String(100), nullable=False),
|
||||
sa.Column("last_name", sa.String(100), nullable=False),
|
||||
sa.Column("role", sa.Enum(
|
||||
"SUPER_ADMIN", "COMPANY_ADMIN", "HR", "MANAGER", "EMPLOYEE",
|
||||
name="userrole",
|
||||
), nullable=False),
|
||||
sa.Column("kiosk_pin_hash", sa.Text),
|
||||
sa.Column("kiosk_qr_token", sa.Text, unique=True),
|
||||
sa.Column("is_active", sa.Boolean, server_default="true"),
|
||||
sa.Column("invite_token_hash", sa.Text),
|
||||
sa.Column("invite_expires", sa.DateTime(timezone=True)),
|
||||
sa.Column("last_login", sa.DateTime(timezone=True)),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_users_email", "users", ["email"])
|
||||
|
||||
# Now add FK for departments.manager_id → users
|
||||
op.create_foreign_key(
|
||||
"fk_departments_manager", "departments", "users", ["manager_id"], ["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
|
||||
# ── sessions ──────────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"sessions",
|
||||
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),
|
||||
sa.Column("refresh_token_hash", sa.Text, nullable=False, unique=True),
|
||||
sa.Column("device", sa.String(255)),
|
||||
sa.Column("ip", sa.String(45)),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_sessions_user_id", "sessions", ["user_id"])
|
||||
|
||||
# ── password_resets ───────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"password_resets",
|
||||
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),
|
||||
sa.Column("token_hash", sa.Text, nullable=False, unique=True),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("used_at", sa.DateTime(timezone=True)),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_password_resets_user_id", "password_resets", ["user_id"])
|
||||
|
||||
# ── audit_logs ────────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"audit_logs",
|
||||
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="SET NULL")),
|
||||
sa.Column("user_id", postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey("users.id", ondelete="SET NULL")),
|
||||
sa.Column("action", sa.String(100), nullable=False),
|
||||
sa.Column("entity_type", sa.String(100)),
|
||||
sa.Column("entity_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.Column("old_value", postgresql.JSONB),
|
||||
sa.Column("new_value", postgresql.JSONB),
|
||||
sa.Column("ip", sa.String(45)),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(), index=True),
|
||||
)
|
||||
op.create_index("ix_audit_logs_company_id", "audit_logs", ["company_id"])
|
||||
op.create_index("ix_audit_logs_user_id", "audit_logs", ["user_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("audit_logs")
|
||||
op.drop_table("password_resets")
|
||||
op.drop_table("sessions")
|
||||
op.drop_constraint("fk_departments_manager", "departments", type_="foreignkey")
|
||||
op.drop_table("users")
|
||||
op.execute("DROP TYPE IF EXISTS userrole")
|
||||
op.drop_table("departments")
|
||||
op.drop_table("companies")
|
||||
@@ -0,0 +1,70 @@
|
||||
"""time entries and work schedules
|
||||
|
||||
Revision ID: 0002_time_entries
|
||||
Revises: 0001_initial
|
||||
Create Date: 2026-03-27
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = "0002_time_entries"
|
||||
down_revision = "0001_initial"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# Enum-Typen explizit definieren (create_type=False → sa.Enum erstellt sie selbst via op.create_table)
|
||||
entrystatus = sa.Enum("pending", "approved", "rejected", name="entrystatus")
|
||||
entrysource = sa.Enum("web", "kiosk", "api", "manual", name="entrysource")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── work_schedules ─────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"work_schedules",
|
||||
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),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("mon_h", sa.Numeric(4, 2), server_default="8.00"),
|
||||
sa.Column("tue_h", sa.Numeric(4, 2), server_default="8.00"),
|
||||
sa.Column("wed_h", sa.Numeric(4, 2), server_default="8.00"),
|
||||
sa.Column("thu_h", sa.Numeric(4, 2), server_default="8.00"),
|
||||
sa.Column("fri_h", sa.Numeric(4, 2), server_default="8.00"),
|
||||
sa.Column("sat_h", sa.Numeric(4, 2), server_default="0.00"),
|
||||
sa.Column("sun_h", sa.Numeric(4, 2), server_default="0.00"),
|
||||
sa.Column("valid_from", sa.Date, nullable=False),
|
||||
)
|
||||
op.create_index("ix_work_schedules_company_id", "work_schedules", ["company_id"])
|
||||
|
||||
# ── time_entries ───────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"time_entries",
|
||||
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),
|
||||
sa.Column("date", sa.Date, nullable=False),
|
||||
sa.Column("start_time", sa.Time(timezone=False), nullable=False),
|
||||
sa.Column("end_time", sa.Time(timezone=False)),
|
||||
sa.Column("break_minutes", sa.Integer, server_default="0"),
|
||||
sa.Column("break_start", sa.Time(timezone=False)),
|
||||
sa.Column("project_id", postgresql.UUID(as_uuid=True)),
|
||||
sa.Column("note", sa.Text),
|
||||
sa.Column("status", entrystatus, nullable=False, server_default="pending"),
|
||||
sa.Column("source", entrysource, nullable=False, server_default="web"),
|
||||
sa.Column("approved_by", postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey("users.id", ondelete="SET NULL")),
|
||||
sa.Column("correction_note", sa.Text),
|
||||
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_time_entries_user_id", "time_entries", ["user_id"])
|
||||
op.create_index("ix_time_entries_date", "time_entries", ["date"])
|
||||
op.create_index("ix_time_entries_user_date", "time_entries", ["user_id", "date"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("time_entries")
|
||||
op.drop_table("work_schedules")
|
||||
entrystatus.drop(op.get_bind(), checkfirst=True)
|
||||
entrysource.drop(op.get_bind(), checkfirst=True)
|
||||
@@ -0,0 +1,95 @@
|
||||
"""absence management
|
||||
|
||||
Revision ID: 0003_absences
|
||||
Revises: 0002_time_entries
|
||||
Create Date: 2026-03-27
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = "0003_absences"
|
||||
down_revision = "0002_time_entries"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
absencestatus = sa.Enum("pending", "approved", "rejected", "cancelled", name="absencestatus")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── absence_types ──────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"absence_types",
|
||||
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),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("color", sa.String(7), server_default="#3B82F6"),
|
||||
sa.Column("requires_approval", sa.Boolean, server_default="true"),
|
||||
sa.Column("deducts_vacation", sa.Boolean, server_default="false"),
|
||||
sa.Column("is_paid", sa.Boolean, server_default="true"),
|
||||
sa.Column("max_days_per_year", sa.Integer),
|
||||
sa.Column("is_active", sa.Boolean, server_default="true"),
|
||||
)
|
||||
op.create_index("ix_absence_types_company_id", "absence_types", ["company_id"])
|
||||
|
||||
# ── absences ───────────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"absences",
|
||||
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),
|
||||
sa.Column("type_id", postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey("absence_types.id", ondelete="RESTRICT"), nullable=False),
|
||||
sa.Column("start_date", sa.Date, nullable=False),
|
||||
sa.Column("end_date", sa.Date, nullable=False),
|
||||
sa.Column("half_day_start", sa.Boolean, server_default="false"),
|
||||
sa.Column("half_day_end", sa.Boolean, server_default="false"),
|
||||
sa.Column("working_days", sa.Numeric(5, 1), server_default="0"),
|
||||
sa.Column("status", absencestatus, nullable=False, server_default="pending"),
|
||||
sa.Column("approved_by", postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey("users.id", ondelete="SET NULL")),
|
||||
sa.Column("substitute_id", postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey("users.id", ondelete="SET NULL")),
|
||||
sa.Column("note", sa.Text),
|
||||
sa.Column("rejection_reason", sa.Text),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index("ix_absences_user_id", "absences", ["user_id"])
|
||||
op.create_index("ix_absences_start_date", "absences", ["start_date"])
|
||||
|
||||
# ── vacation_balances ──────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"vacation_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),
|
||||
sa.Column("year", sa.Integer, nullable=False),
|
||||
sa.Column("entitled_days", sa.Integer, server_default="30"),
|
||||
sa.Column("carried_over", sa.Integer, server_default="0"),
|
||||
sa.Column("used_days", sa.Integer, server_default="0"),
|
||||
sa.UniqueConstraint("user_id", "year", name="uq_vacation_balance_user_year"),
|
||||
)
|
||||
op.create_index("ix_vacation_balances_user_id", "vacation_balances", ["user_id"])
|
||||
|
||||
# ── public_holidays ────────────────────────────────────────────────────────
|
||||
op.create_table(
|
||||
"public_holidays",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("country", sa.String(10), nullable=False),
|
||||
sa.Column("state", sa.String(10)),
|
||||
sa.Column("date", sa.Date, nullable=False),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("year", sa.Integer, nullable=False),
|
||||
sa.UniqueConstraint("country", "state", "date", name="uq_public_holiday"),
|
||||
)
|
||||
op.create_index("ix_public_holidays_date", "public_holidays", ["date"])
|
||||
op.create_index("ix_public_holidays_year", "public_holidays", ["year"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("public_holidays")
|
||||
op.drop_table("vacation_balances")
|
||||
op.drop_table("absences")
|
||||
op.drop_table("absence_types")
|
||||
absencestatus.drop(op.get_bind(), checkfirst=True)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""LDAP integration: ldap_configs table + user auth_provider/ldap_dn columns
|
||||
|
||||
Revision ID: 0004_ldap
|
||||
Revises: 0003_absences
|
||||
Create Date: 2026-03-27
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision = "0004_ldap"
|
||||
down_revision = "0003_absences"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add auth_provider enum
|
||||
authprovider = postgresql.ENUM("local", "ldap", name="authprovider", create_type=False)
|
||||
authprovider.create(op.get_bind(), checkfirst=True)
|
||||
|
||||
# Alter users table
|
||||
op.add_column("users", sa.Column(
|
||||
"auth_provider",
|
||||
sa.Enum("local", "ldap", name="authprovider"),
|
||||
nullable=False,
|
||||
server_default="local",
|
||||
))
|
||||
op.add_column("users", sa.Column("ldap_dn", sa.Text(), nullable=True))
|
||||
op.alter_column("users", "password_hash", nullable=True)
|
||||
|
||||
# Create ldap_configs table
|
||||
op.create_table(
|
||||
"ldap_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("host", sa.String(255), nullable=False),
|
||||
sa.Column("port", sa.Integer(), nullable=False, server_default="389"),
|
||||
sa.Column("use_ssl", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("use_tls", sa.Boolean(), nullable=False, server_default="false"),
|
||||
sa.Column("bind_dn", sa.Text(), nullable=False),
|
||||
sa.Column("bind_password_encrypted", sa.Text(), nullable=False),
|
||||
sa.Column("base_dn", sa.Text(), nullable=False),
|
||||
sa.Column("user_search_filter", sa.String(512), nullable=False,
|
||||
server_default="(objectClass=person)"),
|
||||
sa.Column("attr_email", sa.String(100), nullable=False, server_default="mail"),
|
||||
sa.Column("attr_firstname", sa.String(100), nullable=False, server_default="givenName"),
|
||||
sa.Column("attr_lastname", sa.String(100), nullable=False, server_default="sn"),
|
||||
sa.Column("attr_username", sa.String(100), nullable=False, server_default="sAMAccountName"),
|
||||
sa.Column("attr_department", sa.String(100), nullable=True),
|
||||
sa.Column("last_sync_at", 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_ldap_configs_company_id", "ldap_configs", ["company_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("ldap_configs")
|
||||
op.drop_column("users", "ldap_dn")
|
||||
op.drop_column("users", "auth_provider")
|
||||
# LDAP users have no password_hash — set placeholder before restoring NOT NULL
|
||||
op.execute("UPDATE users SET password_hash = 'LDAP_USER_NO_PASSWORD' WHERE password_hash IS NULL")
|
||||
op.alter_column("users", "password_hash", nullable=False)
|
||||
op.execute("DROP TYPE IF EXISTS authprovider")
|
||||
@@ -0,0 +1,151 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,40 @@
|
||||
"""smtp_configs table
|
||||
|
||||
Revision ID: 0006_smtp
|
||||
Revises: 0005_extensions
|
||||
Create Date: 2026-03-27
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "0006_smtp"
|
||||
down_revision = "0005_extensions"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"smtp_configs",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("company_id", UUID(as_uuid=True),
|
||||
sa.ForeignKey("companies.id", ondelete="CASCADE"),
|
||||
nullable=False),
|
||||
sa.Column("host", sa.String(255), nullable=False),
|
||||
sa.Column("port", sa.Integer, nullable=False, server_default="587"),
|
||||
sa.Column("use_tls", sa.Boolean, nullable=False, server_default="false"),
|
||||
sa.Column("use_starttls", sa.Boolean, nullable=False, server_default="true"),
|
||||
sa.Column("username", sa.String(255)),
|
||||
sa.Column("password_encrypted", sa.Text),
|
||||
sa.Column("from_email", sa.String(255), nullable=False),
|
||||
sa.Column("from_name", sa.String(255), nullable=False, server_default="TimeMaster"),
|
||||
sa.Column("is_enabled", sa.Boolean, nullable=False, server_default="true"),
|
||||
sa.UniqueConstraint("company_id", name="uq_smtp_configs_company"),
|
||||
)
|
||||
op.create_index("ix_smtp_configs_company_id", "smtp_configs", ["company_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_smtp_configs_company_id")
|
||||
op.drop_table("smtp_configs")
|
||||
@@ -0,0 +1,88 @@
|
||||
"""caldav_configs tables + fix smtp_configs missing FK
|
||||
|
||||
Revision ID: 0007_caldav_and_fixes
|
||||
Revises: 0006_smtp
|
||||
Create Date: 2026-03-27
|
||||
|
||||
Fixes:
|
||||
- smtp_configs.company_id had no FK constraint (orphaned records possible)
|
||||
- caldav_company_configs and caldav_user_configs were created via create_all,
|
||||
not tracked by Alembic — now formally managed here
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
|
||||
revision = "0007_caldav_and_fixes"
|
||||
down_revision = "0006_smtp"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── Fix 1: smtp_configs missing FK ───────────────────────────────────────
|
||||
# The FK was omitted in 0006_smtp. Add it now.
|
||||
op.create_foreign_key(
|
||||
"fk_smtp_configs_company_id",
|
||||
"smtp_configs", "companies",
|
||||
["company_id"], ["id"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
|
||||
# ── Fix 2: caldav tables (created via create_all, now Alembic-managed) ───
|
||||
# Use checkfirst=True so fresh installs that don't have the tables yet
|
||||
# get them created, while existing installs skip creation silently.
|
||||
bind = op.get_bind()
|
||||
|
||||
if not bind.dialect.has_table(bind, "caldav_company_configs"):
|
||||
op.create_table(
|
||||
"caldav_company_configs",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("company_id", 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, server_default=""),
|
||||
sa.Column("calendar_url", sa.Text(), nullable=True),
|
||||
sa.Column("username", sa.String(255), nullable=False, server_default=""),
|
||||
sa.Column("password_encrypted", sa.Text(), nullable=False, server_default=""),
|
||||
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"])
|
||||
|
||||
if not bind.dialect.has_table(bind, "caldav_user_configs"):
|
||||
op.create_table(
|
||||
"caldav_user_configs",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("user_id", 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, server_default=""),
|
||||
sa.Column("calendar_url", sa.Text(), nullable=True),
|
||||
sa.Column("username", sa.String(255), nullable=False, server_default=""),
|
||||
sa.Column("password_encrypted", sa.Text(), nullable=False, server_default=""),
|
||||
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:
|
||||
# Remove the smtp FK (restores state to 0006_smtp)
|
||||
op.drop_constraint("fk_smtp_configs_company_id", "smtp_configs", type_="foreignkey")
|
||||
|
||||
# Drop caldav tables only if they were created by this migration
|
||||
# (i.e., on a fresh install path — existing installs: tables stay)
|
||||
op.drop_index("ix_caldav_user_configs_user_id", table_name="caldav_user_configs")
|
||||
op.drop_table("caldav_user_configs")
|
||||
op.drop_index("ix_caldav_company_configs_company_id", table_name="caldav_company_configs")
|
||||
op.drop_table("caldav_company_configs")
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Add tls_verify column to ldap_configs (H-07 security fix)
|
||||
|
||||
Revision ID: 0008_ldap_tls_verify
|
||||
Revises: 0007_caldav_and_fixes
|
||||
Create Date: 2026-03-27
|
||||
|
||||
Security fix H-07: make LDAP certificate validation configurable.
|
||||
Default is False for backwards compatibility; set True in production
|
||||
to enforce CERT_REQUIRED and prevent MITM attacks.
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0008_ldap_tls_verify"
|
||||
down_revision = "0007_caldav_and_fixes"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"ldap_configs",
|
||||
sa.Column("tls_verify", sa.Boolean(), nullable=False, server_default="false"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("ldap_configs", "tls_verify")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""add correction_note to absences
|
||||
|
||||
Revision ID: 0009
|
||||
Revises: 0008
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '0009_absence_correction_note'
|
||||
down_revision = '0008_ldap_tls_verify'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('absences', sa.Column('correction_note', sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('absences', 'correction_note')
|
||||
@@ -0,0 +1,23 @@
|
||||
"""add is_high_rate to public_holidays
|
||||
|
||||
Revision ID: 0010
|
||||
Revises: 0009_absence_correction_note
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '0010_public_holidays'
|
||||
down_revision = '0009_absence_correction_note'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('public_holidays', sa.Column(
|
||||
'is_high_rate', sa.Boolean(), nullable=False, server_default='false'
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('public_holidays', 'is_high_rate')
|
||||
@@ -0,0 +1,23 @@
|
||||
"""add name_format to caldav_company_configs
|
||||
|
||||
Revision ID: 0011
|
||||
Revises: 0010_public_holidays
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '0011_caldav_name_format'
|
||||
down_revision = '0010_public_holidays'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('caldav_company_configs', sa.Column(
|
||||
'name_format', sa.String(20), nullable=False, server_default='full'
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('caldav_company_configs', 'name_format')
|
||||
@@ -0,0 +1,54 @@
|
||||
"""caldav name_template + users.kuerzel
|
||||
|
||||
Revision ID: 0012
|
||||
Revises: 0011_caldav_name_format
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = '0012_caldav_template_and_kuerzel'
|
||||
down_revision = '0011_caldav_name_format'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# Mapping alter Festwert → neues Template
|
||||
_MIGRATION_MAP = {
|
||||
'full': '$vorname $nachname – $typ',
|
||||
'short': '$vorname_short. $nachname – $typ',
|
||||
'initials': '$kuerzel – $typ',
|
||||
'type_only': '$typ',
|
||||
}
|
||||
_DEFAULT = '$vorname $nachname – $typ'
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Neue Spalte name_template hinzufügen
|
||||
op.add_column('caldav_company_configs', sa.Column(
|
||||
'name_template', sa.Text(), nullable=False,
|
||||
server_default=_DEFAULT,
|
||||
))
|
||||
|
||||
# 2. Bestehende Werte aus name_format migrieren
|
||||
for old_val, new_val in _MIGRATION_MAP.items():
|
||||
op.execute(
|
||||
f"UPDATE caldav_company_configs "
|
||||
f"SET name_template = '{new_val}' "
|
||||
f"WHERE name_format = '{old_val}'"
|
||||
)
|
||||
|
||||
# 3. Alte Spalte entfernen
|
||||
op.drop_column('caldav_company_configs', 'name_format')
|
||||
|
||||
# 4. Kürzel-Spalte zu users hinzufügen
|
||||
op.add_column('users', sa.Column(
|
||||
'kuerzel', sa.String(20), nullable=True
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.add_column('caldav_company_configs', sa.Column(
|
||||
'name_format', sa.String(20), nullable=False, server_default='full'
|
||||
))
|
||||
op.drop_column('caldav_company_configs', 'name_template')
|
||||
op.drop_column('users', 'kuerzel')
|
||||
@@ -0,0 +1,44 @@
|
||||
"""projects table + time_entries FK
|
||||
|
||||
Revision ID: 0013_projects
|
||||
Revises: 0012_caldav_template_and_kuerzel
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "0013_projects"
|
||||
down_revision = "0012_caldav_template_and_kuerzel"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Projekte-Tabelle
|
||||
op.create_table(
|
||||
"projects",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("company_id", UUID(as_uuid=True),
|
||||
sa.ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||
sa.Column("name", sa.String(100), nullable=False),
|
||||
sa.Column("description", sa.Text, nullable=True),
|
||||
sa.Column("color", sa.String(7), nullable=False, server_default="#3B82F6"),
|
||||
sa.Column("budget_hours", sa.Numeric(8, 2), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean, nullable=False, server_default="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()),
|
||||
)
|
||||
|
||||
# FK von time_entries.project_id → projects.id
|
||||
op.create_foreign_key(
|
||||
"fk_time_entries_project_id",
|
||||
"time_entries", "projects",
|
||||
["project_id"], ["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("fk_time_entries_project_id", "time_entries", type_="foreignkey")
|
||||
op.drop_table("projects")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""remove projects table
|
||||
|
||||
Revision ID: 0014_remove_projects
|
||||
Revises: 0013_projects
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "0014_remove_projects"
|
||||
down_revision = "0013_projects"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.drop_constraint("fk_time_entries_project_id", "time_entries", type_="foreignkey")
|
||||
op.drop_table("projects")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,23 @@
|
||||
"""add TOTP fields to users
|
||||
|
||||
Revision ID: 0015_totp
|
||||
Revises: 0014_remove_projects
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0015_totp"
|
||||
down_revision = "0014_remove_projects"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("users", sa.Column("totp_secret", sa.String(64), nullable=True))
|
||||
op.add_column("users", sa.Column("totp_enabled", sa.Boolean(), nullable=False, server_default="false"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("users", "totp_enabled")
|
||||
op.drop_column("users", "totp_secret")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""add special_days to vacation_balances
|
||||
|
||||
Revision ID: 0016_vacation_special_days
|
||||
Revises: 0015_totp
|
||||
Create Date: 2026-03-28
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0016_vacation_special_days"
|
||||
down_revision = "0015_totp"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"vacation_balances",
|
||||
sa.Column("special_days", sa.Integer(), nullable=False, server_default="0"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("vacation_balances", "special_days")
|
||||
@@ -0,0 +1,35 @@
|
||||
"""add kiosk_devices table
|
||||
|
||||
Revision ID: 0018_kiosk_devices
|
||||
Revises: 0016_vacation_special_days
|
||||
Create Date: 2026-04-06
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "0018_kiosk_devices"
|
||||
down_revision = "0016_vacation_special_days"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"kiosk_devices",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("company_id", UUID(as_uuid=True),
|
||||
sa.ForeignKey("companies.id", ondelete="CASCADE"), nullable=False),
|
||||
sa.Column("name", sa.String(255), nullable=False),
|
||||
sa.Column("location", sa.String(255), nullable=True),
|
||||
sa.Column("token_hash", sa.String(64), unique=True, nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
||||
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()")),
|
||||
)
|
||||
op.create_index("ix_kiosk_devices_company_id", "kiosk_devices", ["company_id"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_kiosk_devices_company_id", table_name="kiosk_devices")
|
||||
op.drop_table("kiosk_devices")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""add can_manual_time_entry to users
|
||||
|
||||
Revision ID: 0019
|
||||
Revises: 0018
|
||||
Create Date: 2026-04-06
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0019"
|
||||
down_revision = "0018_kiosk_devices"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column("can_manual_time_entry", sa.Boolean(), nullable=False, server_default="false"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("users", "can_manual_time_entry")
|
||||
@@ -0,0 +1,80 @@
|
||||
"""add personnel_number to users + company config
|
||||
|
||||
Revision ID: 0020
|
||||
Revises: 0019
|
||||
Create Date: 2026-05-05
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0020"
|
||||
down_revision = "0019"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# users.personnel_number with format constraint
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column("personnel_number", sa.String(length=50), nullable=True),
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_users_personnel_number_digits",
|
||||
"users",
|
||||
"personnel_number IS NULL OR personnel_number ~ '^[0-9]+$'",
|
||||
)
|
||||
# Partial unique index per company (NULLs allowed, reserved on deactivation)
|
||||
op.create_index(
|
||||
"ix_users_company_personnel_number",
|
||||
"users",
|
||||
["company_id", "personnel_number"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("personnel_number IS NOT NULL"),
|
||||
)
|
||||
|
||||
# companies config
|
||||
op.add_column(
|
||||
"companies",
|
||||
sa.Column(
|
||||
"personnel_number_required",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default="false",
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"companies",
|
||||
sa.Column(
|
||||
"personnel_number_mode",
|
||||
sa.String(length=10),
|
||||
nullable=False,
|
||||
server_default="manual",
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"companies",
|
||||
sa.Column(
|
||||
"personnel_number_next",
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default="1",
|
||||
),
|
||||
)
|
||||
|
||||
# ldap_configs.attr_personnel_number (optional mapping)
|
||||
op.add_column(
|
||||
"ldap_configs",
|
||||
sa.Column("attr_personnel_number", sa.String(length=100), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("ldap_configs", "attr_personnel_number")
|
||||
op.drop_column("companies", "personnel_number_next")
|
||||
op.drop_column("companies", "personnel_number_mode")
|
||||
op.drop_column("companies", "personnel_number_required")
|
||||
op.drop_index("ix_users_company_personnel_number", table_name="users")
|
||||
op.drop_constraint("ck_users_personnel_number_digits", "users", type_="check")
|
||||
op.drop_column("users", "personnel_number")
|
||||
@@ -0,0 +1,30 @@
|
||||
"""add sick_note_required_after_days to companies
|
||||
|
||||
Revision ID: 0022
|
||||
Revises: 0020
|
||||
Create Date: 2026-05-06
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0022"
|
||||
down_revision = "0020"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"companies",
|
||||
sa.Column(
|
||||
"sick_note_required_after_days",
|
||||
sa.Integer(),
|
||||
nullable=False,
|
||||
server_default="3",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("companies", "sick_note_required_after_days")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""add busylight_pull_token_hash + created_at to companies
|
||||
|
||||
Revision ID: 0023
|
||||
Revises: 0022
|
||||
Create Date: 2026-05-06
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0023"
|
||||
down_revision = "0022"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"companies",
|
||||
sa.Column("busylight_pull_token_hash", sa.String(length=64), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"companies",
|
||||
sa.Column("busylight_token_created_at", sa.DateTime(timezone=True), nullable=True),
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"uq_companies_busylight_pull_token_hash",
|
||||
"companies",
|
||||
["busylight_pull_token_hash"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint("uq_companies_busylight_pull_token_hash", "companies", type_="unique")
|
||||
op.drop_column("companies", "busylight_token_created_at")
|
||||
op.drop_column("companies", "busylight_pull_token_hash")
|
||||
Reference in New Issue
Block a user