From 3ea09fc4a3f5b2c79f3851ea4d5e5cdb525c6c31 Mon Sep 17 00:00:00 2001 From: "vladimir.buzalka" Date: Tue, 14 Apr 2026 10:15:00 +0200 Subject: [PATCH] z230 --- NačteníPředpisuWithClaude/09_VytvorTabulky.py | 133 ++++++++++ NačteníPředpisuWithClaude/10_StahnoutXML.py | 239 ++++++++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 NačteníPředpisuWithClaude/09_VytvorTabulky.py create mode 100644 NačteníPředpisuWithClaude/10_StahnoutXML.py diff --git a/NačteníPředpisuWithClaude/09_VytvorTabulky.py b/NačteníPředpisuWithClaude/09_VytvorTabulky.py new file mode 100644 index 0000000..f828368 --- /dev/null +++ b/NačteníPředpisuWithClaude/09_VytvorTabulky.py @@ -0,0 +1,133 @@ +""" +Vytvoří tabulky recept_doklad a recept_plp v MySQL databázi medicus. + +Spuštění: + python 09_VytvorTabulky.py + +Tabulky: + recept_doklad — jeden řádek na celý recept (ID_Dokladu) + recept_plp — jeden řádek na PLP položku (id_lp = predpis.id_lp_predpis) + +Bezpečné opakované spuštění — používá CREATE TABLE IF NOT EXISTS. +Neprovádí DROP. +""" + +import pymysql +import pymysql.cursors + +DB = dict( + host = "192.168.1.76", + user = "root", + password = "Vlado9674+", + database = "medicus", + charset = "utf8mb4", + cursorclass = pymysql.cursors.DictCursor, +) + +DDL = [ + + # ── recept_doklad ───────────────────────────────────────────────────────── + # Jeden řádek na celý recept (ID_Dokladu = ERP kód, např. PPIBVF93285E). + # Data na úrovni dokladu: stav, platnost, pacient snapshot, předepisující. + # + # stav_terminal: + # 0 = PREDEPSANY nebo CASTECNE_VYDANY → skript má znovu stahovat XML + # 1 = PLNE_VYDANY nebo ZRUSENY nebo expirovaný → stahování ukončeno + # + # xml_soubor: relativní cesta k poslednímu naparsovanému XML souboru + """ + CREATE TABLE IF NOT EXISTS recept_doklad ( + id_dokladu VARCHAR(20) NOT NULL PRIMARY KEY, + + -- stav a platnost + stav ENUM( + 'PREDEPSANY', + 'CASTECNE_VYDANY', + 'PLNE_VYDANY', + 'ZRUSENY' + ) NOT NULL, + stav_terminal TINYINT(1) NOT NULL DEFAULT 0 + COMMENT '1 = nepotřebuje další stahování', + datum_vystaveni DATE NOT NULL, + platnost_do DATE, + vypis_do DATE COMMENT 'prodloužení platnosti výpisem', + akutni TINYINT(1), + rodina TINYINT(1) COMMENT 'ad usum proprium', + opakovani INT COMMENT 'NULL = není opakovací', + druh_pojisteni ENUM('VEREJNE','OSTATNI'), + modry_pruh TINYINT(1), + pozn VARCHAR(1000), + zap_doplatek DECIMAL(10,2) COMMENT 'ZapocitatelnyDoplatekZbyvaDoLimitu', + + -- časové razítko z eReceptu + zalozeni DATETIME, + zmena DATETIME, + + -- předepisující lékař (FK na existující tabulku z lékového záznamu) + lekar_kod CHAR(36), + odbornost_kod VARCHAR(10), + odbornost_nazev VARCHAR(100), + lekar_email VARCHAR(100), + + -- pacient snapshot (hodnoty platné k datu předpisu — mohou se měnit) + cp VARCHAR(10) COMMENT 'číslo pojištěnce / RČ', + zp_kod CHAR(3), + zp_nazev VARCHAR(100), + pac_telefon VARCHAR(20), + pac_notifikace ENUM('SMS','EMAIL'), + pac_pohlavi ENUM('M','Z'), + + -- meta + xml_soubor VARCHAR(255) COMMENT 'cesta k poslednímu XML souboru', + stazeno DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + COMMENT 'poslední aktualizace záznamu', + + FOREIGN KEY (lekar_kod) REFERENCES predepisujici (lekar_kod) + ON DELETE SET NULL, + + INDEX idx_stav (stav), + INDEX idx_stav_terminal (stav_terminal), + INDEX idx_platnost (platnost_do), + INDEX idx_lekar (lekar_kod) + ) ENGINE=InnoDB COMMENT='Detail receptu (NacistPredpis) — jeden řádek na ID_Dokladu' + """, + + # ── recept_plp ──────────────────────────────────────────────────────────── + # Jeden řádek na PLP položku (jeden lék na receptu). + # id_lp = UUID = predpis.id_lp_predpis → přímý JOIN s lékovým záznamem. + # Lékové detaily (ATC, název, forma…) jsou záměrně vynechány — + # jsou už v tabulce predpis, duplikovat je nemá smysl. + """ + CREATE TABLE IF NOT EXISTS recept_plp ( + id_lp CHAR(36) NOT NULL PRIMARY KEY + COMMENT 'UUID PLP = predpis.id_lp_predpis', + id_dokladu VARCHAR(20) NOT NULL, + + uhrada ENUM('ZAKLADNI','ZVYSENA','NEHRAZENY'), + prekroceni TINYINT(1), + + FOREIGN KEY (id_dokladu) REFERENCES recept_doklad (id_dokladu) + ON DELETE CASCADE, + + INDEX idx_id_dokladu (id_dokladu) + ) ENGINE=InnoDB COMMENT='PLP položky receptu — JOIN přes id_lp na predpis.id_lp_predpis' + """, +] + + +def vytvor_tabulky(): + conn = pymysql.connect(**DB) + try: + with conn.cursor() as cur: + for ddl in DDL: + tabulka = ddl.strip().split()[5] # CREATE TABLE IF NOT EXISTS + cur.execute(ddl) + print(f" OK {tabulka}") + conn.commit() + print("\nHotovo.") + finally: + conn.close() + + +if __name__ == "__main__": + vytvor_tabulky() diff --git a/NačteníPředpisuWithClaude/10_StahnoutXML.py b/NačteníPředpisuWithClaude/10_StahnoutXML.py new file mode 100644 index 0000000..b91d0cb --- /dev/null +++ b/NačteníPředpisuWithClaude/10_StahnoutXML.py @@ -0,0 +1,239 @@ +""" +Stažení detailu receptů (NacistPredpis) z eRecept SÚKL. + +Logika přeskakování: + - Recept je v recept_doklad se stav_terminal = 1 → přeskočit (vydaný / zrušený) + - Recept není v recept_doklad → stáhnout (nový) + - Recept je v recept_doklad se stav_terminal = 0 → stáhnout znovu (dosud nevyzvednutý) + +Spuštění: + python 10_StahnoutXML.py # všechny od 2025-01-01 + python 10_StahnoutXML.py --od 2026-01-01 + python 10_StahnoutXML.py --limit 50 # testování + +XML odpovědi se ukládají do xml_archive/YYYY-MM-DD/ERP_KOD.xml +""" + +import sys +import time +import uuid +from datetime import datetime, timezone, date +from pathlib import Path + +import random +import fdb +import pymysql +import pymysql.cursors +from requests import Session +from requests_pkcs12 import Pkcs12Adapter + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(errors="replace") + +# ── Konfigurace eRecept ────────────────────────────────────────────────────── +PFX_FILE = r"C:\Users\vlado\PycharmProjects\Recepty\AMBSUKL214235369G_31DEC2024.pfx" +PFX_PASS = "Vlado7309208104++" +API_USER = "e08c89c6-2b1a-4eba-8ed9-4e3e63618379" +API_PASS = "Buzalka@Vladimir2025" +UZIVATEL = "E08C89C6-2B1A-4EBA-8ED9-4E3E63618379" +PRACOVISTE = "00214235367" +ENDPOINT = "https://lekar-soap.erecept.sukl.cz/cuer/Lekar" +NAMESPACE = "http://www.sukl.cz/erp/201704" + +PAUZA_MIN = 4 # sekund mezi voláními API (minimum) +PAUZA_MAX = 6 # sekund mezi voláními API (maximum) + +# ── Konfigurace Firebird ───────────────────────────────────────────────────── +FB_DSN = r'localhost:c:\medicus 3\data\medicus.fdb' +FB_USER = 'SYSDBA' +FB_PASS = 'masterkey' +FB_CHARSET = 'win1250' + +# ── Konfigurace MySQL ──────────────────────────────────────────────────────── +DB = dict( + host = "192.168.1.76", + user = "root", + password = "Vlado9674+", + database = "medicus", + charset = "utf8mb4", + cursorclass = pymysql.cursors.DictCursor, +) + +# ── Adresáře ───────────────────────────────────────────────────────────────── +XML_DIR = Path(__file__).parent / "xml_archive" + +# ── Parametry spuštění (uprav zde) ─────────────────────────────────────────── +DATUM_OD = "2025-01-01" # recepty od tohoto data +LIMIT = 10 # max počet receptů ke stažení; None = bez omezení + + +# ───────────────────────────────────────────────────────────────────────────── + +def nacti_terminal_set(mysql_conn): + """ + Vrátí set ERP kódů, které jsou již terminální (vydané / zrušené / expirované). + Jeden dotaz na začátku — pak jen O(1) lookup v Pythonu. + """ + with mysql_conn.cursor() as cur: + cur.execute("SELECT id_dokladu FROM recept_doklad WHERE stav_terminal = 1") + return {row["id_dokladu"] for row in cur.fetchall()} + + +def nacti_erp_kody(fb_conn, datum_od, limit=None): + """ + Načte unikátní ERP kódy z Firebirdu (recept_epodani.erp) od datum_od. + Vrací list tuplů: (datum, lek, dop, idpac, prijmeni, jmeno, erp_kod) + """ + if limit: + sql = f"SELECT FIRST {int(limit)}" + else: + sql = "SELECT" + + sql += """ + r.datum, r.lek, r.dop, r.idpac, + TRIM(kar.prijmeni) AS prijmeni, TRIM(kar.jmeno) AS jmeno, + ep.erp + FROM recept r + JOIN recept_epodani ep ON r.id_epodani = ep.id + JOIN kar ON r.idpac = kar.idpac + WHERE r.datum >= ? AND ep.erp IS NOT NULL + ORDER BY r.datum DESC + """ + + cur = fb_conn.cursor() + cur.execute(sql, [datum_od]) + rows = cur.fetchall() + cur.close() + + # deduplikace dle ERP kódu — jeden recept může mít více léků (řádků) + seen = set() + unique = [] + for row in rows: + erp = row[6] + if erp not in seen: + seen.add(erp) + unique.append(row) + return unique + + +def volej_nacist_predpis(sess, erp_kod): + """Zavolá NacistPredpis SOAP a vrátí (status_code, response_text).""" + id_zpravy = str(uuid.uuid4()) + odeslano = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00") + + soap_body = ( + '' + '' + '' + f'' + f'' + f'' + f'{UZIVATEL}' + f'{PRACOVISTE}' + f'' + f'' + f'{erp_kod}' + f'' + f'' + f'' + f'{id_zpravy}' + f'202501A' + f'{odeslano}' + f'MEDICUS_____' + f'' + f'' + '' + '' + ) + + headers = { + "Content-Type": 'text/xml; charset="UTF-8"', + "SOAPAction": '"NacistPredpis"', + "User-Agent": "Medicus", + } + + resp = sess.post(ENDPOINT, data=soap_body.encode("utf-8"), headers=headers, timeout=15) + return resp.status_code, resp.text + + +def main(): + datum_od = DATUM_OD + limit = LIMIT + + dnes = date.today().isoformat() + out_dir = XML_DIR / dnes + out_dir.mkdir(parents=True, exist_ok=True) + + # ── 1. Načti terminální sadu z MySQL ───────────────────────────────────── + print("Připojuji MySQL...") + mysql = pymysql.connect(**DB) + terminal = nacti_terminal_set(mysql) + mysql.close() + print(f" Terminálních receptů v DB: {len(terminal)}\n") + + # ── 2. Načti ERP kódy z Firebirdu ──────────────────────────────────────── + print("Připojuji Firebird...") + fb = fdb.connect(dsn=FB_DSN, user=FB_USER, password=FB_PASS, charset=FB_CHARSET) + rows = nacti_erp_kody(fb, datum_od, limit) + fb.close() + print(f" Unikátních ERP kódů v Medicusu (od {datum_od}): {len(rows)}\n") + + if not rows: + print("Žádné recepty k zpracování.") + return + + # ── 3. Filtruj — přeskoč terminální ────────────────────────────────────── + ke_stazeni = [r for r in rows if r[6] not in terminal] + preskoceno = len(rows) - len(ke_stazeni) + print(f" Přeskočeno (terminální): {preskoceno}") + print(f" Ke stažení: {len(ke_stazeni)}\n") + + if not ke_stazeni: + print("Vše je již staženo a terminální. Hotovo.") + return + + # ── 4. SOAP session ─────────────────────────────────────────────────────── + sess = Session() + sess.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_FILE, pkcs12_password=PFX_PASS)) + sess.auth = (API_USER, API_PASS) + + ok = 0 + chyby = 0 + + for i, row in enumerate(ke_stazeni, 1): + datum_rec, lek, dop, idpac, prijmeni, jmeno, erp_kod = row + lek_str = f"{lek} {dop}".strip() if dop else str(lek or "").strip() + label = f"{prijmeni} {jmeno}".strip() + + print(f"[{i:4d}/{len(ke_stazeni)}] {label:30s} {erp_kod} ", end="", flush=True) + + try: + status, text = volej_nacist_predpis(sess, erp_kod) + + je_chyba = status != 200 or "" in text + + if not je_chyba: + xml_file = out_dir / f"{erp_kod}.xml" + xml_file.write_text(text, encoding="utf-8") + print(f"OK {len(text.encode()) / 1024:5.1f} KB {lek_str[:40]}") + ok += 1 + else: + chyba_short = text[:120].replace("\n", " ") + print(f"CHYBA HTTP {status} {chyba_short}") + xml_file = out_dir / f"{erp_kod}_CHYBA.xml" + xml_file.write_text(text, encoding="utf-8") + chyby += 1 + + except Exception as e: + print(f"EXCEPTION {e}") + chyby += 1 + + if i < len(ke_stazeni): + time.sleep(random.uniform(PAUZA_MIN, PAUZA_MAX)) + + print(f"\nHotovo: {ok} OK, {chyby} chyb, {preskoceno} přeskočeno") + print(f"XML: {out_dir}") + + +if __name__ == "__main__": + main()