From 1f690810b388333bc3c692c3183dbecd42828bff Mon Sep 17 00:00:00 2001 From: Vlado Date: Mon, 6 Apr 2026 21:37:25 +0200 Subject: [PATCH] notebookvb --- LékovýZáznamWithClaude/06UlozitDoMySQL.py | 523 +++++++++++++++++++++ LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md | 293 ++++++++++++ 2 files changed, 816 insertions(+) create mode 100644 LékovýZáznamWithClaude/06UlozitDoMySQL.py create mode 100644 LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md diff --git a/LékovýZáznamWithClaude/06UlozitDoMySQL.py b/LékovýZáznamWithClaude/06UlozitDoMySQL.py new file mode 100644 index 0000000..80b7a06 --- /dev/null +++ b/LékovýZáznamWithClaude/06UlozitDoMySQL.py @@ -0,0 +1,523 @@ +""" +Nacte odpoved lekoveho zaznamu (XML) a ulozi ji do MySQL. +Schema: zprava / predpis / predpis_slozka / vydej / vydej_slozka +Typy a delky presne dle XSD (Cuer2Schema.xsd + CuerSchema.xsd, verze 202501A) + +Spusteni: + python 06UlozitDoMySQL.py +nebo: + python 06UlozitDoMySQL.py cesta/k/odpoved.xml +""" + +import sys +from pathlib import Path +import xml.etree.ElementTree as ET + +import pymysql +import pymysql.cursors + +# ── konfigurace ─────────────────────────────────────────────────────────────── +XML_SOUBOR = Path(__file__).parent / "odpoved_lekovy_zaznam.xml" + +DB = dict( + host = "192.168.1.76", + user = "root", + password = "Vlado9674+", + database = "medicus", + charset = "utf8mb4", + cursorclass = pymysql.cursors.DictCursor, +) + +NS = "http://www.sukl.cz/erp/201912" +# ───────────────────────────────────────────────────────────────────────────── + + +def t(el, tag): + """Prvni potomek s danym tagem → text nebo None.""" + found = el.find(f"{{{NS}}}{tag}") + return found.text.strip() if found is not None and found.text else None + + +def datum(s): + return s[:10] if s else None + + +def ts(s): + return s[:19].replace("T", " ") if s else None + + +# ── DDL ─────────────────────────────────────────────────────────────────────── +DDL_TABULKY = [ + # smazat v opacnem poradi kvuli FK + "DROP TABLE IF EXISTS predpis_slozka", + "DROP TABLE IF EXISTS vydej_slozka", + "DROP TABLE IF EXISTS vydej", + "DROP TABLE IF EXISTS predpis", + "DROP TABLE IF EXISTS zprava", + + # ── zprava ──────────────────────────────────────────────────────────────── + # zprava_odpoved_type + zprava_type: + # ID_Zpravy CHAR(36), Verze, Odeslano dateTime + # Aplikace(512), ID_Podani CHAR(36), Prijato dateTime + # jmeno_osoby_type: Prijmeni(35), Jmena(24) + """ + CREATE TABLE zprava ( + id INT AUTO_INCREMENT PRIMARY KEY, + id_zpravy CHAR(36) NOT NULL UNIQUE, + verze VARCHAR(20), + odeslano DATETIME, + aplikace VARCHAR(512), + id_podani CHAR(36), + prijato DATETIME, + pacient_prijmeni VARCHAR(35), + pacient_jmena VARCHAR(24), + pacient_datum_narozeni DATE, + stazeno DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_pacient (pacient_prijmeni, pacient_datum_narozeni) + ) ENGINE=InnoDB + """, + + # ── predpis ─────────────────────────────────────────────────────────────── + # lz_nacteni_predepsany_lp_erp_type: + # ID_LP_Predpis CHAR(36) NOT NULL, KodPredepisujiciho(36) NOT NULL, + # DatumVystaveni date NOT NULL, Mnozstvi int(1-9999) NOT NULL, + # Navod(80) NOT NULL, Opakovani int?, ModryPruh boolean? + # lek — vzdy jen jeden z: HVLPReg / HVLPNereg / IPLP / INN + # hvlp_type: Kod CHAR(7)?, ATC(7)?, Nazev(146)!, Forma(27)?, + # Sila(24)?, CestaPodani(15)?, Baleni VARCHAR(22)? + # inn_predpis_type: Nazev(200) — nejdelsi nazev mezi typy + # iplp_predpis_type: PostupPripravy(4000), Nazev(146), CestaPodani(15), Forma(27) + """ + CREATE TABLE predpis ( + id INT AUTO_INCREMENT PRIMARY KEY, + zprava_id INT NOT NULL, + id_lp_predpis CHAR(36) NOT NULL UNIQUE, + kod_predepisujiciho VARCHAR(36) NOT NULL, + datum_vystaveni DATE NOT NULL, + mnozstvi SMALLINT NOT NULL, + navod VARCHAR(80) NOT NULL, + opakovani INT, + modry_pruh TINYINT(1), + typ_leku ENUM('HVLPReg','HVLPNereg','IPLP','INN'), + lek_kod CHAR(7), + atc VARCHAR(7), + nazev VARCHAR(200), + forma VARCHAR(27), + sila VARCHAR(24), + cesta_podani VARCHAR(15), + baleni VARCHAR(22), + postup_pripravy VARCHAR(4000), + FOREIGN KEY (zprava_id) REFERENCES zprava(id) ON DELETE CASCADE, + INDEX idx_atc (atc), + INDEX idx_datum (datum_vystaveni) + ) ENGINE=InnoDB + """, + + # ── predpis_slozka ──────────────────────────────────────────────────────── + # slozka_iplp_predpis_type: + # Mnozstvi DECIMAL(15,6) NOT NULL, Jednotka ENUM('g','ks') NOT NULL, + # Nazev(200) NOT NULL, Surovina CHAR(7)?, HVLPReg CHAR(7)? + """ + CREATE TABLE predpis_slozka ( + id INT AUTO_INCREMENT PRIMARY KEY, + predpis_id INT NOT NULL, + mnozstvi DECIMAL(15,6) NOT NULL, + jednotka ENUM('g','ks') NOT NULL, + nazev VARCHAR(200) NOT NULL, + surovina CHAR(7), + hvlp_reg CHAR(7), + FOREIGN KEY (predpis_id) REFERENCES predpis(id) ON DELETE CASCADE, + INDEX idx_nazev (nazev) + ) ENGINE=InnoDB + """, + + # ── vydej ───────────────────────────────────────────────────────────────── + # lz_nacteni_vydany_lp_erp_type: + # Mnozstvi DECIMAL(6,2) NOT NULL, Navod(80) NOT NULL, + # Sarze(50) NOT NULL, SerioveCislo(20)?, Pozn(1000)? + # lek — vzdy jen jeden z: HVLPReg / HVLPNereg / IPLP + # iplp_type (vydej): KodVZP CHAR(7)?, PostupPripravy(4000), Nazev(146), CestaPodani(15) + """ + CREATE TABLE vydej ( + id INT AUTO_INCREMENT PRIMARY KEY, + zprava_id INT NOT NULL, + id_lp_vydej CHAR(36) NOT NULL UNIQUE, + id_lp_predpis CHAR(36), + kod_vydavajiciho VARCHAR(36) NOT NULL, + datum_vydeje DATE NOT NULL, + mnozstvi DECIMAL(6,2) NOT NULL, + navod VARCHAR(80) NOT NULL, + exspirace DATE, + sarze VARCHAR(50) NOT NULL, + seriove_cislo VARCHAR(20), + pozn VARCHAR(1000), + typ_leku ENUM('HVLPReg','HVLPNereg','IPLP'), + lek_kod CHAR(7), + atc VARCHAR(7), + nazev VARCHAR(146), + forma VARCHAR(27), + sila VARCHAR(24), + cesta_podani VARCHAR(15), + postup_pripravy VARCHAR(4000), + FOREIGN KEY (zprava_id) REFERENCES zprava(id) ON DELETE CASCADE, + FOREIGN KEY (id_lp_predpis) REFERENCES predpis(id_lp_predpis) ON DELETE SET NULL, + INDEX idx_predpis (id_lp_predpis), + INDEX idx_atc (atc), + INDEX idx_datum (datum_vydeje) + ) ENGINE=InnoDB + """, + + # ── vydej_slozka ────────────────────────────────────────────────────────── + # slozka_iplp_type: + # Mnozstvi DECIMAL(15,6) NOT NULL, Jednotka ENUM('g','ks') NOT NULL, + # Nazev(200) NOT NULL, HrazenoZP DECIMAL(9,2)?, + # Surovina CHAR(7)?, HVLPReg CHAR(7)? + """ + CREATE TABLE vydej_slozka ( + id INT AUTO_INCREMENT PRIMARY KEY, + vydej_id INT NOT NULL, + mnozstvi DECIMAL(15,6) NOT NULL, + jednotka ENUM('g','ks') NOT NULL, + nazev VARCHAR(200) NOT NULL, + hrazeno_zp DECIMAL(9,2), + surovina CHAR(7), + hvlp_reg CHAR(7), + FOREIGN KEY (vydej_id) REFERENCES vydej(id) ON DELETE CASCADE, + INDEX idx_nazev (nazev) + ) ENGINE=InnoDB + """, +] + + +def vytvor_schema(conn): + with conn.cursor() as cur: + for stmt in DDL_TABULKY: + stmt = stmt.strip() + if stmt: + cur.execute(stmt) + conn.commit() + print("Schema OK (5 tabulek smazano a vytvoreno znovu)") + + +# ── parsovani leku ──────────────────────────────────────────────────────────── + +def parsuj_slozky_predpis(lek_el): + """Ze IPLP elementu vraci seznam slovniku pro predpis_slozka.""" + slozky = [] + for s in lek_el.findall(f"{{{NS}}}Slozka"): + slozky.append(dict( + mnozstvi = t(s, "Mnozstvi"), + jednotka = t(s, "Jednotka"), + nazev = t(s, "Nazev"), + surovina = t(s, "Surovina"), + hvlp_reg = t(s, "HVLPReg"), + )) + return slozky + + +def parsuj_slozky_vydej(lek_el): + """Ze IPLP elementu vraci seznam slovniku pro vydej_slozka.""" + slozky = [] + for s in lek_el.findall(f"{{{NS}}}Slozka"): + slozky.append(dict( + mnozstvi = t(s, "Mnozstvi"), + jednotka = t(s, "Jednotka"), + nazev = t(s, "Nazev"), + hrazeno_zp = t(s, "HrazenoZP"), + surovina = t(s, "Surovina"), + hvlp_reg = t(s, "HVLPReg"), + )) + return slozky + + +def parsuj_lek_predpis(p): + """Vraci (lek_fields dict, slozky list). + Typ leku: HVLPReg / HVLPNereg / IPLP / INN — vzdy jen jeden. + """ + for typ in ("HVLPReg", "HVLPNereg"): + lek = p.find(f"{{{NS}}}{typ}") + if lek is not None: + return dict( + typ_leku = typ, + lek_kod = t(lek, "Kod"), + atc = t(lek, "ATC"), + nazev = t(lek, "Nazev"), + forma = t(lek, "Forma"), + sila = t(lek, "Sila"), + cesta_podani = t(lek, "CestaPodani"), + baleni = t(lek, "Baleni"), + postup_pripravy = None, + ), [] + + lek = p.find(f"{{{NS}}}IPLP") + if lek is not None: + return dict( + typ_leku = "IPLP", + lek_kod = None, + atc = None, + nazev = t(lek, "Nazev"), + forma = t(lek, "Forma"), + sila = None, + cesta_podani = t(lek, "CestaPodani"), + baleni = None, + postup_pripravy = t(lek, "PostupPripravy"), + ), parsuj_slozky_predpis(lek) + + lek = p.find(f"{{{NS}}}INN") + if lek is not None: + return dict( + typ_leku = "INN", + lek_kod = None, + atc = None, + nazev = t(lek, "Nazev"), + forma = t(lek, "Forma"), + sila = t(lek, "Sila"), + cesta_podani = t(lek, "CestaPodani"), + baleni = t(lek, "Baleni"), + postup_pripravy = None, + ), [] + + return dict(typ_leku=None, lek_kod=None, atc=None, nazev=None, + forma=None, sila=None, cesta_podani=None, + baleni=None, postup_pripravy=None), [] + + +def parsuj_lek_vydej(v): + """Vraci (lek_fields dict, slozky list). + Typ leku: HVLPReg / HVLPNereg / IPLP — vzdy jen jeden. + """ + for typ in ("HVLPReg", "HVLPNereg"): + lek = v.find(f"{{{NS}}}{typ}") + if lek is not None: + return dict( + typ_leku = typ, + lek_kod = t(lek, "Kod"), + atc = t(lek, "ATC"), + nazev = t(lek, "Nazev"), + forma = t(lek, "Forma"), + sila = t(lek, "Sila"), + cesta_podani = t(lek, "CestaPodani"), + postup_pripravy = None, + ), [] + + lek = v.find(f"{{{NS}}}IPLP") + if lek is not None: + return dict( + typ_leku = "IPLP", + lek_kod = t(lek, "KodVZP"), + atc = None, + nazev = t(lek, "Nazev"), + forma = None, + sila = None, + cesta_podani = t(lek, "CestaPodani"), + postup_pripravy = t(lek, "PostupPripravy"), + ), parsuj_slozky_vydej(lek) + + return dict(typ_leku=None, lek_kod=None, atc=None, nazev=None, + forma=None, sila=None, cesta_podani=None, + postup_pripravy=None), [] + + +# ── parsovani XML ───────────────────────────────────────────────────────────── + +def parsuj_xml(xml_soubor): + tree = ET.parse(xml_soubor) + root = tree.getroot() + odpoved = root[0][0] # Envelope > Body > NacistLekovyZaznamOdpoved + + # Doklad prvni, Zprava druha — presne dle XSD sequence + doklad = odpoved.find(f"{{{NS}}}Doklad") + zpr = odpoved.find(f"{{{NS}}}Zprava") + + # ── Zprava ─────────────────────────────────────────────────────────────── + zprava = dict( + id_zpravy = t(zpr, "ID_Zpravy"), + verze = t(zpr, "Verze"), + odeslano = ts(t(zpr, "Odeslano")), + aplikace = t(zpr, "Aplikace"), + id_podani = t(zpr, "ID_Podani"), + prijato = ts(t(zpr, "Prijato")), + ) + + # ── Pacient ─────────────────────────────────────────────────────────────── + pac = doklad.find(f"{{{NS}}}Pacient") + jmeno = pac.find(f"{{{NS}}}Jmeno") + zprava["pacient_prijmeni"] = t(jmeno, "Prijmeni") if jmeno is not None else None + zprava["pacient_jmena"] = t(jmeno, "Jmena") if jmeno is not None else None + zprava["pacient_datum_narozeni"] = datum(t(pac, "DatumNarozeni")) + + # ── Predpisy ────────────────────────────────────────────────────────────── + predpisy = [] # kazda polozka: (row_dict, slozky_list) + predpis_seznam = doklad.find(f"{{{NS}}}PredpisSeznam") + if predpis_seznam is not None: + for p in predpis_seznam.findall(f"{{{NS}}}Predpis"): + row = dict( + id_lp_predpis = t(p, "ID_LP_Predpis"), + kod_predepisujiciho = t(p, "KodPredepisujiciho"), + datum_vystaveni = datum(t(p, "DatumVystaveni")), + mnozstvi = t(p, "Mnozstvi"), + navod = t(p, "Navod"), + opakovani = t(p, "Opakovani"), + modry_pruh = t(p, "ModryPruh"), + ) + lek_fields, slozky = parsuj_lek_predpis(p) + row.update(lek_fields) + predpisy.append((row, slozky)) + + # ── Vydeji ──────────────────────────────────────────────────────────────── + vydeji = [] # kazda polozka: (row_dict, slozky_list) + vydej_seznam = doklad.find(f"{{{NS}}}VydejSeznam") + if vydej_seznam is not None: + for v in vydej_seznam.findall(f"{{{NS}}}Vydej"): + row = dict( + id_lp_vydej = t(v, "ID_LP_Vydej"), + id_lp_predpis = t(v, "ID_LP_Predpis"), + kod_vydavajiciho = t(v, "KodVydavajiciho"), + datum_vydeje = datum(t(v, "DatumVydeje")), + mnozstvi = t(v, "Mnozstvi"), + navod = t(v, "Navod"), + exspirace = datum(t(v, "Exspirace")), + sarze = t(v, "Sarze"), + seriove_cislo = t(v, "SerioveCislo"), + pozn = t(v, "Pozn"), + ) + lek_fields, slozky = parsuj_lek_vydej(v) + row.update(lek_fields) + vydeji.append((row, slozky)) + + return zprava, predpisy, vydeji + + +# ── ulozeni do DB ───────────────────────────────────────────────────────────── + +def _najdi_id(cur, tabulka, sloupec, hodnota): + """Pomocna funkce — vrati id radku dle unikatniho sloupce.""" + cur.execute(f"SELECT id FROM {tabulka} WHERE {sloupec} = %s", (hodnota,)) + row = cur.fetchone() + return row["id"] if row else None + + +def uloz(conn, zprava, predpisy, vydeji): + iplp_predpisu = 0 + iplp_vydejuu = 0 + + with conn.cursor() as cur: + + # ── zprava ──────────────────────────────────────────────────────────── + cur.execute(""" + INSERT INTO zprava + (id_zpravy, verze, odeslano, aplikace, id_podani, prijato, + pacient_prijmeni, pacient_jmena, pacient_datum_narozeni) + VALUES + (%(id_zpravy)s, %(verze)s, %(odeslano)s, %(aplikace)s, + %(id_podani)s, %(prijato)s, + %(pacient_prijmeni)s, %(pacient_jmena)s, %(pacient_datum_narozeni)s) + ON DUPLICATE KEY UPDATE + prijato = VALUES(prijato), + stazeno = CURRENT_TIMESTAMP + """, zprava) + zprava_id = _najdi_id(cur, "zprava", "id_zpravy", zprava["id_zpravy"]) + print(f" zprava id={zprava_id} ({zprava['id_zpravy']})") + + # ── predpisy + jejich slozky ─────────────────────────────────────────── + vlozeno_p = 0 + vlozeno_ps = 0 + for row, slozky in predpisy: + row["zprava_id"] = zprava_id + cur.execute(""" + INSERT IGNORE INTO predpis + (zprava_id, id_lp_predpis, kod_predepisujiciho, + datum_vystaveni, mnozstvi, navod, opakovani, modry_pruh, + typ_leku, lek_kod, atc, nazev, forma, sila, + cesta_podani, baleni, postup_pripravy) + VALUES + (%(zprava_id)s, %(id_lp_predpis)s, %(kod_predepisujiciho)s, + %(datum_vystaveni)s, %(mnozstvi)s, %(navod)s, + %(opakovani)s, %(modry_pruh)s, + %(typ_leku)s, %(lek_kod)s, %(atc)s, %(nazev)s, %(forma)s, + %(sila)s, %(cesta_podani)s, %(baleni)s, %(postup_pripravy)s) + """, row) + vlozeno_p += cur.rowcount + + if slozky: + iplp_predpisu += 1 + predpis_db_id = cur.lastrowid or _najdi_id(cur, "predpis", "id_lp_predpis", row["id_lp_predpis"]) + for s in slozky: + s["predpis_id"] = predpis_db_id + cur.execute(""" + INSERT INTO predpis_slozka + (predpis_id, mnozstvi, jednotka, nazev, surovina, hvlp_reg) + VALUES + (%(predpis_id)s, %(mnozstvi)s, %(jednotka)s, + %(nazev)s, %(surovina)s, %(hvlp_reg)s) + """, s) + vlozeno_ps += 1 + + print(f" predpisy: {vlozeno_p} novych (celkem {len(predpisy)})") + print(f" predpis_slozka: {vlozeno_ps} slozek z {iplp_predpisu} IPLP predpisu") + + # ── vydeji + jejich slozky ──────────────────────────────────────────── + vlozeno_v = 0 + vlozeno_vs = 0 + for row, slozky in vydeji: + row["zprava_id"] = zprava_id + cur.execute(""" + INSERT IGNORE INTO vydej + (zprava_id, id_lp_vydej, id_lp_predpis, kod_vydavajiciho, + datum_vydeje, mnozstvi, navod, exspirace, sarze, + seriove_cislo, pozn, + typ_leku, lek_kod, atc, nazev, forma, sila, + cesta_podani, postup_pripravy) + VALUES + (%(zprava_id)s, %(id_lp_vydej)s, %(id_lp_predpis)s, %(kod_vydavajiciho)s, + %(datum_vydeje)s, %(mnozstvi)s, %(navod)s, %(exspirace)s, %(sarze)s, + %(seriove_cislo)s, %(pozn)s, + %(typ_leku)s, %(lek_kod)s, %(atc)s, %(nazev)s, %(forma)s, + %(sila)s, %(cesta_podani)s, %(postup_pripravy)s) + """, row) + vlozeno_v += cur.rowcount + + if slozky: + iplp_vydejuu += 1 + vydej_db_id = cur.lastrowid or _najdi_id(cur, "vydej", "id_lp_vydej", row["id_lp_vydej"]) + for s in slozky: + s["vydej_id"] = vydej_db_id + cur.execute(""" + INSERT INTO vydej_slozka + (vydej_id, mnozstvi, jednotka, nazev, + hrazeno_zp, surovina, hvlp_reg) + VALUES + (%(vydej_id)s, %(mnozstvi)s, %(jednotka)s, %(nazev)s, + %(hrazeno_zp)s, %(surovina)s, %(hvlp_reg)s) + """, s) + vlozeno_vs += 1 + + print(f" vydeji: {vlozeno_v} novych (celkem {len(vydeji)})") + print(f" vydej_slozka: {vlozeno_vs} slozek z {iplp_vydejuu} IPLP vydejuu") + + conn.commit() + + +# ── main ────────────────────────────────────────────────────────────────────── + +def main(): + xml = Path(sys.argv[1]) if len(sys.argv) > 1 else XML_SOUBOR + print(f"XML: {xml} ({xml.stat().st_size // 1024} KB)") + + print("Parsovani XML ...") + zprava, predpisy, vydeji = parsuj_xml(xml) + print(f" -> {len(predpisy)} predpisu, {len(vydeji)} vydejuu") + + print("Pripojeni k MySQL ...") + conn = pymysql.connect(**DB) + try: + vytvor_schema(conn) + print("Ukladani ...") + uloz(conn, zprava, predpisy, vydeji) + print("Hotovo OK") + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md b/LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md new file mode 100644 index 0000000..422007a --- /dev/null +++ b/LékovýZáznamWithClaude/LEKOVY_ZAZNAM_DB.md @@ -0,0 +1,293 @@ +# Lékový záznam eRecept → MySQL + +Popis pipeline pro stažení lékového záznamu pacienta z eRecept SÚKL API +a jeho uložení do relační databáze MySQL. + +--- + +## Soubory + +| Soubor | Co dělá | +|--------|---------| +| `05UlozitOdpoved.py` | Zavolá SOAP API, stáhne XML odpověď, uloží ji do `odpoved_lekovy_zaznam.xml` | +| `06UlozitDoMySQL.py` | Naparsuje uložené XML, vytvoří/přepíše tabulky v MySQL, vloží data | + +### Typické spuštění + +```bash +# 1. stáhnout čerstvá data z eReceptu +python 05UlozitOdpoved.py + +# 2. uložit do databáze +python 06UlozitDoMySQL.py + +# nebo předat jiný XML soubor +python 06UlozitDoMySQL.py C:\cesta\k\jine_odpovedi.xml +``` + +--- + +## Autentizace (eRecept SÚKL, ostrý provoz) + +| Parametr | Hodnota | +|----------|---------| +| Endpoint | `https://lekar-soap.erecept.sukl.cz/cuer/Lekar2` | +| mTLS certifikát | `AMBSUKL214235369G_31DEC2024.pfx` (platnost do 31. 12. 2026) | +| HTTP Basic user | UUID lékaře `e08c89c6-2b1a-4eba-8ed9-4e3e63618379` | +| SOAP operace | `NacistLekovyZaznam` | +| XML namespace | `http://www.sukl.cz/erp/201912` | +| Verze zprávy | `202501A` | + +Certifikát = identifikace **ordinace**, UUID+heslo = identifikace **lékaře jako osoby**. + +--- + +## Dotaz — parametry `NacistLekovyZaznamLekarDotaz` + +```xml + + + UUID lékaře + IČP ordinace + + 7 + 60 + + + ...... + YYYY-MM-DD + + + +``` + +--- + +## Struktura XML odpovědi + +Dle XSD `Cuer2Schema.xsd` + `CuerSchema.xsd`, verze `202501A`: + +``` +NacistLekovyZaznamOdpoved +├── Doklad +│ ├── Pacient – jméno + datum narození +│ ├── PredepisujiciSeznam – lékaři, kteří předepisovali +│ ├── VydavajiciSeznam – lékárny, kde byl výdej +│ ├── PredpisSeznam +│ │ └── Predpis × N – co bylo předepsáno +│ ├── VydejSeznam +│ │ └── Vydej × N – co bylo vydáno v lékárně +│ └── DuplicitaSeznam – duplicitní výdeje +└── Zprava – metadata odpovědi (ID, čas, verze SW) +``` + +### Typy léků v Predpis i Vydej + +Každý předpis / výdej obsahuje právě **jeden** z těchto elementů: + +| Typ | Popis | Klíčová pole | +|-----|-------|-------------| +| `HVLPReg` | Registrovaný hromadně vyráběný LP | Kod (SÚKL), ATC, Nazev, Forma, Sila, Baleni | +| `HVLPNereg` | Neregistrovaný HVLP | stejná struktura jako HVLPReg | +| `IPLP` | Individuálně připravovaný LP (magistraliter) | Nazev, PostupPripravy (receptura), Slozka[] | +| `INN` | Předpis účinnou látkou (genericky) | Nazev, Forma, Sila, Baleni | + +#### IPLP — dvojí uložení receptury + +- **Předpis** (`PredpisSeznam/Predpis/IPLP`): lékař zadal recepturu jako **volný text** v `PostupPripravy`. Prvky `Slozka` zde lékař typicky nevyplňuje. +- **Výdej** (`VydejSeznam/Vydej/IPLP`): lékárna při přípravě zaznamenala **strukturované složky** (`Slozka` s množstvím, jednotkou a názvem suroviny). Kvalita dat závisí na lékárně — některé rozkládají do detailu (suroviny, obal, taxa laborum), jiné evidují jen 1 ks jako celek. + +--- + +## Databázové schéma — `medicus` (MySQL 9.6) + +Všechny délky a datové typy jsou přesně dle XSD, **ne odhady**. +Varianta B — lék je denormalizován přímo do řádku předpisu/výdeje. + +### Relační diagram + +``` +zprava (1) + ├── predpis (N) + │ └── predpis_slozka (N) -- složky IPLP z předpisu + └── vydej (N) + └── vydej_slozka (N) -- složky IPLP z výdeje + +vydej.id_lp_predpis → predpis.id_lp_predpis (párování výdeje s předpisem) +``` + +### Tabulka `zprava` + +Jeden řádek = jedna odpověď API (jedno volání `NacistLekovyZaznam`). + +| Sloupec | Typ | Zdroj v XSD | +|---------|-----|-------------| +| `id_zpravy` | CHAR(36) UNIQUE | `zprava_type.ID_Zpravy` | +| `verze` | VARCHAR(20) | `zprava_type.Verze` | +| `odeslano` | DATETIME | `zprava_type.Odeslano` | +| `aplikace` | VARCHAR(512) | `zprava_odpoved_type.Aplikace` | +| `id_podani` | CHAR(36) | `zprava_odpoved_type.ID_Podani` | +| `prijato` | DATETIME | `zprava_odpoved_type.Prijato` | +| `pacient_prijmeni` | VARCHAR(35) | `jmeno_osoby_type.Prijmeni` | +| `pacient_jmena` | VARCHAR(24) | `jmeno_osoby_type.Jmena` | +| `pacient_datum_narozeni` | DATE | `lz_totoznost_type.DatumNarozeni` | +| `stazeno` | DATETIME | automaticky při INSERT | + +### Tabulka `predpis` + +Dle `lz_nacteni_predepsany_lp_erp_type`. + +| Sloupec | Typ | NOT NULL | Poznámka | +|---------|-----|----------|----------| +| `id_lp_predpis` | CHAR(36) | ✓ | UUID z eReceptu, globálně unikátní | +| `kod_predepisujiciho` | VARCHAR(36) | ✓ | UUID lékaře | +| `datum_vystaveni` | DATE | ✓ | | +| `mnozstvi` | SMALLINT | ✓ | 1–9999 | +| `navod` | VARCHAR(80) | ✓ | max 80 znaků dle XSD | +| `opakovani` | INT | | opakování předpisu | +| `modry_pruh` | TINYINT(1) | | modrý pruh (návykové látky) | +| `typ_leku` | ENUM | | HVLPReg / HVLPNereg / IPLP / INN | +| `lek_kod` | CHAR(7) | | kód SÚKL (jen HVLP) | +| `atc` | VARCHAR(7) | | ATC kód (jen HVLP) | +| `nazev` | VARCHAR(200) | | max 200 — INN má nejdelší název | +| `forma` | VARCHAR(27) | | léková forma | +| `sila` | VARCHAR(24) | | síla přípravku | +| `cesta_podani` | VARCHAR(15) | | POR, INH, … | +| `baleni` | VARCHAR(22) | | **string**, ne číslo (může být "100 ks") | +| `postup_pripravy` | VARCHAR(4000) | | receptura IPLP jako volný text | + +### Tabulka `predpis_slozka` + +Dle `slozka_iplp_predpis_type`. Řádky vznikají jen pro IPLP předpisy se strukturovanými složkami. + +| Sloupec | Typ | NOT NULL | Poznámka | +|---------|-----|----------|----------| +| `predpis_id` | INT | ✓ | FK → predpis.id | +| `mnozstvi` | DECIMAL(15,6) | ✓ | přesné množství suroviny | +| `jednotka` | ENUM('g','ks') | ✓ | | +| `nazev` | VARCHAR(200) | ✓ | název suroviny / přípravku | +| `surovina` | CHAR(7) | | kód suroviny v registru SÚKL | +| `hvlp_reg` | CHAR(7) | | pokud je složka registrovaný HVLP | + +### Tabulka `vydej` + +Dle `lz_nacteni_vydany_lp_erp_type`. + +| Sloupec | Typ | NOT NULL | Poznámka | +|---------|-----|----------|----------| +| `id_lp_vydej` | CHAR(36) | ✓ | UUID výdeje | +| `id_lp_predpis` | CHAR(36) | | FK → predpis.id_lp_predpis, NULL = výdej bez e-předpisu | +| `kod_vydavajiciho` | VARCHAR(36) | ✓ | UUID lékárníka | +| `datum_vydeje` | DATE | ✓ | | +| `mnozstvi` | DECIMAL(6,2) | ✓ | **desetinné** — např. 0.5 balení | +| `navod` | VARCHAR(80) | ✓ | | +| `exspirace` | DATE | | exspirace šarže | +| `sarze` | VARCHAR(50) | ✓ | číslo šarže | +| `seriove_cislo` | VARCHAR(20) | | sériové číslo (léky s el. sledováním) | +| `pozn` | VARCHAR(1000) | | poznámka lékárníka | +| `typ_leku` | ENUM | | HVLPReg / HVLPNereg / IPLP | +| `lek_kod` | CHAR(7) | | kód SÚKL (HVLP) nebo KodVZP (IPLP) | +| `atc` | VARCHAR(7) | | jen HVLP | +| `nazev` | VARCHAR(146) | | | +| `forma` | VARCHAR(27) | | | +| `sila` | VARCHAR(24) | | | +| `cesta_podani` | VARCHAR(15) | | | +| `postup_pripravy` | VARCHAR(4000) | | receptura IPLP | + +### Tabulka `vydej_slozka` + +Dle `slozka_iplp_type`. Jako `predpis_slozka`, ale navíc obsahuje `hrazeno_zp`. + +| Sloupec | Typ | NOT NULL | Poznámka | +|---------|-----|----------|----------| +| `vydej_id` | INT | ✓ | FK → vydej.id | +| `mnozstvi` | DECIMAL(15,6) | ✓ | | +| `jednotka` | ENUM('g','ks') | ✓ | | +| `nazev` | VARCHAR(200) | ✓ | název suroviny | +| `hrazeno_zp` | DECIMAL(9,2) | | částka hrazená zdravotní pojišťovnou | +| `surovina` | CHAR(7) | | | +| `hvlp_reg` | CHAR(7) | | | + +--- + +## Klíčové vlastnosti importu (`06UlozitDoMySQL.py`) + +- **Idempotentní**: `INSERT IGNORE` na `id_lp_predpis` / `id_lp_vydej` — opakované spuštění se stejným XML nepřidá duplikáty. +- **Víc pacientů**: `zprava.id_zpravy` je UNIQUE — každé volání API (jiný pacient, jiný čas) vytvoří nový řádek v `zprava`. Předpisy a výdeje napříč pacienty se agregují v `predpis` / `vydej`. +- **Schema se přepisuje**: `vytvor_schema()` vždy DROPne a znovu vytvoří všech 5 tabulek. Při produkčním použití (více pacientů) tuto část odstraňte a spouštějte DDL jen jednou při inicializaci. + +--- + +## Užitečné analytické dotazy + +```sql +-- nejčastěji předepisované ATC skupiny za posledních 12 měsíců +SELECT atc, nazev, COUNT(*) AS pocet, MAX(datum_vystaveni) AS naposledy +FROM predpis +WHERE datum_vystaveni >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH) +GROUP BY atc, nazev +ORDER BY pocet DESC; + +-- co bylo předepsáno ale nevyzvednuto +SELECT p.datum_vystaveni, p.nazev, p.atc, p.navod +FROM predpis p +LEFT JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis +WHERE v.id_lp_vydej IS NULL; + +-- IPLP magistraliter — kompletní receptury s frekvencí +SELECT p.nazev, p.postup_pripravy, + COUNT(*) AS pocet_predpisu +FROM predpis p +WHERE p.typ_leku = 'IPLP' +GROUP BY p.nazev, p.postup_pripravy +ORDER BY pocet_predpisu DESC; + +-- IPLP výdeje — strukturované složky (kde je lékárna zadala) +SELECT v.datum_vydeje, v.nazev AS pripravek, + GROUP_CONCAT(s.mnozstvi, ' ', s.jednotka, ' ', s.nazev + ORDER BY s.id SEPARATOR ' + ') AS slozeni +FROM vydej v +JOIN vydej_slozka s ON s.vydej_id = v.id +WHERE v.typ_leku = 'IPLP' +GROUP BY v.id +ORDER BY v.datum_vydeje DESC; + +-- nejčastěji používané suroviny v magistrech (ze strany výdeje) +SELECT nazev, jednotka, COUNT(*) AS pocet +FROM vydej_slozka +GROUP BY nazev, jednotka +ORDER BY pocet DESC; + +-- párování: co předepsal lékař vs. co lékárna vydala (generická záměna) +SELECT p.datum_vystaveni, + p.nazev AS predepsano, p.atc, + v.nazev AS vydano, v.datum_vydeje +FROM predpis p +JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis +WHERE p.nazev <> v.nazev; +``` + +--- + +## Závislosti (Python) + +``` +requests +requests-pkcs12 +pymysql +``` + +```bash +pip install requests requests-pkcs12 pymysql +``` + +--- + +## XSD zdroje + +Schéma verze `202501A`, soubory v `Dokumentace/2025-04-24/WSDL_XSD/NEPRIORITNI_WEBOVE_SLUZBY/`: + +| Soubor | Obsah | +|--------|-------| +| `Cuer2Schema.xsd` | Typy specifické pro lékový záznam: `NacistLekovyZaznamOdpoved`, `lz_nacteni_predepsany_lp_erp_type`, `lz_nacteni_vydany_lp_erp_type`, `slozka_iplp_*` | +| `CuerSchema.xsd` | Společné typy: `hvlp_type`, `zprava_odpoved_type`, `zprava_type`, `jmeno_osoby_type`, `jednotka` |