Files
michaela.buzalkova 7ec3fcedea lenovo
2026-04-26 20:27:57 +02:00

651 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Denní kombinovaný skript: stažení XML z eRecept + parsování do MySQL + emailový souhrn.
Changelog:
2026-04-26 08:35 Přidán počítadlo přeskočených záznamů při parsování (parse_preskoceno).
Email nyní zobrazuje "X zpracováno | Y přeskočeno (beze změny) | Z chyb"
místo pouhého "0 zpracováno | 0 chyb", které bylo matoucí při
opakovaných denních bězích (po prvním běhu jsou soubory beze změny).
Pořadí:
1. Načte terminální sadu z MySQL
2. Načte ERP kódy z Firebirdu
3. Stáhne XML pro nezpracované recepty (API eRecept)
4. Naparsuje dnešní XML do MySQL
5. Odešle emailový souhrn (i v případě výjimky)
Spuštění:
python 12_DenníStaženíAZpracování.py
python 12_DenníStaženíAZpracování.py --od 2025-01-01
python 12_DenníStaženíAZpracování.py --limit 50
"""
import argparse
import random
import sys
import time
import traceback
import uuid
import xml.etree.ElementTree as ET
from dataclasses import dataclass, field
from datetime import date, datetime, timezone
from pathlib import Path
import pymysql
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")
# ── Konfigurace eRecept ───────────────────────────────────────────────────────
PFX_FILE = Path(__file__).parent.parent / "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 = 1
PAUZA_MAX = 3
# ── Adresáře a email ──────────────────────────────────────────────────────────
XML_DIR = Path(get_dropbox_root()) / "Ordinace" / "Dokumentace_ke_zpracování" / "Zúčtovací zprávy" / "NačteníPředpisuWithClaude" / "xml_archive"
EMAIL_PRIJEMCE = "vladimir.buzalka@buzalka.cz"
# ── Výchozí parametry ─────────────────────────────────────────────────────────
DATUM_OD_DEFAULT = "2025-01-01"
# ── Datové třídy pro souhrn ───────────────────────────────────────────────────
@dataclass
class StazeniRadek:
erp_kod: str
pacient: str
stav: str # "OK" | "CHYBA" | "PRESKOCENO"
detail: str = "" # stav receptu nebo chybová zpráva
datum_vystaveni: str = ""
platnost_do: str = ""
zbyva_vydat: str = ""
@dataclass
class ParseRadek:
erp_kod: str
stav_receptu: str # PREDEPSANY / PLNE_VYDANY / …
terminal: int
plp: int
stav: str # "OK" | "CHYBA"
detail: str = ""
@dataclass
class Souhrn:
datum: str = ""
stazeni: list[StazeniRadek] = field(default_factory=list)
parse: list[ParseRadek] = field(default_factory=list)
parse_preskoceno: int = 0
kriticka_chyba: str = ""
# ─────────────────────────────────────────────────────────────────────────────
# FÁZE 1 — stahování
# ─────────────────────────────────────────────────────────────────────────────
def nacti_terminal_set(mysql_conn):
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):
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 AND r.STORNO = 'F'
ORDER BY r.datum DESC
"""
cur = fb_conn.cursor()
cur.execute(sql, [datum_od])
rows = cur.fetchall()
cur.close()
seen, unique = set(), []
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):
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><Uzivatel>{UZIVATEL}</Uzivatel><Pracoviste>{PRACOVISTE}</Pracoviste></Pristupujici>'
f'<Identifikator><ID_Dokladu>{erp_kod}</ID_Dokladu></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
_STAV_POPIS = {
"PREDEPSANY": "nevyzvednutý",
"CASTECNE_VYDANY": "částečně vydaný",
"PLNE_VYDANY": "plně vydaný",
"ZRUSENY": "zrušený",
"PRIPRAVOVANY": "připravovaný",
}
def _stav_z_xml(xml_text):
"""Vytáhne stav receptu — funguje i s namespace prefixem (<ns:Stav>)."""
for kod in _STAV_POPIS:
if f">{kod}<" in xml_text:
return _STAV_POPIS[kod]
return "?"
def _datum_z_xml(xml_text, tag):
"""Vytáhne datum z XML tagu (prvý výskyt), vrátí jen YYYY-MM-DD nebo ''."""
try:
start = xml_text.index(f"{tag}>") + len(tag) + 1
end = xml_text.index("<", start)
return xml_text[start:end].strip()[:10]
except ValueError:
return ""
def _zbyva_vydat_z_xml(xml_text):
"""Vrátí 'Nx NAZEV SILA; ...' pro léky, u nichž zbývá vydat (předepsáno vydáno > 0)."""
try:
ns = f"{{{NAMESPACE}}}"
root = ET.fromstring(xml_text)
doklad = root.find(f".//{ns}Doklad")
if doklad is None:
return ""
predepsano = {} # id_lp -> {"mnozstvi": int, "nazev": str}
for plp in doklad.findall(f"{ns}PLP"):
id_lp_el = plp.find(f"{ns}ID_LP")
mn_el = plp.find(f"{ns}Mnozstvi")
if id_lp_el is None or mn_el is None:
continue
id_lp = (id_lp_el.text or "").strip()
try:
mnozstvi = int(mn_el.text)
except (ValueError, TypeError):
continue
hvlp = plp.find(f"{ns}HVLPReg")
nazev = ""
if hvlp is not None:
n_el = hvlp.find(f"{ns}Nazev")
s_el = hvlp.find(f"{ns}Sila")
nazev_base = (n_el.text or "").strip() if n_el is not None else ""
sila = (s_el.text or "").strip() if s_el is not None else ""
nazev = f"{nazev_base} {sila}".strip()
predepsano[id_lp] = {"mnozstvi": mnozstvi, "nazev": nazev}
vydano = {} # id_lp -> int (součet přes všechny Vydej)
for vydej in doklad.findall(f"{ns}Vydej"):
for vlp in vydej.findall(f"{ns}VLP"):
id_el = vlp.find(f"{ns}IdLpErp")
mn_el = vlp.find(f"{ns}Mnozstvi")
if id_el is None or mn_el is None:
continue
id_lp = (id_el.text or "").strip()
try:
mn = int(mn_el.text)
except (ValueError, TypeError):
continue
vydano[id_lp] = vydano.get(id_lp, 0) + mn
zbytky = []
for id_lp, info in predepsano.items():
zbyva = info["mnozstvi"] - vydano.get(id_lp, 0)
if zbyva > 0:
zbytky.append(f"{zbyva}× {info['nazev']}")
return "; ".join(zbytky)
except Exception:
return ""
def faze_stazeni(datum_od, limit, out_dir, souhrn: Souhrn):
print("Připojuji MySQL...")
mysql = connect_mysql(database="medicus", cursorclass=pymysql.cursors.DictCursor)
terminal = nacti_terminal_set(mysql)
mysql.close()
print(f" Terminálních receptů v DB: {len(terminal)}\n")
print("Připojuji Firebird...")
fb = get_medicus_connection()
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")
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")
for erp in terminal:
souhrn.stazeni.append(StazeniRadek(erp_kod=erp, pacient="", stav="PRESKOCENO", detail="terminální"))
if not ke_stazeni:
print("Vše terminální, nic ke stažení.")
return
sess = Session()
sess.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_FILE, pkcs12_password=PFX_PASS))
sess.auth = (API_USER, API_PASS)
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")
kb = len(text.encode()) / 1024
stav_receptu = _stav_z_xml(text)
datum_vystaveni = _datum_z_xml(text, "DatumVystaveni")
platnost_do = _datum_z_xml(text, "PlatnostDo")
zbyva_vydat = _zbyva_vydat_z_xml(text)
print(f"OK {kb:5.1f} KB {stav_receptu} {lek_str[:40]}")
souhrn.stazeni.append(StazeniRadek(
erp_kod=erp_kod, pacient=label, stav="OK",
detail=f"{stav_receptu} | {lek_str[:50]}",
datum_vystaveni=datum_vystaveni, platnost_do=platnost_do,
zbyva_vydat=zbyva_vydat,
))
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")
souhrn.stazeni.append(StazeniRadek(erp_kod=erp_kod, pacient=label, stav="CHYBA", detail=chyba_short[:120]))
except Exception as e:
print(f"EXCEPTION {e}")
souhrn.stazeni.append(StazeniRadek(erp_kod=erp_kod, pacient=label, stav="CHYBA", detail=str(e)[:120]))
if i < len(ke_stazeni):
time.sleep(random.uniform(PAUZA_MIN, PAUZA_MAX))
# ─────────────────────────────────────────────────────────────────────────────
# FÁZE 2 — parsování
# ─────────────────────────────────────────────────────────────────────────────
NS_PARSE = f"{{{NAMESPACE}}}"
def _t(el, tag):
found = el.find(f"{NS_PARSE}{tag}")
return found.text.strip() if found is not None and found.text else None
def _ts(s):
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):
try:
root = ET.fromstring(xml_text)
doklad = root.find(f".//{NS_PARSE}Doklad")
if doklad is None:
return None
except ET.ParseError:
return None
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"))
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
pac = doklad.find(f"{NS_PARSE}Pacient")
zp_el = pac.find(f"{NS_PARSE}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
pred = doklad.find(f"{NS_PARSE}Predepisujici")
lekar_el = pred.find(f"{NS_PARSE}Lekar") if pred is not None else None
odb_el = pred.find(f"{NS_PARSE}Odbornost") if pred is not None else None
lekar_kod_raw = _t(lekar_el, "Kod") if lekar_el is not None else None
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_list = []
for plp_el in doklad.findall(f"{NS_PARSE}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):
with conn.cursor() as cur:
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)
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 faze_parsovani(dnes_str, out_dir, souhrn: Souhrn):
xml_soubory = {
xml_file.stem: xml_file
for xml_file in out_dir.glob("*.xml")
if not xml_file.stem.endswith("_CHYBA")
}
print(f"\nParsování: {len(xml_soubory)} XML souborů z {dnes_str}\n")
if not xml_soubory:
print(" Žádné XML ke zpracování.")
return
conn = connect_mysql(database="medicus", cursorclass=pymysql.cursors.DictCursor)
with conn.cursor() as cur:
cur.execute("ALTER TABLE recept_plp MODIFY uhrada VARCHAR(20)")
cur.execute("ALTER TABLE recept_doklad MODIFY pac_pohlavi VARCHAR(5)")
try:
cur.execute("ALTER TABLE recept_doklad DROP FOREIGN KEY recept_doklad_ibfk_1")
except Exception:
pass
conn.commit()
with conn.cursor() as cur:
cur.execute("SELECT id_dokladu, xml_soubor FROM recept_doklad WHERE xml_soubor IS NOT NULL")
zpracovane = {row["id_dokladu"]: row["xml_soubor"] for row in cur.fetchall()}
for i, (erp_kod, xml_file) in enumerate(xml_soubory.items(), 1):
rel_path = str(xml_file.relative_to(XML_DIR))
print(f"[{i:4d}/{len(xml_soubory)}] {erp_kod} ", end="", flush=True)
if zpracovane.get(erp_kod) == rel_path:
print("přeskočeno (beze změny)")
souhrn.parse_preskoceno += 1
continue
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í")
souhrn.parse.append(ParseRadek(erp_kod=erp_kod, stav_receptu="", terminal=0, plp=0, stav="CHYBA", detail="chyba parsování XML"))
continue
doklad, plp_list = vysledek
try:
uloz(conn, doklad, plp_list)
stav_r = doklad["stav"] or ""
term = doklad["stav_terminal"]
print(f"{stav_r:20s} terminal={term} PLP={len(plp_list)}")
souhrn.parse.append(ParseRadek(erp_kod=erp_kod, stav_receptu=stav_r, terminal=term, plp=len(plp_list), stav="OK"))
except Exception as e:
print(f"CHYBA DB {e}")
conn.rollback()
souhrn.parse.append(ParseRadek(erp_kod=erp_kod, stav_receptu="", terminal=0, plp=0, stav="CHYBA", detail=str(e)[:200]))
conn.close()
# ─────────────────────────────────────────────────────────────────────────────
# EMAIL
# ─────────────────────────────────────────────────────────────────────────────
def _barva(stav):
return {"OK": "#1a7a2a", "CHYBA": "#c0392b", "PRESKOCENO": "#888"}.get(stav, "#333")
def _badge(stav):
return f'<span style="background:{_barva(stav)};color:#fff;padding:1px 6px;border-radius:3px;font-size:0.85em">{stav}</span>'
def sestav_email(souhrn: Souhrn) -> tuple[str, str]:
stazeno_ok = [r for r in souhrn.stazeni if r.stav == "OK"]
stazeno_chyby = [r for r in souhrn.stazeni if r.stav == "CHYBA"]
parse_ok = [r for r in souhrn.parse if r.stav == "OK"]
parse_chyby = [r for r in souhrn.parse if r.stav == "CHYBA"]
ma_chybu = bool(souhrn.kriticka_chyba or stazeno_chyby or parse_chyby)
predmet = f"eRecept {souhrn.datum}{'⚠ CHYBA' if ma_chybu else 'OK'} ({len(stazeno_ok)} staženo, {len(parse_ok)} zpracováno, {souhrn.parse_preskoceno} přeskočeno)"
css = "font-family:Arial,sans-serif;font-size:14px;color:#222"
h2 = "margin:20px 0 4px;font-size:15px;border-bottom:1px solid #ddd;padding-bottom:3px"
td = "padding:3px 10px;border-bottom:1px solid #eee"
th = "padding:4px 10px;background:#f0f0f0;text-align:left;border-bottom:1px solid #ccc"
def tabulka_stazeni(radky, nadpis):
if not radky:
return ""
rows = "".join(
f"<tr><td style='{td}'>{r.erp_kod}</td>"
f"<td style='{td}'>{r.datum_vystaveni}</td>"
f"<td style='{td}'>{r.platnost_do}</td>"
f"<td style='{td}'>{r.pacient}</td>"
f"<td style='{td}'>{_badge(r.stav)}</td>"
f"<td style='{td}'>{r.detail}</td>"
f"<td style='{td}'>{r.zbyva_vydat}</td></tr>"
for r in radky
)
return (
f"<h2 style='{h2}'>{nadpis}</h2>"
f"<table style='border-collapse:collapse;width:100%'>"
f"<tr><th style='{th}'>ERP kód</th><th style='{th}'>Vystaveno</th>"
f"<th style='{th}'>Platnost do</th><th style='{th}'>Pacient</th>"
f"<th style='{th}'>Stav</th><th style='{th}'>Detail</th>"
f"<th style='{th}'>Zbývá vydat</th></tr>"
f"{rows}</table>"
)
def tabulka_parse(radky, nadpis):
if not radky:
return ""
rows = "".join(
f"<tr><td style='{td}'>{r.erp_kod}</td><td style='{td}'>{r.stav_receptu}</td>"
f"<td style='{td}'>{'ano' if r.terminal else 'ne'}</td>"
f"<td style='{td}'>{r.plp}</td>"
f"<td style='{td}'>{_badge(r.stav)}</td><td style='{td}'>{r.detail}</td></tr>"
for r in radky
)
return (
f"<h2 style='{h2}'>{nadpis}</h2>"
f"<table style='border-collapse:collapse;width:100%'>"
f"<tr><th style='{th}'>ERP kód</th><th style='{th}'>Stav receptu</th>"
f"<th style='{th}'>Terminální</th><th style='{th}'>PLP</th>"
f"<th style='{th}'>Stav</th><th style='{th}'>Detail</th></tr>"
f"{rows}</table>"
)
krit = ""
if souhrn.kriticka_chyba:
krit = (
f"<div style='background:#fdecea;border:1px solid #f5c6cb;padding:12px;margin:12px 0;border-radius:4px'>"
f"<strong>Kritická chyba:</strong><pre style='white-space:pre-wrap;margin:6px 0'>{souhrn.kriticka_chyba}</pre></div>"
)
body = (
f"<div style='{css}'>"
f"<h1 style='font-size:18px;margin-bottom:6px'>eRecept — denní souhrn {souhrn.datum}</h1>"
f"{krit}"
f"<p><strong>Stažení:</strong> {len(stazeno_ok)} OK &nbsp;|&nbsp; {len(stazeno_chyby)} chyb</p>"
f"<p><strong>Parsování:</strong> {len(parse_ok)} zpracováno &nbsp;|&nbsp; {souhrn.parse_preskoceno} přeskočeno (beze změny) &nbsp;|&nbsp; {len(parse_chyby)} chyb</p>"
+ tabulka_stazeni(stazeno_ok, "Stažené recepty")
+ tabulka_stazeni(stazeno_chyby, "Chyby při stahování")
+ tabulka_parse(parse_ok, "Zpracované recepty")
+ tabulka_parse(parse_chyby, "Chyby při parsování")
+ "</div>"
)
return predmet, body
# ─────────────────────────────────────────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--od", default=DATUM_OD_DEFAULT, help="Recepty od data (YYYY-MM-DD)")
parser.add_argument("--limit", type=int, default=None, help="Max počet receptů ke stažení")
args = parser.parse_args()
dnes = date.today().isoformat()
out_dir = XML_DIR / dnes
out_dir.mkdir(parents=True, exist_ok=True)
souhrn = Souhrn(datum=dnes)
try:
print("=" * 60)
print(f" FÁZE 1: Stahování XML ({dnes})")
print("=" * 60)
faze_stazeni(args.od, args.limit, out_dir, souhrn)
print("\n" + "=" * 60)
print(f" FÁZE 2: Parsování XML ({dnes})")
print("=" * 60)
faze_parsovani(dnes, out_dir, souhrn)
except Exception:
souhrn.kriticka_chyba = traceback.format_exc()
print(f"\nKRITICKÁ CHYBA:\n{souhrn.kriticka_chyba}", file=sys.stderr)
# ── Souhrn do terminálu ───────────────────────────────────────────────────
stazeno_ok = sum(1 for r in souhrn.stazeni if r.stav == "OK")
stazeno_chyby = sum(1 for r in souhrn.stazeni if r.stav == "CHYBA")
parse_ok = sum(1 for r in souhrn.parse if r.stav == "OK")
parse_chyby = sum(1 for r in souhrn.parse if r.stav == "CHYBA")
print(f"\n{'='*60}")
print(f" SOUHRN {dnes}")
print(f" Stažení: {stazeno_ok} OK, {stazeno_chyby} chyb")
print(f" Parsování: {parse_ok} zpracováno, {souhrn.parse_preskoceno} přeskočeno, {parse_chyby} chyb")
if souhrn.kriticka_chyba:
print(" !! Kritická chyba — viz email")
print(f"{'='*60}\n")
# ── Email ─────────────────────────────────────────────────────────────────
try:
predmet, body = sestav_email(souhrn)
send_mail(to=EMAIL_PRIJEMCE, subject=predmet, body=body, html=True)
print(f"Email odeslán: {EMAIL_PRIJEMCE}")
except Exception as e:
print(f"CHYBA odeslání emailu: {e}", file=sys.stderr)
if __name__ == "__main__":
main()