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
+66
View File
@@ -0,0 +1,66 @@
"""TEMPLATE Mustervorlage für Router-Tests. Abgeleitet von test_hours_payouts.py.
Anleitung:
1. Nach backend/tests/test_xxx_things.py kopieren, Endpunkte/Felder anpassen
2. pytest-Konvention: alle Fixtures scope="session" + loop_scope="session"
(asyncpg + pytest-asyncio 1.x Anforderung siehe project_pytest_asyncio)
3. Ausführen NUR auf dem Server (root@192.168.1.137), nie lokal:
ssh root@192.168.1.137 'cd /opt/timemaster/backend && source venv/bin/activate && python -m pytest tests/test_xxx_things.py -v'
"""
import pytest
import pytest_asyncio
from httpx import AsyncClient
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def xxx_thing_headers(client: AsyncClient):
resp = await client.post("/api/v1/auth/register", json={
"company_name": "XxxThing GmbH",
"first_name": "Test",
"last_name": "User",
"email": "admin@xxxthinggmbh.de",
"password": "Secret123",
})
assert resp.status_code == 201, resp.text
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
@pytest.mark.asyncio(loop_scope="session")
async def test_create_item(client: AsyncClient, xxx_thing_headers):
r = await client.post("/api/v1/xxx-things",
json={"user_id": "...", "note": "Test"}, headers=xxx_thing_headers)
assert r.status_code == 201, r.text
assert r.json()["status"] == "requested"
@pytest.mark.asyncio(loop_scope="session")
async def test_approve_item(client: AsyncClient, xxx_thing_headers):
create = await client.post("/api/v1/xxx-things",
json={"user_id": "...", "note": "Test"}, headers=xxx_thing_headers)
item_id = create.json()["id"]
ap = await client.post(f"/api/v1/xxx-things/{item_id}/approve", json={}, headers=xxx_thing_headers)
assert ap.status_code == 200, ap.text
assert ap.json()["status"] == "approved"
# Doppel-Approve muss scheitern (409)
ap2 = await client.post(f"/api/v1/xxx-things/{item_id}/approve", json={}, headers=xxx_thing_headers)
assert ap2.status_code == 409
@pytest.mark.asyncio(loop_scope="session")
async def test_company_isolation(client: AsyncClient, xxx_thing_headers):
"""Cross-Tenant-Zugriff muss 404 liefern, nicht 200/403 (verrät keine Existenz)."""
other = await client.post("/api/v1/auth/register", json={
"company_name": "Andere Firma GmbH",
"first_name": "Other", "last_name": "Admin",
"email": "admin@andere-firma.de", "password": "Secret123",
})
other_headers = {"Authorization": f"Bearer {other.json()['access_token']}"}
create = await client.post("/api/v1/xxx-things",
json={"user_id": "...", "note": "Geheim"}, headers=xxx_thing_headers)
item_id = create.json()["id"]
r = await client.get("/api/v1/xxx-things", headers=other_headers)
assert all(item["id"] != item_id for item in r.json()["items"])