Files
timemaster/backend/app/routers/reports.py
T
patrickandClaude Sonnet 5 5cbeebe66d
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled
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
2026-08-06 22:19:54 +02:00

233 lines
10 KiB
Python

from datetime import date, timedelta
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.dependencies import CurrentUser, require_role
from app.models.user import User, UserRole
from app.schemas.report import (
AbsenceReport,
CompanyDashboard,
EmployeeDashboard,
OvertimeReport,
OvertimeReportDetailed,
TeamDashboard,
TimeReport,
)
from app.services.report_service import report_service
router = APIRouter(tags=["Dashboard & Reports"])
_manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
_admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
# ── Dashboard ──────────────────────────────────────────────────────────────────
@router.get("/dashboard/me", response_model=EmployeeDashboard)
async def my_dashboard(
current_user: CurrentUser,
db: AsyncSession = Depends(get_db),
):
"""Mitarbeiter-Dashboard: eigene Stunden, Urlaub, Status heute."""
return await report_service.employee_dashboard(current_user, db)
@router.get("/dashboard/team", response_model=TeamDashboard)
async def team_dashboard(
current_user: User = require_role(*_manager_roles),
db: AsyncSession = Depends(get_db),
):
"""Team-Dashboard: Anwesenheit, ausstehende Genehmigungen."""
return await report_service.team_dashboard(current_user, db)
@router.get("/dashboard/company", response_model=CompanyDashboard)
async def company_dashboard(
current_user: User = require_role(*_admin_roles),
db: AsyncSession = Depends(get_db),
):
"""Unternehmens-Dashboard: Gesamtübersicht, Überstunden, kommende Abwesenheiten."""
return await report_service.company_dashboard(current_user, db)
# ── Reports ────────────────────────────────────────────────────────────────────
def _default_date_from() -> date:
today = date.today()
return today.replace(day=1)
def _default_date_to() -> date:
return date.today()
@router.get("/reports/time", response_model=TimeReport)
async def time_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Zeiterfassungsbericht (JSON). EMPLOYEE sieht nur eigene Einträge."""
return await report_service.time_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
@router.get("/reports/absences", response_model=AbsenceReport)
async def absence_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Abwesenheitsbericht (JSON). EMPLOYEE sieht nur eigene Abwesenheiten."""
return await report_service.absence_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
@router.get("/reports/overtime", response_model=OvertimeReport)
async def overtime_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Überstundenbericht (JSON). EMPLOYEE sieht nur eigene Daten."""
return await report_service.overtime_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
@router.get("/reports/overtime/detail", response_model=OvertimeReportDetailed)
async def overtime_report_detail(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
db: AsyncSession = Depends(get_db),
):
"""Erweiterter Überstundenbericht mit Wochen- und Tagesaufschlüsselung."""
return await report_service.overtime_report_detail(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
# ── Export ─────────────────────────────────────────────────────────────────────
@router.get("/reports/time/export")
async def export_time_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
format: str = Query("csv", pattern="^(csv|xlsx|pdf)$"),
db: AsyncSession = Depends(get_db),
):
"""Zeiterfassungsbericht als CSV, XLSX oder PDF herunterladen."""
report = await report_service.time_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
filename = f"zeiterfassung_{date_from}_{date_to}"
if format == "pdf":
content = report_service.time_report_to_pdf(report)
return Response(content=content, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}.pdf"})
if format == "xlsx":
content = report_service.to_xlsx(report_service._time_rows_to_dicts(report.rows), sheet_name="Zeiterfassung")
return Response(content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}.xlsx"})
content = report_service.to_csv(report_service._time_rows_to_dicts(report.rows))
return Response(content=content, media_type="text/csv; charset=utf-8",
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,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
format: str = Query("csv", pattern="^(csv|xlsx|pdf)$"),
db: AsyncSession = Depends(get_db),
):
"""Abwesenheitsbericht als CSV, XLSX oder PDF herunterladen."""
report = await report_service.absence_report(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
filename = f"abwesenheiten_{date_from}_{date_to}"
if format == "pdf":
content = report_service.absence_report_to_pdf(report)
return Response(content=content, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}.pdf"})
if format == "xlsx":
content = report_service.to_xlsx(report_service._absence_rows_to_dicts(report.rows), sheet_name="Abwesenheiten")
return Response(content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}.xlsx"})
content = report_service.to_csv(report_service._absence_rows_to_dicts(report.rows))
return Response(content=content, media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"})
@router.get("/reports/overtime/export")
async def export_overtime_report(
current_user: CurrentUser,
date_from: date = Query(default_factory=_default_date_from),
date_to: date = Query(default_factory=_default_date_to),
user_id: UUID | None = Query(None),
format: str = Query("csv", pattern="^(csv|xlsx|pdf)$"),
db: AsyncSession = Depends(get_db),
):
"""Überstundenbericht als CSV, XLSX oder PDF herunterladen (Detailansicht)."""
detail = await report_service.overtime_report_detail(
current_user.company_id, current_user, db, date_from, date_to, user_id
)
filename = f"ueberstunden_{date_from}_{date_to}"
if format == "pdf":
content = report_service.overtime_detail_to_pdf(detail)
return Response(content=content, media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename={filename}.pdf"})
if format == "xlsx":
content = report_service.to_xlsx(report_service._overtime_detail_to_dicts(detail), sheet_name="Überstunden")
return Response(content=content,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}.xlsx"})
content = report_service.to_csv(report_service._overtime_detail_to_dicts(detail))
return Response(content=content, media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f"attachment; filename={filename}.csv"})