mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 16:03:41 +00:00
- 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>
193 lines
6.5 KiB
Python
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()
|