feat(reports): DATEV-Monatsblatt-Export (Blanko-Vorlage) für Steuerberater

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
This commit is contained in:
2026-08-06 22:19:54 +02:00
co-authored by Claude Sonnet 5
parent f3ed234e56
commit 1dcc04ae07
10 changed files with 901 additions and 2 deletions
+69
View File
@@ -0,0 +1,69 @@
"""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")