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
+23 -1
View File
@@ -1,7 +1,7 @@
from datetime import date, timedelta
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession
@@ -152,6 +152,28 @@ async def export_time_report(
headers={"Content-Disposition": f"attachment; filename={filename}.csv"})
@router.get("/reports/datev-monthly/export")
async def export_datev_monthly(
current_user: CurrentUser,
user_id: UUID,
year: int = Query(..., ge=2000, le=2100),
month: int = Query(..., ge=1, le=12),
db: AsyncSession = Depends(get_db),
):
"""DATEV-Monatsblatt (Blanko-Layout) als PDF für Steuerberater-Übergabe.
EMPLOYEE darf nur den eigenen Monat exportieren."""
if current_user.role not in _manager_roles and user_id != current_user.id:
raise HTTPException(403, "Nur eigenes Monatsblatt exportierbar")
sheet = await report_service.datev_monthly_report(
current_user.company_id, user_id, year, month, db
)
content = report_service.datev_monthly_report_to_pdf(sheet)
filename = f"datev_stundenaufzeichnung_{sheet.user_name.replace(' ', '_')}_{year}_{month:02d}.pdf"
return Response(content=content, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}"})
@router.get("/reports/absences/export")
async def export_absence_report(
current_user: CurrentUser,