Files
ordinaceprojekt/Recepty/NačteníPředpisuWithClaude/11_ParseXML.py
T
Vladimir Buzalka adb84523cd Přidán podprojekt Recepty (eRecept SÚKL)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 07:06:17 +02:00

303 lines
12 KiB
Python

"""
Naparsuje XML soubory z xml_archive a uloží data do MySQL tabulek
recept_doklad a recept_plp.
Pro každý ERP kód zpracuje NEJNOVĚJŠÍ XML soubor (nejvyšší datum v archivu).
Opakované spuštění je bezpečné — používá UPSERT a INSERT IGNORE.
Spuštění:
python 11_ParseXML.py # celý archiv
python 11_ParseXML.py --datum 2026-04-14 # jen konkrétní den
"""
import sys
import argparse
from datetime import date
from pathlib import Path
import xml.etree.ElementTree as ET
import pymysql
import pymysql.cursors
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(errors="replace")
# ── Konfigurace ───────────────────────────────────────────────────────────────
DB = dict(
host = "192.168.1.76",
user = "root",
password = "Vlado9674+",
database = "medicus",
charset = "utf8mb4",
cursorclass = pymysql.cursors.DictCursor,
)
XML_DIR = Path(__file__).parent / "xml_archive"
NS = "http://www.sukl.cz/erp/201704"
# ── Parametry spuštění (uprav zde, nebo nech None = celý archiv) ──────────────
DATUM_FILTR = None # např. "2026-04-14", nebo None = celý archiv
# ─────────────────────────────────────────────────────────────────────────────
def t(el, tag):
"""Vrátí text prvního potomka s daným tagem, nebo None."""
found = el.find(f"{{{NS}}}{tag}")
return found.text.strip() if found is not None and found.text else None
def ts(s):
"""ISO datetime → MySQL DATETIME string."""
return s[:19].replace("T", " ") if s else None
def bool_el(el, tag):
return 1 if t(el, tag) == "true" else 0
def parsuj_xml(xml_text, xml_soubor):
"""
Naparsuje XML odpověď NacistPredpis.
Vrátí (doklad_dict, [plp_dict, ...]) nebo None při chybě.
"""
try:
root = ET.fromstring(xml_text)
doklad = root.find(f".//{{{NS}}}Doklad")
if doklad is None:
return None
except ET.ParseError:
return None
# ── doklad ────────────────────────────────────────────────────────────────
id_dokladu = t(doklad, "ID_Dokladu")
stav = t(doklad, "Stav")
datum_vystaveni = t(doklad, "DatumVystaveni")
platnost_do_s = t(doklad, "PlatnostDo")
vypis_do = t(doklad, "VypisDo")
akutni = bool_el(doklad, "Akutni")
rodina = bool_el(doklad, "Rodina")
opakovani_s = t(doklad, "Opakovani")
druh_pojisteni = t(doklad, "DruhPojisteni")
modry_pruh = bool_el(doklad, "ModryPruh")
pozn = t(doklad, "Pozn")
zap_s = t(doklad, "ZapocitatelnyDoplatekZbyvaDoLimitu")
zmena = ts(t(doklad, "Zmena"))
zalozeni = ts(t(doklad, "Zalozeni"))
# stav_terminal
platnost_date = date.fromisoformat(platnost_do_s) if platnost_do_s else None
expiroval = platnost_date is not None and platnost_date < date.today()
stav_terminal = 1 if stav in ("PLNE_VYDANY", "ZRUSENY") or expiroval else 0
# ── pacient ───────────────────────────────────────────────────────────────
pac = doklad.find(f"{{{NS}}}Pacient")
zp_el = pac.find(f"{{{NS}}}ZP") if pac is not None else None
cp = t(pac, "CP") if pac is not None else None
zp_kod = t(zp_el, "Kod") if zp_el is not None else None
zp_nazev = t(zp_el, "Nazev") if zp_el is not None else None
pac_telefon = t(pac, "Telefon") if pac is not None else None
pac_notif = t(pac, "Notifikace") if pac is not None else None
pac_pohlavi = t(pac, "Pohlavi") if pac is not None else None
# ── predepisujici ─────────────────────────────────────────────────────────
pred = doklad.find(f"{{{NS}}}Predepisujici")
lekar_el = pred.find(f"{{{NS}}}Lekar") if pred is not None else None
odb_el = pred.find(f"{{{NS}}}Odbornost") if pred is not None else None
lekar_kod_raw = t(lekar_el, "Kod") if lekar_el is not None else None
# "skryto" není UUID → uložíme NULL
lekar_kod = lekar_kod_raw if lekar_kod_raw and lekar_kod_raw.lower() != "skryto" else None
odb_kod = t(odb_el, "Kod") if odb_el is not None else None
odb_nazev = t(odb_el, "Nazev") if odb_el is not None else None
lekar_email = t(pred, "Email") if pred is not None else None
doklad_dict = dict(
id_dokladu = id_dokladu,
stav = stav,
stav_terminal = stav_terminal,
datum_vystaveni = datum_vystaveni,
platnost_do = platnost_do_s,
vypis_do = vypis_do,
akutni = akutni,
rodina = rodina,
opakovani = int(opakovani_s) if opakovani_s else None,
druh_pojisteni = druh_pojisteni,
modry_pruh = modry_pruh,
pozn = pozn,
zap_doplatek = float(zap_s) if zap_s else None,
zmena = zmena,
zalozeni = zalozeni,
lekar_kod = lekar_kod,
odbornost_kod = odb_kod,
odbornost_nazev = odb_nazev,
lekar_email = lekar_email,
cp = cp,
zp_kod = zp_kod,
zp_nazev = zp_nazev,
pac_telefon = pac_telefon,
pac_notifikace = pac_notif,
pac_pohlavi = pac_pohlavi,
xml_soubor = str(xml_soubor),
)
# ── PLP položky ───────────────────────────────────────────────────────────
plp_list = []
for plp_el in doklad.findall(f"{{{NS}}}PLP"):
id_lp = t(plp_el, "ID_LP")
uhrada = t(plp_el, "Uhrada")
prekroceni = 1 if t(plp_el, "Prekroceni") == "true" else 0
if id_lp:
plp_list.append(dict(
id_lp = id_lp,
id_dokladu = id_dokladu,
uhrada = uhrada,
prekroceni = prekroceni,
))
return doklad_dict, plp_list
def uloz(conn, doklad, plp_list):
"""UPSERT dokladu a INSERT IGNORE PLP položek."""
with conn.cursor() as cur:
# recept_doklad — ON DUPLICATE KEY UPDATE (stav se může změnit)
cur.execute("""
INSERT INTO recept_doklad
(id_dokladu, stav, stav_terminal, datum_vystaveni, platnost_do,
vypis_do, akutni, rodina, opakovani, druh_pojisteni, modry_pruh,
pozn, zap_doplatek, zmena, zalozeni,
lekar_kod, odbornost_kod, odbornost_nazev, lekar_email,
cp, zp_kod, zp_nazev, pac_telefon, pac_notifikace, pac_pohlavi,
xml_soubor, stazeno)
VALUES
(%(id_dokladu)s, %(stav)s, %(stav_terminal)s, %(datum_vystaveni)s, %(platnost_do)s,
%(vypis_do)s, %(akutni)s, %(rodina)s, %(opakovani)s, %(druh_pojisteni)s, %(modry_pruh)s,
%(pozn)s, %(zap_doplatek)s, %(zmena)s, %(zalozeni)s,
%(lekar_kod)s, %(odbornost_kod)s, %(odbornost_nazev)s, %(lekar_email)s,
%(cp)s, %(zp_kod)s, %(zp_nazev)s, %(pac_telefon)s, %(pac_notifikace)s, %(pac_pohlavi)s,
%(xml_soubor)s, NOW())
ON DUPLICATE KEY UPDATE
stav = VALUES(stav),
stav_terminal = VALUES(stav_terminal),
platnost_do = VALUES(platnost_do),
druh_pojisteni = VALUES(druh_pojisteni),
pozn = VALUES(pozn),
zap_doplatek = VALUES(zap_doplatek),
zmena = VALUES(zmena),
zp_kod = VALUES(zp_kod),
zp_nazev = VALUES(zp_nazev),
pac_telefon = VALUES(pac_telefon),
pac_notifikace = VALUES(pac_notifikace),
xml_soubor = VALUES(xml_soubor),
stazeno = NOW()
""", doklad)
# recept_plp — INSERT IGNORE (UUID je stabilní, nemění se)
for plp in plp_list:
cur.execute("""
INSERT IGNORE INTO recept_plp (id_lp, id_dokladu, uhrada, prekroceni)
VALUES (%(id_lp)s, %(id_dokladu)s, %(uhrada)s, %(prekroceni)s)
""", plp)
conn.commit()
def najdi_nejnovejsi_xml(datum_filtr=None):
"""
Projde xml_archive, vrátí dict {erp_kod: Path} s nejnovějším XML pro každý kód.
Přeskočí soubory končící _CHYBA.xml.
datum_filtr: pokud zadáno (YYYY-MM-DD), zpracuje jen daný den.
"""
nejnovejsi = {}
if datum_filtr:
slozky = [XML_DIR / datum_filtr]
else:
slozky = sorted(XML_DIR.iterdir()) # seřazeno dle názvu = chronologicky
for slozka in slozky:
if not slozka.is_dir():
continue
for xml_file in slozka.glob("*.xml"):
if xml_file.stem.endswith("_CHYBA"):
continue
erp_kod = xml_file.stem
# pozdější složka přepíše dřívější → nejnovější vyhraje
nejnovejsi[erp_kod] = xml_file
return nejnovejsi
def nacti_zpracovane(conn):
"""Vrátí dict {id_dokladu: xml_soubor} pro všechny již zpracované záznamy."""
with conn.cursor() as cur:
cur.execute("SELECT id_dokladu, xml_soubor FROM recept_doklad WHERE xml_soubor IS NOT NULL")
return {row["id_dokladu"]: row["xml_soubor"] for row in cur.fetchall()}
def main():
datum_filtr = DATUM_FILTR
xml_mapa = najdi_nejnovejsi_xml(datum_filtr)
celkem = len(xml_mapa)
print(f"Nalezeno {celkem} XML souborů v archivu\n")
if not celkem:
print("Žádné soubory.")
return
conn = pymysql.connect(**DB)
# Jednorázové opravy schématu (bezpečné opakovat)
with conn.cursor() as cur:
# uhrada: původní ENUM neobsahoval PACIENT
cur.execute("ALTER TABLE recept_plp MODIFY uhrada VARCHAR(20)")
# pac_pohlavi: XML posílá 'Ž' (ne 'Z')
cur.execute("ALTER TABLE recept_doklad MODIFY pac_pohlavi VARCHAR(5)")
# lekar_kod FK: predepisujici se plní z lékového záznamu, ne z detailu
# → FK by blokoval vložení, zrušíme ji
try:
cur.execute("ALTER TABLE recept_doklad DROP FOREIGN KEY recept_doklad_ibfk_1")
except Exception:
pass # FK už byl zrušen dříve
conn.commit()
# Načti již zpracované soubory — přeskočíme ty, jejichž cesta se nezměnila
zpracovane = nacti_zpracovane(conn)
ok = chyb = preskoceno = 0
for i, (erp_kod, xml_file) in enumerate(xml_mapa.items(), 1):
rel_path = str(xml_file.relative_to(XML_DIR))
if zpracovane.get(erp_kod) == rel_path:
preskoceno += 1
continue
print(f"[{i:4d}/{celkem}] {erp_kod} ", end="", flush=True)
xml_text = xml_file.read_text(encoding="utf-8")
vysledek = parsuj_xml(xml_text, xml_file.relative_to(XML_DIR))
if vysledek is None:
print("CHYBA parsování")
chyb += 1
continue
doklad, plp_list = vysledek
try:
uloz(conn, doklad, plp_list)
print(f"{doklad['stav']:20s} terminal={doklad['stav_terminal']} PLP={len(plp_list)}")
ok += 1
except Exception as e:
print(f"CHYBA DB {e}")
conn.rollback()
chyb += 1
conn.close()
print(f"\nHotovo: {ok} zpracováno, {preskoceno} přeskočeno (beze změny), {chyb} chyb")
if __name__ == "__main__":
main()