diff --git a/Dotazy/DOTAZY.md b/Dotazy/DOTAZY.md new file mode 100644 index 0000000..696d1a5 --- /dev/null +++ b/Dotazy/DOTAZY.md @@ -0,0 +1,250 @@ +# Dotazy — přehled lékového záznamu pacienta + +Skripty pro zobrazení a export lékového záznamu konkrétního pacienta z MySQL databáze `medicus`. +Pacient se identifikuje **rodným číslem** — to se vyhledá v lokální Firebird databázi Medicusu, +odkud se získá příjmení a datum narození, a teprve těmito dvěma hodnotami se najde pacient v MySQL. + +--- + +## Soubory + +| Soubor | Co dělá | +|--------|---------| +| `prehled_pacienta.py` | Konzolový výpis — lékaři + předpisy pacienta | +| `prehled_pacienta_excel.py` | Export do formátovaného souboru Excel (.xlsx) | + +--- + +## Nastavení (obě skripty) + +Na začátku každého souboru jsou tři proměnné: + +```python +RODNE_CISLO = "440802/018" # rodné číslo — funguje s lomítkem i bez: "4408020183" +DATUM_OD = "01.01.2025" # předpisy od tohoto data; None = všechny předpisy +VYSTUP_DIR = None # pouze excel: složka výstupu; None = stejná jako skript +``` + +--- + +## Spuštění + +```bash +# Konzolový výpis +.venv\Scripts\python.exe Dotazy\prehled_pacienta.py + +# Export do Excelu +.venv\Scripts\python.exe Dotazy\prehled_pacienta_excel.py +``` + +--- + +## Zdroje dat + +### 1. Firebird — Medicus (`medicus.fdb`) + +Slouží výhradně k identifikaci pacienta podle rodného čísla. + +``` +DSN: localhost:c:\medicus 3\data\medicus.fdb +User: SYSDBA / masterkey +Charset: win1250 +Tabulka: KAR +``` + +Dotaz: +```sql +SELECT KAR.PRIJMENI, KAR.JMENO, KAR.DATNAR +FROM KAR WHERE KAR.RODCIS = ? +``` + +Rodné číslo se normalizuje před dotazem — odstraní se lomítko a mezery: +```python +rc = rc.replace("/", "").replace(" ", "").strip() +``` + +### 2. MySQL — databáze `medicus` + +Obsahuje lékové záznamy stažené z eReceptu SÚKL. + +``` +Host: 192.168.1.76 +User: root +DB: medicus +``` + +Pacient se vyhledá podle příjmení a data narození (získaných z Firebirdu): +```sql +SELECT id, prijmeni, jmena, datum_narozeni +FROM pacient +WHERE prijmeni = %s AND datum_narozeni = %s +``` + +--- + +## Co se zobrazuje + +### Část 1 — Předepisující lékaři + +Všichni lékaři, kteří pacientovi za celou dobu předepsali alespoň jeden lék, +seřazeni sestupně podle počtu předpisů. + +Sloupce: `#` | `Lékař` | `Odbornost` | `Pracoviště a adresa` | `Předpisů` + +```sql +SELECT pr.prijmeni, pr.jmena, + pr.icp, + CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa, + COUNT(*) AS pocet_predpisu +FROM zprava z +JOIN predpis p ON p.zprava_id = z.id +JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho +WHERE z.pacient_id = %s +GROUP BY pr.lekar_kod, pr.prijmeni, pr.jmena, pr.icp, + pr.pzs_nazev, pr.ulice, pr.psc, pr.mesto +ORDER BY pocet_predpisu DESC +``` + +### Část 2 — Všechny předpisy + +Předpisy od `DATUM_OD`, seřazené sestupně dle data vystavení. + +Zobrazuje se **vydaný lék** (z tabulky `vydej`), nikoli předepsaný název. +Pokud lék nebyl vyzvednut, zobrazí se předepsaný název s příznakem `*NV`. + +Sloupce: `#` | `Datum` | `Vydaný lék` | `ATC` | `Návod` | `Lékař` | `Odbornost` | `Adresa` + +```sql +SELECT p.datum_vystaveni, + COALESCE(v.nazev, p.nazev) AS vydany_lek, + v.nazev IS NULL AS nevyzvednuto, + p.atc, + p.navod, + pr.prijmeni, + pr.jmena, + pr.icp, + CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa +FROM zprava z +JOIN predpis p ON p.zprava_id = z.id +JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho +LEFT JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis +WHERE z.pacient_id = %s + AND p.datum_vystaveni >= %s -- pouze pokud DATUM_OD není None +ORDER BY p.datum_vystaveni DESC +``` + +Klíčový princip `COALESCE(v.nazev, p.nazev)`: +- `v.nazev` — název léku, který lékárna **skutečně vydala** (může být jiná značka než předepsaná) +- `p.nazev` — název léku, který lékař **předepsal** (zobrazí se jen pokud výdej neexistuje → `*NV`) + +--- + +## Odbornost lékaře + +Odbornost se odvozuje z posledních 3 číslic pole `predepisujici.icp` (IČP pracoviště). + +``` +ICP: 09305001 → kód odbornosti: 001 → Praktický lékař +ICP: 08006272 → kód odbornosti: 272 → Alergologie +ICP: 08075603 → kód odbornosti: 603 → Onkologie +``` + +Funkce: +```python +def odbornost_z_icp(icp): + if not icp or len(icp) < 3: + return "" + return ODBORNOST.get(icp[-3:], f"odb. {icp[-3:]}") +``` + +Pro neznámé kódy se zobrazí `odb. XXX` (XXX = třímístný číselný kód). + +### Slovník ODBORNOST — vybrané klíčové kódy + +| Kód | Odbornost | Kód | Odbornost | +|-----|-----------|-----|-----------| +| 001 | Praktický lékař | 101 | Vnitřní lékařství | +| 002 | Pediatr (prakt.) | 104 | Kardiologie | +| 003 | Chirurgie | 105 | Gastroenterologie | +| 004 | Ortopedie | 108 | Nefrologie | +| 005 | ORL | 110 | Diabetologie | +| 006 | Gynekologie | 121 | Endokrinologie | +| 007 | Urologie | 156 | Hematologie | +| 008 | Neurologie | 169 | Revmatologie | +| 009 | Psychiatrie | 263 | Urologie | +| 012 | Dermatovenerologie | 272 | Alergologie | +| 018 | Pneumologie | 283 | Dětská neurochir. | +| 021 | Radiodiagnostika | 302 | Radiodiagnostika | +| 024 | Klin. biochemie | 324 | Klin. onkologie | +| 060 | Dětská chirurgie | 590 | Lékárenství | +| 074 | Neurochirurgie | 603 | Onkologie | +| 091 | Gynekolog. onkologie | 704 | Kardiochirurgie | +| 096 | Léčebná rehabilitace | 801 | Fyzioterapie | + +Celý slovník obsahuje ~170 kódů (viz zdrojový kód skriptů). +Kompletní číselník VZP/SÚKL: (sekce číselníky). + +--- + +## Excel export (`prehled_pacienta_excel.py`) + +Soubor se ukládá do stejné složky jako skript (nebo do `VYSTUP_DIR`). + +### Pojmenování souborů + +``` +LZ_{Prijmeni}_{Jmeno}_{datum_narozeni}.xlsx ← základní +LZ_{Prijmeni}_{Jmeno}_{datum_narozeni}_v2.xlsx ← pokud základní existuje +LZ_{Prijmeni}_{Jmeno}_{datum_narozeni}_v3.xlsx ← atd. +``` + +Versioning zabrání přepsání dříve exportovaných souborů. + +### Vzhled a formátování + +| Prvek | Barva | Popis | +|-------|-------|-------| +| Záhlaví (jméno pacienta) | `#1F4E79` tmavě modrá | tučné, 14pt | +| Záhlaví tabulky | `#1F4E79` tmavě modrá | bílý text, 10pt | +| Nadpis sekce | `#2E75B6` střední modrá | bílý text, 11pt | +| Info o pacientovi | `#DEEAF1` světle modrá | datum narozeni, datum tisku, předpisy od | +| Sudé řádky | `#EBF3FB` velmi světle modrá | střídání řádků | +| Liché řádky | `#FFFFFF` bílá | | +| Nevyzvednuto | `#FCE4D6` lososová | zvýraznění celého řádku | +| Ohraničení | `#B8CCE4` světle modrá | tenká linka | + +- Font: **Arial** ve všech buňkách +- Automatická šířka sloupců a výška řádků (`autofit`) +- Zmrazení prvního řádku (`freeze_panes = "A2"`) +- 8 sloupců: `#` | `Lékař/Datum` | `Odbornost/Vydaný lék` | `Pracoviště/ATC` | … | `Předpisů/Pracoviště a adresa` + +### Tabulka lékařů (8 sloupců) + +`#` | `Lékař` | `Odbornost` | `Pracoviště` | `Ulice` | `PSČ` | `Město` | `Předpisů` + +### Tabulka předpisů (8 sloupců) + +`#` | `Datum` | `Vydaný lék` | `ATC` | `Návod` | `Lékař` | `Odbornost` | `Pracoviště a adresa` + +--- + +## Závislosti + +``` +pymysql ← MySQL klient +fdb ← Firebird klient +openpyxl ← Excel export (pouze prehled_pacienta_excel.py) +``` + +Všechny jsou součástí `requirements.txt` a nainstalují se přes `setup.ps1`. + +--- + +## Typické chybové situace + +| Chyba | Příčina | Řešení | +|-------|---------|--------| +| `Rodne cislo nenalezeno v Medicusu` | RC není v tabulce KAR | Zkontrolovat číslo, ověřit v Medicusu | +| `Pacient nema zaznam v MySQL` | Lékový záznam nebyl stažen | Spustit `07StahnoutVsechny.py` nebo `reimport_z_xml.py` | +| `PermissionError` při ukládání xlsx | Soubor je otevřen v Excelu | Zavřít Excel a spustit znovu — verzování uloží jako `_v2` | +| Odbornost zobrazena jako `odb. XXX` | Kód není ve slovníku | Informativní stav — kód je platný, jen není pojmenován | diff --git a/Dotazy/LZ_Buzalka_Vladimír_1973-09-20.xlsx b/Dotazy/LZ_Buzalka_Vladimír_1973-09-20.xlsx new file mode 100644 index 0000000..0b1fca2 Binary files /dev/null and b/Dotazy/LZ_Buzalka_Vladimír_1973-09-20.xlsx differ diff --git a/Dotazy/LZ_Havelka_Miroslav_1944-08-02.xlsx b/Dotazy/LZ_Havelka_Miroslav_1944-08-02.xlsx new file mode 100644 index 0000000..6581ff0 Binary files /dev/null and b/Dotazy/LZ_Havelka_Miroslav_1944-08-02.xlsx differ diff --git a/Dotazy/LZ_Havelka_Miroslav_1944-08-02_162618.xlsx b/Dotazy/LZ_Havelka_Miroslav_1944-08-02_162618.xlsx new file mode 100644 index 0000000..24a1e9b Binary files /dev/null and b/Dotazy/LZ_Havelka_Miroslav_1944-08-02_162618.xlsx differ diff --git a/Dotazy/LZ_Havelka_Miroslav_1944-08-02_v2.xlsx b/Dotazy/LZ_Havelka_Miroslav_1944-08-02_v2.xlsx new file mode 100644 index 0000000..1a58b7e Binary files /dev/null and b/Dotazy/LZ_Havelka_Miroslav_1944-08-02_v2.xlsx differ diff --git a/Dotazy/LZ_Havelka_Miroslav_1944-08-02_v3.xlsx b/Dotazy/LZ_Havelka_Miroslav_1944-08-02_v3.xlsx new file mode 100644 index 0000000..88d05b3 Binary files /dev/null and b/Dotazy/LZ_Havelka_Miroslav_1944-08-02_v3.xlsx differ diff --git a/Dotazy/prehled_pacienta.py b/Dotazy/prehled_pacienta.py index d18beb5..1ca7a5b 100644 --- a/Dotazy/prehled_pacienta.py +++ b/Dotazy/prehled_pacienta.py @@ -16,8 +16,210 @@ import fdb import pymysql import pymysql.cursors +# Kody odbornosti dle SUKL / VZP (posledni 3 cislice ICP) +ODBORNOST = { + # Základní ambulantní odbornosti + "001": "Praktický lékař", + "002": "Pediatr (prakt.)", + "003": "Chirurgie", + "004": "Ortopedie", + "005": "ORL", + "006": "Gynekologie", + "007": "Urologie", + "008": "Neurologie", + "009": "Psychiatrie", + "010": "Oftalmologie", + "011": "Zubní lékařství", + "012": "Dermatovenerologie", + "013": "Infekční lékařství", + "014": "Radiodiagnostika", + "015": "Stomatochirurgie", + "016": "Čelistní ortopedie", + "017": "Dětská psychiatrie", + "018": "Pneumologie", + "019": "Anesteziologie", + "020": "Rehabilitace", + "021": "Radiodiagnostika", + "022": "Radioterapie", + "023": "Nukleární medicína", + "024": "Klin. biochemie", + "025": "Alergologie/imunologie", + "026": "Hematologie", + "027": "Soudní lékařství", + "028": "Soudní psychiatrie", + "029": "Lékařská genetika", + "031": "Gastroenterologie", + "032": "Nefrologie", + "033": "Kardiologie", + "034": "Endokrinologie/diab.", + "035": "Revmatologie", + "040": "Vnitřní lékařství", + "041": "Geriatrie", + "042": "Klin. farmakologie", + "043": "Diabetologie", + "044": "Endokrinologie", + "045": "Hepatologie", + "052": "Dětská neurologie", + "060": "Dětská chirurgie", + "065": "Plastická chirurgie", + "066": "Cévní chirurgie", + "067": "Kardiochirurgie", + "072": "Foniatrie", + "074": "Neurochirurgie", + "077": "Maxilofaciální chir.", + "079": "Hrudní chirurgie", + "082": "Urologie", + "083": "Andrologie", + "085": "Proktologie", + "091": "Gynekolog. onkologie", + "092": "Reprodukční medicína", + "096": "Léčebná rehabilitace", + "097": "Fyzioterapie", + # Interní a specializované odbornosti + "101": "Vnitřní lékařství", + "102": "Kardiologie", + "104": "Kardiologie", + "105": "Gastroenterologie", + "106": "Hepatologie", + "107": "Nefrologie", + "108": "Nefrologie", + "110": "Diabetologie", + "111": "Endokrinologie", + "114": "Pneumologie", + "115": "Ftizeologie", + "121": "Endokrinologie", + "122": "Diabetologie", + "129": "Andrologie", + "143": "Psychiatrie", + "144": "Psychoterapie", + "145": "Adiktologie", + "148": "Dětská psychiatrie", + "155": "Oční onkologie", + "156": "Hematologie", + "157": "Hemostáza", + "160": "Neurologie", + "162": "Epileptologie", + "163": "Dětská neurologie", + "164": "Neurorehabilit.", + "168": "Klin. neurofyziologie", + "169": "Revmatologie", + "174": "Ortoped. protetika", + "181": "Infektologie", + "183": "Tropická medicína", + "185": "Mikrobiologie", + "188": "Virologie", + # Chirurgické a dětské odbornosti + "200": "Stomatologie", + "201": "Stomatochirurgie", + "202": "Maxilofaciální chir.", + "203": "Parodontologie", + "204": "Ortodoncie", + "205": "Zubní protetika", + "206": "Dětská stomatologie", + "220": "Pediatrie", + "221": "Neonatologie", + "222": "Dětská endokrinol.", + "223": "Dětská gastroenterol.", + "234": "Dětská hematologie", + "239": "Dětská nefrologie", + "243": "Dětská pneumologie", + "245": "Dětská psychiatrie", + "246": "Dětská revmatologie", + "247": "Dětská kardiologie", + "250": "Dětská neurologie", + "251": "Dětská neurologie", + "258": "Dětská onkologie", + "261": "Dětská chirurgie", + "262": "Dětská ortopedie", + "263": "Urologie", + "264": "Dětská stomatologie", + "271": "Dětská klin. biochem.", + "272": "Alergologie", + "273": "Dětská alergologie", + "281": "Dětská dermatologie", + "282": "Dětská radiologie", + "283": "Dětská neurochir.", + "289": "Dětská kardiochir.", + "291": "Dětská onkol. chir.", + "294": "Dětská oftalmologie", + "295": "Dětská gynekologie", + # Onkologie a zobrazovací metody + "300": "Onkologie", + "301": "Klin. onkologie", + "302": "Radiodiagnostika", + "303": "Radioterapie", + "304": "Nukleární medicína", + "305": "Nukleární kardiologie", + "316": "Klin. genetika", + "319": "Soudní lékařství", + "321": "Cytologie", + "324": "Klin. onkologie", + "333": "Onkologie", + # Stomatologie (500-599) + "501": "Zubní lékařství", + "502": "Čelistní ortopedie", + "503": "Stomatochirurgie", + "508": "Parodontologie", + "509": "Ortodoncie", + "510": "Dětská stomatologie", + "513": "Zubní protetika", + "535": "Orální medicína", + "555": "Stomatologie", + "558": "Zubní lékařství", + "559": "Stomatologie", + "560": "Stomatologie", + "562": "Stomatologie", + "571": "Stomatologie", + "574": "Stomatologie", + "580": "Stomatologie", + "581": "Stomatologie", + "582": "Stomatologie", + "584": "Stomatologie", + "590": "Lékárenství", + # Onkologie (600-699) + "600": "Onkologie", + "601": "Klin. onkologie", + "603": "Onkologie", + "606": "Radioterapie", + "607": "Nukleární medicína", + "615": "Onkologie", + # Kardiochirurgie, ostatní (700+) + "700": "Chirurgie", + "701": "Cévní chirurgie", + "702": "Hrudní chirurgie", + "704": "Kardiochirurgie", + "705": "Chirurgie", + "706": "Plastická chirurgie", + "719": "Dětská chirurgie", + "721": "Ortopedie", + "722": "Ortopedie", + "723": "Ortopedie", + # Fyzioterapie, rehabilitace (800+) + "801": "Fyzioterapie", + "802": "Ergoterapie", + "852": "Fyzioterapie", + "853": "Fyzioterapie", + "858": "Fyzioterapie", + "860": "Fyzioterapie", + "862": "Fyzioterapie", + "873": "Fyzioterapie", + "880": "Rehabilitace", + "881": "Endokrinologie", + "885": "Rehabilitace", + "889": "Rehabilitace", + "890": "Rehabilitace", +} + + +def odbornost_z_icp(icp): + """Vrati nazev odbornosti z ICP kodu (posledni 3 cislice).""" + if not icp or len(icp) < 3: + return "" + kod = icp[-3:] + return ODBORNOST.get(kod, f"odb. {kod}") + # ── NASTAVENÍ ───────────────────────────────────────────────────────────────── -RODNE_CISLO = "7/1234" # s lomitkem i bez lomitka +RODNE_CISLO = "440802/018" # funguje s lomitkem i bez: 7309208104 nebo 730920/8104 DATUM_OD = "01.01.2025" # None = vsechny predpisy # ───────────────────────────────────────────────────────────────────────────── @@ -37,8 +239,8 @@ DB = dict( cursorclass = pymysql.cursors.DictCursor, ) -SEP = "-" * 100 -SEP2 = "-" * 140 +SEP = "-" * 110 +SEP2 = "-" * 165 def parse_datum(s, nazev): @@ -85,13 +287,14 @@ def tiskni_lekare(cur, pacient_id, prijmeni, jmena, datum_narozeni): cur.execute( """ SELECT pr.prijmeni, pr.jmena, + pr.icp, CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa, COUNT(*) AS pocet_predpisu FROM zprava z JOIN predpis p ON p.zprava_id = z.id JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho WHERE z.pacient_id = %s - GROUP BY pr.lekar_kod, pr.prijmeni, pr.jmena, pr.pzs_nazev, pr.ulice, pr.psc, pr.mesto + GROUP BY pr.lekar_kod, pr.prijmeni, pr.jmena, pr.icp, pr.pzs_nazev, pr.ulice, pr.psc, pr.mesto ORDER BY pocet_predpisu DESC """, (pacient_id,), @@ -102,11 +305,12 @@ def tiskni_lekare(cur, pacient_id, prijmeni, jmena, datum_narozeni): print(f" PACIENT: {prijmeni} {jmena} | nar. {datum_narozeni.strftime('%d.%m.%Y')}") print(SEP) print(f"\nPREDEPISUJICI LEKARI:") - print(f"{'#':<4} {'Lekar':<30} {'Pracoviste a adresa':<55} {'Predpisu':>8}") + print(f"{'#':<4} {'Lekar':<30} {'Odbornost':<25} {'Pracoviste a adresa':<50} {'Predpisu':>8}") print(SEP) for i, r in enumerate(rows, 1): - lekar = f"{r['prijmeni']} {r['jmena']}" - print(f"{i:<4} {lekar:<30} {r['adresa']:<55} {r['pocet_predpisu']:>8}") + lekar = f"{r['prijmeni']} {r['jmena']}" + odb = odbornost_z_icp(r['icp']) + print(f"{i:<4} {lekar:<30} {odb:<25} {r['adresa']:<50} {r['pocet_predpisu']:>8}") if not rows: print(" Zadne predpisy nenalezeny.") @@ -118,11 +322,13 @@ def tiskni_predpisy(cur, pacient_id, datum_od): cur.execute( f""" SELECT p.datum_vystaveni, - COALESCE(v.nazev, '(nevyzvednuto)') AS vydany_lek, + COALESCE(v.nazev, p.nazev) AS vydany_lek, + v.nazev IS NULL AS nevyzvednuto, p.atc, p.navod, pr.prijmeni, pr.jmena, + pr.icp, CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa FROM zprava z JOIN predpis p ON p.zprava_id = z.id @@ -138,15 +344,18 @@ def tiskni_predpisy(cur, pacient_id, datum_od): od_text = f"od {datum_od.strftime('%d.%m.%Y')}" if datum_od else "vse" print(f"\nVSECHNY PREDPISY ({od_text}) — celkem {len(rows)}:") - print(f"{'#':<4} {'Datum':<12} {'Vydany lek':<30} {'ATC':<8} {'Navod':<25} {'Lekar':<25} Adresa") + print(f"{'#':<4} {'Datum':<12} {'Vydany lek':<30} {'ATC':<8} {'Navod':<20} {'Lekar':<25} {'Odbornost':<22} Adresa") print(SEP2) for i, r in enumerate(rows, 1): datum = r["datum_vystaveni"].strftime("%d.%m.%Y") lekar = f"{r['prijmeni']} {r['jmena']}" - lek = (r["vydany_lek"] or "")[:29] - navod = (r["navod"] or "")[:24] + lek = (r["vydany_lek"] or "")[:28] + if r["nevyzvednuto"]: + lek = f"{lek} *NV" + navod = (r["navod"] or "")[:19] atc = (r["atc"] or "") - print(f"{i:<4} {datum:<12} {lek:<30} {atc:<8} {navod:<25} {lekar:<25} {r['adresa']}") + odb = odbornost_z_icp(r["icp"])[:21] + print(f"{i:<4} {datum:<12} {lek:<30} {atc:<8} {navod:<20} {lekar:<25} {odb:<22} {r['adresa']}") if not rows: print(" Zadne predpisy nenalezeny.") print() diff --git a/Dotazy/prehled_pacienta_excel.py b/Dotazy/prehled_pacienta_excel.py new file mode 100644 index 0000000..d77ed23 --- /dev/null +++ b/Dotazy/prehled_pacienta_excel.py @@ -0,0 +1,515 @@ +""" +Export prehledu lekoveho zaznamu pacienta do Excelu. + +Nastaveni: + RODNE_CISLO ... rodne cislo pacienta (s lomitkem i bez) + DATUM_OD ... predpisy od tohoto data ve formatu DD.MM.RRRR (None = vsechny) + VYSTUP_DIR ... slozka kam se ulozi Excel (None = stejna slozka jako skript) +""" + +from datetime import datetime +from pathlib import Path +import sys +import fdb +import pymysql +import pymysql.cursors +from openpyxl import Workbook +from openpyxl.styles import (Font, PatternFill, Alignment, Border, Side, + GradientFill) +from openpyxl.utils import get_column_letter + +# Kody odbornosti dle SUKL / VZP (posledni 3 cislice ICP) +ODBORNOST = { + "001": "Praktický lékař", + "002": "Pediatr (prakt.)", + "003": "Chirurgie", + "004": "Ortopedie", + "005": "ORL", + "006": "Gynekologie", + "007": "Urologie", + "008": "Neurologie", + "009": "Psychiatrie", + "010": "Oftalmologie", + "011": "Zubní lékařství", + "012": "Dermatovenerologie", + "013": "Infekční lékařství", + "014": "Radiodiagnostika", + "015": "Stomatochirurgie", + "016": "Čelistní ortopedie", + "017": "Dětská psychiatrie", + "018": "Pneumologie", + "019": "Anesteziologie", + "020": "Rehabilitace", + "021": "Radiodiagnostika", + "022": "Radioterapie", + "023": "Nukleární medicína", + "024": "Klin. biochemie", + "025": "Alergologie/imunologie", + "026": "Hematologie", + "027": "Soudní lékařství", + "028": "Soudní psychiatrie", + "029": "Lékařská genetika", + "031": "Gastroenterologie", + "032": "Nefrologie", + "033": "Kardiologie", + "034": "Endokrinologie/diab.", + "035": "Revmatologie", + "040": "Vnitřní lékařství", + "041": "Geriatrie", + "042": "Klin. farmakologie", + "043": "Diabetologie", + "044": "Endokrinologie", + "045": "Hepatologie", + "052": "Dětská neurologie", + "060": "Dětská chirurgie", + "065": "Plastická chirurgie", + "066": "Cévní chirurgie", + "067": "Kardiochirurgie", + "072": "Foniatrie", + "074": "Neurochirurgie", + "077": "Maxilofaciální chir.", + "079": "Hrudní chirurgie", + "082": "Urologie", + "083": "Andrologie", + "085": "Proktologie", + "091": "Gynekolog. onkologie", + "092": "Reprodukční medicína", + "096": "Léčebná rehabilitace", + "097": "Fyzioterapie", + "101": "Vnitřní lékařství", + "102": "Kardiologie", + "104": "Kardiologie", + "105": "Gastroenterologie", + "106": "Hepatologie", + "107": "Nefrologie", + "108": "Nefrologie", + "110": "Diabetologie", + "111": "Endokrinologie", + "114": "Pneumologie", + "115": "Ftizeologie", + "121": "Endokrinologie", + "122": "Diabetologie", + "129": "Andrologie", + "143": "Psychiatrie", + "144": "Psychoterapie", + "145": "Adiktologie", + "148": "Dětská psychiatrie", + "155": "Oční onkologie", + "156": "Hematologie", + "157": "Hemostáza", + "160": "Neurologie", + "162": "Epileptologie", + "163": "Dětská neurologie", + "164": "Neurorehabilit.", + "168": "Klin. neurofyziologie", + "169": "Revmatologie", + "174": "Ortoped. protetika", + "181": "Infektologie", + "183": "Tropická medicína", + "185": "Mikrobiologie", + "188": "Virologie", + "200": "Stomatologie", + "201": "Stomatochirurgie", + "202": "Maxilofaciální chir.", + "203": "Parodontologie", + "204": "Ortodoncie", + "205": "Zubní protetika", + "206": "Dětská stomatologie", + "220": "Pediatrie", + "221": "Neonatologie", + "222": "Dětská endokrinol.", + "223": "Dětská gastroenterol.", + "234": "Dětská hematologie", + "239": "Dětská nefrologie", + "243": "Dětská pneumologie", + "245": "Dětská psychiatrie", + "246": "Dětská revmatologie", + "247": "Dětská kardiologie", + "250": "Dětská neurologie", + "251": "Dětská neurologie", + "258": "Dětská onkologie", + "261": "Dětská chirurgie", + "262": "Dětská ortopedie", + "263": "Urologie", + "264": "Dětská stomatologie", + "271": "Dětská klin. biochem.", + "272": "Alergologie", + "273": "Dětská alergologie", + "281": "Dětská dermatologie", + "282": "Dětská radiologie", + "283": "Dětská neurochir.", + "289": "Dětská kardiochir.", + "291": "Dětská onkol. chir.", + "294": "Dětská oftalmologie", + "295": "Dětská gynekologie", + "300": "Onkologie", + "301": "Klin. onkologie", + "302": "Radiodiagnostika", + "303": "Radioterapie", + "304": "Nukleární medicína", + "305": "Nukleární kardiologie", + "316": "Klin. genetika", + "319": "Soudní lékařství", + "321": "Cytologie", + "324": "Klin. onkologie", + "333": "Onkologie", + "501": "Zubní lékařství", + "502": "Čelistní ortopedie", + "503": "Stomatochirurgie", + "508": "Parodontologie", + "509": "Ortodoncie", + "510": "Dětská stomatologie", + "513": "Zubní protetika", + "535": "Orální medicína", + "555": "Stomatologie", + "558": "Zubní lékařství", + "559": "Stomatologie", + "560": "Stomatologie", + "562": "Stomatologie", + "571": "Stomatologie", + "574": "Stomatologie", + "580": "Stomatologie", + "581": "Stomatologie", + "582": "Stomatologie", + "584": "Stomatologie", + "590": "Lékárenství", + "600": "Onkologie", + "601": "Klin. onkologie", + "603": "Onkologie", + "606": "Radioterapie", + "607": "Nukleární medicína", + "615": "Onkologie", + "700": "Chirurgie", + "701": "Cévní chirurgie", + "702": "Hrudní chirurgie", + "704": "Kardiochirurgie", + "705": "Chirurgie", + "706": "Plastická chirurgie", + "719": "Dětská chirurgie", + "721": "Ortopedie", + "722": "Ortopedie", + "723": "Ortopedie", + "801": "Fyzioterapie", + "802": "Ergoterapie", + "852": "Fyzioterapie", + "853": "Fyzioterapie", + "858": "Fyzioterapie", + "860": "Fyzioterapie", + "862": "Fyzioterapie", + "873": "Fyzioterapie", + "880": "Rehabilitace", + "881": "Endokrinologie", + "885": "Rehabilitace", + "889": "Rehabilitace", + "890": "Rehabilitace", +} + + +def odbornost_z_icp(icp): + """Vrati nazev odbornosti z ICP kodu (posledni 3 cislice).""" + if not icp or len(icp) < 3: + return "" + return ODBORNOST.get(icp[-3:], f"odb. {icp[-3:]}") + + +# ── NASTAVENÍ ───────────────────────────────────────────────────────────────── +RODNE_CISLO = "440802/018" +DATUM_OD = "01.01.2025" # None = vsechny predpisy +VYSTUP_DIR = None # None = stejny adresar jako skript +# ───────────────────────────────────────────────────────────────────────────── + +FB = dict( + dsn = r"localhost:c:\medicus 3\data\medicus.fdb", + user = "SYSDBA", + password = "masterkey", + charset = "win1250", +) + +DB = dict( + host = "192.168.1.76", + user = "root", + password = "Vlado9674+", + database = "medicus", + charset = "utf8mb4", + cursorclass = pymysql.cursors.DictCursor, +) + +# ── Barvy ───────────────────────────────────────────────────────────────────── +C_HEADER_BG = "1F4E79" # tmave modra — hlavicka tabulky +C_HEADER_FG = "FFFFFF" # bila — text hlavicky +C_TITLE_BG = "2E75B6" # stredni modra — nadpis sekce +C_TITLE_FG = "FFFFFF" +C_INFO_BG = "DEEAF1" # svetle modra — info o pacientovi +C_ROW_ODD = "FFFFFF" # bila +C_ROW_EVEN = "EBF3FB" # velmi svetle modra — striped +C_NEVYZV_BG = "FCE4D6" # lososova — nevyzvednuto +C_BORDER = "B8CCE4" + + +def thin_border(): + s = Side(style="thin", color=C_BORDER) + return Border(left=s, right=s, top=s, bottom=s) + + +def header_fill(color): + return PatternFill("solid", fgColor=color) + + +def parse_datum(s, nazev): + try: + return datetime.strptime(s, "%d.%m.%Y").date() + except (ValueError, TypeError): + sys.exit(f"Spatny format data '{nazev}': '{s}'") + + +def najdi_v_firebirdu(rc): + rc = rc.replace("/", "").replace(" ", "") + conn = fdb.connect(**FB) + try: + cur = conn.cursor() + cur.execute("SELECT KAR.PRIJMENI, KAR.JMENO, KAR.DATNAR FROM KAR WHERE KAR.RODCIS = ?", (rc,)) + row = cur.fetchone() + if not row: + sys.exit(f"Rodne cislo '{rc}' nenalezeno v Medicusu.") + return {"prijmeni": row[0].strip(), "jmeno": row[1].strip(), "datnar": row[2]} + finally: + conn.close() + + +def nacti_data(prijmeni, datum_narozeni, datum_od): + conn = pymysql.connect(**DB) + try: + with conn.cursor() as cur: + cur.execute( + "SELECT id, prijmeni, jmena, datum_narozeni FROM pacient " + "WHERE prijmeni = %s AND datum_narozeni = %s", + (prijmeni, datum_narozeni) + ) + pac = cur.fetchone() + if not pac: + sys.exit(f"Pacient '{prijmeni}' nar. {datum_narozeni} nema zaznam v MySQL.") + + # Lekari + cur.execute(""" + SELECT pr.prijmeni, pr.jmena, + pr.icp, + pr.pzs_nazev, pr.ulice, pr.psc, pr.mesto, + COUNT(*) AS pocet + FROM zprava z + JOIN predpis p ON p.zprava_id = z.id + JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho + WHERE z.pacient_id = %s + GROUP BY pr.lekar_kod, pr.prijmeni, pr.jmena, pr.icp, + pr.pzs_nazev, pr.ulice, pr.psc, pr.mesto + ORDER BY pocet DESC + """, (pac["id"],)) + lekari = cur.fetchall() + + # Predpisy + podminka = "AND p.datum_vystaveni >= %s" if datum_od else "" + params = (pac["id"], datum_od) if datum_od else (pac["id"],) + cur.execute(f""" + SELECT p.datum_vystaveni, + COALESCE(v.nazev, p.nazev) AS vydany_lek, + v.nazev IS NULL AS nevyzvednuto, + p.atc, p.navod, + pr.prijmeni AS lek_prijmeni, pr.jmena AS lek_jmena, + pr.icp, + pr.pzs_nazev, pr.ulice, pr.psc, pr.mesto + FROM zprava z + JOIN predpis p ON p.zprava_id = z.id + JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho + LEFT JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis + WHERE z.pacient_id = %s {podminka} + ORDER BY p.datum_vystaveni DESC + """, params) + predpisy = cur.fetchall() + + return pac, lekari, predpisy + finally: + conn.close() + + +def nastav_sirky(ws, sirky): + for col, width in sirky.items(): + ws.column_dimensions[col].width = width + + +def autofit(ws, min_width=5, max_width=60, padding=2): + """Autofit sloupcu a radku podle obsahu.""" + col_widths = {} + for row in ws.iter_rows(): + for cell in row: + if cell.value is None: + continue + # Preskoc mergnuté bunky — jejich sirka se pocita ze zakladni bunky + if isinstance(cell, type(cell)) and hasattr(cell, 'column'): + text = str(cell.value) + # Tučný text je trochu širší + factor = 1.15 if (cell.font and cell.font.bold) else 1.0 + width = len(text) * factor + padding + col = get_column_letter(cell.column) + col_widths[col] = max(col_widths.get(col, min_width), width) + + for col, width in col_widths.items(): + ws.column_dimensions[col].width = min(max(width, min_width), max_width) + + # Autofit výšky řádků (wrap_text obsah) + for row in ws.iter_rows(): + max_lines = 1 + for cell in row: + if cell.value and cell.alignment and cell.alignment.wrap_text: + col_w = ws.column_dimensions[get_column_letter(cell.column)].width or 10 + lines = max(1, int(len(str(cell.value)) / max(col_w, 1)) + 1) + max_lines = max(max_lines, lines) + row_num = row[0].row + if max_lines > 1: + ws.row_dimensions[row_num].height = max(ws.row_dimensions[row_num].height or 15, + max_lines * 14) + + +def zapis_nadpis_sekce(ws, row, text, n_cols): + ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=n_cols) + cell = ws.cell(row=row, column=1, value=text) + cell.font = Font(name="Arial", bold=True, size=11, color=C_TITLE_FG) + cell.fill = header_fill(C_TITLE_BG) + cell.alignment = Alignment(horizontal="left", vertical="center", indent=1) + ws.row_dimensions[row].height = 20 + return row + 1 + + +def zapis_hlavicku(ws, row, hlavicka, n_cols=None): + for col, text in enumerate(hlavicka, 1): + cell = ws.cell(row=row, column=col, value=text) + cell.font = Font(name="Arial", bold=True, size=10, color=C_HEADER_FG) + cell.fill = header_fill(C_HEADER_BG) + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True) + cell.border = thin_border() + ws.row_dimensions[row].height = 28 + return row + 1 + + +def zapis_radek(ws, row, hodnoty, highlight=False): + bg = C_NEVYZV_BG if highlight else (C_ROW_EVEN if row % 2 == 0 else C_ROW_ODD) + fill = header_fill(bg) + for col, val in enumerate(hodnoty, 1): + cell = ws.cell(row=row, column=col, value=val) + cell.font = Font(name="Arial", size=10) + cell.fill = fill + cell.alignment = Alignment(vertical="center", wrap_text=True) + cell.border = thin_border() + ws.row_dimensions[row].height = 18 + return row + 1 + + +def vytvor_excel(pac, lekari, predpisy, datum_od, fb_pac): + wb = Workbook() + ws = wb.active + ws.title = "Lekovy zaznam" + + # ── Záhlaví — info o pacientovi ────────────────────────────────────────── + n_cols = 8 + ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=n_cols) + title_cell = ws.cell(row=1, column=1, + value=f"LÉKOVÝ ZÁZNAM — {pac['prijmeni'].upper()} {fb_pac['jmeno'].upper()}") + title_cell.font = Font(name="Arial", bold=True, size=14, color=C_HEADER_FG) + title_cell.fill = header_fill(C_HEADER_BG) + title_cell.alignment = Alignment(horizontal="left", vertical="center", indent=1) + ws.row_dimensions[1].height = 32 + + info = [ + ("Datum narození:", pac["datum_narozeni"].strftime("%d.%m.%Y")), + ("Datum tisku:", datetime.today().strftime("%d.%m.%Y")), + ("Předpisy od:", datum_od.strftime("%d.%m.%Y") if datum_od else "vše"), + ] + for i, (label, val) in enumerate(info, 2): + ws.merge_cells(start_row=i, start_column=1, end_row=i, end_column=2) + ws.merge_cells(start_row=i, start_column=3, end_row=i, end_column=n_cols) + lbl = ws.cell(row=i, column=1, value=label) + lbl.font = Font(name="Arial", bold=True, size=10) + lbl.fill = header_fill(C_INFO_BG) + lbl.alignment = Alignment(vertical="center", indent=1) + val_cell = ws.cell(row=i, column=3, value=val) + val_cell.font = Font(name="Arial", size=10) + val_cell.fill = header_fill(C_INFO_BG) + val_cell.alignment = Alignment(vertical="center") + ws.row_dimensions[i].height = 16 + + row = len(info) + 3 # prázdný řádek + + # ── Tabulka lékařů ─────────────────────────────────────────────────────── + row = zapis_nadpis_sekce(ws, row, "PŘEDEPISUJÍCÍ LÉKAŘI", n_cols) + row = zapis_hlavicku(ws, row, ["#", "Lékař", "Odbornost", "Pracoviště", "Ulice", "PSČ", "Město", "Předpisů"]) + for i, r in enumerate(lekari, 1): + adresa_ulice = r.get("ulice") or "" + row = zapis_radek(ws, row, [ + i, + f"{r['prijmeni']} {r['jmena']}", + odbornost_z_icp(r.get("icp")), + r.get("pzs_nazev") or "", + adresa_ulice, + r.get("psc") or "", + r.get("mesto") or "", + r["pocet"], + ]) + + row += 1 # prázdný řádek + + # ── Tabulka předpisů ───────────────────────────────────────────────────── + od_text = datum_od.strftime("%d.%m.%Y") if datum_od else "vše" + row = zapis_nadpis_sekce(ws, row, f"VŠECHNY PŘEDPISY (od {od_text}) — celkem {len(predpisy)}", n_cols) + row = zapis_hlavicku(ws, row, ["#", "Datum", "Vydaný lék", "ATC", "Návod", "Lékař", "Odbornost", "Pracoviště a adresa"]) + for i, r in enumerate(predpisy, 1): + nevyzv = bool(r["nevyzvednuto"]) + adresa = (f"{r.get('pzs_nazev') or ''}, {r.get('ulice') or ''}, " + f"{r.get('psc') or ''} {r.get('mesto') or ''}").strip(", ") + row = zapis_radek(ws, row, [ + i, + r["datum_vystaveni"].strftime("%d.%m.%Y") if r["datum_vystaveni"] else "", + r["vydany_lek"], + r.get("atc") or "", + r.get("navod") or "", + f"{r['lek_prijmeni']} {r['lek_jmena']}", + odbornost_z_icp(r.get("icp")), + adresa, + ], highlight=nevyzv) + + # ── Autofit sloupců a řádků ─────────────────────────────────────────────── + autofit(ws, min_width=5, max_width=60) + + # Zmraz záhlaví + ws.freeze_panes = "A2" + + return wb + + +def main(): + datum_od = parse_datum(DATUM_OD, "DATUM_OD") if DATUM_OD else None + + fb_pac = najdi_v_firebirdu(RODNE_CISLO) + prijmeni = fb_pac["prijmeni"] + datum_narozeni = fb_pac["datnar"] + + print(f"Nacitam data: {prijmeni} {fb_pac['jmeno']} nar. {datum_narozeni} ...") + pac, lekari, predpisy = nacti_data(prijmeni, datum_narozeni, datum_od) + print(f" {len(lekari)} lekaru, {len(predpisy)} predpisu") + + wb = vytvor_excel(pac, lekari, predpisy, datum_od, fb_pac) + + vyst = Path(VYSTUP_DIR) if VYSTUP_DIR else Path(__file__).parent + zaklad = vyst / f"LZ_{prijmeni}_{fb_pac['jmeno']}_{datum_narozeni}.xlsx" + if not zaklad.exists(): + soubor = zaklad + else: + i = 2 + while True: + soubor = vyst / f"LZ_{prijmeni}_{fb_pac['jmeno']}_{datum_narozeni}_v{i}.xlsx" + if not soubor.exists(): + break + i += 1 + wb.save(soubor) + print(f"Ulozeno: {soubor}") + + +if __name__ == "__main__": + main() diff --git a/LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md b/LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md index 9627bbc..6cfdc57 100644 --- a/LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md +++ b/LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md @@ -10,18 +10,67 @@ z eRecept SÚKL API a jejich uložení do relační databáze MySQL. | Soubor | Co dělá | |--------|---------| | `05UlozitOdpoved.py` | Stáhne XML pro **jednoho** pacienta (ruční test/ladění) | -| `06UlozitDoMySQL.py` | DDL schématu, parsování XML, import do MySQL — používá se jako knihovna i samostatně | +| `06UlozitDoMySQL.py` | DDL schématu, parsování XML, import do MySQL — používá se jako **knihovna**, ne spouštět přímo! | | `07StahnoutVsechny.py` | **Hlavní skript** — načte pacienty z Medicusu, stáhne lékové záznamy, uloží XML i DB záznamy | +| `reimport_z_xml.py` | Reimport XML ze zálohy bez volání API — viz sekce níže | ``` -LékovýZáznamWithClaude/ -├── 05UlozitOdpoved.py -├── 06UlozitDoMySQL.py -├── 07StahnoutVsechny.py -├── LEKOVY_ZAZNAM_DB.md -├── Logs/ ← log každého běhu (UTF-8, YYYY-MM-DD_HH-MM-SS.log) -├── Tests/ ← starší vývojové skripty -└── xml_archive/ ← archiv XML odpovědí (YYYY-MM-DD/Prijmeni_Jmena_datnar.xml) +recept/ +├── setup.ps1 ← vytvoří .venv, nainstaluje závislosti, Playwright chromium +├── requirements.txt ← seznam Python závislostí +├── .venv/ ← virtuální prostředí (Python 3.x) +│ +├── LékovýZáznamWithClaude/ +│ ├── 05UlozitOdpoved.py +│ ├── 06UlozitDoMySQL.py +│ ├── 07StahnoutVsechny.py +│ ├── reimport_z_xml.py +│ ├── LEKOVY_ZAZNAM_DB.md ← tento soubor +│ ├── Logs/ ← log každého běhu (UTF-8, YYYY-MM-DD_HH-MM-SS.log) +│ ├── Tests/ ← starší vývojové skripty +│ └── xml_archive/ ← archiv XML odpovědí (YYYY-MM-DD/Prijmeni_Jmena_datnar.xml) +│ +└── Dotazy/ + ├── prehled_pacienta.py ← konzolový přehled pacienta + ├── prehled_pacienta_excel.py ← export přehledu pacienta do Excelu + └── DOTAZY.md ← dokumentace dotazovacích skriptů +``` + +> **⚠️ NIKDY nespouštět `06UlozitDoMySQL.py` přímo** — zavolá `vytvor_schema()`, +> která provede `DROP TABLE` a smaže celou databázi. +> Pro import dat vždy použít `07StahnoutVsechny.py` nebo `reimport_z_xml.py`. + +--- + +## Nastavení prostředí (jednorázově) + +```powershell +# PowerShell — spustit jednou po naklonování projektu +cd U:\recept +.\setup.ps1 +``` + +`setup.ps1` provede: +1. Vytvoří `.venv` s Python interpretem z `C:\Python\python.exe` +2. Nainstaluje všechny závislosti z `requirements.txt` +3. Nainstaluje Playwright Chromium (pro případné automatizace) + +Po nastavení aktivace: +```powershell +.venv\Scripts\Activate.ps1 +``` + +### requirements.txt + +``` +requests +requests-pkcs12 +pymysql +fdb +zeep +mysql-connector-python +playwright +openpyxl ``` --- @@ -29,9 +78,6 @@ LékovýZáznamWithClaude/ ## Typické spuštění ```bash -# Čistý start — jednou (DROP + CREATE schéma, importuje odpoved_lekovy_zaznam.xml) -python 06UlozitDoMySQL.py - # Hromadné stažení všech registrovaných pacientů python 07StahnoutVsechny.py @@ -40,6 +86,9 @@ python 07StahnoutVsechny.py --prijmeni Buzalka,Buzalková,Kusinová # Dávkování po částech python 07StahnoutVsechny.py --offset 100 --limit 50 + +# Reimport ze zálohy XML (bez volání API) — viz níže +python reimport_z_xml.py ``` --- @@ -275,7 +324,7 @@ Lékaři, kteří pacientovi předepisovali (ze všech ordinací). | `prijmeni` | VARCHAR(35) | | | `jmena` | VARCHAR(24) | | | `icz` | CHAR(8) | IČZ zdravotnického zařízení | -| `icp` | CHAR(8) | IČP pracoviště | +| `icp` | CHAR(8) | IČP pracoviště — **poslední 3 číslice = kód odbornosti** (001 = prakt. lékař, 272 = alergologie…) | | `pzs_nazev` | VARCHAR(200) | název zdravotnického zařízení | | `ulice` | VARCHAR(150) | | | `mesto` | VARCHAR(100) | | @@ -374,10 +423,96 @@ SELECT prijmeni, jmena, datum_narozeni, poznamka FROM pacient WHERE poznamka IS NOT NULL ORDER BY prijmeni; + +-- lékaři dle odbornosti — kolik předpisů pochází od které speciality +SELECT RIGHT(pr.icp, 3) AS odb_kod, COUNT(*) AS pocet_predpisu +FROM predpis p +JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho +WHERE pr.icp IS NOT NULL +GROUP BY RIGHT(pr.icp, 3) +ORDER BY pocet_predpisu DESC; + +-- lékový záznam pacienta dle rodného čísla (přes Firebird → MySQL) +-- krok 1: z Medicusu zjistit příjmení a datum narozeni pro RC 7309208104 +-- krok 2: +SELECT pac.prijmeni, pac.jmena, pac.datum_narozeni, + p.datum_vystaveni, + COALESCE(v.nazev, p.nazev) AS vydany_lek, + v.nazev IS NULL AS nevyzvednuto, + p.atc, p.navod, + pr.prijmeni AS lekar, RIGHT(pr.icp, 3) AS odb_kod +FROM pacient pac +JOIN zprava z ON z.pacient_id = pac.id +JOIN predpis p ON p.zprava_id = z.id +JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho +LEFT JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis +WHERE pac.prijmeni = 'Buzalka' AND pac.datum_narozeni = '1973-09-20' +ORDER BY p.datum_vystaveni DESC; ``` --- +## Reimport ze zálohy XML (`reimport_z_xml.py`) + +Slouží k opětovnému naplnění MySQL databáze z lokálních XML souborů **bez volání eRecept API**. +Použití: obnova po neúmyslném smazání databáze, migrace na nový server, re-parsování při změně schématu. + +### Jak funguje + +1. Načte všechny registrované pacienty z Firebirdu (ICP `09305001`, odbornost `001`) +2. Pro každý XML soubor v archivu: + - Naparsuje XML (volá `parsuj_xml()` z `06UlozitDoMySQL.py`) + - Dohledá pacienta v Firebirdu dle příjmení + data narození z XML + - Pokud je registrovaný → `upsert` pacienta do MySQL (INSERT ON DUPLICATE KEY UPDATE) + - Zavolá `uloz()` — INSERT IGNORE, takže duplicity se ignorují +3. Výpis průběhu: `[ 1/1177] Buzalka_Vladimir_1973-09-20.xml OK 12p 18v` + +### Spuštění + +```bash +# Výchozí adresář: xml_archive/2026-04-11 +python reimport_z_xml.py + +# Konkrétní podadresář +python reimport_z_xml.py xml_archive/2026-04-11 + +# Celý archiv rekurzivně (všechna data) +python reimport_z_xml.py xml_archive +``` + +### Konfigurace v souboru + +```python +XML_ADRESAR = Path(__file__).parent / "xml_archive" / "2026-04-11" # výchozí adresář +ICP = "09305001" # IČP ordinace pro filtr registrovaných pacientů +ODB = "001" # odbornost (001 = praktický lékař) +``` + +### Poznámky + +- Pacienti, kteří nejsou v Firebirdu registrováni pod daným ICP/ODB, se přeskočí + (pokud ale existují v MySQL z předchozího importu, data se aktualizují) +- Firebird slouží jako autoritativní zdroj identit — `idpac` z KAR se propíše do MySQL `pacient.idpac` +- `INSERT IGNORE` zajistí idempotentnost — opakované spuštění nepřidá duplikáty + +--- + +## Dotazovací skripty (`Dotazy/`) + +Viz samostatnou dokumentaci: [`Dotazy/DOTAZY.md`](../Dotazy/DOTAZY.md) + +Stručný přehled: + +| Skript | Co dělá | +|--------|---------| +| `prehled_pacienta.py` | Konzolový výpis lékového záznamu pacienta (lékaři + předpisy) | +| `prehled_pacienta_excel.py` | Totéž, ale exportuje do formátovaného souboru Excel (.xlsx) | + +Pacient se identifikuje **rodným číslem** (nastavení `RODNE_CISLO` v záhlaví skriptu). +Oba skripty zobrazují **vydaný lék** (ne předepsaný), **odbornost lékaře** a příznak `*NV` pro nevyzvednuto. + +--- + ## Závislosti (Python) ``` @@ -385,10 +520,15 @@ requests requests-pkcs12 pymysql fdb +zeep +mysql-connector-python +playwright +openpyxl ``` ```bash -pip install requests requests-pkcs12 pymysql fdb +# Instalace (nebo použít setup.ps1) +pip install requests requests-pkcs12 pymysql fdb openpyxl ``` --- diff --git a/LékovýZáznamWithClaude/reimport_z_xml.py b/LékovýZáznamWithClaude/reimport_z_xml.py new file mode 100644 index 0000000..82168ad --- /dev/null +++ b/LékovýZáznamWithClaude/reimport_z_xml.py @@ -0,0 +1,194 @@ +""" +Reimport vsech XML souboru z xml_archive do MySQL — bez volani API. + +Pouziti: + python reimport_z_xml.py # vsechna XML z 2026-04-11 + python reimport_z_xml.py xml_archive/2026-04-11 # konkretni adresar + python reimport_z_xml.py xml_archive # vsechny podadresare rekurzivne +""" + +import sys +import importlib.util +from pathlib import Path +from datetime import date +import fdb +import pymysql +import pymysql.cursors + +# Windows konzole +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(errors="replace") + +# ── Konfigurace ─────────────────────────────────────────────────────────────── +XML_ADRESAR = Path(__file__).parent / "xml_archive" / "2026-04-11" + +FB = dict( + dsn = r"localhost:c:\medicus 3\data\medicus.fdb", + user = "SYSDBA", + password = "masterkey", + charset = "win1250", +) + +DB = dict( + host = "192.168.1.76", + user = "root", + password = "Vlado9674+", + database = "medicus", + charset = "utf8mb4", + cursorclass = pymysql.cursors.DictCursor, +) + +ICP = "09305001" +ODB = "001" +# ───────────────────────────────────────────────────────────────────────────── + +# Nacteni parsovaci logiky z 06UlozitDoMySQL.py +_spec = importlib.util.spec_from_file_location( + "m06", Path(__file__).parent / "06UlozitDoMySQL.py" +) +_m06 = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_m06) + +parsuj_xml = _m06.parsuj_xml +uloz = _m06.uloz +inicializuj_schema = _m06.inicializuj_schema + + +def nacti_pacienty_z_fb(): + """Vrati slovnik {(prijmeni_upper, datnar): idpac} ze vsech pacientu v Medicusu.""" + conn = fdb.connect(**FB) + try: + cur = conn.cursor() + dnes = date.today().isoformat() + cur.execute(""" + SELECT KAR.IDPAC, KAR.PRIJMENI, KAR.JMENO, KAR.DATNAR + FROM KAR + WHERE KAR.vyrazen = 'N' + AND EXISTS ( + SELECT 1 FROM registr r + JOIN icp i ON r.idicp = i.idicp + WHERE r.idpac = kar.idpac + AND r.datum <= ? + AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= ?) + AND r.priznak IN ('V','D','A') + AND i.icp = ? + AND i.odb = ? + ) + """, (dnes, dnes, ICP, ODB)) + result = {} + for row in cur.fetchall(): + idpac, prijmeni, jmeno, datnar = row + klic = (prijmeni.strip().upper(), datnar) + result[klic] = {"idpac": idpac, "prijmeni": prijmeni.strip(), "jmeno": jmeno.strip(), "datnar": datnar} + print(f"Firebird: nacteno {len(result)} registrovanych pacientu") + return result + finally: + conn.close() + + +def upsert_pacient(cur, pac): + cur.execute(""" + INSERT INTO pacient (idpac, prijmeni, jmena, datum_narozeni) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + prijmeni = VALUES(prijmeni), + jmena = VALUES(jmena) + """, (pac["idpac"], pac["prijmeni"], pac["jmeno"], pac["datnar"])) + cur.execute("SELECT id FROM pacient WHERE idpac = %s", (pac["idpac"],)) + return cur.fetchone()["id"] + + +def main(): + # Adresar z argumentu nebo default + adresar = Path(sys.argv[1]) if len(sys.argv) > 1 else XML_ADRESAR + if not adresar.is_dir(): + sys.exit(f"Adresar neexistuje: {adresar}") + + # Najdi vsechna XML rekurzivne + xml_soubory = sorted(adresar.rglob("*.xml")) + if not xml_soubory: + sys.exit(f"Zadne XML soubory nalezeny v: {adresar}") + print(f"Nalezeno {len(xml_soubory)} XML souboru v: {adresar}") + + # Nacti pacienty z Firebirdu + fb_pacienti = nacti_pacienty_z_fb() + + # Pripoj se k MySQL a inicializuj schema + conn = pymysql.connect(**DB) + try: + inicializuj_schema(conn) + + ok = chyba = preskoceno = 0 + p_celkem = v_celkem = 0 + + for i, xml_path in enumerate(xml_soubory, 1): + rel = xml_path.relative_to(Path(__file__).parent) + try: + zprava, predpisy, vydeji, predepisujici, vydavajici = parsuj_xml(xml_path) + except Exception as e: + print(f"[{i:4}/{len(xml_soubory)}] {xml_path.name:<45} CHYBA parsovani: {e}") + chyba += 1 + continue + + # Zjisti prijmeni a datum narozeni z XML odpovedi + pac_prijmeni = (zprava.get("pacient_prijmeni") or "").upper() + pac_datnar = zprava.get("pacient_datum_narozeni") # string YYYY-MM-DD nebo None + + # Prevod na date objekt pro porovnani s Firebirdem + if pac_datnar and isinstance(pac_datnar, str): + try: + from datetime import datetime + pac_datnar_d = datetime.strptime(pac_datnar[:10], "%Y-%m-%d").date() + except ValueError: + pac_datnar_d = None + elif hasattr(pac_datnar, "year"): + pac_datnar_d = pac_datnar + else: + pac_datnar_d = None + + klic = (pac_prijmeni, pac_datnar_d) + fb_pac = fb_pacienti.get(klic) + + if not fb_pac: + # Pacient neni registrovan — uloz bez idpac (bude ignorovan pri hromadnem behu) + # Zkus najit v MySQL podle jmena a data + with conn.cursor() as cur: + cur.execute( + "SELECT id FROM pacient WHERE prijmeni = %s AND datum_narozeni = %s", + (zprava.get("pacient_prijmeni"), pac_datnar) + ) + row = cur.fetchone() + if row: + pacient_id = row["id"] + else: + preskoceno += 1 + print(f"[{i:4}/{len(xml_soubory)}] {xml_path.name:<45} PRESKOCENO (neni v registru)") + continue + else: + with conn.cursor() as cur: + pacient_id = upsert_pacient(cur, fb_pac) + conn.commit() + + try: + stats = uloz(conn, zprava, predpisy, vydeji, predepisujici, vydavajici, + pacient_id=pacient_id, xml_soubor=str(rel)) + conn.commit() + p_celkem += stats["predpisy_novych"] + v_celkem += stats["vydeji_novych"] + print(f"[{i:4}/{len(xml_soubory)}] {xml_path.name:<45} OK " + f"{stats['predpisy_novych']:3}p {stats['vydeji_novych']:3}v") + ok += 1 + except Exception as e: + conn.rollback() + print(f"[{i:4}/{len(xml_soubory)}] {xml_path.name:<45} CHYBA ukladani: {e}") + chyba += 1 + + print() + print(f"Hotovo: {ok} OK, {chyba} chyb, {preskoceno} preskoceno") + print(f"Celkem vlozeno: {p_celkem} predpisu, {v_celkem} vydejuu") + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/tmp_check_odb.py b/tmp_check_odb.py new file mode 100644 index 0000000..756ae9f --- /dev/null +++ b/tmp_check_odb.py @@ -0,0 +1,38 @@ +import fdb, sys + +conn = fdb.connect( + dsn=r"localhost:c:\medicus 3\data\medicus.fdb", + user="SYSDBA", password="masterkey", charset="win1250" +) +cur = conn.cursor() + +# Try CSSZ_ODB +try: + cur.execute("SELECT FIRST 30 * FROM CSSZ_ODB") + cols = [d[0] for d in cur.description] + print("CSSZ_ODB cols:", cols) + for r in cur.fetchall(): + print(r) +except Exception as e: + print("CSSZ_ODB:", e) + +# Try ODB table +try: + cur.execute("SELECT FIRST 30 * FROM ODB") + cols = [d[0] for d in cur.description] + print("ODB cols:", cols) + for r in cur.fetchall(): + print(r) +except Exception as e: + print("ODB:", e) + +# List all tables with ODB in name +try: + cur.execute("SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$RELATION_NAME LIKE '%ODB%' AND RDB$SYSTEM_FLAG = 0") + for r in cur.fetchall(): + print("Table:", r[0].strip()) +except Exception as e: + print("table list:", e) + +conn.close() +print("DONE") diff --git a/tmp_out.txt b/tmp_out.txt new file mode 100644 index 0000000..0371114 --- /dev/null +++ b/tmp_out.txt @@ -0,0 +1,38 @@ +CSSZ_ODB cols: ['ID', 'KOD', 'NAZEV', 'PLATNOST_OD', 'PLATNOST_DO', 'POZNAMKA'] +(104, 'L01', 'alergologie a klinickß imunologie', datetime.date(2019, 9, 1), None, None) +(105, 'L02', 'anesteziologie a intenzivnÝ medicÝna', datetime.date(2019, 9, 1), None, None) +(106, 'L04', 'cÚvnÝ chirurgie', datetime.date(2019, 9, 1), None, None) +(107, 'L05', 'dermatovenerologie', datetime.date(2019, 9, 1), None, None) +(108, 'L07', 'dýtskß chirurgie', datetime.date(2019, 9, 1), None, None) +(109, 'L08', 'dýtskÚ lÚka°stvÝ', datetime.date(2019, 9, 1), None, None) +(110, 'L09', 'endokrinologie a diabetologie', datetime.date(2019, 9, 1), None, None) +(111, 'L10', 'gastroenterologie', datetime.date(2019, 9, 1), None, None) +(112, 'L11', 'geriatrie', datetime.date(2019, 9, 1), None, None) +(113, 'L12', 'gynekologie a porodnictvÝ', datetime.date(2019, 9, 1), None, None) +(114, 'L13', 'hematologie a transf˙znÝ lÚka°stvÝ', datetime.date(2019, 9, 1), None, None) +(115, 'L14', 'hygiena a epidemiologie', datetime.date(2019, 9, 1), None, None) +(116, 'L15', 'chirurgie', datetime.date(2019, 9, 1), None, None) +(117, 'L16', 'infekŔnÝ lÚka°stvÝ', datetime.date(2019, 9, 1), None, None) +(118, 'L17', 'kardiochirurgie', datetime.date(2019, 9, 1), None, None) +(119, 'L18', 'kardiologie', datetime.date(2019, 9, 1), None, None) +(120, 'L19', 'klinickß biochemie', datetime.date(2019, 9, 1), None, None) +(121, 'L20', 'klinickß onkologie', datetime.date(2019, 9, 1), None, None) +(122, 'L21', 'lÚka°skß genetika', datetime.date(2019, 9, 1), None, None) +(123, 'L22', 'lÚka°skß mikrobiologie', datetime.date(2019, 9, 1), None, None) +(124, 'L23', 'nefrologie', datetime.date(2019, 9, 1), None, None) +(125, 'L24', 'neurochirurgie', datetime.date(2019, 9, 1), None, None) +(126, 'L25', 'neurologie', datetime.date(2019, 9, 1), None, None) +(127, 'L26', 'nukleßrnÝ medicÝna', datetime.date(2019, 9, 1), None, None) +(128, 'L27', 'oftalmologie', datetime.date(2019, 9, 1), None, None) +(129, 'L28', 'ortopedie a traumatologie pohybovÚho ˙strojÝ', datetime.date(2019, 9, 1), None, None) +(130, 'L29', 'otorinolaryngologie a chirurgie hlavy a krku', datetime.date(2019, 9, 1), None, None) +(131, 'L30', 'patologie', datetime.date(2019, 9, 1), None, None) +(132, 'L31', 'plastickß chirurgie', datetime.date(2019, 9, 1), None, None) +(133, 'L32', 'pneumologie a ftizeologie', datetime.date(2019, 9, 1), None, None) +ODB: ('Error while preparing SQL statement:\n- SQLCODE: -204\n- Dynamic SQL Error\n- SQL error code = -204\n- Table unknown\n- ODB\n- At line 1, column 24', -204, 335544569) +Table: HODBOD +Table: ODBORN +Table: ODBORN_MASK +Table: ODBORN_SKUP +Table: CSSZ_ODB +DONE