#!/usr/bin/env python3 """ poukaz_dp_gen.py Generátor Poukazu na vyšetření/ošetření DP (VZP-06dp/2024). Formulář je kreslen od nuly pomocí reportlab – žádné překrývání PDF šablony. Závislosti: pip install reportlab """ from pathlib import Path from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont # ── Registrace fontu s českou podporou ──────────────────────────────────────── def _reg_font(): candidates = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/arialbd.ttf", ] reg, reg_bold = False, False for p in candidates: if Path(p).exists(): if not reg and "Bold" not in p and "bd" not in p: pdfmetrics.registerFont(TTFont("Czech", p)) reg = True elif not reg_bold and ("Bold" in p or "bd" in p): pdfmetrics.registerFont(TTFont("Czech-Bold", p)) reg_bold = True if not reg: # fallback – Helvetica (funguje pro základní znaky) return "Helvetica", "Helvetica-Bold" if not reg_bold: return "Czech", "Czech" return "Czech", "Czech-Bold" FONT, FONT_BOLD = _reg_font() # ═══════════════════════════════════════════════════════════════════════════════ # DATA FORMULÁŘE – sem doplňte hodnoty z Medicusu # ═══════════════════════════════════════════════════════════════════════════════ DATA = { # ── Hlavička ────────────────────────────────────────────────────────────── "kod_pojistovny": "111", # 3 číslice, VZP = 111 "icp": "12345678", # IČP pracoviště (max 8 číslic) "odbornost": "001", # odbornost (3 číslice) "datum": "22.05.2026", # datum vystavení "poradove_cislo": "42", # pořadové číslo / nepřerušené DP "platnost_do": "22.08.2026", # platnost poukazu # ── Pacient ─────────────────────────────────────────────────────────────── "pacient": "Novák Jan", "cislo_pojistence": "7001015432", # rodné číslo / č. pojištěnce "zkladni_dg": "I10", # MKN kód základní diagnózy "variabilni_symbol": "123456", "ost_dg": "E11", # ostatní diagnózy "kod_nahrady": "", # ── Adresa a kontakty ───────────────────────────────────────────────────── "adresa_pacienta": "Dlouhá 12, 110 00 Praha 1, tel: 777 123 456", "dalsi_prislusnici": "ne", # "ano" / "ne" "kontaktni_osoba": "Nováková Marie (manželka), tel: 777 654 321", # ── Klinické údaje ──────────────────────────────────────────────────────── "pecovatelska_sluzba": "ne", # "ano" / "ne" "mobilita": "b", # "a" = plná / "b" = omezená "mobilita_detail": "chodí s holí", "smyslove_omezeni": "zraková vada – brýle", "sebeobsluha": "b", # "a" = plná / "b" = omezená "sebeobsluha_detail": "potřebuje pomoc při hygieně", # Každá položka = jeden řádek (max 3) "medikace": [ "Metformin 1000 mg 1-0-1, Amlodipine 5 mg 1-0-0", "Inzulín Lantus 20j večer", ], "dalsi_informace": [ "alergie: penicilin; inkontinence moči", "byt 2. patro bez výtahu", ], "cil_dp": [ "Edukace v aplikaci inzulínu, kontrola glykémie, péče o DM nohu", ], # ── Požadované výkony (max 5) ───────────────────────────────────────────── # kod … kód výkonu (max 5 číslic) # popis … text na prvním řádku # popis2 … pokračování na druhém řádku (nepovinné) "pozadovano": [ {"kod": "06101", "popis": "Komplexní ošetřovatelská péče – 1× denně, 5× týdně", "popis2": ""}, {"kod": "06129", "popis": "Aplikace inzulínu – 1× denně, 7× týdně", "popis2": ""}, {"kod": "06111", "popis": "Odběr biologického materiálu – 1× týdně", "popis2": ""}, {"kod": "", "popis": "", "popis2": ""}, {"kod": "", "popis": "", "popis2": ""}, ], } # ═══════════════════════════════════════════════════════════════════════════════ # GEOMETRIE (souřadnice od SHORA, konverze na RL přes yt()) # ═══════════════════════════════════════════════════════════════════════════════ PAGE_W, PAGE_H = A4 # 595 × 842 pt # Vnější zaoblený rámeček FX1, FX2 = 28, 567 # levý / pravý okraj FB, FT = 52, 818 # spodní / horní okraj (RL souřadnice od zdola) IL = FX1 + 7 # inner left (odsazení textu od okraje) IR = FX2 - 7 # inner right def yt(from_top: float) -> float: """Převede y-od-shora (pt) na reportlab y-od-zdola.""" return PAGE_H - from_top # ═══════════════════════════════════════════════════════════════════════════════ # POMOCNÉ FUNKCE # ═══════════════════════════════════════════════════════════════════════════════ def txt(c, x, y_rl, text, size=7.5, bold=False, align="left"): if not text: return c.setFont(FONT_BOLD if bold else FONT, size) if align == "right": c.drawRightString(x, y_rl, str(text)) elif align == "center": c.drawCentredString(x, y_rl, str(text)) else: c.drawString(x, y_rl, str(text)) def hline(c, x1, y_rl, x2=None, w=0.5): if x2 is None: x2 = IR c.setLineWidth(w) c.setDash([]) c.line(x1, y_rl, x2, y_rl) def vline(c, x, y1_rl, y2_rl, w=0.5): c.setLineWidth(w) c.setDash([]) c.line(x, y1_rl, x, y2_rl) def dotline(c, x1, y_rl, x2=None): if x2 is None: x2 = IR c.setLineWidth(0.35) c.setDash([1.5, 2.5]) c.line(x1, y_rl, x2, y_rl) c.setDash([]) def chkbox(c, x, y_rl, checked: bool, size=7): """Zaškrtávací políčko.""" c.setLineWidth(0.6) c.setDash([]) c.rect(x, y_rl - 1, size, size, stroke=1, fill=0) if checked: c.setFont(FONT_BOLD, size - 0.5) c.drawString(x + 1, y_rl, "✓") def digit_box(c, x, y_rl, width, height, digits, value_text=""): """Prostý rámeček pro zadávání hodnot (bez vnitřních oddělovačů).""" c.setLineWidth(0.7) c.setDash([]) c.rect(x, y_rl, width, height, stroke=1, fill=0) if value_text: c.setFont(FONT, height * 0.65) c.drawString(x + 3, y_rl + height * 0.2, value_text[:digits]) def wrap(text: str, max_chars: int) -> list[str]: """Rozlomí text na řádky po max_chars znacích (na hranici slova).""" if not text: return [""] words = text.split() lines, cur = [], "" for w in words: if cur and len(cur) + 1 + len(w) > max_chars: lines.append(cur) cur = w else: cur = (cur + " " + w).strip() if cur: lines.append(cur) return lines or [""] # ═══════════════════════════════════════════════════════════════════════════════ # SEKCE 1: HLAVIČKA # ═══════════════════════════════════════════════════════════════════════════════ # # ┌─────────────────┬──────────┬──────────────────────┬──────────────────────┐ # │ Kód pojišťovny │ požaduje │ IČP [________] Datum [________] │ Pořadové číslo… # │ [___] │ díl A │ Odbornost [___] │ nepřerušené DP: # └─────────────────┴──────────┴──────────────────────┴──────────────────────┘ # Platnost do: HDR_TOP = 25 # horní okraj formuláře (od shora) HDR_MID = 50 # vodorovná čára mezi řádky hlavičky HDR_BOT = 73 # dolní okraj hlavičky # Svislé dělicí čáry v hlavičce HX1 = 130 # po Kód pojišťovny HX2 = 198 # po požaduje díl A HX3 = 295 # mezi IČP a Datum HX4 = 388 # konec IČP+Datum sekce (začátek Pořadové číslo) def draw_hlavicka(c, d): # Vnější rámeček hlavičky hline(c, FX1, yt(HDR_BOT), FX2, w=1.2) hline(c, FX1, yt(HDR_MID), HX4, w=0.5) # Svislé čáry for x in (HX1, HX2, HX4): vline(c, x, yt(HDR_TOP), yt(HDR_BOT), w=0.8) vline(c, HX3, yt(HDR_TOP), yt(HDR_MID), w=0.5) # Kód pojišťovny txt(c, IL, yt(HDR_TOP + 7), "Kód pojišťovny", size=6.5) digit_box(c, IL, yt(HDR_MID - 5), 40, 12, 3, d.get("kod_pojistovny", "")) # požaduje díl A cx = (HX1 + HX2) / 2 txt(c, cx, yt(HDR_TOP + 12), "požaduje", size=7, bold=True, align="center") txt(c, cx, yt(HDR_TOP + 21), "díl A", size=7, bold=True, align="center") # IČP txt(c, HX2 + 3, yt(HDR_TOP + 7), "IČP", size=6.5) digit_box(c, HX2 + 22, yt(HDR_MID - 5), HX3 - HX2 - 27, 12, 8, d.get("icp", "")) # Datum txt(c, HX3 + 3, yt(HDR_TOP + 7), "Datum", size=6.5) digit_box(c, HX3 + 28, yt(HDR_MID - 5), HX4 - HX3 - 32, 12, 10, d.get("datum", "")) # Odbornost txt(c, HX2 + 3, yt(HDR_MID + 7), "Odbornost", size=6.5) digit_box(c, HX2 + 48, yt(HDR_BOT - 5), HX3 - HX2 - 52, 12, 3, d.get("odbornost", "")) # Pořadové číslo (pravý sloupec) txt(c, HX4 + 4, yt(HDR_TOP + 21), "Pořadové číslo poukazu", size=6.5) txt(c, HX4 + 4, yt(HDR_TOP + 30), "nepřerušené DP:", size=6.5) txt(c, HX4 + 80, yt(HDR_TOP + 36), d.get("poradove_cislo", ""), size=8) # Platnost do txt(c, HX4 + 4, yt(HDR_MID + 19), "Platnost do:", size=6.5) hline(c, HX4 + 50, yt(HDR_MID + 13), FX2 - 4, w=0.4) txt(c, HX4 + 54, yt(HDR_MID + 25), d.get("platnost_do", ""), size=8) # ═══════════════════════════════════════════════════════════════════════════════ # SEKCE 2: NADPIS # ═══════════════════════════════════════════════════════════════════════════════ TTL_TOP = HDR_BOT # 73 TTL_BOT = TTL_TOP + 22 # 95 def draw_nadpis(c): hline(c, FX1, yt(TTL_BOT), FX2, w=0.8) # "POUKAZ NA VYŠETŘENÍ / OŠETŘENÍ" c.setFont(FONT_BOLD, 12) c.drawCentredString(270, yt(TTL_TOP + 15), "POUKAZ NA VYŠETŘENÍ / OŠETŘENÍ") # "DP" – velké c.setFont(FONT_BOLD, 22) c.drawString(390, yt(TTL_TOP + 18), "DP") # ═══════════════════════════════════════════════════════════════════════════════ # SEKCE 3: BLOK PACIENTA # ═══════════════════════════════════════════════════════════════════════════════ PAC_TOP = TTL_BOT # 95 PAC_ROW = 19 # výška každého řádku PAC_BOT = PAC_TOP + 4 * PAC_ROW # 171 # Svislé dělicí čáry v bloku pacienta PX1 = 340 # Č. pojištěnce / Základní diagnóza PX2 = 340 # (sdílí s PX1 pro 2. a 3. řádek) PX3 = 410 # Ost. dg. / Kód náhrady # Šířka bloku pacienta (bez pravého sloupce s razítkem) PAC_RIGHT = 540 # pravý okraj tabulky pacienta def draw_pacient(c, d): # Vnější pravý okraj tabulky pacienta vline(c, PAC_RIGHT, yt(PAC_TOP), yt(PAC_BOT), w=1.0) # Řádek 1: Pacient y1 = yt(PAC_TOP + PAC_ROW) hline(c, FX1, y1, PAC_RIGHT, w=0.6) txt(c, IL, y1 + 5, "Pacient", size=7) txt(c, IL + 50, y1 + 5, d.get("pacient", ""), size=8.5) # Řádek 2: Č. pojištěnce | Základní diagnóza y2 = yt(PAC_TOP + 2 * PAC_ROW) hline(c, FX1, y2, PAC_RIGHT, w=0.6) vline(c, PX1, yt(PAC_TOP + PAC_ROW), y2, w=0.5) txt(c, IL, y2 + 5, "Č. pojištěnce", size=7) digit_box(c, IL + 68, y2 + 3, 130, 13, 10, d.get("cislo_pojistence", "")) txt(c, PX1 + 4, y2 + 5, "Základní diagnóza", size=7) digit_box(c, PX1 + 86, y2 + 3, 65, 13, 5, d.get("zkladni_dg", "")) # Řádek 3: Variabilní symbol | Ost. dg. | Kód náhrady y3 = yt(PAC_TOP + 3 * PAC_ROW) hline(c, FX1, y3, PAC_RIGHT, w=0.6) vline(c, PX2, yt(PAC_TOP + 2 * PAC_ROW), y3, w=0.5) vline(c, PX3, yt(PAC_TOP + 2 * PAC_ROW), y3, w=0.5) txt(c, IL, y3 + 5, "Variabilní symbol", size=7) digit_box(c, IL + 83, y3 + 3, 100, 13, 8, d.get("variabilni_symbol", "")) txt(c, PX2 + 4, y3 + 5, "Ost. dg.", size=7) digit_box(c, PX2 + 38, y3 + 3, 60, 13, 5, d.get("ost_dg", "")) txt(c, PX3 + 4, y3 + 5, "Kód náhrady", size=7) digit_box(c, PX3 + 54, y3 + 3, 50, 13, 4, d.get("kod_nahrady", "")) # Řádek 4: Ad zařízení domácí péče + razítko y4 = yt(PAC_BOT) hline(c, FX1, y4, PAC_RIGHT, w=0.6) txt(c, IL, y4 + 5, "Ad zařízení domácí péče:", size=7, bold=True) # razítko a podpis (pravý sloupec, malé kurzívní) c.setFont(FONT, 6) c.drawRightString(FX2 - 4, y4 + 4, "razítko a podpis požadujícího") hline(c, PAC_RIGHT + 5, y4 + 2, FX2 - 4, w=0.4) # ═══════════════════════════════════════════════════════════════════════════════ # SEKCE 4: TEXTOVÁ POLE # ═══════════════════════════════════════════════════════════════════════════════ def draw_textova_pole(c, d) -> float: """Vrací cur_top (od shora) po posledním poli.""" top = PAC_BOT + 6 # 177 od shora LBL = 7.5 # velikost fontu popisků VAL = 8.0 # velikost fontu hodnot GAP = 6 # mezera mezi sekcemi def label_and_value(label_text, value_text, label_width, max_chars=70): nonlocal top top += GAP y_rl = yt(top + 9) txt(c, IL, y_rl, label_text, size=LBL) txt(c, IL + label_width, y_rl, value_text[:max_chars] if value_text else "", size=VAL) dotline(c, IL + label_width, y_rl - 2) top += 16 def extra_dotline(value_text="", indent=0): nonlocal top top += 13 y_rl = yt(top) dotline(c, IL + indent, y_rl) if value_text: txt(c, IL + indent, y_rl + 3, value_text[:80], size=VAL) def checkbox_row(label_text, field_name, label_width=None): nonlocal top top += GAP y_rl = yt(top + 9) txt(c, IL, y_rl, label_text, size=LBL) lw = label_width or (len(label_text) * 4.2) cx = IL + lw + 5 val = d.get(field_name, "ne").lower() chkbox(c, cx, y_rl, checked=(val == "ano"), size=7) txt(c, cx + 9, y_rl, "ano", size=LBL) chkbox(c, cx + 34, y_rl, checked=(val == "ne"), size=7) txt(c, cx + 43, y_rl, "ne", size=LBL) top += 16 # ── Adresa pacienta ─────────────────────────────────────────────────────── adresa_lines = wrap(d.get("adresa_pacienta", ""), 75) label_and_value("Adresa pacienta (místo poskytování DP) a telefon:", adresa_lines[0], 260) extra_dotline(adresa_lines[1] if len(adresa_lines) > 1 else "") # ── Příslušníci domácnosti ──────────────────────────────────────────────── checkbox_row("Další příslušníci domácnosti na této adrese:", "dalsi_prislusnici", label_width=242) # ── Kontaktní osoba ─────────────────────────────────────────────────────── kont_lines = wrap(d.get("kontaktni_osoba", ""), 55) label_and_value( "Kontaktní osoba pro DP (jméno, vztah k pacientovi, adresa a telefon – je-li rozdílná od adresy pacienta):", kont_lines[0], 432, max_chars=55 ) extra_dotline(kont_lines[1] if len(kont_lines) > 1 else "") # ── Pečovatelská služba ─────────────────────────────────────────────────── checkbox_row("Pacient v péči pečovatelské služby:", "pecovatelska_sluzba", label_width=186) # ── Mobilita ────────────────────────────────────────────────────────────── top += GAP y_mob = yt(top + 9) txt(c, IL, y_mob, "Mobilita pacienta:", size=LBL) mob = d.get("mobilita", "a").lower() chkbox(c, IL + 98, y_mob, checked=(mob == "a"), size=7) txt(c, IL + 107, y_mob, "a) plná", size=LBL) top += 16 top += 2 y_mob2 = yt(top + 9) chkbox(c, IL + 98, y_mob2, checked=(mob == "b"), size=7) txt(c, IL + 107, y_mob2, "b) omezená:", size=LBL) mob_detail = d.get("mobilita_detail", "") if mob == "b" else "" txt(c, IL + 167, y_mob2, mob_detail[:60], size=VAL) dotline(c, IL + 163, y_mob2 - 2) top += 16 # ── Smyslové omezení ────────────────────────────────────────────────────── label_and_value("Smyslové omezení:", d.get("smyslove_omezeni", ""), 95) # ── Sebeobsluha ─────────────────────────────────────────────────────────── top += GAP y_sebe = yt(top + 9) txt(c, IL, y_sebe, "Schopnost základní sebeobsluhy, včetně dodržování léčebného režimu:", size=LBL) sebe = d.get("sebeobsluha", "a").lower() chkbox(c, IL + 374, y_sebe, checked=(sebe == "a"), size=7) txt(c, IL + 383, y_sebe, "a) plná", size=LBL) top += 16 top += 2 y_sebe2 = yt(top + 9) chkbox(c, IL + 98, y_sebe2, checked=(sebe == "b"), size=7) txt(c, IL + 107, y_sebe2, "b) omezená:", size=LBL) sebe_detail = d.get("sebeobsluha_detail", "") if sebe == "b" else "" txt(c, IL + 167, y_sebe2, sebe_detail[:60], size=VAL) dotline(c, IL + 163, y_sebe2 - 2) top += 16 # ── Medikace ────────────────────────────────────────────────────────────── med = d.get("medikace", []) if isinstance(med, str): med = [med] med_lines = [] for item in med: med_lines.extend(wrap(item, 80)) top += GAP y_med = yt(top + 9) txt(c, IL, y_med, "Významné údaje o současné medikaci, včetně aplikace inzulínu a diety:", size=LBL) txt(c, IL + 357, y_med, med_lines[0][:35] if med_lines else "", size=VAL) dotline(c, IL + 353, y_med - 2) top += 16 extra_dotline(med_lines[1] if len(med_lines) > 1 else "") extra_dotline(med_lines[2] if len(med_lines) > 2 else "") top += 3 # ── Další informace ─────────────────────────────────────────────────────── info = d.get("dalsi_informace", []) if isinstance(info, str): info = [info] info_lines = [] for item in info: info_lines.extend(wrap(item, 80)) top += GAP y_info = yt(top + 9) txt(c, IL, y_info, "Další informace (alergie, kontinence, údaje o bydlišti atd.):", size=LBL) txt(c, IL + 315, y_info, info_lines[0][:40] if info_lines else "", size=VAL) dotline(c, IL + 311, y_info - 2) top += 16 extra_dotline(info_lines[1] if len(info_lines) > 1 else "") extra_dotline(info_lines[2] if len(info_lines) > 2 else "") top += 3 # ── Cíl DP ──────────────────────────────────────────────────────────────── cil = d.get("cil_dp", []) if isinstance(cil, str): cil = [cil] cil_lines = [] for item in cil: cil_lines.extend(wrap(item, 80)) top += GAP y_cil = yt(top + 9) txt(c, IL, y_cil, "Cíl předepsané DP, kterého má být dosaženo:", size=LBL) txt(c, IL + 236, y_cil, cil_lines[0][:50] if cil_lines else "", size=VAL) dotline(c, IL + 232, y_cil - 2) top += 16 extra_dotline(cil_lines[1] if len(cil_lines) > 1 else "") top += 5 return top # ═══════════════════════════════════════════════════════════════════════════════ # SEKCE 5: POŽADOVÁNO # ═══════════════════════════════════════════════════════════════════════════════ def draw_pozadovano(c, d, start_top: float): top = start_top + 5 # Nadpis top += 8 y_hdr = yt(top + 9) txt(c, IL, y_hdr, "Požadováno:", size=8, bold=True) txt(c, IL + 68, y_hdr, "(Pro úhradu požadované péče zdravotní pojišťovnou je nezbytná jednoznačná specifikace požadavku,", size=6.5) top += 12 txt(c, IL + 68, yt(top + 5), "včetně počtu v jednom dni a frekvence v týdnu)", size=6.5) top += 10 # Výkony (5 řádků) KOD_X = IL # x-start kódového rámečku KOD_W = 108 # šířka kódového rámečku KOD_H = 22 # výška kódového rámečku TXT_X = KOD_X + KOD_W + 8 # x-start textové části ROW_H = 42 # výška jednoho výkonu (rámeček + 2 řádky textu) for vykon in (d.get("pozadovano", []) + [{}, {}, {}, {}, {}])[:5]: y_top_row = yt(top) # Kódový rámeček c.setLineWidth(0.9) c.setDash([]) c.rect(KOD_X, y_top_row - KOD_H - 4, KOD_W, KOD_H, stroke=1, fill=0) # Hodnota kódu kod_val = vykon.get("kod", "") if kod_val: txt(c, KOD_X + 5, y_top_row - KOD_H + 2, kod_val, size=9, bold=True) # Textový řádek 1 txt(c, TXT_X, y_top_row - 10, vykon.get("popis", "")[:75], size=8) dotline(c, TXT_X, y_top_row - 13, IR) # Textový řádek 2 (pokračování) txt(c, TXT_X, y_top_row - 25, vykon.get("popis2", "")[:75], size=8) dotline(c, TXT_X, y_top_row - 28, IR) top += ROW_H # ═══════════════════════════════════════════════════════════════════════════════ # HLAVNÍ FUNKCE # ═══════════════════════════════════════════════════════════════════════════════ def generuj_poukaz(data: dict, vystup: Path): c = canvas.Canvas(str(vystup), pagesize=A4) # Vnější zaoblený rámeček c.setLineWidth(1.8) c.roundRect(FX1, FB, FX2 - FX1, FT - FB, 6, stroke=1, fill=0) draw_hlavicka(c, data) draw_nadpis(c) draw_pacient(c, data) cur_top = draw_textova_pole(c, data) draw_pozadovano(c, data, cur_top) # Zápatí txt(c, FX1 + 2, FB - 12, "VZP-06dp/2024", size=6) c.save() print(f"✓ Uloženo: {vystup}") # ── Spuštění ────────────────────────────────────────────────────────────────── if __name__ == "__main__": folder = Path(__file__).parent # Najde nejvyšší existující číslo verze a přidá 1 existing = [f for f in folder.glob("Poukaz DP vyplneny_v*.pdf")] nums = [] for f in existing: try: nums.append(int(f.stem.rsplit("_v", 1)[1])) except (ValueError, IndexError): pass next_v = max(nums, default=2) + 1 out = folder / f"Poukaz DP vyplneny_v{next_v}.pdf" generuj_poukaz(DATA, out) dá 1 existing = [f for f in folder.glob("Poukaz DP vyplneny_v*.pdf")] nums = [] for f in existing: try: nums.append(int(f.stem.rsplit("_v", 1)[1])) except (ValueError, IndexError): pass next_v = max(nums, default=2) + 1 out = folder / f"Poukaz DP vyplneny_v{next_v}.pdf" generuj_poukaz(DATA, out)