This commit is contained in:
2026-04-13 16:45:07 +02:00
parent dae0558c98
commit a667fb8ba3
12 changed files with 1410 additions and 26 deletions
+250
View File
@@ -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: <https://www.sukl.cz> (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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+221 -12
View File
@@ -16,8 +16,210 @@ import fdb
import pymysql import pymysql
import pymysql.cursors 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Í ───────────────────────────────────────────────────────────────── # ── 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 DATUM_OD = "01.01.2025" # None = vsechny predpisy
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -37,8 +239,8 @@ DB = dict(
cursorclass = pymysql.cursors.DictCursor, cursorclass = pymysql.cursors.DictCursor,
) )
SEP = "-" * 100 SEP = "-" * 110
SEP2 = "-" * 140 SEP2 = "-" * 165
def parse_datum(s, nazev): def parse_datum(s, nazev):
@@ -85,13 +287,14 @@ def tiskni_lekare(cur, pacient_id, prijmeni, jmena, datum_narozeni):
cur.execute( cur.execute(
""" """
SELECT pr.prijmeni, pr.jmena, SELECT pr.prijmeni, pr.jmena,
pr.icp,
CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa, CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa,
COUNT(*) AS pocet_predpisu COUNT(*) AS pocet_predpisu
FROM zprava z FROM zprava z
JOIN predpis p ON p.zprava_id = z.id JOIN predpis p ON p.zprava_id = z.id
JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho
WHERE z.pacient_id = %s 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 ORDER BY pocet_predpisu DESC
""", """,
(pacient_id,), (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(f" PACIENT: {prijmeni} {jmena} | nar. {datum_narozeni.strftime('%d.%m.%Y')}")
print(SEP) print(SEP)
print(f"\nPREDEPISUJICI LEKARI:") 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) print(SEP)
for i, r in enumerate(rows, 1): for i, r in enumerate(rows, 1):
lekar = f"{r['prijmeni']} {r['jmena']}" lekar = f"{r['prijmeni']} {r['jmena']}"
print(f"{i:<4} {lekar:<30} {r['adresa']:<55} {r['pocet_predpisu']:>8}") odb = odbornost_z_icp(r['icp'])
print(f"{i:<4} {lekar:<30} {odb:<25} {r['adresa']:<50} {r['pocet_predpisu']:>8}")
if not rows: if not rows:
print(" Zadne predpisy nenalezeny.") print(" Zadne predpisy nenalezeny.")
@@ -118,11 +322,13 @@ def tiskni_predpisy(cur, pacient_id, datum_od):
cur.execute( cur.execute(
f""" f"""
SELECT p.datum_vystaveni, 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.atc,
p.navod, p.navod,
pr.prijmeni, pr.prijmeni,
pr.jmena, pr.jmena,
pr.icp,
CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa CONCAT(pr.pzs_nazev, ', ', pr.ulice, ', ', pr.psc, ' ', pr.mesto) AS adresa
FROM zprava z FROM zprava z
JOIN predpis p ON p.zprava_id = z.id 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" 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"\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) print(SEP2)
for i, r in enumerate(rows, 1): for i, r in enumerate(rows, 1):
datum = r["datum_vystaveni"].strftime("%d.%m.%Y") datum = r["datum_vystaveni"].strftime("%d.%m.%Y")
lekar = f"{r['prijmeni']} {r['jmena']}" lekar = f"{r['prijmeni']} {r['jmena']}"
lek = (r["vydany_lek"] or "")[:29] lek = (r["vydany_lek"] or "")[:28]
navod = (r["navod"] or "")[:24] if r["nevyzvednuto"]:
lek = f"{lek} *NV"
navod = (r["navod"] or "")[:19]
atc = (r["atc"] or "") 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: if not rows:
print(" Zadne predpisy nenalezeny.") print(" Zadne predpisy nenalezeny.")
print() print()
+515
View File
@@ -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()
+154 -14
View File
@@ -10,18 +10,67 @@ z eRecept SÚKL API a jejich uložení do relační databáze MySQL.
| Soubor | Co dělá | | Soubor | Co dělá |
|--------|---------| |--------|---------|
| `05UlozitOdpoved.py` | Stáhne XML pro **jednoho** pacienta (ruční test/ladění) | | `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 | | `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/ recept/
├── 05UlozitOdpoved.py ├── setup.ps1 ← vytvoří .venv, nainstaluje závislosti, Playwright chromium
├── 06UlozitDoMySQL.py ├── requirements.txt ← seznam Python závislostí
├── 07StahnoutVsechny.py ├── .venv/ ← virtuální prostředí (Python 3.x)
├── LEKOVY_ZAZNAM_DB.md
├── Logs/ ← log každého běhu (UTF-8, YYYY-MM-DD_HH-MM-SS.log) ├── LékovýZáznamWithClaude/
├── Tests/ ← starší vývojové skripty │ ├── 05UlozitOdpoved.py
└── xml_archive/ ← archiv XML odpovědí (YYYY-MM-DD/Prijmeni_Jmena_datnar.xml) │ ├── 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í ## Typické spuštění
```bash ```bash
# Čistý start — jednou (DROP + CREATE schéma, importuje odpoved_lekovy_zaznam.xml)
python 06UlozitDoMySQL.py
# Hromadné stažení všech registrovaných pacientů # Hromadné stažení všech registrovaných pacientů
python 07StahnoutVsechny.py python 07StahnoutVsechny.py
@@ -40,6 +86,9 @@ python 07StahnoutVsechny.py --prijmeni Buzalka,Buzalková,Kusinová
# Dávkování po částech # Dávkování po částech
python 07StahnoutVsechny.py --offset 100 --limit 50 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) | | | `prijmeni` | VARCHAR(35) | |
| `jmena` | VARCHAR(24) | | | `jmena` | VARCHAR(24) | |
| `icz` | CHAR(8) | IČZ zdravotnického zařízení | | `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í | | `pzs_nazev` | VARCHAR(200) | název zdravotnického zařízení |
| `ulice` | VARCHAR(150) | | | `ulice` | VARCHAR(150) | |
| `mesto` | VARCHAR(100) | | | `mesto` | VARCHAR(100) | |
@@ -374,10 +423,96 @@ SELECT prijmeni, jmena, datum_narozeni, poznamka
FROM pacient FROM pacient
WHERE poznamka IS NOT NULL WHERE poznamka IS NOT NULL
ORDER BY prijmeni; 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) ## Závislosti (Python)
``` ```
@@ -385,10 +520,15 @@ requests
requests-pkcs12 requests-pkcs12
pymysql pymysql
fdb fdb
zeep
mysql-connector-python
playwright
openpyxl
``` ```
```bash ```bash
pip install requests requests-pkcs12 pymysql fdb # Instalace (nebo použít setup.ps1)
pip install requests requests-pkcs12 pymysql fdb openpyxl
``` ```
--- ---
+194
View File
@@ -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()
+38
View File
@@ -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")
+38
View File
@@ -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