Neuer PDF-Export GET /reports/datev-monthly/export: ein Blatt pro Mitarbeiter/Monat im Layout der DATEV-Stundenaufzeichnungs-Vorlage (Beginn/Pause/Ende/Dauer + K/U/UU/F/SA/SU-Kürzel + Bemerkungen + Summe + Unterschriftsfelder). Führt time_entries, Absences und Feiertage pro Kalendertag zusammen. Außerdem: Mustervorlagen für neue Backend-Module (Model/Schema/Router/ Migration/Test) und eine Frontend-Page-Vorlage, abgeleitet vom hours_payouts-Modul als aktuellstem sauberen Muster. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CviFgc739S89xS97pvjszj
70 lines
3.0 KiB
Python
70 lines
3.0 KiB
Python
"""TEMPLATE – Mustervorlage für Migration mit NEUER Tabelle + RLS-Policy.
|
||
|
||
Abgeleitet vom Muster in migrations/versions/0035_absence_comments.py.
|
||
|
||
WICHTIG (siehe project_dsgvo_multitenant_priority):
|
||
- JEDE neue company-bezogene Tabelle braucht company_id-Spalte + RLS-Policy
|
||
- NACH dieser Migration: tests/conftest.py muss die RLS-Policy manuell
|
||
replizieren (Test-DB nutzt keine echten Migrationen) – sonst schlagen
|
||
Company-Isolation-Tests unbemerkt fehl (false green)
|
||
- Nur neue SPALTEN auf bereits RLS-gefencten Tabellen brauchen KEINE
|
||
RLS-Änderung (siehe 0042_payout_requests.py als Gegenbeispiel)
|
||
|
||
Anleitung:
|
||
1. Nach backend/migrations/versions/00XX_xxx_things.py kopieren
|
||
2. revision/down_revision auf nächste freie Nummer setzen (siehe
|
||
"Datenbank-Migrationen – Chronik" in docmost für die aktuell höchste Nummer)
|
||
3. Tabellenname/Spalten anpassen
|
||
4. conftest.py RLS-Replikation ergänzen
|
||
5. Lokal (auf 137!) testen: alembic upgrade head, dann pytest
|
||
"""
|
||
from alembic import op
|
||
from sqlalchemy import text
|
||
|
||
revision = "00XX" # TODO: nächste freie Nummer
|
||
down_revision = "00XX-1" # TODO: vorherige Migration
|
||
branch_labels = None
|
||
depends_on = None
|
||
|
||
_BYPASS = "COALESCE(current_setting('app.bypass_rls', true), 'off') = 'on'"
|
||
_CID = "company_id = NULLIF(current_setting('app.company_id', true), '')::uuid"
|
||
_USING = f"({_BYPASS} OR {_CID})"
|
||
|
||
|
||
def _exec(sql: str) -> None:
|
||
op.execute(text(sql))
|
||
|
||
|
||
def upgrade() -> None:
|
||
# 1) Tabelle anlegen
|
||
_exec("""
|
||
CREATE TABLE IF NOT EXISTS xxx_things (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
note TEXT,
|
||
status VARCHAR(12) NOT NULL DEFAULT 'requested',
|
||
rejection_reason TEXT,
|
||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE SET NULL,
|
||
decided_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||
decided_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||
)
|
||
""")
|
||
_exec("CREATE INDEX IF NOT EXISTS ix_xxx_things_company_id ON xxx_things(company_id)")
|
||
_exec("CREATE INDEX IF NOT EXISTS ix_xxx_things_user_id ON xxx_things(user_id)")
|
||
|
||
# 2) RLS (company_id-gefenced)
|
||
_exec("ALTER TABLE xxx_things ENABLE ROW LEVEL SECURITY")
|
||
_exec("ALTER TABLE xxx_things FORCE ROW LEVEL SECURITY")
|
||
for cmd in ("select", "insert", "update", "delete"):
|
||
_exec(f"DROP POLICY IF EXISTS rls_xxx_things_{cmd} ON xxx_things")
|
||
_exec(f"CREATE POLICY rls_xxx_things_select ON xxx_things FOR SELECT USING {_USING}")
|
||
_exec(f"CREATE POLICY rls_xxx_things_insert ON xxx_things FOR INSERT WITH CHECK {_USING}")
|
||
_exec(f"CREATE POLICY rls_xxx_things_update ON xxx_things FOR UPDATE USING {_USING} WITH CHECK {_USING}")
|
||
_exec(f"CREATE POLICY rls_xxx_things_delete ON xxx_things FOR DELETE USING {_USING}")
|
||
|
||
|
||
def downgrade() -> None:
|
||
_exec("DROP TABLE IF EXISTS xxx_things")
|