This commit is contained in:
2026-04-14 10:15:00 +02:00
parent 73984c748a
commit 3ea09fc4a3
2 changed files with 372 additions and 0 deletions
@@ -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 <name>
cur.execute(ddl)
print(f" OK {tabulka}")
conn.commit()
print("\nHotovo.")
finally:
conn.close()
if __name__ == "__main__":
vytvor_tabulky()
@@ -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 = (
'<?xml version="1.0" encoding="UTF-8"?>'
'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">'
'<soapenv:Body>'
f'<NacteniPredpisuDotaz xmlns="{NAMESPACE}">'
f'<Doklad>'
f'<Pristupujici>'
f'<Uzivatel>{UZIVATEL}</Uzivatel>'
f'<Pracoviste>{PRACOVISTE}</Pracoviste>'
f'</Pristupujici>'
f'<Identifikator>'
f'<ID_Dokladu>{erp_kod}</ID_Dokladu>'
f'</Identifikator>'
f'</Doklad>'
f'<Zprava>'
f'<ID_Zpravy>{id_zpravy}</ID_Zpravy>'
f'<Verze>202501A</Verze>'
f'<Odeslano>{odeslano}</Odeslano>'
f'<SW_Klienta>MEDICUS_____</SW_Klienta>'
f'</Zprava>'
f'</NacteniPredpisuDotaz>'
'</soapenv:Body>'
'</soapenv:Envelope>'
)
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 "<soap:Fault" in text or "Fault>" 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()