diff --git a/Insurance/SeznamPojistencu/01_parse_seznam.py b/Insurance/SeznamPojistencu/01_parse_seznam.py new file mode 100644 index 0000000..2e05979 --- /dev/null +++ b/Insurance/SeznamPojistencu/01_parse_seznam.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys as _sys +_sys.stdout.reconfigure(encoding="utf-8", errors="replace") +_sys.stderr.reconfigure(encoding="utf-8", errors="replace") +""" +01_parse_seznam.py +================== +Najde a rozparsuje dávky VZP – Seznam registrovaných pojištěnců (III-1.1.2). +Formát souboru: Fxxxmmur.nnn, pevná šířka, kódování CP852. + +Záhlaví (typ H, délka 20): + HTYP C 1 0 typ věty 'H' + HICP N 8 1 IČP lékaře + HPUP N 5 9 počet uznávaných pojištěnců + HROK N 2 14 poslední dvojčíslí roku + HMES N 2 16 měsíc + HDEN N 2 18 den + +Registrace (typ I, délka 82): + ITYP C 1 0 typ věty 'I' + IPOR N 4 1 pořadové číslo + IVS N 2 5 věková skupina + IPRI C 30 7 příjmení + IJME C 24 37 jméno + ICIP C 10 61 číslo pojištěnce + IDUOD D 8 71 datum uznání registrace (DDMMRRRR) + ICPO C 3 79 kód pojišťovny +""" + +import re +from pathlib import Path +from datetime import date, timedelta + +DAVKY_DIR = Path(r"U:\Dropbox\Ordinace\Dokumentace_ke_zpracování\Zúčtovací zprávy\111 VZP Podání") +ENCODING = "cp852" + + +def parse_davku(path: Path) -> dict: + """Vrátí dict s klíči 'hlavicka' a 'pojistenci'.""" + lines = path.read_bytes().splitlines() + hlavicka = None + pojistenci = [] + + for raw in lines: + line = raw.decode(ENCODING, errors="replace") + if not line: + continue + + if line[0] == "H": + hlavicka = { + "icp": line[1:9].strip(), + "pocet": int(line[9:14].strip() or 0), + "rok": 2000 + int(line[14:16]), + "mesic": int(line[16:18]), + "den": int(line[18:20]), + } + + elif line[0] == "I": + if len(line) < 82: + continue + dat_raw = line[71:79] # DDMMRRRR + try: + datum = date(int(dat_raw[4:8]), int(dat_raw[2:4]), int(dat_raw[0:2])) + except ValueError: + datum = None + + pojistenci.append({ + "por": int(line[1:5].strip() or 0), + "vs": line[5:7].strip(), + "prijmeni": line[7:37].strip(), + "jmeno": line[37:61].strip(), + "cip": line[61:71].strip(), + "datum_od": datum, + "pojistovna": line[79:82].strip(), + }) + + return {"hlavicka": hlavicka, "pojistenci": pojistenci, "soubor": path.name} + + +def najdi_davky(adresar: Path) -> list[Path]: + """Vrátí seřazený seznam souborů odpovídajících vzoru Fxxx*.nnn.""" + return sorted( + [p for p in adresar.iterdir() + if re.search(r"F\d{3}.*\.\d{3}$", p.name, re.IGNORECASE)], + key=lambda p: p.name + ) + + +def tiskni_davku(d: dict) -> None: + h = d["hlavicka"] + if h: + print(f"\n=== {d['soubor']} ===") + print(f" IČP: {h['icp']} | datum: {h['den']:02d}.{h['mesic']:02d}.{h['rok']} | pojištěnců: {h['pocet']}") + print(f" {'#':>4} {'Příjmení':<30} {'Jméno':<24} {'ČIP':<10} {'Datum od':>10} Pojiš.") + print(f" {'-'*4} {'-'*30} {'-'*24} {'-'*10} {'-'*10} {'-'*6}") + else: + print(f"\n=== {d['soubor']} === (záhlaví chybí)") + + for p in d["pojistenci"]: + datum_str = p["datum_od"].strftime("%d.%m.%Y") if p["datum_od"] else "?" + print(f" {p['por']:>4} {p['prijmeni']:<30} {p['jmeno']:<24} {p['cip']:<10} {datum_str:>10} {p['pojistovna']}") + + +# ── MAIN ────────────────────────────────────────────────────────────────────── + +davky = najdi_davky(DAVKY_DIR) +print(f"Nalezeno dávek: {len(davky)}") + +for cesta in davky: + data = parse_davku(cesta) + tiskni_davku(data) + +print(f"\nCelkem dávek: {len(davky)}") diff --git a/Insurance/SeznamPojistencu/02_import_do_db.py b/Insurance/SeznamPojistencu/02_import_do_db.py new file mode 100644 index 0000000..5a3cad1 --- /dev/null +++ b/Insurance/SeznamPojistencu/02_import_do_db.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys as _sys +_sys.stdout.reconfigure(encoding="utf-8", errors="replace") +_sys.stderr.reconfigure(encoding="utf-8", errors="replace") +""" +02_import_do_db.py +================== +Vytvoří tabulku seznam_pojistencu_davky v medevio DB a naimportuje +všechny dávky F111*.nnn ze složky Podání. + +Spuštění: + python 02_import_do_db.py # import všech dávek + python 02_import_do_db.py --reset # smaže a znovu vytvoří tabulku před importem +""" + +import re +import sys +import argparse +from pathlib import Path +from datetime import date + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny")) +from mysql_db import connect_mysql + +DAVKY_DIR = Path(r"U:\Dropbox\Ordinace\Dokumentace_ke_zpracování\Zúčtovací zprávy\111 VZP Podání") +ENCODING = "cp852" + +CREATE_SQL = """ +CREATE TABLE IF NOT EXISTS seznam_pojistencu_davky ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + soubor VARCHAR(60) NOT NULL COMMENT 'Název souboru dávky', + icp VARCHAR(8) NOT NULL COMMENT 'IČP lékaře (HICP)', + davka_rok SMALLINT NOT NULL COMMENT 'Rok dávky (HROK)', + davka_mesic TINYINT NOT NULL COMMENT 'Měsíc dávky (HMES)', + davka_den TINYINT NOT NULL COMMENT 'Den pořízení seznamu (HDEN)', + por SMALLINT NOT NULL COMMENT 'Pořadové číslo v dávce (IPOR)', + vs VARCHAR(2) NOT NULL COMMENT 'Věková skupina (IVS)', + prijmeni VARCHAR(30) NOT NULL COMMENT 'Příjmení pojištěnce (IPRI)', + jmeno VARCHAR(24) NOT NULL COMMENT 'Jméno pojištěnce (IJME)', + cip VARCHAR(10) NOT NULL COMMENT 'Číslo pojištěnce (ICIP)', + datum_od DATE NULL COMMENT 'Datum uznání registrace (IDUOD)', + pojistovna VARCHAR(3) NOT NULL COMMENT 'Kód pojišťovny (ICPO)', + vytvoreno TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_soubor_por (soubor, por) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci + COMMENT='VZP seznam registrovaných pojištěnců III-1.1.2'; +""" + + +def parse_davku(path: Path) -> dict: + lines = path.read_bytes().splitlines() + hlavicka = None + pojistenci = [] + + for raw in lines: + line = raw.decode(ENCODING, errors="replace") + if not line: + continue + + if line[0] == "H": + hlavicka = { + "icp": line[1:9].strip(), + "pocet": int(line[9:14].strip() or 0), + "rok": 2000 + int(line[14:16]), + "mesic": int(line[16:18]), + "den": int(line[18:20]), + } + + elif line[0] == "I": + if len(line) < 82: + continue + dat_raw = line[71:79] # DDMMRRRR + try: + datum = date(int(dat_raw[4:8]), int(dat_raw[2:4]), int(dat_raw[0:2])) + except ValueError: + datum = None + + pojistenci.append({ + "por": int(line[1:5].strip() or 0), + "vs": line[5:7].strip(), + "prijmeni": line[7:37].strip(), + "jmeno": line[37:61].strip(), + "cip": line[61:71].strip(), + "datum_od": datum, + "pojistovna": line[79:82].strip(), + }) + + return {"hlavicka": hlavicka, "pojistenci": pojistenci, "soubor": path.name} + + +def najdi_davky(adresar: Path) -> list[Path]: + return sorted( + [p for p in adresar.iterdir() + if re.search(r"F\d{3}.*\.\d{3}$", p.name, re.IGNORECASE)], + key=lambda p: p.name + ) + + +def import_davku(cur, davka: dict) -> tuple[int, int]: + """Vrátí (vloženo, přeskočeno duplicit).""" + h = davka["hlavicka"] + if not h: + print(f" SKIP {davka['soubor']} — chybí záhlaví") + return 0, 0 + + rows = [ + (davka["soubor"], h["icp"], h["rok"], h["mesic"], h["den"], + p["por"], p["vs"], p["prijmeni"], p["jmeno"], + p["cip"], p["datum_od"], p["pojistovna"]) + for p in davka["pojistenci"] + ] + + # INSERT IGNORE přeskočí duplicity (unikátní klíč soubor+por) bez chyby + affected = cur.executemany( + """INSERT IGNORE INTO seznam_pojistencu_davky + (soubor, icp, davka_rok, davka_mesic, davka_den, + por, vs, prijmeni, jmeno, cip, datum_od, pojistovna) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""", + rows + ) + vlozeno = affected if affected else 0 + preskoceno = len(rows) - vlozeno + return vlozeno, preskoceno + + +# ── MAIN ────────────────────────────────────────────────────────────────────── + +parser = argparse.ArgumentParser(description="Import VZP dávek do MySQL") +parser.add_argument("--reset", action="store_true", + help="Smaže a znovu vytvoří tabulku před importem") +args = parser.parse_args() + +conn = connect_mysql() +cur = conn.cursor() + +if args.reset: + print("DROP TABLE seznam_pojistencu_davky ...") + cur.execute("DROP TABLE IF EXISTS seznam_pojistencu_davky") + +print("Vytváření tabulky (pokud neexistuje) ...") +cur.execute(CREATE_SQL) + +davky = najdi_davky(DAVKY_DIR) +print(f"Nalezeno dávek: {len(davky)}\n") + +celkem_vlozeno = celkem_preskoceno = 0 + +for cesta in davky: + davka = parse_davku(cesta) + h = davka["hlavicka"] + if h: + print(f"{davka['soubor']} ({h['den']:02d}.{h['mesic']:02d}.{h['rok']}, {len(davka['pojistenci'])} záznamů)") + else: + print(f"{davka['soubor']} (záhlaví chybí)") + + vlozeno, preskoceno = import_davku(cur, davka) + celkem_vlozeno += vlozeno + celkem_preskoceno += preskoceno + print(f" → vloženo: {vlozeno}, duplicit přeskočeno: {preskoceno}") + +cur.close() +conn.close() + +print(f"\nHotovo. Celkem vloženo: {celkem_vlozeno}, přeskočeno: {celkem_preskoceno}") diff --git a/Insurance/SeznamPojistencu/03_porovnani_davek.py b/Insurance/SeznamPojistencu/03_porovnani_davek.py new file mode 100644 index 0000000..4862eaa --- /dev/null +++ b/Insurance/SeznamPojistencu/03_porovnani_davek.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys as _sys +_sys.stdout.reconfigure(encoding="utf-8", errors="replace") +_sys.stderr.reconfigure(encoding="utf-8", errors="replace") +""" +03_porovnani_davek.py +===================== +Porovná všechny po sobě jdoucí dávky VZP a vytvoří Excel se třemi listy: + 1. Přehled – souhrnná tabulka přechodů (kolik odešlo / přibylo) + 2. Odešli – detail všech, kdo v dané dávce chyběli oproti předchozí + 3. Přibylo – detail všech, kdo v dané dávce přibyly oproti předchozí + +Pro data s více soubory se použijí unikátní CIPy (dávky jsou totožné). +""" + +import sys +from datetime import date +from pathlib import Path +from collections import defaultdict + +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny")) +from mysql_db import connect_mysql + +OUTPUT = Path(__file__).parent / "porovnani_davek.xlsx" + +# ── Barvy ───────────────────────────────────────────────────────────────────── +CLR_HEADER = "1F4E79" # tmavě modrá +CLR_SUBHDR = "2E75B6" # střední modrá +CLR_ODEŠLI = "FCE4D6" # světle lososová +CLR_PŘIBYLO = "E2EFDA" # světle zelená +CLR_ZEBRA = "F2F2F2" # šedá pro zebra řádky +CLR_SUMMARY = "DEEAF1" # světle modrá pro souhrn + +# ── Styly ───────────────────────────────────────────────────────────────────── +def hdr_font(white=True): + return Font(bold=True, color="FFFFFF" if white else "000000", size=11) + +def cell_border(): + thin = Side(style="thin", color="BFBFBF") + return Border(left=thin, right=thin, top=thin, bottom=thin) + +def set_header(cell, text, bg=CLR_HEADER, white=True): + cell.value = text + cell.font = hdr_font(white) + cell.fill = PatternFill("solid", fgColor=bg) + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = cell_border() + +def set_cell(cell, value, bg=None, bold=False, align="left", num_fmt=None): + cell.value = value + cell.font = Font(bold=bold, size=10) + cell.alignment = Alignment(horizontal=align, vertical="center") + cell.border = cell_border() + if bg: + cell.fill = PatternFill("solid", fgColor=bg) + if num_fmt: + cell.number_format = num_fmt + +# ── Načtení dat z DB ────────────────────────────────────────────────────────── + +print("Připojuji se k DB ...") +conn = connect_mysql() +cur = conn.cursor() + +# Unikátní CIPy per datum + jméno (z prvního souboru daného data) +cur.execute(""" + SELECT davka_rok, davka_mesic, davka_den, cip, + MIN(prijmeni) AS prijmeni, MIN(jmeno) AS jmeno + FROM seznam_pojistencu_davky + GROUP BY davka_rok, davka_mesic, davka_den, cip + ORDER BY davka_rok, davka_mesic, davka_den +""") +rows = cur.fetchall() +conn.close() + +# Seskupení do dict: datum -> {cip: (prijmeni, jmeno)} +davky: dict[date, dict[str, tuple]] = defaultdict(dict) +for rok, mes, den, cip, pri, jme in rows: + d = date(rok, mes, den) + davky[d][cip] = (pri, jme) + +data = sorted(davky.keys()) +print(f"Nalezeno {len(data)} unikátních datumů dávek.") + +# ── Porovnání ──────────────────────────────────────────────────────────────── + +prehled = [] # (dat_od, dat_do, stav_od, odešlo, přibylo, stav_do) +odešli = [] # (dat_do, cip, prijmeni, jmeno) +přibylo = [] # (dat_do, cip, prijmeni, jmeno) + +for i in range(1, len(data)): + d1, d2 = data[i-1], data[i] + cip1 = set(davky[d1]) + cip2 = set(davky[d2]) + + vyšli = cip1 - cip2 + vstou = cip2 - cip1 + + prehled.append((d1, d2, len(cip1), len(vyšli), len(vstou), len(cip2))) + + for cip in sorted(vyšli): + pri, jme = davky[d1][cip] + odešli.append((d2, cip, pri, jme)) + + for cip in sorted(vstou): + pri, jme = davky[d2][cip] + přibylo.append((d2, cip, pri, jme)) + +# ── Excel ───────────────────────────────────────────────────────────────────── + +wb = openpyxl.Workbook() + +# ── List 1: Přehled ─────────────────────────────────────────────────────────── +ws = wb.active +ws.title = "Přehled" +ws.freeze_panes = "A3" + +# Nadpis +ws.merge_cells("A1:F1") +t = ws["A1"] +t.value = "Porovnání po sobě jdoucích dávek VZP – seznam registrovaných pojištěnců" +t.font = Font(bold=True, size=13, color="FFFFFF") +t.fill = PatternFill("solid", fgColor=CLR_HEADER) +t.alignment = Alignment(horizontal="center", vertical="center") +ws.row_dimensions[1].height = 28 + +# Záhlaví sloupců +headers = ["Datum od", "Datum do", "Stav\nna začátku", "Odešlo", "Přibylo", "Stav\nna konci"] +for col, h in enumerate(headers, 1): + set_header(ws.cell(2, col), h) +ws.row_dimensions[2].height = 32 + +# Data +for row_i, (d1, d2, stav_od, vyšli, vstou, stav_do) in enumerate(prehled, 3): + bg = CLR_ZEBRA if row_i % 2 == 0 else None + set_cell(ws.cell(row_i, 1), d1.strftime("%d.%m.%Y"), bg, align="center") + set_cell(ws.cell(row_i, 2), d2.strftime("%d.%m.%Y"), bg, align="center") + set_cell(ws.cell(row_i, 3), stav_od, bg, align="right") + set_cell(ws.cell(row_i, 4), -vyšli, CLR_ODEŠLI if vyšli else bg, bold=bool(vyšli), align="right") + set_cell(ws.cell(row_i, 5), vstou, CLR_PŘIBYLO if vstou else bg, bold=bool(vstou), align="right") + set_cell(ws.cell(row_i, 6), stav_do, bg, align="right") + +# Souhrnný řádek +sr = len(prehled) + 3 +ws.merge_cells(f"A{sr}:B{sr}") +sc = ws.cell(sr, 1) +sc.value = "CELKEM pohybů" +sc.font = Font(bold=True, size=10) +sc.fill = PatternFill("solid", fgColor=CLR_SUMMARY) +sc.alignment = Alignment(horizontal="center", vertical="center") +sc.border = cell_border() + +celk_odešlo = sum(p[3] for p in prehled) +celk_přibylo = sum(p[4] for p in prehled) +set_cell(ws.cell(sr, 3), "", CLR_SUMMARY) +set_cell(ws.cell(sr, 4), -celk_odešlo, CLR_SUMMARY, bold=True, align="right") +set_cell(ws.cell(sr, 5), celk_přibylo, CLR_SUMMARY, bold=True, align="right") +set_cell(ws.cell(sr, 6), "", CLR_SUMMARY) + +# Šířky sloupců +for col, w in zip(range(1, 7), [14, 14, 14, 10, 10, 12]): + ws.column_dimensions[get_column_letter(col)].width = w + +# ── List 2: Odešli ──────────────────────────────────────────────────────────── +ws2 = wb.create_sheet("Odešli") +ws2.freeze_panes = "A3" + +ws2.merge_cells("A1:D1") +t2 = ws2["A1"] +t2.value = f"Pacienti, kteří odešli – celkem {len(odešli)} pohybů" +t2.font = Font(bold=True, size=13, color="FFFFFF") +t2.fill = PatternFill("solid", fgColor="C55A11") +t2.alignment = Alignment(horizontal="center", vertical="center") +ws2.row_dimensions[1].height = 28 + +for col, h in enumerate(["Datum dávky", "Číslo pojištěnce", "Příjmení", "Jméno"], 1): + set_header(ws2.cell(2, col), h, bg="C55A11") +ws2.row_dimensions[2].height = 24 + +for row_i, (dat, cip, pri, jme) in enumerate(odešli, 3): + bg = CLR_ODEŠLI if row_i % 2 == 0 else None + set_cell(ws2.cell(row_i, 1), dat.strftime("%d.%m.%Y"), bg, align="center") + set_cell(ws2.cell(row_i, 2), cip, bg, align="center") + set_cell(ws2.cell(row_i, 3), pri, bg) + set_cell(ws2.cell(row_i, 4), jme, bg) + +for col, w in zip(range(1, 5), [14, 16, 28, 22]): + ws2.column_dimensions[get_column_letter(col)].width = w + +# ── List 3: Přibylo ─────────────────────────────────────────────────────────── +ws3 = wb.create_sheet("Přibylo") +ws3.freeze_panes = "A3" + +ws3.merge_cells("A1:D1") +t3 = ws3["A1"] +t3.value = f"Pacienti, kteří přibylo – celkem {len(přibylo)} pohybů" +t3.font = Font(bold=True, size=13, color="FFFFFF") +t3.fill = PatternFill("solid", fgColor="375623") +t3.alignment = Alignment(horizontal="center", vertical="center") +ws3.row_dimensions[1].height = 28 + +for col, h in enumerate(["Datum dávky", "Číslo pojištěnce", "Příjmení", "Jméno"], 1): + set_header(ws3.cell(2, col), h, bg="375623") +ws3.row_dimensions[2].height = 24 + +for row_i, (dat, cip, pri, jme) in enumerate(přibylo, 3): + bg = CLR_PŘIBYLO if row_i % 2 == 0 else None + set_cell(ws3.cell(row_i, 1), dat.strftime("%d.%m.%Y"), bg, align="center") + set_cell(ws3.cell(row_i, 2), cip, bg, align="center") + set_cell(ws3.cell(row_i, 3), pri, bg) + set_cell(ws3.cell(row_i, 4), jme, bg) + +for col, w in zip(range(1, 5), [14, 16, 28, 22]): + ws3.column_dimensions[get_column_letter(col)].width = w + +# ── Uložení ─────────────────────────────────────────────────────────────────── +wb.save(OUTPUT) +print(f"\nExcel uložen: {OUTPUT}") +print(f" Přehled: {len(prehled)} přechodů") +print(f" Odešli: {len(odešli)} pohybů") +print(f" Přibylo: {len(přibylo)} pohybů") diff --git a/Insurance/SeznamPojistencu/04_najdi_zlomy.py b/Insurance/SeznamPojistencu/04_najdi_zlomy.py new file mode 100644 index 0000000..43eb626 --- /dev/null +++ b/Insurance/SeznamPojistencu/04_najdi_zlomy.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys as _sys +_sys.stdout.reconfigure(encoding="utf-8", errors="replace") +_sys.stderr.reconfigure(encoding="utf-8", errors="replace") +""" +04_najdi_zlomy.py +================= +Pro pacienty z seznam_pojistencu_davky, kteří NEMAJÍ záznam v vzp_registrace_lekari +(skript kdojelekar je nezachytil), najde bod zlomu registrace u naší ambulance. + +Algoritmus: + 1. Dotaz VZP dnes — je pacient stále registrován u nás (ICP=09305001)? + ANO → datum_ukonceni z odpovědi = výsledek + 2. NE → hledáme rokem dozadu (od dnes, −1 rok, −2 roky …) + dokud nenajdeme rok kdy BYL registrován → tím ohraničíme interval [lo, hi] + 3. V intervalu [lo, hi] binární hledání na den přesně + (nebo pokud datum_ukonceni z VZP odpovědi není 3000, použijeme ho přímo) + +Výsledek se uloží do tabulky seznam_pojistencu_zlomy a vytiskne na konzoli. +""" + +import time +import sys +from datetime import date, timedelta +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny")) +from mysql_db import connect_mysql +from vzpb2b_client import VZPB2BClient + +# ── Konfigurace ─────────────────────────────────────────────────────────────── +PFX_PATH = str(Path(__file__).resolve().parents[1] / "Certificates" / "picka.pfx") +PFX_PASSWORD = "Vlado7309208104+" +ICZ = "09305000" +NASA_ICP = "09305001" +API_PAUSE = 2 # sekundy mezi VZP dotazy + +CREATE_SQL = """ +CREATE TABLE IF NOT EXISTS seznam_pojistencu_zlomy ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + cip VARCHAR(12) NOT NULL, + prijmeni VARCHAR(30) NOT NULL, + jmeno VARCHAR(24) NOT NULL, + posledni_davka DATE NOT NULL COMMENT 'Poslední měsíc kdy byl v dávce', + zlom_datum DATE NULL COMMENT 'Poslední den registrace u nás (NULL=stále aktivní)', + zlom_zdroj VARCHAR(60) NULL COMMENT 'Jak byl zlom určen', + stav VARCHAR(20) NOT NULL COMMENT 'aktivní / ukončen / nenalezen', + dotazeno_dne DATE NOT NULL, + UNIQUE KEY uq_cip (cip) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Zlomy registrace pro pacienty bez záznamu v kdojelekar'; +""" + +# ── VZP klient ──────────────────────────────────────────────────────────────── +vzp = VZPB2BClient("prod", PFX_PATH, PFX_PASSWORD, icz=ICZ) + + +def je_registrovan(rc: str, k_datu: date) -> tuple[bool, date | None]: + """ + Vrátí (registrován_u_nas: bool, datum_ukonceni: date|None). + datum_ukonceni = None pokud nelze parsovat nebo pacient není u nás. + """ + xml = vzp.registrace_lekare(rc, k_datu.isoformat(), odbornosti=["001"]) + time.sleep(API_PAUSE) + try: + zaznamy = vzp.parse_registrace_lekare(xml) + except Exception as e: + print(f" [CHYBA parsování] {e}") + return False, None + + for z in zaznamy: + if z.get("ma_lekare") and z.get("ICP") == NASA_ICP and z.get("kod_odbornosti") == "001": + du_str = z.get("datum_ukonceni") + try: + du = date.fromisoformat(du_str) if du_str else None + except ValueError: + du = None + return True, du + + return False, None + + +def najdi_zlom(rc: str, posledni_davka: date) -> tuple[date | None, str, str]: + """ + Vrátí (zlom_datum, zlom_zdroj, stav). + zlom_datum = poslední den kdy byl registrován (None = stále aktivní). + """ + today = date.today() + + # ── Krok 1: dotaz dnes ──────────────────────────────────────────────────── + print(f" [dnes {today}]", end=" ", flush=True) + reg, du = je_registrovan(rc, today) + + if reg: + if du and du.year < 3000: + print(f"registrován, ukončení {du}") + return du, "VZP datum_ukonceni (dnes)", "ukončen" + else: + print("registrován, bez data ukončení → stále aktivní") + return None, "VZP dnes aktivní", "aktivní" + + print("NENÍ registrován") + + # ── Krok 2: hledání po rocích dozadu ───────────────────────────────────── + # lo = víme, že tam BYL (posledni_davka) + # hi = víme, že tam NENÍ (today) + lo: date = posledni_davka + hi: date = today + + probe = today.replace(year=today.year - 1) + while probe >= posledni_davka: + print(f" [rok {probe}]", end=" ", flush=True) + reg_p, du_p = je_registrovan(rc, probe) + if reg_p: + lo = probe + print(f"registrován") + if du_p and du_p.year < 3000: + print(f" → datum_ukonceni z VZP: {du_p}") + return du_p, f"VZP datum_ukonceni (dotaz {probe})", "ukončen" + break + else: + hi = probe + print("není") + try: + probe = probe.replace(year=probe.year - 1) + except ValueError: + break + else: + # Ani v posledni_davka není registrován — neobvyklé + print(f" ! Ani k datu {posledni_davka} není registrován — zkouším přímo") + reg_lo, du_lo = je_registrovan(rc, posledni_davka) + if not reg_lo: + return None, "nenalezen ani k datu poslední dávky", "nenalezen" + lo = posledni_davka + if du_lo and du_lo.year < 3000: + return du_lo, f"VZP datum_ukonceni ({posledni_davka})", "ukončen" + + # ── Krok 3: binární hledání v intervalu [lo, hi] ───────────────────────── + print(f" Binární hledání: {lo} … {hi}") + iterace = 0 + while (hi - lo).days > 1: + iterace += 1 + mid = lo + timedelta(days=(hi - lo).days // 2) + print(f" [{iterace}. iterace: {mid}]", end=" ", flush=True) + reg_m, du_m = je_registrovan(rc, mid) + if reg_m: + lo = mid + print("registrován") + if du_m and du_m.year < 3000: + print(f" → datum_ukonceni z VZP: {du_m}") + return du_m, f"VZP datum_ukonceni (binární {mid})", "ukončen" + else: + hi = mid + print("není") + + print(f" → Zlom: poslední den registrace = {lo}") + return lo, f"binární hledání ({iterace} kroků)", "ukončen" + + +# ── Načtení pacientů ────────────────────────────────────────────────────────── + +conn = connect_mysql() +cur = conn.cursor() +cur.execute(CREATE_SQL) + +# Unikátní CIP v seznamu (VZP, pojišťovna 111) +cur.execute("SELECT DISTINCT cip FROM seznam_pojistencu_davky WHERE pojistovna='111'") +vsechny_cip = {r[0] for r in cur.fetchall()} + +# CIP které jsou v registrace_lekari (u nás, odb 001) +cur.execute(""" + SELECT DISTINCT rc FROM vzp_registrace_lekari + WHERE kod_odbornosti='001' AND ICP='09305001' AND ma_lekare=1 +""") +zname_cip = {r[0] for r in cur.fetchall()} + +# Nespárované +nesparovane_cip = vsechny_cip - zname_cip + +# Doplním jméno a posledni_davka +cur.execute(""" + SELECT cip, MIN(prijmeni), MIN(jmeno), + MAX(DATE(CONCAT(davka_rok, '-', LPAD(davka_mesic,2,'0'), '-01'))) + FROM seznam_pojistencu_davky + WHERE pojistovna='111' + GROUP BY cip +""") +info = {r[0]: (r[1], r[2], r[3]) for r in cur.fetchall()} + +pacienti = [ + (cip, *info[cip]) + for cip in sorted(nesparovane_cip) + if cip in info +] + +print(f"Pacientů ke zpracování: {len(pacienti)}\n") +print("=" * 70) + +# ── Hlavní smyčka ───────────────────────────────────────────────────────────── + +vysledky = [] +for cip, prijmeni, jmeno, posledni_davka in pacienti: + print(f"\n{prijmeni} {jmeno} (CIP: {cip}, poslední dávka: {posledni_davka})") + try: + zlom, zdroj, stav = najdi_zlom(cip, posledni_davka) + except Exception as e: + print(f" CHYBA: {e}") + zlom, zdroj, stav = None, f"chyba: {e}", "chyba" + + vysledky.append((cip, prijmeni, jmeno, posledni_davka, zlom, zdroj, stav)) + + cur.execute(""" + INSERT INTO seznam_pojistencu_zlomy + (cip, prijmeni, jmeno, posledni_davka, zlom_datum, zlom_zdroj, stav, dotazeno_dne) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + posledni_davka=VALUES(posledni_davka), + zlom_datum=VALUES(zlom_datum), + zlom_zdroj=VALUES(zlom_zdroj), + stav=VALUES(stav), + dotazeno_dne=VALUES(dotazeno_dne) + """, (cip, prijmeni, jmeno, posledni_davka, zlom, zdroj, stav, date.today())) + +cur.close() +conn.close() + +# ── Výsledky ────────────────────────────────────────────────────────────────── +print("\n" + "=" * 70) +print(f"\n{'Příjmení':<25} {'Jméno':<20} {'CIP':<12} {'Poslední dávka':<15} {'Zlom':<12} Stav") +print("-" * 95) +for cip, pri, jme, pd, zd, zdroj, stav in vysledky: + zlom_str = str(zd) if zd else "—" + print(f"{pri:<25} {jme:<20} {cip:<12} {str(pd):<15} {zlom_str:<12} {stav}") diff --git a/Insurance/SeznamPojistencu/06_nasledny_lekar.py b/Insurance/SeznamPojistencu/06_nasledny_lekar.py new file mode 100644 index 0000000..23204b4 --- /dev/null +++ b/Insurance/SeznamPojistencu/06_nasledny_lekar.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys as _sys +_sys.stdout.reconfigure(encoding="utf-8", errors="replace") +_sys.stderr.reconfigure(encoding="utf-8", errors="replace") +""" +06_nasledny_lekar.py +==================== +Pro všechny ukončené pacienty (103) se dotáže VZP ke dni ukonceni+1 +na jejich nového praktického lékaře (odbornost 001). + +Výsledek je jeden ze tří stavů: + nenalezen → pacient u VZP neexistuje (zemřel / přestal být pojištěný) + bez_lekare → pacient existuje, ale nemá GP (dosud se nepřehlásil) + prehlasil → přehlásil se k novému lékaři (ukládáme ICP, jméno, datum) + +Ukládá do tabulky seznam_pojistencu_nasledny_lekar. +Přeskočí pacienty, kteří tam už jsou (resumovatelný běh). +""" + +import sys +import time +import xml.etree.ElementTree as ET +from datetime import date, timedelta +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny")) +from mysql_db import connect_mysql +from vzpb2b_client import VZPB2BClient + +PFX_PATH = str(Path(__file__).resolve().parents[1] / "Certificates" / "picka.pfx") +PFX_PASSWORD = "Vlado7309208104+" +ICZ = "09305000" +API_PAUSE = 2 + +CREATE_SQL = """ +CREATE TABLE IF NOT EXISTS seznam_pojistencu_nasledny_lekar ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + cip VARCHAR(12) NOT NULL, + prijmeni VARCHAR(60) NOT NULL, + jmeno VARCHAR(40) NOT NULL, + datum_ukonceni DATE NOT NULL COMMENT 'Datum ukončení u nás', + datum_dotazu DATE NOT NULL COMMENT 'Dotaz k datu ukonceni+1', + stav_vzp VARCHAR(20) NOT NULL COMMENT 'nenalezen / bez_lekare / prehlasil', + stav_vyrizeni VARCHAR(10) NULL COMMENT 'stavVyrizeniPozadavku z VZP', + novy_icp VARCHAR(20) NULL, + novy_icz VARCHAR(20) NULL, + novy_nazev VARCHAR(200) NULL COMMENT 'nazevSZZ — jméno lékaře', + novy_ordinace VARCHAR(200) NULL COMMENT 'nazevICP — název ordinace', + datum_prehlaseni DATE NULL COMMENT 'datumRegistrace u nového lékaře', + dotazeno_dne DATE NOT NULL, + UNIQUE KEY uq_cip (cip) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + COMMENT='Stav pojištěnce ke dni ukončení registrace u naší ordinace'; +""" + +NS = "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1" + + +def parse_nasledny(xml_text: str) -> dict: + """ + Vrátí dict s klíči: stav_vzp, stav_vyrizeni, novy_icp, novy_icz, + novy_nazev, novy_ordinace, datum_prehlaseni. + """ + try: + root = ET.fromstring(xml_text) + except ET.ParseError: + return {"stav_vzp": "chyba_xml", "stav_vyrizeni": None, + "novy_icp": None, "novy_icz": None, "novy_nazev": None, + "novy_ordinace": None, "datum_prehlaseni": None} + + def find(el, tag): + e = el.find(f"{{{NS}}}{tag}") + return e.text.strip() if e is not None and e.text else None + + stav_vyrizeni = find(root, "stavVyrizeniPozadavku") + + # Hledám odbornost 001 s jiným lékařem + odbornosti = root.findall(f".//{{{NS}}}odbornost") + for odb in odbornosti: + # Vnější element — má ICZ + icz = find(odb, "ICZ") + icp = find(odb, "ICP") + if not icp: + continue + # Ověřím odbornost (vnořený subelement) + sub = odb.find(f"{{{NS}}}odbornost") + if sub is None: + continue + kod = find(sub, "kod") + if kod != "001": + continue + # Nalezen nový GP + dr = date.fromisoformat(find(odb, "datumRegistrace")) \ + if find(odb, "datumRegistrace") else None + return { + "stav_vzp": "prehlasil", + "stav_vyrizeni": stav_vyrizeni, + "novy_icp": icp, + "novy_icz": icz, + "novy_nazev": find(odb, "nazevSZZ"), + "novy_ordinace": find(odb, "nazevICP"), + "datum_prehlaseni": dr, + } + + # Žádná odbornost 001 nenalezena + if stav_vyrizeni == "0": + stav = "nenalezen" + elif stav_vyrizeni == "1": + stav = "bez_lekare" + else: + # Zkusím ještě — pokud jsou nějaké záznamy ale ne 001, je to bez_lekare + stav = "nenalezen" if not odbornosti else "bez_lekare" + + return {"stav_vzp": stav, "stav_vyrizeni": stav_vyrizeni, + "novy_icp": None, "novy_icz": None, "novy_nazev": None, + "novy_ordinace": None, "datum_prehlaseni": None} + + +# ── Načtení ukončených pacientů ─────────────────────────────────────────────── + +conn = connect_mysql() +cur = conn.cursor() +cur.execute(CREATE_SQL) + +# Ukončení z vzp_registrace_lekari +cur.execute(""" + SELECT r.rc, r.prijmeni, r.jmeno, r.datum_ukonceni + FROM vzp_registrace_lekari r + INNER JOIN ( + SELECT rc, MAX(k_datu) mk + FROM vzp_registrace_lekari + WHERE kod_odbornosti='001' AND ICP='09305001' AND ma_lekare=1 + GROUP BY rc + ) l ON r.rc=l.rc AND r.k_datu=l.mk + WHERE r.kod_odbornosti='001' AND r.ICP='09305001' + AND r.datum_ukonceni < '3000-01-01' AND r.datum_ukonceni < CURDATE() +""") +pacienti = [(r[0], r[1] or "", r[2] or "", r[3]) for r in cur.fetchall()] + +# Doplním ze zlomů (13 nespárovaných) +cur.execute(""" + SELECT cip, prijmeni, jmeno, zlom_datum + FROM seznam_pojistencu_zlomy + WHERE stav='ukončen' AND zlom_datum IS NOT NULL AND zlom_datum < CURDATE() +""") +for r in cur.fetchall(): + if r[0] not in {p[0] for p in pacienti}: + pacienti.append((r[0], r[1], r[2], r[3])) + +# Přeskočím již zpracované +cur.execute("SELECT cip FROM seznam_pojistencu_nasledny_lekar") +hotovi = {r[0] for r in cur.fetchall()} +ke_zpracovani = [(c, p, j, d) for c, p, j, d in pacienti if c not in hotovi] + +print(f"Ukončených celkem: {len(pacienti)}") +print(f"Již zpracováno: {len(hotovi)}") +print(f"Ke zpracování: {len(ke_zpracovani)}") + +if not ke_zpracovani: + print("Vše již zpracováno.") + cur.close(); conn.close(); sys.exit(0) + +vzp = VZPB2BClient("prod", PFX_PATH, PFX_PASSWORD, icz=ICZ) + +# ── Hlavní smyčka ───────────────────────────────────────────────────────────── +print() +sirka = max(len(f"{p} {j}") for _, p, j, _ in ke_zpracovani) + 2 + +for i, (cip, pri, jme, du) in enumerate(ke_zpracovani, 1): + datum_dotazu = du + timedelta(days=1) + jmeno_str = f"{pri} {jme}" + print(f"[{i:>3}/{len(ke_zpracovani)}] {jmeno_str:<{sirka}} ({cip}) k {datum_dotazu}", end=" ", flush=True) + + try: + xml = vzp.registrace_lekare(cip, datum_dotazu.isoformat(), odbornosti=["001"]) + time.sleep(API_PAUSE) + vysl = parse_nasledny(xml) + except Exception as e: + print(f"CHYBA: {e}") + vysl = {"stav_vzp": "chyba", "stav_vyrizeni": str(e), + "novy_icp": None, "novy_icz": None, "novy_nazev": None, + "novy_ordinace": None, "datum_prehlaseni": None} + + # Výpis + stav = vysl["stav_vzp"] + if stav == "prehlasil": + print(f"→ přehlásil se: {vysl['novy_nazev']} (ICP {vysl['novy_icp']}, od {vysl['datum_prehlaseni']})") + elif stav == "bez_lekare": + print("→ bez nového lékaře") + elif stav == "nenalezen": + print("→ nenalezen (zemřel / nepojištěný)") + else: + print(f"→ {stav}") + + cur.execute(""" + INSERT INTO seznam_pojistencu_nasledny_lekar + (cip, prijmeni, jmeno, datum_ukonceni, datum_dotazu, stav_vzp, + stav_vyrizeni, novy_icp, novy_icz, novy_nazev, novy_ordinace, + datum_prehlaseni, dotazeno_dne) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) + ON DUPLICATE KEY UPDATE + datum_ukonceni=VALUES(datum_ukonceni), + datum_dotazu=VALUES(datum_dotazu), + stav_vzp=VALUES(stav_vzp), stav_vyrizeni=VALUES(stav_vyrizeni), + novy_icp=VALUES(novy_icp), novy_icz=VALUES(novy_icz), + novy_nazev=VALUES(novy_nazev), novy_ordinace=VALUES(novy_ordinace), + datum_prehlaseni=VALUES(datum_prehlaseni), + dotazeno_dne=VALUES(dotazeno_dne) + """, (cip, pri, jme, du, datum_dotazu, stav, + vysl["stav_vyrizeni"], vysl["novy_icp"], vysl["novy_icz"], + vysl["novy_nazev"], vysl["novy_ordinace"], vysl["datum_prehlaseni"], + date.today())) + +cur.close() +conn.close() + +# ── Souhrn ──────────────────────────────────────────────────────────────────── +print(f"\nHotovo. Zpracováno {len(ke_zpracovani)} pacientů.") diff --git a/Insurance/SeznamPojistencu/99_report_excel.py b/Insurance/SeznamPojistencu/99_report_excel.py new file mode 100644 index 0000000..f4d756b --- /dev/null +++ b/Insurance/SeznamPojistencu/99_report_excel.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys as _sys +_sys.stdout.reconfigure(encoding="utf-8", errors="replace") +_sys.stderr.reconfigure(encoding="utf-8", errors="replace") +""" +05_report_excel.py +================== +Kompletní Excel report všech VZP pojištěnců z dávek. +Kombinuje čtyři zdroje: + - seznam_pojistencu_davky → první/poslední dávka, zda v aktuální + - vzp_registrace_lekari → datum_ukonceni od VZP (989 pacientů) + - seznam_pojistencu_zlomy → bod zlomu pro 13 nespárovaných + - seznam_pojistencu_nasledny_lekar → nový lékař / nenalezen po ukončení + +Listy: + 1. Všichni pojištěnci – kompletní přehled, řazeno příjmení + 2. Aktivní – stále registrováni + 3. Ukončení – registrace ukončena + nový lékař / osud + 4. Nenalezeni – bez dostatečných dat +""" + +import sys +from datetime import date +from pathlib import Path + +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny")) +from mysql_db import connect_mysql + +TODAY = date.today() +OUTPUT = Path(__file__).parent / "report_pojistenci.xlsx" + +# ── Barvy ───────────────────────────────────────────────────────────────────── +C_HDR_DARK = "1F4E79" +C_HDR_MED = "2E75B6" +C_AKTIVNI = "E2EFDA" # zelená +C_UKONCEN = "FCE4D6" # lososová +C_NENALEZEN = "FFF2CC" # žlutá +C_ZEBRA = "F2F2F2" +C_TITLE_AKT = "375623" +C_TITLE_UKO = "C55A11" +C_TITLE_NEN = "7F6000" + +THIN = Side(style="thin", color="BFBFBF") + +def border(): + return Border(left=THIN, right=THIN, top=THIN, bottom=THIN) + +def hdr(cell, text, bg=C_HDR_DARK, fg="FFFFFF", size=10): + cell.value = text + cell.font = Font(bold=True, color=fg, size=size) + cell.fill = PatternFill("solid", fgColor=bg) + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = border() + +def cell(ws, row, col, value, bg=None, bold=False, align="left", fmt=None, color="000000"): + c = ws.cell(row, col, value) + c.font = Font(bold=bold, size=10, color=color) + c.alignment = Alignment(horizontal=align, vertical="center") + c.border = border() + if bg: + c.fill = PatternFill("solid", fgColor=bg) + if fmt: + c.number_format = fmt + return c + +def title_row(ws, row, text, ncols, bg, fg="FFFFFF", height=26): + ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=ncols) + c = ws.cell(row, 1) + c.value = text + c.font = Font(bold=True, size=13, color=fg) + c.fill = PatternFill("solid", fgColor=bg) + c.alignment = Alignment(horizontal="center", vertical="center") + ws.row_dimensions[row].height = height + +def autofit(ws, widths): + for i, w in enumerate(widths, 1): + ws.column_dimensions[get_column_letter(i)].width = w + +# ── Načtení dat ─────────────────────────────────────────────────────────────── +print("Načítám data z DB ...") +conn = connect_mysql() +cur = conn.cursor() + +# 1. Ze seznam_pojistencu_davky: rozsah přítomnosti + počet dávek +cur.execute(""" + SELECT + cip, + MIN(prijmeni) AS prijmeni, + MIN(jmeno) AS jmeno, + MIN(DATE(CONCAT(davka_rok,'-',LPAD(davka_mesic,2,'0'),'-01'))) AS prvni_davka, + MAX(DATE(CONCAT(davka_rok,'-',LPAD(davka_mesic,2,'0'),'-01'))) AS posledni_davka, + COUNT(DISTINCT CONCAT(davka_rok,davka_mesic)) AS pocet_davek + FROM seznam_pojistencu_davky + WHERE pojistovna='111' + GROUP BY cip +""") +seznam = {r[0]: {"prijmeni": r[1], "jmeno": r[2], + "prvni": r[3], "posledni": r[4], "pocet_davek": r[5]} + for r in cur.fetchall()} + +# Nejnovější datum dávky (pro sloupec "v aktuální dávce") +cur.execute(""" + SELECT MAX(DATE(CONCAT(davka_rok,'-',LPAD(davka_mesic,2,'0'),'-01'))) + FROM seznam_pojistencu_davky WHERE pojistovna='111' +""") +nejnovejsi_davka = cur.fetchone()[0] + +cur.execute(""" + SELECT DISTINCT cip FROM seznam_pojistencu_davky + WHERE pojistovna='111' + AND CONCAT(davka_rok,LPAD(davka_mesic,2,'0')) = ( + SELECT MAX(CONCAT(davka_rok,LPAD(davka_mesic,2,'0'))) + FROM seznam_pojistencu_davky WHERE pojistovna='111' + ) +""") +v_aktualni = {r[0] for r in cur.fetchall()} + +# 2. Z vzp_registrace_lekari: nejnovější záznam na pacienta (u nás, odb 001) +cur.execute(""" + SELECT r.rc, r.datum_zahajeni, r.datum_ukonceni, r.k_datu + FROM vzp_registrace_lekari r + INNER JOIN ( + SELECT rc, MAX(k_datu) AS max_k + FROM vzp_registrace_lekari + WHERE kod_odbornosti='001' AND ICP='09305001' AND ma_lekare=1 + GROUP BY rc + ) latest ON r.rc = latest.rc AND r.k_datu = latest.max_k + WHERE r.kod_odbornosti='001' AND r.ICP='09305001' AND r.ma_lekare=1 +""") +registrace = {r[0]: {"zahajeni": r[1], "ukonceni": r[2], "k_datu": r[3]} + for r in cur.fetchall()} + +# 3. Ze seznam_pojistencu_zlomy (13 nespárovaných) +cur.execute("SELECT cip, zlom_datum, zlom_zdroj, stav FROM seznam_pojistencu_zlomy") +zlomy = {r[0]: {"ukonceni": r[1], "zdroj": r[2], "stav": r[3]} + for r in cur.fetchall()} + +# 4. Následný lékař po ukončení +cur.execute(""" + SELECT cip, stav_vzp, novy_nazev, novy_ordinace, novy_icp, datum_prehlaseni + FROM seznam_pojistencu_nasledny_lekar +""") +nasledni = {r[0]: {"stav_vzp": r[1], "novy_nazev": r[2], + "novy_ordinace": r[3], "novy_icp": r[4], + "datum_prehlaseni": r[5]} + for r in cur.fetchall()} + +cur.close() +conn.close() + +# ── Sestavení řádků ─────────────────────────────────────────────────────────── +print(f"Pacientů celkem: {len(seznam)}") + +radky = [] +for cip, s in seznam.items(): + v_akt = cip in v_aktualni + + if cip in registrace: + reg = registrace[cip] + du = reg["ukonceni"] + zdroj = f"vzp_registrace ({reg['k_datu']})" + elif cip in zlomy: + z = zlomy[cip] + du = z["ukonceni"] + zdroj = z["zdroj"] + else: + du = None + zdroj = "—" + + if du is None: + stav = "aktivní" + elif du.year >= 3000: + stav = "aktivní" + du = None # nezobrazujeme 3000-01-01 + elif du >= TODAY: + stav = "aktivní" + else: + stav = "ukončen" + + if zdroj == "—" and not v_akt: + stav = "nenalezen" + + # Následný lékař + nl = nasledni.get(cip) + if nl: + po_ukonceni = nl["stav_vzp"] # prehlasil / nenalezen / bez_lekare + novy_lekar = nl["novy_nazev"] or nl["novy_ordinace"] or "" + if nl["novy_icp"]: + novy_lekar += f" (ICP {nl['novy_icp']})" + datum_prehl = nl["datum_prehlaseni"] + else: + po_ukonceni = "" + novy_lekar = "" + datum_prehl = None + + radky.append({ + "cip": cip, + "prijmeni": s["prijmeni"], + "jmeno": s["jmeno"], + "prvni": s["prvni"], + "posledni": s["posledni"], + "pocet_davek": s["pocet_davek"], + "v_aktualni": v_akt, + "ukonceni": du, + "zdroj": zdroj, + "stav": stav, + "po_ukonceni": po_ukonceni, + "novy_lekar": novy_lekar, + "datum_prehl": datum_prehl, + }) + +radky.sort(key=lambda r: (r["prijmeni"], r["jmeno"])) + +aktivni = [r for r in radky if r["stav"] == "aktivní"] +ukonceni = sorted([r for r in radky if r["stav"] == "ukončen"], + key=lambda r: r["ukonceni"] or date.min) +nenalezeni = [r for r in radky if r["stav"] == "nenalezen"] + +print(f" Aktivní: {len(aktivni)}") +print(f" Ukončení: {len(ukonceni)}") +print(f" Nenalezeni: {len(nenalezeni)}") + +# ── Excel ───────────────────────────────────────────────────────────────────── +SLOUPCE_ALL = ["Příjmení", "Jméno", "ČIP", "První\ndávka", "Poslední\ndávka", + "Počet\ndávek", "V aktuální\ndávce", "Ukončení\nregistrace", + "Stav", "Po ukončení", "Nový lékař / poznámka", "Datum\npřehlášení"] +WIDTHS_ALL = [24, 18, 13, 12, 13, 9, 10, 14, 10, 13, 40, 13] + +SLOUPCE = SLOUPCE_ALL # alias pro funkci zapsat_list +WIDTHS = WIDTHS_ALL +NCOLS = len(SLOUPCE) +DATE_FMT = "DD.MM.YYYY" + +def zapsat_list(ws, nadpis, bg_title, seznam_radku, stav_bg): + ws.freeze_panes = "A3" + title_row(ws, 1, nadpis, NCOLS, bg_title) + ws.row_dimensions[2].height = 30 + for col, h in enumerate(SLOUPCE, 1): + hdr(ws.cell(2, col), h) + autofit(ws, WIDTHS) + + PO_CLR = {"prehlasil": "375623", "nenalezen": "C55A11", + "bez_lekare": "7F6000", "": "000000"} + PO_TXT = {"prehlasil": "přehlásil se", "nenalezen": "nenalezen", + "bez_lekare": "bez lékaře", "": ""} + + for ri, r in enumerate(seznam_radku, 3): + bg = stav_bg if ri % 2 == 0 else None + cell(ws, ri, 1, r["prijmeni"], bg) + cell(ws, ri, 2, r["jmeno"], bg) + cell(ws, ri, 3, r["cip"], bg, align="center") + cell(ws, ri, 4, r["prvni"], bg, align="center", fmt=DATE_FMT) + cell(ws, ri, 5, r["posledni"], bg, align="center", fmt=DATE_FMT) + cell(ws, ri, 6, r["pocet_davek"], bg, align="right") + akt_txt = "✓" if r["v_aktualni"] else "–" + akt_clr = "375623" if r["v_aktualni"] else "C55A11" + cell(ws, ri, 7, akt_txt, bg, bold=True, align="center", color=akt_clr) + cell(ws, ri, 8, r["ukonceni"], bg, align="center", fmt=DATE_FMT) + cell(ws, ri, 9, r["stav"], bg, align="center", bold=True, + color=("375623" if r["stav"]=="aktivní" else + "C55A11" if r["stav"]=="ukončen" else "7F6000")) + po = r.get("po_ukonceni", "") + cell(ws, ri, 10, PO_TXT.get(po, po), bg, align="center", bold=bool(po), + color=PO_CLR.get(po, "000000")) + cell(ws, ri, 11, r.get("novy_lekar", ""), bg) + cell(ws, ri, 12, r.get("datum_prehl"), bg, align="center", fmt=DATE_FMT) + +wb = openpyxl.Workbook() + +# List 1 – Všichni +ws1 = wb.active +ws1.title = "Všichni pojištěnci" +zapsat_list(ws1, + f"VZP pojištěnci — kompletní přehled ({TODAY.strftime('%d.%m.%Y')}) | " + f"celkem: {len(radky)} | aktivní: {len(aktivni)} | ukončení: {len(ukonceni)}", + C_HDR_DARK, radky, C_ZEBRA) + +# List 2 – Aktivní +ws2 = wb.create_sheet("Aktivní") +zapsat_list(ws2, + f"Aktivní pojištěnci ({TODAY.strftime('%d.%m.%Y')}) — celkem: {len(aktivni)}", + C_TITLE_AKT, aktivni, C_AKTIVNI) + +# List 3 – Ukončení +ws3 = wb.create_sheet("Ukončení") +zapsat_list(ws3, + f"Ukončená registrace — celkem: {len(ukonceni)}", + C_TITLE_UKO, ukonceni, C_UKONCEN) + +# List 4 – Nenalezeni +if nenalezeni: + ws4 = wb.create_sheet("Nenalezeni") + zapsat_list(ws4, + f"Bez dostatečných dat — celkem: {len(nenalezeni)}", + C_TITLE_NEN, nenalezeni, C_NENALEZEN) + +wb.save(OUTPUT) +print(f"\nExcel uložen: {OUTPUT}") diff --git a/Insurance/SeznamPojistencu/zadej_davku.py b/Insurance/SeznamPojistencu/Tests/zadej_davku.py similarity index 100% rename from Insurance/SeznamPojistencu/zadej_davku.py rename to Insurance/SeznamPojistencu/Tests/zadej_davku.py diff --git a/Insurance/SeznamPojistencu/porovnani_davek.xlsx b/Insurance/SeznamPojistencu/porovnani_davek.xlsx new file mode 100644 index 0000000..bd992f4 Binary files /dev/null and b/Insurance/SeznamPojistencu/porovnani_davek.xlsx differ diff --git a/Insurance/SeznamPojistencu/report_pojistenci.xlsx b/Insurance/SeznamPojistencu/report_pojistenci.xlsx new file mode 100644 index 0000000..a155cc2 Binary files /dev/null and b/Insurance/SeznamPojistencu/report_pojistenci.xlsx differ