notebookvb

This commit is contained in:
Vladimir Buzalka
2026-04-26 09:47:47 +02:00
parent 2447b4cf8e
commit 1f9d7bbe78
3 changed files with 480 additions and 43 deletions
@@ -37,11 +37,12 @@ from datetime import datetime, timezone, date
from pathlib import Path
from xml.sax.saxutils import escape as xml_escape
import fdb
import pymysql
import pymysql.cursors
from requests import Session
from requests_pkcs12 import Pkcs12Adapter
from Knihovny.medicus_db import get_medicus_connection
from Knihovny.mysql_db import connect_mysql
from Knihovny.najdi_dropbox import get_dropbox_root
# Windows konzole — nahrad neunikatni znaky misto padu
if hasattr(sys.stdout, "reconfigure"):
@@ -72,26 +73,11 @@ POCET_MESICU_MAX = 60
PAUZA_MIN = 10 # sekund
PAUZA_MAX = 20 # sekund
# ── Konfigurace Firebird ──────────────────────────────────────────────────────
FB_DSN = r'localhost:c:\medicus 3\data\medicus.fdb'
FB_USER = 'SYSDBA'
FB_PASS = 'masterkey'
FB_CHARSET = 'win1250'
ICP = '09305001'
ODB = '001'
# ── Konfigurace MySQL ─────────────────────────────────────────────────────────
DB = dict(
host = "192.168.1.76",
user = "root",
password = "Vlado9674+",
database = "medicus",
charset = "utf8mb4",
cursorclass = pymysql.cursors.DictCursor,
)
ICP = '09305001'
ODB = '001'
# ── Adresare ──────────────────────────────────────────────────────────────────
XML_DIR = Path(__file__).parent / "xml_archive"
XML_DIR = Path(get_dropbox_root()) / "Ordinace" / "Dokumentace_ke_zpracování" / "Zúčtovací zprávy" / "LékovýZáznamWithClaude" / "xml_archive"
LOGS_DIR = Path(__file__).parent / "Logs"
@@ -159,7 +145,7 @@ _SQL_FILTR = """
def nacti_pacienty(prijmeni_filtr=None):
conn = fdb.connect(dsn=FB_DSN, user=FB_USER, password=FB_PASS, charset=FB_CHARSET)
conn = get_medicus_connection()
try:
cur = conn.cursor()
if prijmeni_filtr:
@@ -334,7 +320,7 @@ def main():
log.info("Zadni pacienti — konec.")
return
conn = pymysql.connect(**DB)
conn = connect_mysql(database="medicus", cursorclass=pymysql.cursors.DictCursor)
inicializuj_schema(conn)
log.debug("MySQL schema OK")
@@ -0,0 +1,466 @@
"""
Týdenní aktualizace lékových záznamů všech registrovaných pacientů z eRecept SÚKL.
Logika počtu měsíců:
- nový pacient (žádná zpráva v DB) → 60 měsíců (maximum)
- pacient s chybou Z002 (poznamka) → 60 měsíců (zkusit znovu, možná se ztotožní)
- pacient s předchozím stažením → ceil(dny od posledního stažení / 30) + 1
(jednoměsíční překryv, INSERT IGNORE zajistí bez duplikátů)
Spuštění:
python 08TýdenníAktualizaceLékovéhoZáznamu.py
python 08TýdenníAktualizaceLékovéhoZáznamu.py --prijmeni Buzalka,Buzalkova
"""
import argparse
import importlib.util
import logging
import math
import random
import sys
import time
import uuid
from datetime import datetime, timezone, date
from pathlib import Path
from xml.sax.saxutils import escape as xml_escape
import html
import pymysql.cursors
from requests import Session
from requests_pkcs12 import Pkcs12Adapter
from Knihovny.EmailMessagingGraph import send_mail
from Knihovny.medicus_db import get_medicus_connection
from Knihovny.mysql_db import connect_mysql
from Knihovny.najdi_dropbox import get_dropbox_root
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(errors="replace")
# ── Import parsovaci logiky z 06 ──────────────────────────────────────────────
_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
# ── 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/Lekar2"
POCET_ZNAKU_ATC = 7
POCET_MESICU_MAX = 60
PAUZA_MIN = 2
PAUZA_MAX = 4
VYBRAT_NAHODNE = True # True = testovací běh (10 náhodných pacientů), False = všichni
EMAIL_PRIJEMCE = "vladimir.buzalka@buzalka.cz"
ICP = "09305001"
ODB = "001"
# ── Adresáře ──────────────────────────────────────────────────────────────────
XML_DIR = Path(get_dropbox_root()) / "Ordinace" / "Dokumentace_ke_zpracování" / "Zúčtovací zprávy" / "LékovýZáznamWithClaude" / "xml_archive"
LOGS_DIR = Path(__file__).parent / "Logs"
# ── Logging ───────────────────────────────────────────────────────────────────
def setup_logging(dnes_str, cas_str):
LOGS_DIR.mkdir(exist_ok=True)
log_soubor = LOGS_DIR / f"{dnes_str}_{cas_str}.log"
log = logging.getLogger("lz08")
log.setLevel(logging.DEBUG)
fh = logging.FileHandler(log_soubor, encoding="utf-8")
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S"))
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
ch.setFormatter(logging.Formatter("%(message)s"))
log.addHandler(fh)
log.addHandler(ch)
return log, log_soubor
# ── Firebird: načtení registrovaných pacientů ─────────────────────────────────
_SQL_VSICHNI = """
SELECT
KAR.IDPAC,
KAR.PRIJMENI,
KAR.JMENO,
KAR.DATNAR
FROM KAR
WHERE (vyrazen = 'N')
AND EXISTS (
SELECT id 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 = ?)
)
ORDER BY KAR.PRIJMENI_UP, KAR.RODCIS
"""
_SQL_FILTR = """
SELECT
KAR.IDPAC,
KAR.PRIJMENI,
KAR.JMENO,
KAR.DATNAR
FROM KAR
WHERE (vyrazen = 'N')
AND KAR.PRIJMENI IN ({ph})
ORDER BY KAR.PRIJMENI_UP, KAR.RODCIS
"""
def nacti_pacienty(prijmeni_filtr=None):
conn = get_medicus_connection()
try:
cur = conn.cursor()
if prijmeni_filtr:
ph = ",".join("?" * len(prijmeni_filtr))
cur.execute(_SQL_FILTR.format(ph=ph), prijmeni_filtr)
else:
dnes = date.today().isoformat()
cur.execute(_SQL_VSICHNI, (dnes, dnes, ICP, ODB))
cols = [d[0].lower() for d in cur.description]
return [dict(zip(cols, row)) for row in cur.fetchall()]
finally:
conn.close()
# ── MySQL ─────────────────────────────────────────────────────────────────────
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, poznamka FROM pacient WHERE idpac = %s", (pac["idpac"],))
return cur.fetchone()
def posledni_stazeni(cur, pacient_id):
cur.execute(
"SELECT MAX(stazeno) AS posledni FROM zprava WHERE pacient_id = %s",
(pacient_id,)
)
row = cur.fetchone()
return row["posledni"] if row and row["posledni"] else None
def vypocti_pocet_mesicu(posledni, ma_chybu):
"""60 měsíců pro nové pacienty a ty s chybou Z002, jinak diferenciálně."""
if ma_chybu or posledni is None:
return POCET_MESICU_MAX
delta_dni = (datetime.now() - posledni).days
return min(math.ceil(delta_dni / 30) + 1, POCET_MESICU_MAX)
def nacti_chybove_idpac(conn):
"""Vrátí množinu idpac pacientů, kteří mají nastavenou poznamku (Z002 apod.)."""
with conn.cursor() as cur:
cur.execute("SELECT idpac FROM pacient WHERE poznamka IS NOT NULL")
return {row["idpac"] for row in cur.fetchall()}
def uloz_poznamku(conn, pacient_id, poznamka):
with conn.cursor() as cur:
cur.execute(
"UPDATE pacient SET poznamka = %s WHERE id = %s",
(poznamka, pacient_id)
)
conn.commit()
# ── SOAP volání ───────────────────────────────────────────────────────────────
def extrahuj_soap_fault(xml_text):
try:
import xml.etree.ElementTree as ET
NS_SOAP = "http://schemas.xmlsoap.org/soap/envelope/"
NS_SUKL = "http://www.sukl.cz/erp/201912"
root = ET.fromstring(xml_text)
body = root.find(f"{{{NS_SOAP}}}Body")
if body is None:
return "Chybejici SOAP Body"
fault = body.find(f"{{{NS_SOAP}}}Fault") or body.find("Fault")
if fault is not None:
faultstring = (fault.findtext("faultstring")
or fault.findtext("faultcode")
or "Nezname SOAP Fault")
detail = fault.find("detail")
if detail is not None and detail.text:
faultstring = f"{faultstring}: {detail.text.strip()[:200]}"
return faultstring
if body.find(f"{{{NS_SUKL}}}NacistLekovyZaznamOdpoved") is None:
first = list(body)
tag = first[0].tag if first else "prazdne Body"
return f"Neocekavana odpoved: {tag}"
return None
except Exception as e:
return f"Chyba pri parsovani odpovedi: {e}"
def nacti_lekovy_zaznam(sess, prijmeni, jmena, datum_narozeni, pocet_mesicu):
id_zpravy = str(uuid.uuid4())
odeslano = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00")
soap = (
'<?xml version="1.0" encoding="UTF-8"?>'
'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">'
'<soapenv:Body>'
f'<NacistLekovyZaznamLekarDotaz xmlns="http://www.sukl.cz/erp/201912">'
f'<Doklad>'
f'<Pristupujici>'
f'<Uzivatel>{UZIVATEL}</Uzivatel>'
f'<Pracoviste>{PRACOVISTE}</Pracoviste>'
f'</Pristupujici>'
f'<PocetZnakuATC>{POCET_ZNAKU_ATC}</PocetZnakuATC>'
f'<PocetMesicu>{pocet_mesicu}</PocetMesicu>'
f'<Pacient><Totoznost><Jmeno>'
f'<Prijmeni>{xml_escape(prijmeni)}</Prijmeni>'
f'<Jmena>{xml_escape(jmena)}</Jmena>'
f'</Jmeno><DatumNarozeni>{datum_narozeni}</DatumNarozeni>'
f'</Totoznost></Pacient>'
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'</NacistLekovyZaznamLekarDotaz>'
'</soapenv:Body>'
'</soapenv:Envelope>'
)
headers = {
"Content-Type": 'text/xml; charset="UTF-8"',
"SOAPAction": '"NacistLekovyZaznam"',
"User-Agent": "Medicus",
}
resp = sess.post(ENDPOINT, data=soap.encode("utf-8"), headers=headers, timeout=60)
if resp.status_code != 200:
raise RuntimeError(f"HTTP {resp.status_code}: {resp.text[:300]}")
return resp.text
# ── XML archiv ────────────────────────────────────────────────────────────────
def uloz_xml_na_disk(xml_text, prijmeni, jmena, datnar_str, dnes_str):
adr = XML_DIR / dnes_str
adr.mkdir(parents=True, exist_ok=True)
nazev = f"{prijmeni}_{jmena}_{datnar_str}.xml".replace(" ", "_")
soubor = adr / nazev
soubor.write_text(xml_text, encoding="utf-8")
return str(soubor)
# ── Email ─────────────────────────────────────────────────────────────────────
def sestav_email(dnes_str, radky, ok, chyby, celkem, testovaci, kriticka_chyba=None):
ma_chybu = chyby > 0 or kriticka_chyba
prefix = "[TEST] " if testovaci else ""
predmet = (f"{prefix}Lékový záznam {dnes_str}"
f"{'⚠ CHYBA' if ma_chybu else 'OK'} "
f"({ok} OK, {chyby} chyb, {celkem} celkem)")
css = "font-family:Arial,sans-serif;font-size:14px;color:#222"
th = "padding:4px 10px;background:#f0f0f0;text-align:left;border-bottom:1px solid #ccc"
td = "padding:3px 10px;border-bottom:1px solid #eee"
def badge(stav):
barva = "#d4edda" if stav == "OK" else "#fdecea"
return f"<span style='background:{barva};padding:2px 7px;border-radius:3px'>{stav}</span>"
radky_html = "".join(
f"<tr>"
f"<td style='{td}'>{html.escape(r['jmeno'])}</td>"
f"<td style='{td}'>{badge(r['stav'])}</td>"
f"<td style='{td}'>{html.escape(r['detail'])}</td>"
f"</tr>"
for r in radky
)
krit = ""
if kriticka_chyba:
krit = (
f"<div style='background:#fdecea;border:1px solid #f5c6cb;padding:12px;"
f"margin:12px 0;border-radius:4px'>"
f"<strong>Kritická chyba:</strong>"
f"<pre style='white-space:pre-wrap;margin:6px 0'>{kriticka_chyba}</pre></div>"
)
body = (
f"<div style='{css}'>"
f"<h1 style='font-size:18px;margin-bottom:6px'>Lékový záznam — týdenní souhrn {dnes_str}</h1>"
+ (f"<p style='color:#888'><em>Testovací běh — náhodný vzorek {celkem} pacientů</em></p>" if testovaci else "")
+ krit
+ f"<p><strong>{ok} OK</strong> &nbsp;|&nbsp; <strong>{chyby} chyb</strong> &nbsp;|&nbsp; {celkem} celkem</p>"
f"<table style='border-collapse:collapse;width:100%'>"
f"<tr><th style='{th}'>Pacient</th><th style='{th}'>Stav</th><th style='{th}'>Detail</th></tr>"
f"{radky_html}"
f"</table>"
f"</div>"
)
return predmet, body
# ── Hlavní smyčka ─────────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description="Týdenní aktualizace lékových záznamů z eReceptu")
ap.add_argument("--prijmeni", default=None,
help="Filtr příjmení oddělená čárkou (pro testování), např: Buzalka,Buzalkova")
args = ap.parse_args()
dnes_str = date.today().isoformat()
cas_str = datetime.now().strftime("%H-%M-%S")
log, log_soubor = setup_logging(dnes_str, cas_str)
prijmeni_filtr = None
if args.prijmeni:
prijmeni_filtr = [p.strip() for p in args.prijmeni.split(",")]
log.info(f"Filtr příjmení: {prijmeni_filtr}")
log.info("Načítám pacienty z Medicusu...")
pacienti = nacti_pacienty(prijmeni_filtr)
if VYBRAT_NAHODNE and not prijmeni_filtr:
import random as _random
pacienti = _random.sample(pacienti, min(10, len(pacienti)))
log.info(f"TESTOVACÍ BĚH — náhodný vzorek {len(pacienti)} pacientů")
celkem = len(pacienti)
log.info(f"Pacientů ke zpracování: {celkem} | log: {log_soubor.name}")
if not celkem:
log.info("Žádní pacienti — konec.")
return
conn = connect_mysql(database="medicus", cursorclass=pymysql.cursors.DictCursor)
inicializuj_schema(conn)
log.debug("MySQL schema OK")
# Seřadit: nejdřív pacienti bez chyby, Z002 na konec
chybove_idpac = nacti_chybove_idpac(conn)
pacienti.sort(key=lambda p: 1 if p["idpac"] in chybove_idpac else 0)
z002_pocet = len(chybove_idpac & {p["idpac"] for p in pacienti})
log.info(f"Pořadí: {celkem - z002_pocet} bez chyby, pak {z002_pocet} Z002")
sess = Session()
sess.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_FILE, pkcs12_password=PFX_PASS))
sess.auth = (API_USER, API_PASS)
ok = chyby = 0
email_radky = []
kriticka_chyba = None
try:
for i, pac in enumerate(pacienti, 1):
prijmeni = pac["prijmeni"]
jmena = pac["jmeno"]
datnar = pac["datnar"]
datnar_str = datnar.isoformat() if hasattr(datnar, "isoformat") else str(datnar)
jmeno_str = f"{prijmeni} {jmena}"
with conn.cursor() as cur:
row = upsert_pacient(cur, pac)
pacient_id = row["id"]
ma_chybu = row["poznamka"] is not None
posledni = posledni_stazeni(cur, pacient_id)
conn.commit()
pocet_mesicu = vypocti_pocet_mesicu(posledni, ma_chybu)
log.debug(f"[{i:4}/{celkem}] {jmeno_str} (*{datnar_str}) "
f"{pocet_mesicu}m "
f"({'Z002' if ma_chybu else posledni.strftime('%Y-%m-%d') if posledni else 'nový'})")
try:
xml_text = nacti_lekovy_zaznam(sess, prijmeni, jmena, datnar_str, pocet_mesicu)
except Exception as e:
zprava_chyby = str(e)[:400]
log.info(f"[{i:4}/{celkem}] {jmeno_str:<30} CHYBA {zprava_chyby[:60]}")
uloz_poznamku(conn, pacient_id, zprava_chyby)
email_radky.append({"jmeno": jmeno_str, "stav": "CHYBA", "detail": zprava_chyby[:100]})
chyby += 1
continue
soap_fault = extrahuj_soap_fault(xml_text)
if soap_fault:
log.info(f"[{i:4}/{celkem}] {jmeno_str:<30} CHYBA {soap_fault[:60]}")
uloz_poznamku(conn, pacient_id, soap_fault[:400])
email_radky.append({"jmeno": jmeno_str, "stav": "CHYBA", "detail": soap_fault[:100]})
chyby += 1
continue
xml_soubor = uloz_xml_na_disk(xml_text, prijmeni, jmena, datnar_str, dnes_str)
kb = Path(xml_soubor).stat().st_size // 1024
try:
zprava_d, predpisy, vydeji, predepisujici, vydavajici = parsuj_xml(Path(xml_soubor))
stats = uloz(conn, zprava_d, predpisy, vydeji, predepisujici, vydavajici,
pacient_id=pacient_id, xml_soubor=xml_soubor)
uloz_poznamku(conn, pacient_id, None)
detail = f"{stats['predpisy_celkem']}p {stats['vydeji_celkem']}v {kb} KB"
log.info(f"[{i:4}/{celkem}] {jmeno_str:<30} OK {detail}")
email_radky.append({"jmeno": jmeno_str, "stav": "OK", "detail": detail})
ok += 1
except Exception as e:
zprava_chyby = str(e)[:400]
log.info(f"[{i:4}/{celkem}] {jmeno_str:<30} CHYBA {zprava_chyby[:60]}")
uloz_poznamku(conn, pacient_id, zprava_chyby)
email_radky.append({"jmeno": jmeno_str, "stav": "CHYBA", "detail": zprava_chyby[:100]})
chyby += 1
if i < celkem:
pauza = random.randint(PAUZA_MIN, PAUZA_MAX)
log.debug(f" čekám {pauza}s ...")
time.sleep(pauza)
except Exception:
import traceback
kriticka_chyba = traceback.format_exc()
log.error(f"KRITICKÁ CHYBA:\n{kriticka_chyba}")
finally:
conn.close()
sess.close()
log.info("=" * 55)
log.info(f"Hotovo: {ok} OK | {chyby} chyb | celkem {celkem} pacientů")
# ── Email se souhrnem a logem jako přílohou — odesílá se vždy ────────────
try:
predmet, body = sestav_email(dnes_str, email_radky, ok, chyby, celkem,
VYBRAT_NAHODNE, kriticka_chyba)
send_mail(to=EMAIL_PRIJEMCE, subject=predmet, body=body, html=True,
attachments=log_soubor)
log.info(f"Email odeslán: {EMAIL_PRIJEMCE}")
except Exception as e:
log.warning(f"CHYBA odeslání emailu: {e}")
if __name__ == "__main__":
main()
@@ -11,32 +11,17 @@ import sys
import importlib.util
from pathlib import Path
from datetime import date
import fdb
import pymysql
import pymysql.cursors
from Knihovny.medicus_db import get_medicus_connection
from Knihovny.mysql_db import connect_mysql
from Knihovny.najdi_dropbox import get_dropbox_root
# 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,
)
XML_ADRESAR = Path(get_dropbox_root()) / "Ordinace" / "Dokumentace_ke_zpracování" / "Zúčtovací zprávy" / "LékovýZáznamWithClaude" / "xml_archive"
ICP = "09305001"
ODB = "001"
@@ -56,7 +41,7 @@ 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)
conn = get_medicus_connection()
try:
cur = conn.cursor()
dnes = date.today().isoformat()
@@ -114,7 +99,7 @@ def main():
fb_pacienti = nacti_pacienty_z_fb()
# Pripoj se k MySQL a inicializuj schema
conn = pymysql.connect(**DB)
conn = connect_mysql(database="medicus", cursorclass=pymysql.cursors.DictCursor)
try:
inicializuj_schema(conn)