dak.c2s/backend/app/services/wochenuebersicht_export.py
CCS Admin 6f6a721973 feat: add Wochenübersicht export + ICD import auto-detect
- New Excel export service for weekly DAK summary sheets (c2s / c2s_g_s variants)
- New API endpoint GET /reports/wochenuebersicht (admin-only)
- ICD import auto-detects format (coding template vs. Wochenübersicht KVNR-based)
- New admin frontend page with download form
- Route + sidebar navigation entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:28:17 +00:00

193 lines
6.5 KiB
Python

"""Wochenübersicht Excel export for DAK weekly summary sheets.
Generates .xlsx files matching the format sent to DAK for ICD code entry.
Two variants exist:
- "c2s" → Fallgruppen onko + intensiv (columns ZMO / ZMI)
- "c2s_g_s" → Fallgruppen galle + sd (columns Galle / Schild)
Each KW block has 4 header rows + data rows + 2 blank separator rows.
KWs are sorted descending (newest first).
"""
from __future__ import annotations
from datetime import date
from io import BytesIO
from typing import Any
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from app.services.excel_export import HEADER_FILL, HEADER_FONT
# ---------------------------------------------------------------------------
# Type configuration
# ---------------------------------------------------------------------------
WOCHENUEBERSICHT_TYPES: dict[str, dict[str, Any]] = {
"c2s": {
"fallgruppen": ("onko", "intensiv"),
"fg_labels": ("ZMO", "ZMI"),
"filename_infix": "c2s",
},
"c2s_g_s": {
"fallgruppen": ("galle", "sd"),
"fg_labels": ("Galle", "Schild"),
"filename_infix": "c2s_g_s",
},
}
# Column headers (row 4 of each KW block)
COL_HEADERS = [
"KVNR", # A
"Datum", # B
"Erstgespräch", # C
"Zweitmeinung/Vorbereitung (bei Abbruch)", # D
"Zweitmeinung/Vorbereitung + Erteilung", # E
"Schriftliche Dokumentation (Gutachten)", # F
# G and H are dynamic (FG labels)
# I = ICD-10
]
THIN_BORDER = Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin"),
)
def _kw_date_range(jahr: int, kw: int) -> tuple[date, date]:
"""Return (Monday, Sunday) for the given ISO year/week."""
monday = date.fromisocalendar(jahr, kw, 1)
sunday = date.fromisocalendar(jahr, kw, 7)
return monday, sunday
def _format_date_range(monday: date, sunday: date) -> str:
"""Format as 'DD.MM. - DD.MM.YYYY'."""
return f"{monday.strftime('%d.%m.')} - {sunday.strftime('%d.%m.%Y')}"
def generate_wochenuebersicht_xlsx(
cases_by_kw: dict[int, list[Any]],
export_type: str,
jahr: int,
) -> bytes:
"""Generate a Wochenübersicht Excel workbook.
Args:
cases_by_kw: Cases grouped by calendar week number.
export_type: One of ``"c2s"`` or ``"c2s_g_s"``.
jahr: The report year.
Returns:
The ``.xlsx`` file contents as bytes.
"""
type_cfg = WOCHENUEBERSICHT_TYPES[export_type]
fg1, fg2 = type_cfg["fallgruppen"]
fg1_label, fg2_label = type_cfg["fg_labels"]
wb = Workbook()
ws = wb.active
ws.title = "Wochenübersicht"
# Column widths
ws.column_dimensions["A"].width = 14
ws.column_dimensions["B"].width = 12
ws.column_dimensions["C"].width = 14
ws.column_dimensions["D"].width = 18
ws.column_dimensions["E"].width = 18
ws.column_dimensions["F"].width = 16
ws.column_dimensions["G"].width = 10
ws.column_dimensions["H"].width = 10
ws.column_dimensions["I"].width = 14
current_row = 1
# Sort KWs descending (newest first)
sorted_kws = sorted(cases_by_kw.keys(), reverse=True)
for kw in sorted_kws:
cases = cases_by_kw[kw]
monday, sunday = _kw_date_range(jahr, kw)
# --- Row 1: "Kalenderwoche:" label + number ---
ws.cell(row=current_row, column=5, value="Kalenderwoche:")
ws.cell(row=current_row, column=5).font = HEADER_FONT
ws.cell(row=current_row, column=6, value=kw)
ws.cell(row=current_row, column=6).font = HEADER_FONT
current_row += 1
# --- Row 2: Date range ---
ws.cell(row=current_row, column=5, value=_format_date_range(monday, sunday))
ws.cell(row=current_row, column=5).font = Font(italic=True)
current_row += 1
# --- Row 3: Instruction ---
ws.cell(
row=current_row,
column=3,
value="Bitte entsprechendes Feld mit x kennzeichnen",
)
ws.cell(row=current_row, column=3).font = Font(italic=True, color="FF808080")
current_row += 1
# --- Row 4: Column headers ---
headers = COL_HEADERS + [fg1_label, fg2_label, "ICD-10"]
for ci, header in enumerate(headers, start=1):
cell = ws.cell(row=current_row, column=ci, value=header)
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.border = THIN_BORDER
cell.alignment = Alignment(wrap_text=True, vertical="center")
current_row += 1
# --- Data rows ---
# Sort cases by datum ascending within each KW
sorted_cases = sorted(cases, key=lambda c: (c.datum or date.min))
for case in sorted_cases:
# A: KVNR
ws.cell(row=current_row, column=1, value=case.kvnr or "")
# B: Datum
ws.cell(
row=current_row,
column=2,
value=case.datum.strftime("%d.%m.%Y") if case.datum else "",
)
# C: Erstgespräch — always x
ws.cell(row=current_row, column=3, value="x")
# D: Zweitmeinung/Vorbereitung (bei Abbruch)
if case.ablehnung or case.abbruch:
ws.cell(row=current_row, column=4, value="x")
# E: Zweitmeinung/Vorbereitung + Erteilung
if case.unterlagen and not case.ablehnung and not case.abbruch:
ws.cell(row=current_row, column=5, value="x")
# F: Schriftliche Dokumentation (Gutachten)
if case.gutachten:
ws.cell(row=current_row, column=6, value="x")
# G: FG1 (ZMO / Galle)
if case.fallgruppe == fg1:
ws.cell(row=current_row, column=7, value="x")
# H: FG2 (ZMI / Schild)
if case.fallgruppe == fg2:
ws.cell(row=current_row, column=8, value="x")
# I: ICD-10
ws.cell(row=current_row, column=9, value=case.icd or "")
# Apply thin borders to data row
for ci in range(1, 10):
ws.cell(row=current_row, column=ci).border = THIN_BORDER
ws.cell(row=current_row, column=ci).alignment = Alignment(
horizontal="center" if ci >= 3 else "left"
)
current_row += 1
# --- 2 blank separator rows ---
current_row += 2
buf = BytesIO()
wb.save(buf)
return buf.getvalue()