Compare commits

..

43 Commits

Author SHA1 Message Date
administrator 06b1f87107 z230 2026-05-06 13:24:43 +02:00
administrator 15f70988dc z230 2026-05-06 09:44:08 +02:00
Vladimir Buzalka 0fe37c2434 notebookvb 2026-05-06 07:19:20 +02:00
Vladimir Buzalka 7a4847e1cc notebookvb 2026-05-06 05:42:22 +02:00
Vladimir Buzalka 4f13f075ff notebookvb 2026-05-05 17:33:33 +02:00
administrator a5a4b7c349 z230 2026-05-05 12:55:31 +02:00
administrator 14accd3d78 z230 2026-05-05 09:02:04 +02:00
administrator 4112b5d3d4 z230 2026-05-05 08:38:57 +02:00
Vladimir Buzalka ffb3db1e07 notebookvb 2026-05-04 21:26:44 +02:00
administrator 417cf86b2d z230 2026-05-04 16:39:51 +02:00
administrator 194ac6c62e z230 2026-05-04 12:47:00 +02:00
administrator eed6e192f1 z230 2026-05-04 12:10:10 +02:00
administrator 804dce8794 z230 2026-05-04 08:08:13 +02:00
Vladimir Buzalka 371eed9971 notebookvb 2026-05-03 07:02:22 +02:00
Vladimir Buzalka d013e43d34 notebookvb 2026-05-03 05:51:43 +02:00
Vladimir Buzalka 88602cb406 notebookvb 2026-05-01 09:43:21 +02:00
Vladimir Buzalka 1b904e3da0 notebookvb 2026-04-30 07:09:02 +02:00
administrator 2e929f1d77 z230 2026-04-29 12:33:19 +02:00
administrator b58232b7d4 z230 2026-04-29 08:36:04 +02:00
Vladimir Buzalka daad4adeab notebookvb 2026-04-29 06:55:23 +02:00
Vladimir Buzalka a9c143ba24 notebookvb 2026-04-29 06:51:47 +02:00
Vladimir Buzalka a1b9c93506 notebookvb 2026-04-29 06:41:45 +02:00
Vladimir Buzalka 3c3a12d5a6 notebookvb 2026-04-29 06:24:11 +02:00
administrator 4aee1a05bd z230 2026-04-28 16:53:36 +02:00
administrator b1f246bc54 z230 2026-04-28 16:40:04 +02:00
Vladimir Buzalka 6cff5f1b91 notebookvb 2026-04-28 06:25:20 +02:00
Vladimir Buzalka ef5d837f34 notebookvb 2026-04-27 07:09:32 +02:00
Vladimir Buzalka 4c81529718 notebookvb 2026-04-27 07:02:24 +02:00
michaela.buzalkova c98001ae93 lenovo 2026-04-27 06:52:09 +02:00
michaela.buzalkova 4f3c774469 lenovo 2026-04-26 20:32:17 +02:00
michaela.buzalkova 7ec3fcedea lenovo 2026-04-26 20:27:57 +02:00
michaela.buzalkova 47c4789a06 lenovo 2026-04-26 15:40:48 +02:00
Vladimir Buzalka 1f9d7bbe78 notebookvb 2026-04-26 09:47:47 +02:00
Vladimir Buzalka 2447b4cf8e notebookvb 2026-04-26 08:36:18 +02:00
Vladimir Buzalka 78ed84209c notebookvb 2026-04-26 08:32:14 +02:00
michaela.buzalkova 0bfa9c48e4 lenovo 2026-04-25 12:55:21 +02:00
michaela.buzalkova 718d27aad5 fix: odstranění hardcoded cesty U:\OrdinaceProjekt z CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 09:24:58 +02:00
Vladimir Buzalka e2c61eddb9 notebookvb 2026-04-25 09:19:49 +02:00
Vladimir Buzalka 9812d48ce9 notebookvb 2026-04-25 09:18:09 +02:00
Vladimir Buzalka c29ff51209 notebookvb 2026-04-25 09:16:24 +02:00
Vladimir Buzalka add3b46223 notebookvb 2026-04-25 09:06:59 +02:00
administrator 5785ceecbc z230 2026-04-24 09:54:59 +02:00
administrator 365fcd16ba z230 2026-04-24 09:51:45 +02:00
11552 changed files with 57066 additions and 20930 deletions
+17 -1
View File
@@ -1,3 +1,19 @@
# OrdinaceProjekt
Paměť projektu je v `.claude/memory/` — přečti ji na začátku každé konverzace.
## DŮLEŽITÉ — pracovní adresář
Hlavní projekt je **adresář obsahující tento soubor CLAUDE.md** (kořen projektu OrdinaceProjekt).
Výsledné soubory (skripty, knihovny, data) vždy ukládej do tohoto kořenového adresáře nebo jeho podadresářů.
Worktree (`.claude/worktrees/*`) slouží jen pro interní práci Claude, ne jako výstup.
## Přečti na začátku každé konverzace
Každý adresář se skriptem má vlastní `NOTES.md` s technickými detaily. Přečti relevantní NOTES.md podle toho, čeho se konverzace týká.
## Přehled skriptů
| Skript | Adresář | Popis |
|--------|---------|-------|
| `stahni_str8ts.py` | `SběrDatRůzné/DailyStr8ts/` | Stahuje daily Str8ts puzzle jako PDF, odesílá emailem — viz [NOTES.md](SběrDatRůzné/DailyStr8ts/NOTES.md) |
| `10_StahnoutXML.py`, `11_ParseXML.py` | `Recepty/NačteníPředpisuWithClaude/` | Pipeline pro stahování detailů receptů z eRecept SÚKL — viz [NacistPredpis_DOKUMENTACE.md](Recepty/NačteníPředpisuWithClaude/NacistPredpis_DOKUMENTACE.md) |
+108
View File
@@ -0,0 +1,108 @@
# KdoJeLékař — poznámky k vývoji
## Cíl
Zjistit pro pacienty z Medicus DB, kdo je jejich registrující **praktický lékař (001)**, **gynekolog (002)** a **stomatolog (014)** — dotazem na VZP B2B portál.
---
## Stav k 29. 4. 2026 — hotovo
- Certifikát ✅, tabulky ✅, produkční skript ✅
- Připraveno ke spuštění — přepnout `TEST_MODE = False`
---
## Soubory v tomto adresáři
| Soubor | Popis |
|--------|-------|
| `kdojelekar_tydenni.py` | Produkční skript — batch všech pacientů, ukládá do MySQL |
| `_test_temp.py` | Testovací skript — dotaz na jedno RC, výpis XML + parsovaný výsledek |
| `_test_no_odb.py` | Test bez filtru odborností — sloužil k ověření struktury odpovědi |
---
## Certifikát
**`u:\ordinaceprojekt\Insurance\Certificates\picka.pfx`** / heslo **`Vlado7309208104+`**
Ověřeno 29. 4. 2026 (HTTP 200). Stejný certifikát používá i `StavPojisteni\zkontroluj_a_odesli_zlomy.py`.
---
## VZP B2B služba: `RegistracePojistencePZSB2B`
### Endpoint (produkce)
```
https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B
```
### Autentizace
mTLS — klientský certifikát `.pfx`, stejný mechanismus jako u `stavPojisteniB2B`.
### Struktura odpovědi
Pro každou odbornost kde má pacient lékaře vrátí jeden `<odbornost>` element.
Pokud lékař není, VZP element vynechá — skript ukládá placeholder řádek s `ma_lekare=0`.
| XML tag | Uloženo jako | Popis |
|---------|-------------|-------|
| `ICZ` | `ICZ` | IČZ zdravotnického zařízení |
| `ICP` | `ICP` | IČP lékaře |
| `nazevICP` | `nazev_lekare` | Název pracoviště |
| `nazevSZZ` | `nazev_zzz` | Jméno lékaře |
| `zdravotniPojistovna/kod` | `poj_kod` | Kód pojišťovny pacienta |
| `zdravotniPojistovna/zkratka` | `poj_zkratka` | Zkratka pojišťovny |
| `odbornost/kod` | `kod_odbornosti` | Kód odbornosti (001/002/014) |
| `datumRegistrace` | `datum_registrace` | Kdy pacient podepsal registraci |
| `datumZahajeni` | `datum_zahajeni` | Od kdy registrace platí u VZP |
| `datumUkonceni` | `datum_ukonceni` | Do kdy (3000-01-01 = bez konce) |
| `stavVyrizeniPozadavku` | `stav_vyrizeni` | Stavový kód odpovědi VZP |
**Poznámka k parsování:** VZP vrací pro každý nalezený záznam dva `<odbornost>` elementy —
vnější (s ICZ/ICP/jménem) a vnořený subelement (jen kód+název). Parser používá
`findall(".//seznamOdbornosti/odbornost")` který zachytí jen vnější.
---
## MySQL tabulky
### `vzp_registrace_lekari`
Jeden řádek na `(rc, k_datu, kod_odbornosti)`. UNIQUE klíč = `(rc, k_datu, kod_odbornosti)`.
Historie se hromadí — každý týdenní běh přidá nové řádky.
### `vzp_registrace_raw`
Jeden řádek na `(rc, k_datu)` — celé raw XML odpovědi.
Slouží k případnému přepočtu bez opakování API dotazů. UNIQUE klíč = `(rc, k_datu)`.
---
## Produkční skript `kdojelekar_tydenni.py`
### Konfigurace (začátek souboru)
| Proměnná | Výchozí | Popis |
|----------|---------|-------|
| `API_PAUSE` | `2` | Sekundy mezi VZP dotazy |
| `TEST_MODE` | `True` | False = produkční běh |
| `ODBORNOSTI` | `["001","002","014"]` | Dotazované odbornosti |
### Logika
1. Načte aktivně registrované pacienty z Medicus (přesný select dle SELECTS.md, IČP 09305001)
2. V produkčním běhu přeskočí pacienty, kteří už mají záznam v `vzp_registrace_raw` pro dnešní datum — **resumovatelný běh**
3. Pro každého pacienta zavolá VZP B2B, uloží raw XML + parsované záznamy
4. Placeholdery pro odbornosti bez lékaře ukládá s `ma_lekare=0`
### Knihovny
- `Knihovny/vzpb2b_client.py` → metody `registrace_lekare()` a `parse_registrace_lekare()`
- `Knihovny/medicus_db.py``get_active_registered_patients()` (opraveno 29. 4. 2026)
- `Knihovny/mysql_db.py``connect_mysql()`
---
## Plán dalšího postupu
1. ~~Certifikát~~ — vyřešeno, `picka.pfx` / `Vlado7309208104+`
2. ~~Ověřit funkčnost~~ — hotovo, HTTP 200 s daty
3. ~~Produkční skript~~ — hotovo, `kdojelekar_tydenni.py`
4. ~~MySQL tabulky~~ — hotovo, `vzp_registrace_lekari` + `vzp_registrace_raw`
5. Naplánovat týdenní spouštění (Windows Task Scheduler nebo Claude schedule)
6. Zvážit detekci změn lékaře (analogie zlomů u StavPojisteni) — zatím není v plánu
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys, requests
sys.stdout.reconfigure(encoding='utf-8')
from requests_pkcs12 import Pkcs12Adapter
from datetime import date
import xml.etree.ElementTree as ET
ENDPOINT = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B"
PFX_PATH = r"/Insurance/Certificates/picka.pfx"
PFX_PASS = "Vlado7309208104+"
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"rp": "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1",
}
envelope = """<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns1:registracePojistencePZSB2B xmlns:ns1="http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1">
<ns1:cisloPojistence>7309208104</ns1:cisloPojistence>
<ns1:kDatu>2026-04-29</ns1:kDatu>
</ns1:registracePojistencePZSB2B>
</soap:Body>
</soap:Envelope>"""
session = requests.Session()
session.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS))
resp = session.post(ENDPOINT, data=envelope.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": "process"},
timeout=30, verify=True)
print(f"HTTP: {resp.status_code}")
root = ET.fromstring(resp.text)
items = root.findall(".//rp:odbornost", NS)
print(f"Pocet odbornosti: {len(items)}")
for i, it in enumerate(items):
print(f"\n--- odbornost #{i+1} ---")
print(ET.tostring(it, encoding="unicode"))
st = root.find(".//rp:stavVyrizeniPozadavku", NS)
print(f"stav: {st.text if st is not None else '?'}")
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
sys.path.insert(0, r"u:\insurance")
from requests_pkcs12 import Pkcs12Adapter
import requests
import xml.etree.ElementTree as ET
from datetime import date
ENDPOINT = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/RegistracePojistencePZSB2B"
PFX_PATH = r"/Insurance/Certificates/picka.pfx"
PFX_PASS = "Vlado7309208104+"
RC = "7309208104"
K_DATU = date.today().isoformat()
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"rp": "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1",
}
envelope = f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="{NS['soap']}">
<soap:Body>
<ns1:registracePojistencePZSB2B xmlns:ns1="{NS['rp']}">
<ns1:cisloPojistence>{RC}</ns1:cisloPojistence>
<ns1:kDatu>{K_DATU}</ns1:kDatu>
</ns1:registracePojistencePZSB2B>
</soap:Body>
</soap:Envelope>"""
session = requests.Session()
session.mount("https://", Pkcs12Adapter(pkcs12_filename=PFX_PATH, pkcs12_password=PFX_PASS))
resp = session.post(ENDPOINT, data=envelope.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": "process"},
timeout=30, verify=True)
print(f"HTTP: {resp.status_code}\n")
print("=== RAW XML ===")
print(resp.text)
print("\n=== PARSED ===")
root = ET.fromstring(resp.text)
items = root.findall(".//rp:seznamOdbornosti/rp:odbornost", NS)
if not items:
st = root.find(".//rp:stavVyrizeniPozadavku", NS)
print(f"Žádné záznamy. stavVyrizeniPozadavku={st.text if st is not None else '?'}")
else:
for it in items:
def g(tag):
el = it.find(f"rp:{tag}", NS)
return el.text.strip() if el is not None and el.text else None
odb = it.find("rp:odbornost", NS)
odb_kod = odb.find("rp:kod", NS).text.strip() if odb is not None and odb.find("rp:kod", NS) is not None else None
odb_naz = odb.find("rp:nazev", NS).text.strip() if odb is not None and odb.find("rp:nazev", NS) is not None else None
print(f" odbornost: {odb_kod} {odb_naz}")
print(f" ICZ: {g('ICZ')}")
print(f" ICP: {g('ICP')}")
print(f" nazevICP: {g('nazevICP')}")
print(f" nazevSZZ: {g('nazevSZZ')}")
print(f" datumRegistrace: {g('datumRegistrace')}")
print(f" datumZahajeni: {g('datumZahajeni')}")
print(f" datumUkonceni: {g('datumUkonceni')}")
print()
st = root.find(".//rp:stavVyrizeniPozadavku", NS)
print(f"stavVyrizeniPozadavku: {st.text.strip() if st is not None and st.text else '?'}")
@@ -0,0 +1,362 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Exportuje 151 pacientů registrovaných v Medicusu k 1.1.2025,
u nichž VZP k tomuto datu nevykazuje registraci v odbornosti 001 u IČP 09305001.
Výstup: Excel s komentářem a aktuálním stavem v Medicusu.
"""
import sys
from pathlib import Path
from datetime import date
from collections import defaultdict
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from Knihovny.mysql_db import connect_mysql
import fdb, socket
from openpyxl import Workbook
from openpyxl.styles import (Font, PatternFill, Alignment, Border, Side,
GradientFill)
from openpyxl.utils import get_column_letter
# ── Konfigurace ────────────────────────────────────────────────────────────────
K_DATU_HIST = "2025-01-01"
TODAY = date.today()
OUT_FILE = Path(__file__).resolve().parent / f"neregistrovani_vzp_20250101.xlsx"
POJ_NAZVY = {
"111": "VZP",
"201": "ČPZP",
"205": "ČPZP (ex-OZP)",
"207": "OZP",
"209": "ZPŠ",
"211": "ZPMV",
"213": "RBP",
}
# ── Barvy ──────────────────────────────────────────────────────────────────────
BLUE_HEADER = "1F497D"
WHITE = "FFFFFF"
LIGHT_BLUE = "DCE6F1"
LIGHT_GREEN = "EBF1DE"
LIGHT_YELLOW = "FFFFC0"
LIGHT_RED = "FCE4D6"
LIGHT_GREY = "F2F2F2"
ORANGE = "F4B942"
# ── Data z MySQL ───────────────────────────────────────────────────────────────
mysql = connect_mysql()
cur = mysql.cursor()
cur.execute("""
SELECT rc FROM vzp_registrace_raw WHERE k_datu = %s
AND rc NOT IN (
SELECT rc FROM vzp_registrace_lekari
WHERE k_datu = %s AND kod_odbornosti = '001'
AND ICP = '09305001' AND ma_lekare = 1
)
""", (K_DATU_HIST, K_DATU_HIST))
problematicke_rcs = [row[0] for row in cur.fetchall()]
ph = ",".join(["%s"] * len(problematicke_rcs))
cur.execute(f"""
SELECT rc, prijmeni, jmeno, kod_odbornosti, ma_lekare, ICP,
nazev_lekare, nazev_zzz, poj_kod, poj_zkratka
FROM vzp_registrace_lekari
WHERE k_datu = %s AND rc IN ({ph}) AND kod_odbornosti = '001'
""", (K_DATU_HIST, *problematicke_rcs))
vzp = {}
for rc, prijmeni, jmeno, odb, ma, icp, nazev_lek, nazev_zzz, poj_kod, poj_zkr in cur.fetchall():
vzp[rc] = {"prijmeni": prijmeni or "", "jmeno": jmeno or "",
"ma_lekare": bool(ma), "ICP": icp or "",
"nazev_lekare": nazev_lek or "", "nazev_zzz": nazev_zzz or "",
"poj_kod": poj_kod or "", "poj_zkratka": poj_zkr or ""}
mysql.close()
# ── Data z Medicusu ────────────────────────────────────────────────────────────
computer_name = socket.gethostname().upper()
dsn_map = {
"LEKAR": r"localhost:M:\medicus\data\medicus.fdb",
"SESTRA": r"192.168.1.10:m:\medicus\data\medicus.fdb",
"LENOVO": r"192.168.1.10:m:\medicus\data\medicus.fdb",
}
dsn = dsn_map.get(computer_name, r"localhost:c:\medicus 3\data\medicus.fdb")
fb_conn = fdb.connect(dsn=dsn, user="SYSDBA", password="masterkey", charset="win1250")
fb_cur = fb_conn.cursor()
# Aktuálně aktivní pacienti
fb_cur.execute("""
SELECT kar.rodcis FROM kar
WHERE kar.vyrazen = 'N' AND kar.rodcis IS NOT NULL AND kar.rodcis <> ''
AND EXISTS (
SELECT 1 FROM registr r JOIN icp i ON r.idicp = i.idicp
WHERE r.idpac = kar.idpac
AND r.datum <= CURRENT_DATE
AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= CURRENT_DATE)
AND r.priznak IN ('V','D','A')
AND i.icp = '09305001' AND i.odb = '001'
)
""")
aktualne_aktivni = {(row[0] or "").strip() for row in fb_cur.fetchall()}
# Detail všech 151 pacientů
ph_fb = ",".join(["?" for _ in problematicke_rcs])
fb_cur.execute(f"""
SELECT kar.rodcis, kar.prijmeni, kar.jmeno, kar.poj, kar.vyrazen,
r.datum, r.datum_zruseni, r.priznak
FROM kar
LEFT JOIN registr r ON r.idpac = kar.idpac
LEFT JOIN icp i ON r.idicp = i.idicp AND i.icp = '09305001' AND i.odb = '001'
WHERE kar.rodcis IN ({ph_fb})
ORDER BY kar.rodcis, r.datum DESC
""", problematicke_rcs)
medicus = {}
for row in fb_cur.fetchall():
rc = (row[0] or "").strip()
if rc not in medicus:
medicus[rc] = {
"prijmeni": (row[1] or "").strip(),
"jmeno": (row[2] or "").strip(),
"poj": str(row[3] or "").strip(),
"vyrazen": (row[4] or "").strip(),
"reg_datum": row[5],
"reg_datum_zruseni":row[6],
"reg_priznak": (row[7] or "").strip(),
}
fb_conn.close()
# ── Kategorizace ───────────────────────────────────────────────────────────────
def kategorie(rc, vzp_row, med_row, aktivni_set):
poj = med_row.get("poj", "") if med_row else ""
if not vzp_row:
if poj != "111":
return "JINÁ POJIŠŤOVNA", "VZP neregistruje — pojištěnec jiné pojišťovny.", LIGHT_BLUE
return "BEZ VZP ZÁZNAMU", "VZP nevrátila žádný záznam přesto, že jde o pojištěnce VZP. Pravděpodobně chybí registrace u VZP.", LIGHT_RED
if vzp_row["ma_lekare"]:
return "REGISTROVÁN JINDE", f"VZP eviduje registraci u jiného lékaře: {vzp_row['nazev_zzz']} (ICP {vzp_row['ICP']}).", LIGHT_RED
return "BEZ LÉKAŘE U VZP", "VZP eviduje pojištěnce, ale bez registrujícího lékaře v odbornosti 001.", LIGHT_YELLOW
# ── Sestavení řádků ────────────────────────────────────────────────────────────
rows = []
for rc in problematicke_rcs:
vzp_row = vzp.get(rc)
med_row = medicus.get(rc)
aktivni = rc in aktualne_aktivni
prijmeni = (med_row or vzp_row or {}).get("prijmeni", "")
jmeno = (med_row or vzp_row or {}).get("jmeno", "")
poj_kod = med_row.get("poj", "") if med_row else (vzp_row or {}).get("poj_kod", "")
poj_nazev = POJ_NAZVY.get(poj_kod, poj_kod)
med_stav = "Aktivní" if aktivni else ("Odregistrován" if med_row else "Nenalezen v Medicusu")
reg_datum = med_row.get("reg_datum") if med_row else None
reg_datum_zrus = med_row.get("reg_datum_zruseni") if med_row else None
kat, komentar, barva = kategorie(rc, vzp_row, med_row, aktualne_aktivni)
vzp_icp = vzp_row["ICP"] if vzp_row and vzp_row["ma_lekare"] else ""
vzp_lek = vzp_row["nazev_zzz"] if vzp_row and vzp_row["ma_lekare"] else ""
vzp_zzz = vzp_row["nazev_lekare"] if vzp_row and vzp_row["ma_lekare"] else ""
rows.append({
"prijmeni": prijmeni,
"jmeno": jmeno,
"rc": rc,
"poj_kod": poj_kod,
"poj_nazev": poj_nazev,
"med_stav": med_stav,
"reg_datum": reg_datum.strftime("%d.%m.%Y") if reg_datum else "",
"reg_zruseni": reg_datum_zrus.strftime("%d.%m.%Y") if reg_datum_zrus else "",
"kategorie": kat,
"komentar": komentar,
"vzp_icp": vzp_icp,
"vzp_lek": vzp_lek,
"vzp_zzz": vzp_zzz,
"barva": barva,
})
rows.sort(key=lambda r: (r["kategorie"], r["prijmeni"], r["jmeno"]))
# ── Excel ──────────────────────────────────────────────────────────────────────
wb = Workbook()
# ────────────────────────────────────────────────────────────────────────────────
# SHEET 1: Přehled
# ────────────────────────────────────────────────────────────────────────────────
ws_info = wb.active
ws_info.title = "Přehled"
def hdr_cell(ws, row, col, value):
c = ws.cell(row=row, column=col, value=value)
c.font = Font(name="Arial", bold=True, color=WHITE, size=11)
c.fill = PatternFill("solid", fgColor=BLUE_HEADER)
c.alignment = Alignment(horizontal="center", vertical="center")
return c
def val_cell(ws, row, col, value, bold=False, bg=None):
c = ws.cell(row=row, column=col, value=value)
c.font = Font(name="Arial", bold=bold, size=10)
c.alignment = Alignment(wrap_text=True, vertical="top")
if bg:
c.fill = PatternFill("solid", fgColor=bg)
return c
ws_info.column_dimensions["A"].width = 36
ws_info.column_dimensions["B"].width = 18
ws_info.column_dimensions["C"].width = 60
# Titulek
ws_info.merge_cells("A1:C1")
t = ws_info["A1"]
t.value = f"Pacienti registrovaní v Medicusu k 1. 1. 2025, ale dle VZP bez registrace u IČP 09305001"
t.font = Font(name="Arial", bold=True, size=13, color=WHITE)
t.fill = PatternFill("solid", fgColor=BLUE_HEADER)
t.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
ws_info.row_dimensions[1].height = 36
ws_info.merge_cells("A2:C2")
ws_info["A2"].value = f"Vygenerováno: {TODAY.strftime('%d. %m. %Y')} | Stav v Medicusu k dnešnímu dni"
ws_info["A2"].font = Font(name="Arial", italic=True, size=9, color="595959")
ws_info["A2"].alignment = Alignment(horizontal="center")
# Souhrn počtů
counts = defaultdict(int)
counts_aktivni = defaultdict(int)
for r in rows:
counts[r["kategorie"]] += 1
if r["med_stav"] == "Aktivní":
counts_aktivni[r["kategorie"]] += 1
hdr_cell(ws_info, 4, 1, "Kategorie")
hdr_cell(ws_info, 4, 2, "Počet pacientů")
hdr_cell(ws_info, 4, 3, "Komentář")
KAT_BARVY = {
"REGISTROVÁN JINDE": LIGHT_RED,
"BEZ LÉKAŘE U VZP": LIGHT_YELLOW,
"JINÁ POJIŠŤOVNA": LIGHT_BLUE,
"BEZ VZP ZÁZNAMU": LIGHT_RED,
}
KAT_POPIS = {
"REGISTROVÁN JINDE": "VZP k 1.1.2025 eviduje registraci u jiného praktického lékaře. Pacient se pravděpodobně přeregistroval jinam, aniž by byl v Medicusu odregistrován.",
"BEZ LÉKAŘE U VZP": "VZP eviduje pojištěnce, ale v odbornosti 001 mu neeviduje žádného lékaře. Může jít o opožděné zpracování přihlášky nebo technickou chybu.",
"JINÁ POJIŠŤOVNA": "Pacient není pojištěncem VZP — VZP o něm data nemá, proto nebylo vráceno nic. To je očekávané chování.",
"BEZ VZP ZÁZNAMU": "VZP pojištěnec (111), ale VZP nevrátila žádný záznam. Může jít o nesprávné RC, neaktivní pojistný vztah nebo chybu v komunikaci.",
}
for i, kat in enumerate(["REGISTROVÁN JINDE", "BEZ LÉKAŘE U VZP", "JINÁ POJIŠŤOVNA", "BEZ VZP ZÁZNAMU"]):
r = 5 + i
bg = KAT_BARVY[kat]
val_cell(ws_info, r, 1, kat, bold=True, bg=bg)
val_cell(ws_info, r, 2, f"{counts[kat]} ({counts_aktivni[kat]} stále aktivní)", bg=bg)
val_cell(ws_info, r, 3, KAT_POPIS[kat], bg=bg)
ws_info.row_dimensions[r].height = 42
ws_info.row_dimensions[4].height = 20
# Celkem
val_cell(ws_info, 10, 1, "CELKEM", bold=True)
val_cell(ws_info, 10, 2, f"{len(rows)} ({sum(counts_aktivni.values())} aktivní)", bold=True)
# Metodika
ws_info.merge_cells("A12:C12")
ws_info["A12"].value = "Metodika"
ws_info["A12"].font = Font(name="Arial", bold=True, size=11, color=BLUE_HEADER)
metodika_text = (
"Skript kdojelekar_20250101.py dotázal VZP B2B na registrujícího lékaře (odbornost 001) "
"pro každého pacienta registrovaného k 1. 1. 2025 v Medicusu u IČP 09305001. "
"Pacienti v tomto souboru jsou ti, u nichž VZP k danému datu nevrátila záznam s ICP=09305001 a ma_lekare=1. "
"Stav v Medicusu je aktuální k dnešnímu dni (" + TODAY.strftime("%d. %m. %Y") + ")."
)
ws_info.merge_cells("A13:C13")
c = ws_info["A13"]
c.value = metodika_text
c.font = Font(name="Arial", size=9, color="595959")
c.alignment = Alignment(wrap_text=True, vertical="top")
ws_info.row_dimensions[13].height = 56
# Doporučení
ws_info.merge_cells("A15:C15")
ws_info["A15"].value = "Doporučení"
ws_info["A15"].font = Font(name="Arial", bold=True, size=11, color=BLUE_HEADER)
ws_info.merge_cells("A16:C16")
c = ws_info["A16"]
c.value = (
"1. REGISTROVÁN JINDE — ověřit s pacientem při návštěvě, zda se přeregistroval; pokud ano, odregistrovat v Medicusu.\n"
"2. BEZ LÉKAŘE U VZP — zkontrolovat, zda přihláška registrace byla správně odeslána a VZP ji eviduje.\n"
"3. JINÁ POJIŠŤOVNA — tyto pacienty prověřit u příslušné pojišťovny (ČPZP, OZP…) analogickým dotazem.\n"
"4. BEZ VZP ZÁZNAMU — ověřit správnost RC a aktivitu pojistného vztahu přímo u VZP."
)
c.font = Font(name="Arial", size=10)
c.alignment = Alignment(wrap_text=True, vertical="top")
ws_info.row_dimensions[16].height = 80
# ────────────────────────────────────────────────────────────────────────────────
# SHEET 2: Data
# ────────────────────────────────────────────────────────────────────────────────
ws = wb.create_sheet("Pacienti")
COLS = [
("Příjmení", 20),
("Jméno", 14),
("Rodné číslo", 14),
("Pojišťovna", 12),
("Stav v Medicusu\ndnes", 16),
("Datum registrace\nv Medicusu", 16),
("Datum zrušení\nv Medicusu", 16),
("Kategorie VZP problému", 22),
("Komentář", 52),
("VZP ICP jiného lékaře", 18),
("VZP — jméno lékaře", 28),
("VZP — název ZZZ", 36),
]
for col_idx, (header, width) in enumerate(COLS, 1):
c = ws.cell(row=1, column=col_idx, value=header)
c.font = Font(name="Arial", bold=True, color=WHITE, size=10)
c.fill = PatternFill("solid", fgColor=BLUE_HEADER)
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
ws.column_dimensions[get_column_letter(col_idx)].width = width
ws.row_dimensions[1].height = 32
ws.freeze_panes = "A2"
thin = Side(style="thin", color="BFBFBF")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
for row_idx, r in enumerate(rows, 2):
bg = r["barva"]
data = [
r["prijmeni"], r["jmeno"], r["rc"], f"{r['poj_kod']} {r['poj_nazev']}",
r["med_stav"], r["reg_datum"], r["reg_zruseni"],
r["kategorie"], r["komentar"],
r["vzp_icp"], r["vzp_lek"], r["vzp_zzz"],
]
for col_idx, value in enumerate(data, 1):
c = ws.cell(row=row_idx, column=col_idx, value=value)
c.font = Font(name="Arial", size=9)
c.fill = PatternFill("solid", fgColor=bg)
c.border = border
c.alignment = Alignment(vertical="top", wrap_text=(col_idx in (9, 11, 12)))
if r["med_stav"] == "Aktivní" and col_idx == 5:
c.font = Font(name="Arial", size=9, bold=True, color="375623")
elif r["med_stav"] != "Aktivní" and col_idx == 5:
c.font = Font(name="Arial", size=9, color="843C0C")
ws.row_dimensions[row_idx].height = 32
# AutoFilter
ws.auto_filter.ref = f"A1:{get_column_letter(len(COLS))}1"
wb.save(OUT_FILE)
print(f"Uloženo: {OUT_FILE}")
print(f"Celkem řádků: {len(rows)}")
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
kdojelekar_20250101.py
======================
Jednorázový skript: vybere pacienty registrované k 01.01.2025
a dotáže se VZP B2B na jejich registrujícího lékaře k tomuto datu.
Výsledky uloží do stejných MySQL tabulek jako týdenní skript
(vzp_registrace_lekari, vzp_registrace_raw) s k_datu = 2025-01-01.
Resumovatelný — přeskočí pacienty, kteří již mají raw XML pro k_datu=2025-01-01.
"""
import sys
import time
from pathlib import Path
from datetime import date
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from Knihovny.vzpb2b_client import VZPB2BClient
from Knihovny.mysql_db import connect_mysql
from Knihovny.medicus_db import get_medicus_connection
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
K_DATU = date(2025, 1, 1)
API_PAUSE = 2
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASS = "Vlado7309208104+"
ODBORNOSTI = None # None = bez filtru odborností
# ── INIT ──────────────────────────────────────────────────────────────────────
vzp = VZPB2BClient("prod", str(PFX_PATH), PFX_PASS)
mysql = connect_mysql()
# ── PACIENTI Z MEDICUS (registrovaní k 01.01.2025) ───────────────────────────
import fdb, socket
computer_name = socket.gethostname().upper()
dsn_map = {
"LEKAR": r"localhost:M:\medicus\data\medicus.fdb",
"SESTRA": r"192.168.1.10:m:\medicus\data\medicus.fdb",
"LENOVO": r"192.168.1.10:m:\medicus\data\medicus.fdb",
}
dsn = dsn_map.get(computer_name, r"localhost:c:\medicus 3\data\medicus.fdb")
fb_conn = fdb.connect(dsn=dsn, user="SYSDBA", password="masterkey", charset="win1250")
fb_cur = fb_conn.cursor()
fb_cur.execute("""
SELECT kar.rodcis, kar.prijmeni, kar.jmeno, kar.poj
FROM kar
WHERE kar.vyrazen = 'N'
AND kar.rodcis IS NOT NULL
AND kar.rodcis <> ''
AND EXISTS (
SELECT r.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 = '09305001'
AND i.odb = '001'
)
ORDER BY kar.prijmeni, kar.rodcis
""", (K_DATU.isoformat(), K_DATU.isoformat()))
cols = [d[0].strip().lower() for d in fb_cur.description]
pacienti = [dict(zip(cols, row)) for row in fb_cur.fetchall()]
fb_conn.close()
print(f"Pacientu registrovanych k {K_DATU}: {len(pacienti)}")
# ── RESUME: přeskočit již hotové ─────────────────────────────────────────────
with mysql.cursor() as cur:
cur.execute("SELECT rc FROM vzp_registrace_raw WHERE k_datu = %s", (K_DATU,))
hotove = {row[0] for row in cur.fetchall()}
pacienti = [p for p in pacienti if (p.get("rodcis") or "").strip() not in hotove]
print(f"Zbyvá zpracovat: {len(pacienti)} ({len(hotove)} již hotovo)\n")
# ── BATCH ─────────────────────────────────────────────────────────────────────
call_count = 0
for i, pac in enumerate(pacienti):
rc = (pac.get("rodcis") or "").strip()
prijmeni = (pac.get("prijmeni") or "").strip()
jmeno = (pac.get("jmeno") or "").strip()
if not rc:
continue
if call_count > 0:
time.sleep(API_PAUSE)
call_count += 1
print(f"[{i+1}/{len(pacienti)}] {prijmeni} {jmeno} ({rc}) ...", end=" ", flush=True)
try:
xml = vzp.registrace_lekare(rc=rc, k_datu=K_DATU.isoformat(), odbornosti=ODBORNOSTI)
zaznamy = vzp.parse_registrace_lekare(xml)
except Exception as e:
print(f"CHYBA: {e}")
continue
print(f"{len(zaznamy)} lekar(u)")
for z in zaznamy:
print(f" {z['kod_odbornosti']}: {z['nazev_lekare']} / {z['nazev_zzz']}"
f" [{z['datum_zahajeni']} - {z['datum_ukonceni']}]")
if not zaznamy:
print(" (zadny lekar v zadne odbornosti)")
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_registrace_raw (rc, k_datu, raw_xml)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE raw_xml=VALUES(raw_xml)
""", (rc, K_DATU, xml))
with mysql.cursor() as cur:
for z in zaznamy:
cur.execute("""
INSERT INTO vzp_registrace_lekari
(rc, prijmeni, jmeno, k_datu, kod_odbornosti, ma_lekare,
ICZ, ICP, nazev_lekare, nazev_zzz,
poj_kod, poj_zkratka,
datum_registrace, datum_zahajeni, datum_ukonceni,
stav_vyrizeni)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
prijmeni=VALUES(prijmeni), jmeno=VALUES(jmeno),
ma_lekare=VALUES(ma_lekare),
ICZ=VALUES(ICZ), ICP=VALUES(ICP),
nazev_lekare=VALUES(nazev_lekare), nazev_zzz=VALUES(nazev_zzz),
poj_kod=VALUES(poj_kod), poj_zkratka=VALUES(poj_zkratka),
datum_registrace=VALUES(datum_registrace),
datum_zahajeni=VALUES(datum_zahajeni),
datum_ukonceni=VALUES(datum_ukonceni),
stav_vyrizeni=VALUES(stav_vyrizeni)
""", (
rc, prijmeni, jmeno, K_DATU, z["kod_odbornosti"], 1 if z["ma_lekare"] else 0,
z["ICZ"], z["ICP"], z["nazev_lekare"], z["nazev_zzz"],
z["poj_kod"], z["poj_zkratka"],
z["datum_registrace"], z["datum_zahajeni"], z["datum_ukonceni"],
z["stav_vyrizeni"],
))
print(f"\nHotovo. VZP dotazu: {call_count}")
mysql.close()
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
kdojelekar_tydenni.py
======================
Projde aktivně registrované pacienty z Medicus a pro každého zjistí
registrujícího lékaře u VZP (odbornosti 001, 002, 014).
Výsledky uloží do MySQL tabulky vzp_registrace_lekari.
Spouštět týdně. Mezi dotazy 2s prodleva (API_DELAY).
TEST_MODE = True → zpracuje jen 3 náhodné pacienty, bez zápisu do DB.
TEST_MODE = False → produkční běh, zapíše vše.
"""
import sys
import time
import random
from pathlib import Path
from datetime import date
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from Knihovny.vzpb2b_client import VZPB2BClient
from Knihovny.mysql_db import connect_mysql
from Knihovny.medicus_db import get_medicus_db
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
API_PAUSE = 2 # sekundy mezi VZP dotazy
TEST_MODE = False # False = produkční běh
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASS = "Vlado7309208104+"
ODBORNOSTI = None # None = bez filtru, VZP vrátí všechny odbornosti
TODAY = date.today()
# ── INIT ──────────────────────────────────────────────────────────────────────
vzp = VZPB2BClient("prod", str(PFX_PATH), PFX_PASS)
mysql = connect_mysql()
# ── PACIENTI Z MEDICUS ────────────────────────────────────────────────────────
medicus = get_medicus_db()
pacienti = medicus.get_active_registered_patients(as_dict=True)
medicus.close()
print(f"Aktivně registrovaných pacientů: {len(pacienti)}")
if TEST_MODE:
pacienti = random.sample(pacienti, min(3, len(pacienti)))
print(f"TEST MODE — zpracuji {len(pacienti)} náhodné pacienty, BEZ zápisu do DB\n")
else:
# Načti RC která už dnes mají uložený raw XML — ty přeskočíme
with mysql.cursor() as cur:
cur.execute("SELECT rc FROM vzp_registrace_raw WHERE k_datu = %s", (TODAY,))
hotove = {row[0] for row in cur.fetchall()}
pacienti = [p for p in pacienti if (p.get("rodcis") or "").strip() not in hotove]
print(f"PRODUKČNÍ běh — k_datu={TODAY}, zbývá zpracovat: {len(pacienti)} pacientů"
f" ({len(hotove)} již hotovo dnes)\n")
# ── BATCH ─────────────────────────────────────────────────────────────────────
call_count = 0
for i, pac in enumerate(pacienti):
rc = (pac.get("rodcis") or "").strip()
prijmeni = (pac.get("prijmeni") or "").strip()
jmeno = (pac.get("jmeno") or "").strip()
if not rc:
continue
if call_count > 0:
time.sleep(API_PAUSE)
call_count += 1
print(f"[{i+1}/{len(pacienti)}] {prijmeni} {jmeno} ({rc}) ...", end=" ", flush=True)
try:
xml = vzp.registrace_lekare(rc=rc, k_datu=TODAY.isoformat(), odbornosti=ODBORNOSTI)
zaznamy = vzp.parse_registrace_lekare(xml)
except Exception as e:
print(f"CHYBA: {e}")
continue
print(f"{len(zaznamy)} lékař(ů)")
for z in zaznamy:
print(f" {z['kod_odbornosti']}: {z['nazev_lekare']} / {z['nazev_zzz']}"
f" [{z['datum_zahajeni']} {z['datum_ukonceni']}]")
if not zaznamy:
print(f" (žádný lékař v žádné odbornosti)")
if TEST_MODE:
continue
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_registrace_raw (rc, k_datu, raw_xml)
VALUES (%s, %s, %s)
ON DUPLICATE KEY UPDATE raw_xml=VALUES(raw_xml)
""", (rc, TODAY, xml))
with mysql.cursor() as cur:
for z in zaznamy:
cur.execute("""
INSERT INTO vzp_registrace_lekari
(rc, prijmeni, jmeno, k_datu, kod_odbornosti, ma_lekare,
ICZ, ICP, nazev_lekare, nazev_zzz,
poj_kod, poj_zkratka,
datum_registrace, datum_zahajeni, datum_ukonceni,
stav_vyrizeni)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
prijmeni=VALUES(prijmeni), jmeno=VALUES(jmeno),
ma_lekare=VALUES(ma_lekare),
ICZ=VALUES(ICZ), ICP=VALUES(ICP),
nazev_lekare=VALUES(nazev_lekare), nazev_zzz=VALUES(nazev_zzz),
poj_kod=VALUES(poj_kod), poj_zkratka=VALUES(poj_zkratka),
datum_registrace=VALUES(datum_registrace),
datum_zahajeni=VALUES(datum_zahajeni),
datum_ukonceni=VALUES(datum_ukonceni),
stav_vyrizeni=VALUES(stav_vyrizeni)
""", (
rc, prijmeni, jmeno, TODAY, z["kod_odbornosti"], 1 if z["ma_lekare"] else 0,
z["ICZ"], z["ICP"], z["nazev_lekare"], z["nazev_zzz"],
z["poj_kod"], z["poj_zkratka"],
z["datum_registrace"], z["datum_zahajeni"], z["datum_ukonceni"],
z["stav_vyrizeni"],
))
print(f"\nHotovo. VZP dotazů: {call_count}")
if TEST_MODE:
print("(TEST MODE — nic nebylo zapsáno do DB)")
mysql.close()
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
01_parse_seznam.py
==================
Najde a rozparsuje dávky VZP Seznam registrovaných pojištěnců (III-1.1.2).
Formát souboru: Fxxxmmur.nnn, pevná šířka, kódování CP852.
Záhlaví (typ H, délka 20):
HTYP C 1 0 typ věty 'H'
HICP N 8 1 IČP lékaře
HPUP N 5 9 počet uznávaných pojištěnců
HROK N 2 14 poslední dvojčíslí roku
HMES N 2 16 měsíc
HDEN N 2 18 den
Registrace (typ I, délka 82):
ITYP C 1 0 typ věty 'I'
IPOR N 4 1 pořadové číslo
IVS N 2 5 věková skupina
IPRI C 30 7 příjmení
IJME C 24 37 jméno
ICIP C 10 61 číslo pojištěnce
IDUOD D 8 71 datum uznání registrace (DDMMRRRR)
ICPO C 3 79 kód pojišťovny
"""
import re
from pathlib import Path
from datetime import date, timedelta
DAVKY_DIR = Path(r"U:\Dropbox\Ordinace\Dokumentace_ke_zpracování\Zúčtovací zprávy\111 VZP Podání")
ENCODING = "cp852"
def parse_davku(path: Path) -> dict:
"""Vrátí dict s klíči 'hlavicka' a 'pojistenci'."""
lines = path.read_bytes().splitlines()
hlavicka = None
pojistenci = []
for raw in lines:
line = raw.decode(ENCODING, errors="replace")
if not line:
continue
if line[0] == "H":
hlavicka = {
"icp": line[1:9].strip(),
"pocet": int(line[9:14].strip() or 0),
"rok": 2000 + int(line[14:16]),
"mesic": int(line[16:18]),
"den": int(line[18:20]),
}
elif line[0] == "I":
if len(line) < 82:
continue
dat_raw = line[71:79] # DDMMRRRR
try:
datum = date(int(dat_raw[4:8]), int(dat_raw[2:4]), int(dat_raw[0:2]))
except ValueError:
datum = None
pojistenci.append({
"por": int(line[1:5].strip() or 0),
"vs": line[5:7].strip(),
"prijmeni": line[7:37].strip(),
"jmeno": line[37:61].strip(),
"cip": line[61:71].strip(),
"datum_od": datum,
"pojistovna": line[79:82].strip(),
})
return {"hlavicka": hlavicka, "pojistenci": pojistenci, "soubor": path.name}
def najdi_davky(adresar: Path) -> list[Path]:
"""Vrátí seřazený seznam souborů odpovídajících vzoru Fxxx*.nnn."""
return sorted(
[p for p in adresar.iterdir()
if re.search(r"F\d{3}.*\.\d{3}$", p.name, re.IGNORECASE)],
key=lambda p: p.name
)
def tiskni_davku(d: dict) -> None:
h = d["hlavicka"]
if h:
print(f"\n=== {d['soubor']} ===")
print(f" IČP: {h['icp']} | datum: {h['den']:02d}.{h['mesic']:02d}.{h['rok']} | pojištěnců: {h['pocet']}")
print(f" {'#':>4} {'Příjmení':<30} {'Jméno':<24} {'ČIP':<10} {'Datum od':>10} Pojiš.")
print(f" {'-'*4} {'-'*30} {'-'*24} {'-'*10} {'-'*10} {'-'*6}")
else:
print(f"\n=== {d['soubor']} === (záhlaví chybí)")
for p in d["pojistenci"]:
datum_str = p["datum_od"].strftime("%d.%m.%Y") if p["datum_od"] else "?"
print(f" {p['por']:>4} {p['prijmeni']:<30} {p['jmeno']:<24} {p['cip']:<10} {datum_str:>10} {p['pojistovna']}")
# ── MAIN ──────────────────────────────────────────────────────────────────────
davky = najdi_davky(DAVKY_DIR)
print(f"Nalezeno dávek: {len(davky)}")
for cesta in davky:
data = parse_davku(cesta)
tiskni_davku(data)
print(f"\nCelkem dávek: {len(davky)}")
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
02_import_do_db.py
==================
Vytvoří tabulku seznam_pojistencu_davky v medevio DB a naimportuje
všechny dávky F111*.nnn ze složky Podání.
Spuštění:
python 02_import_do_db.py # import všech dávek
python 02_import_do_db.py --reset # smaže a znovu vytvoří tabulku před importem
"""
import re
import sys
import argparse
from pathlib import Path
from datetime import date
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny"))
from mysql_db import connect_mysql
DAVKY_DIR = Path(r"U:\Dropbox\Ordinace\Dokumentace_ke_zpracování\Zúčtovací zprávy\111 VZP Podání")
ENCODING = "cp852"
CREATE_SQL = """
CREATE TABLE IF NOT EXISTS seznam_pojistencu_davky (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
soubor VARCHAR(60) NOT NULL COMMENT 'Název souboru dávky',
icp VARCHAR(8) NOT NULL COMMENT 'IČP lékaře (HICP)',
davka_rok SMALLINT NOT NULL COMMENT 'Rok dávky (HROK)',
davka_mesic TINYINT NOT NULL COMMENT 'Měsíc dávky (HMES)',
davka_den TINYINT NOT NULL COMMENT 'Den pořízení seznamu (HDEN)',
por SMALLINT NOT NULL COMMENT 'Pořadové číslo v dávce (IPOR)',
vs VARCHAR(2) NOT NULL COMMENT 'Věková skupina (IVS)',
prijmeni VARCHAR(30) NOT NULL COMMENT 'Příjmení pojištěnce (IPRI)',
jmeno VARCHAR(24) NOT NULL COMMENT 'Jméno pojištěnce (IJME)',
cip VARCHAR(10) NOT NULL COMMENT 'Číslo pojištěnce (ICIP)',
datum_od DATE NULL COMMENT 'Datum uznání registrace (IDUOD)',
pojistovna VARCHAR(3) NOT NULL COMMENT 'Kód pojišťovny (ICPO)',
vytvoreno TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_soubor_por (soubor, por)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_czech_ci
COMMENT='VZP seznam registrovaných pojištěnců III-1.1.2';
"""
def parse_davku(path: Path) -> dict:
lines = path.read_bytes().splitlines()
hlavicka = None
pojistenci = []
for raw in lines:
line = raw.decode(ENCODING, errors="replace")
if not line:
continue
if line[0] == "H":
hlavicka = {
"icp": line[1:9].strip(),
"pocet": int(line[9:14].strip() or 0),
"rok": 2000 + int(line[14:16]),
"mesic": int(line[16:18]),
"den": int(line[18:20]),
}
elif line[0] == "I":
if len(line) < 82:
continue
dat_raw = line[71:79] # DDMMRRRR
try:
datum = date(int(dat_raw[4:8]), int(dat_raw[2:4]), int(dat_raw[0:2]))
except ValueError:
datum = None
pojistenci.append({
"por": int(line[1:5].strip() or 0),
"vs": line[5:7].strip(),
"prijmeni": line[7:37].strip(),
"jmeno": line[37:61].strip(),
"cip": line[61:71].strip(),
"datum_od": datum,
"pojistovna": line[79:82].strip(),
})
return {"hlavicka": hlavicka, "pojistenci": pojistenci, "soubor": path.name}
def najdi_davky(adresar: Path) -> list[Path]:
return sorted(
[p for p in adresar.iterdir()
if re.search(r"F\d{3}.*\.\d{3}$", p.name, re.IGNORECASE)],
key=lambda p: p.name
)
def import_davku(cur, davka: dict) -> tuple[int, int]:
"""Vrátí (vloženo, přeskočeno duplicit)."""
h = davka["hlavicka"]
if not h:
print(f" SKIP {davka['soubor']} — chybí záhlaví")
return 0, 0
rows = [
(davka["soubor"], h["icp"], h["rok"], h["mesic"], h["den"],
p["por"], p["vs"], p["prijmeni"], p["jmeno"],
p["cip"], p["datum_od"], p["pojistovna"])
for p in davka["pojistenci"]
]
# INSERT IGNORE přeskočí duplicity (unikátní klíč soubor+por) bez chyby
affected = cur.executemany(
"""INSERT IGNORE INTO seznam_pojistencu_davky
(soubor, icp, davka_rok, davka_mesic, davka_den,
por, vs, prijmeni, jmeno, cip, datum_od, pojistovna)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
rows
)
vlozeno = affected if affected else 0
preskoceno = len(rows) - vlozeno
return vlozeno, preskoceno
# ── MAIN ──────────────────────────────────────────────────────────────────────
parser = argparse.ArgumentParser(description="Import VZP dávek do MySQL")
parser.add_argument("--reset", action="store_true",
help="Smaže a znovu vytvoří tabulku před importem")
args = parser.parse_args()
conn = connect_mysql()
cur = conn.cursor()
if args.reset:
print("DROP TABLE seznam_pojistencu_davky ...")
cur.execute("DROP TABLE IF EXISTS seznam_pojistencu_davky")
print("Vytváření tabulky (pokud neexistuje) ...")
cur.execute(CREATE_SQL)
davky = najdi_davky(DAVKY_DIR)
print(f"Nalezeno dávek: {len(davky)}\n")
celkem_vlozeno = celkem_preskoceno = 0
for cesta in davky:
davka = parse_davku(cesta)
h = davka["hlavicka"]
if h:
print(f"{davka['soubor']} ({h['den']:02d}.{h['mesic']:02d}.{h['rok']}, {len(davka['pojistenci'])} záznamů)")
else:
print(f"{davka['soubor']} (záhlaví chybí)")
vlozeno, preskoceno = import_davku(cur, davka)
celkem_vlozeno += vlozeno
celkem_preskoceno += preskoceno
print(f" → vloženo: {vlozeno}, duplicit přeskočeno: {preskoceno}")
cur.close()
conn.close()
print(f"\nHotovo. Celkem vloženo: {celkem_vlozeno}, přeskočeno: {celkem_preskoceno}")
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
03_porovnani_davek.py
=====================
Porovná všechny po sobě jdoucí dávky VZP a vytvoří Excel se třemi listy:
1. Přehled souhrnná tabulka přechodů (kolik odešlo / přibylo)
2. Odešli detail všech, kdo v dané dávce chyběli oproti předchozí
3. Přibylo detail všech, kdo v dané dávce přibyly oproti předchozí
Pro data s více soubory se použijí unikátní CIPy (dávky jsou totožné).
"""
import sys
from datetime import date
from pathlib import Path
from collections import defaultdict
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny"))
from mysql_db import connect_mysql
OUTPUT = Path(__file__).parent / "porovnani_davek.xlsx"
# ── Barvy ─────────────────────────────────────────────────────────────────────
CLR_HEADER = "1F4E79" # tmavě modrá
CLR_SUBHDR = "2E75B6" # střední modrá
CLR_ODEŠLI = "FCE4D6" # světle lososová
CLR_PŘIBYLO = "E2EFDA" # světle zelená
CLR_ZEBRA = "F2F2F2" # šedá pro zebra řádky
CLR_SUMMARY = "DEEAF1" # světle modrá pro souhrn
# ── Styly ─────────────────────────────────────────────────────────────────────
def hdr_font(white=True):
return Font(bold=True, color="FFFFFF" if white else "000000", size=11)
def cell_border():
thin = Side(style="thin", color="BFBFBF")
return Border(left=thin, right=thin, top=thin, bottom=thin)
def set_header(cell, text, bg=CLR_HEADER, white=True):
cell.value = text
cell.font = hdr_font(white)
cell.fill = PatternFill("solid", fgColor=bg)
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = cell_border()
def set_cell(cell, value, bg=None, bold=False, align="left", num_fmt=None):
cell.value = value
cell.font = Font(bold=bold, size=10)
cell.alignment = Alignment(horizontal=align, vertical="center")
cell.border = cell_border()
if bg:
cell.fill = PatternFill("solid", fgColor=bg)
if num_fmt:
cell.number_format = num_fmt
# ── Načtení dat z DB ──────────────────────────────────────────────────────────
print("Připojuji se k DB ...")
conn = connect_mysql()
cur = conn.cursor()
# Unikátní CIPy per datum + jméno (z prvního souboru daného data)
cur.execute("""
SELECT davka_rok, davka_mesic, davka_den, cip,
MIN(prijmeni) AS prijmeni, MIN(jmeno) AS jmeno
FROM seznam_pojistencu_davky
GROUP BY davka_rok, davka_mesic, davka_den, cip
ORDER BY davka_rok, davka_mesic, davka_den
""")
rows = cur.fetchall()
conn.close()
# Seskupení do dict: datum -> {cip: (prijmeni, jmeno)}
davky: dict[date, dict[str, tuple]] = defaultdict(dict)
for rok, mes, den, cip, pri, jme in rows:
d = date(rok, mes, den)
davky[d][cip] = (pri, jme)
data = sorted(davky.keys())
print(f"Nalezeno {len(data)} unikátních datumů dávek.")
# ── Porovnání ────────────────────────────────────────────────────────────────
prehled = [] # (dat_od, dat_do, stav_od, odešlo, přibylo, stav_do)
odešli = [] # (dat_do, cip, prijmeni, jmeno)
přibylo = [] # (dat_do, cip, prijmeni, jmeno)
for i in range(1, len(data)):
d1, d2 = data[i-1], data[i]
cip1 = set(davky[d1])
cip2 = set(davky[d2])
vyšli = cip1 - cip2
vstou = cip2 - cip1
prehled.append((d1, d2, len(cip1), len(vyšli), len(vstou), len(cip2)))
for cip in sorted(vyšli):
pri, jme = davky[d1][cip]
odešli.append((d2, cip, pri, jme))
for cip in sorted(vstou):
pri, jme = davky[d2][cip]
přibylo.append((d2, cip, pri, jme))
# ── Excel ─────────────────────────────────────────────────────────────────────
wb = openpyxl.Workbook()
# ── List 1: Přehled ───────────────────────────────────────────────────────────
ws = wb.active
ws.title = "Přehled"
ws.freeze_panes = "A3"
# Nadpis
ws.merge_cells("A1:F1")
t = ws["A1"]
t.value = "Porovnání po sobě jdoucích dávek VZP seznam registrovaných pojištěnců"
t.font = Font(bold=True, size=13, color="FFFFFF")
t.fill = PatternFill("solid", fgColor=CLR_HEADER)
t.alignment = Alignment(horizontal="center", vertical="center")
ws.row_dimensions[1].height = 28
# Záhlaví sloupců
headers = ["Datum od", "Datum do", "Stav\nna začátku", "Odešlo", "Přibylo", "Stav\nna konci"]
for col, h in enumerate(headers, 1):
set_header(ws.cell(2, col), h)
ws.row_dimensions[2].height = 32
# Data
for row_i, (d1, d2, stav_od, vyšli, vstou, stav_do) in enumerate(prehled, 3):
bg = CLR_ZEBRA if row_i % 2 == 0 else None
set_cell(ws.cell(row_i, 1), d1.strftime("%d.%m.%Y"), bg, align="center")
set_cell(ws.cell(row_i, 2), d2.strftime("%d.%m.%Y"), bg, align="center")
set_cell(ws.cell(row_i, 3), stav_od, bg, align="right")
set_cell(ws.cell(row_i, 4), -vyšli, CLR_ODEŠLI if vyšli else bg, bold=bool(vyšli), align="right")
set_cell(ws.cell(row_i, 5), vstou, CLR_PŘIBYLO if vstou else bg, bold=bool(vstou), align="right")
set_cell(ws.cell(row_i, 6), stav_do, bg, align="right")
# Souhrnný řádek
sr = len(prehled) + 3
ws.merge_cells(f"A{sr}:B{sr}")
sc = ws.cell(sr, 1)
sc.value = "CELKEM pohybů"
sc.font = Font(bold=True, size=10)
sc.fill = PatternFill("solid", fgColor=CLR_SUMMARY)
sc.alignment = Alignment(horizontal="center", vertical="center")
sc.border = cell_border()
celk_odešlo = sum(p[3] for p in prehled)
celk_přibylo = sum(p[4] for p in prehled)
set_cell(ws.cell(sr, 3), "", CLR_SUMMARY)
set_cell(ws.cell(sr, 4), -celk_odešlo, CLR_SUMMARY, bold=True, align="right")
set_cell(ws.cell(sr, 5), celk_přibylo, CLR_SUMMARY, bold=True, align="right")
set_cell(ws.cell(sr, 6), "", CLR_SUMMARY)
# Šířky sloupců
for col, w in zip(range(1, 7), [14, 14, 14, 10, 10, 12]):
ws.column_dimensions[get_column_letter(col)].width = w
# ── List 2: Odešli ────────────────────────────────────────────────────────────
ws2 = wb.create_sheet("Odešli")
ws2.freeze_panes = "A3"
ws2.merge_cells("A1:D1")
t2 = ws2["A1"]
t2.value = f"Pacienti, kteří odešli celkem {len(odešli)} pohybů"
t2.font = Font(bold=True, size=13, color="FFFFFF")
t2.fill = PatternFill("solid", fgColor="C55A11")
t2.alignment = Alignment(horizontal="center", vertical="center")
ws2.row_dimensions[1].height = 28
for col, h in enumerate(["Datum dávky", "Číslo pojištěnce", "Příjmení", "Jméno"], 1):
set_header(ws2.cell(2, col), h, bg="C55A11")
ws2.row_dimensions[2].height = 24
for row_i, (dat, cip, pri, jme) in enumerate(odešli, 3):
bg = CLR_ODEŠLI if row_i % 2 == 0 else None
set_cell(ws2.cell(row_i, 1), dat.strftime("%d.%m.%Y"), bg, align="center")
set_cell(ws2.cell(row_i, 2), cip, bg, align="center")
set_cell(ws2.cell(row_i, 3), pri, bg)
set_cell(ws2.cell(row_i, 4), jme, bg)
for col, w in zip(range(1, 5), [14, 16, 28, 22]):
ws2.column_dimensions[get_column_letter(col)].width = w
# ── List 3: Přibylo ───────────────────────────────────────────────────────────
ws3 = wb.create_sheet("Přibylo")
ws3.freeze_panes = "A3"
ws3.merge_cells("A1:D1")
t3 = ws3["A1"]
t3.value = f"Pacienti, kteří přibylo celkem {len(přibylo)} pohybů"
t3.font = Font(bold=True, size=13, color="FFFFFF")
t3.fill = PatternFill("solid", fgColor="375623")
t3.alignment = Alignment(horizontal="center", vertical="center")
ws3.row_dimensions[1].height = 28
for col, h in enumerate(["Datum dávky", "Číslo pojištěnce", "Příjmení", "Jméno"], 1):
set_header(ws3.cell(2, col), h, bg="375623")
ws3.row_dimensions[2].height = 24
for row_i, (dat, cip, pri, jme) in enumerate(přibylo, 3):
bg = CLR_PŘIBYLO if row_i % 2 == 0 else None
set_cell(ws3.cell(row_i, 1), dat.strftime("%d.%m.%Y"), bg, align="center")
set_cell(ws3.cell(row_i, 2), cip, bg, align="center")
set_cell(ws3.cell(row_i, 3), pri, bg)
set_cell(ws3.cell(row_i, 4), jme, bg)
for col, w in zip(range(1, 5), [14, 16, 28, 22]):
ws3.column_dimensions[get_column_letter(col)].width = w
# ── Uložení ───────────────────────────────────────────────────────────────────
wb.save(OUTPUT)
print(f"\nExcel uložen: {OUTPUT}")
print(f" Přehled: {len(prehled)} přechodů")
print(f" Odešli: {len(odešli)} pohybů")
print(f" Přibylo: {len(přibylo)} pohybů")
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
04_najdi_zlomy.py
=================
Pro pacienty z seznam_pojistencu_davky, kteří NEMAJÍ záznam v vzp_registrace_lekari
(skript kdojelekar je nezachytil), najde bod zlomu registrace u naší ambulance.
Algoritmus:
1. Dotaz VZP dnes — je pacient stále registrován u nás (ICP=09305001)?
ANO → datum_ukonceni z odpovědi = výsledek
2. NE → hledáme rokem dozadu (od dnes, 1 rok, 2 roky …)
dokud nenajdeme rok kdy BYL registrován → tím ohraničíme interval [lo, hi]
3. V intervalu [lo, hi] binární hledání na den přesně
(nebo pokud datum_ukonceni z VZP odpovědi není 3000, použijeme ho přímo)
Výsledek se uloží do tabulky seznam_pojistencu_zlomy a vytiskne na konzoli.
"""
import time
import sys
from datetime import date, timedelta
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny"))
from mysql_db import connect_mysql
from vzpb2b_client import VZPB2BClient
# ── Konfigurace ───────────────────────────────────────────────────────────────
PFX_PATH = str(Path(__file__).resolve().parents[1] / "Certificates" / "picka.pfx")
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "09305000"
NASA_ICP = "09305001"
API_PAUSE = 2 # sekundy mezi VZP dotazy
CREATE_SQL = """
CREATE TABLE IF NOT EXISTS seznam_pojistencu_zlomy (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
cip VARCHAR(12) NOT NULL,
prijmeni VARCHAR(30) NOT NULL,
jmeno VARCHAR(24) NOT NULL,
posledni_davka DATE NOT NULL COMMENT 'Poslední měsíc kdy byl v dávce',
zlom_datum DATE NULL COMMENT 'Poslední den registrace u nás (NULL=stále aktivní)',
zlom_zdroj VARCHAR(60) NULL COMMENT 'Jak byl zlom určen',
stav VARCHAR(20) NOT NULL COMMENT 'aktivní / ukončen / nenalezen',
dotazeno_dne DATE NOT NULL,
UNIQUE KEY uq_cip (cip)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Zlomy registrace pro pacienty bez záznamu v kdojelekar';
"""
# ── VZP klient ────────────────────────────────────────────────────────────────
vzp = VZPB2BClient("prod", PFX_PATH, PFX_PASSWORD, icz=ICZ)
def je_registrovan(rc: str, k_datu: date) -> tuple[bool, date | None]:
"""
Vrátí (registrován_u_nas: bool, datum_ukonceni: date|None).
datum_ukonceni = None pokud nelze parsovat nebo pacient není u nás.
"""
xml = vzp.registrace_lekare(rc, k_datu.isoformat(), odbornosti=["001"])
time.sleep(API_PAUSE)
try:
zaznamy = vzp.parse_registrace_lekare(xml)
except Exception as e:
print(f" [CHYBA parsování] {e}")
return False, None
for z in zaznamy:
if z.get("ma_lekare") and z.get("ICP") == NASA_ICP and z.get("kod_odbornosti") == "001":
du_str = z.get("datum_ukonceni")
try:
du = date.fromisoformat(du_str) if du_str else None
except ValueError:
du = None
return True, du
return False, None
def najdi_zlom(rc: str, posledni_davka: date) -> tuple[date | None, str, str]:
"""
Vrátí (zlom_datum, zlom_zdroj, stav).
zlom_datum = poslední den kdy byl registrován (None = stále aktivní).
"""
today = date.today()
# ── Krok 1: dotaz dnes ────────────────────────────────────────────────────
print(f" [dnes {today}]", end=" ", flush=True)
reg, du = je_registrovan(rc, today)
if reg:
if du and du.year < 3000:
print(f"registrován, ukončení {du}")
return du, "VZP datum_ukonceni (dnes)", "ukončen"
else:
print("registrován, bez data ukončení → stále aktivní")
return None, "VZP dnes aktivní", "aktivní"
print("NENÍ registrován")
# ── Krok 2: hledání po rocích dozadu ─────────────────────────────────────
# lo = víme, že tam BYL (posledni_davka)
# hi = víme, že tam NENÍ (today)
lo: date = posledni_davka
hi: date = today
probe = today.replace(year=today.year - 1)
while probe >= posledni_davka:
print(f" [rok {probe}]", end=" ", flush=True)
reg_p, du_p = je_registrovan(rc, probe)
if reg_p:
lo = probe
print(f"registrován")
if du_p and du_p.year < 3000:
print(f" → datum_ukonceni z VZP: {du_p}")
return du_p, f"VZP datum_ukonceni (dotaz {probe})", "ukončen"
break
else:
hi = probe
print("není")
try:
probe = probe.replace(year=probe.year - 1)
except ValueError:
break
else:
# Ani v posledni_davka není registrován — neobvyklé
print(f" ! Ani k datu {posledni_davka} není registrován — zkouším přímo")
reg_lo, du_lo = je_registrovan(rc, posledni_davka)
if not reg_lo:
return None, "nenalezen ani k datu poslední dávky", "nenalezen"
lo = posledni_davka
if du_lo and du_lo.year < 3000:
return du_lo, f"VZP datum_ukonceni ({posledni_davka})", "ukončen"
# ── Krok 3: binární hledání v intervalu [lo, hi] ─────────────────────────
print(f" Binární hledání: {lo}{hi}")
iterace = 0
while (hi - lo).days > 1:
iterace += 1
mid = lo + timedelta(days=(hi - lo).days // 2)
print(f" [{iterace}. iterace: {mid}]", end=" ", flush=True)
reg_m, du_m = je_registrovan(rc, mid)
if reg_m:
lo = mid
print("registrován")
if du_m and du_m.year < 3000:
print(f" → datum_ukonceni z VZP: {du_m}")
return du_m, f"VZP datum_ukonceni (binární {mid})", "ukončen"
else:
hi = mid
print("není")
print(f" → Zlom: poslední den registrace = {lo}")
return lo, f"binární hledání ({iterace} kroků)", "ukončen"
# ── Načtení pacientů ──────────────────────────────────────────────────────────
conn = connect_mysql()
cur = conn.cursor()
cur.execute(CREATE_SQL)
# Unikátní CIP v seznamu (VZP, pojišťovna 111)
cur.execute("SELECT DISTINCT cip FROM seznam_pojistencu_davky WHERE pojistovna='111'")
vsechny_cip = {r[0] for r in cur.fetchall()}
# CIP které jsou v registrace_lekari (u nás, odb 001)
cur.execute("""
SELECT DISTINCT rc FROM vzp_registrace_lekari
WHERE kod_odbornosti='001' AND ICP='09305001' AND ma_lekare=1
""")
zname_cip = {r[0] for r in cur.fetchall()}
# Nespárované
nesparovane_cip = vsechny_cip - zname_cip
# Doplním jméno a posledni_davka
cur.execute("""
SELECT cip, MIN(prijmeni), MIN(jmeno),
MAX(DATE(CONCAT(davka_rok, '-', LPAD(davka_mesic,2,'0'), '-01')))
FROM seznam_pojistencu_davky
WHERE pojistovna='111'
GROUP BY cip
""")
info = {r[0]: (r[1], r[2], r[3]) for r in cur.fetchall()}
pacienti = [
(cip, *info[cip])
for cip in sorted(nesparovane_cip)
if cip in info
]
print(f"Pacientů ke zpracování: {len(pacienti)}\n")
print("=" * 70)
# ── Hlavní smyčka ─────────────────────────────────────────────────────────────
vysledky = []
for cip, prijmeni, jmeno, posledni_davka in pacienti:
print(f"\n{prijmeni} {jmeno} (CIP: {cip}, poslední dávka: {posledni_davka})")
try:
zlom, zdroj, stav = najdi_zlom(cip, posledni_davka)
except Exception as e:
print(f" CHYBA: {e}")
zlom, zdroj, stav = None, f"chyba: {e}", "chyba"
vysledky.append((cip, prijmeni, jmeno, posledni_davka, zlom, zdroj, stav))
cur.execute("""
INSERT INTO seznam_pojistencu_zlomy
(cip, prijmeni, jmeno, posledni_davka, zlom_datum, zlom_zdroj, stav, dotazeno_dne)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
posledni_davka=VALUES(posledni_davka),
zlom_datum=VALUES(zlom_datum),
zlom_zdroj=VALUES(zlom_zdroj),
stav=VALUES(stav),
dotazeno_dne=VALUES(dotazeno_dne)
""", (cip, prijmeni, jmeno, posledni_davka, zlom, zdroj, stav, date.today()))
cur.close()
conn.close()
# ── Výsledky ──────────────────────────────────────────────────────────────────
print("\n" + "=" * 70)
print(f"\n{'Příjmení':<25} {'Jméno':<20} {'CIP':<12} {'Poslední dávka':<15} {'Zlom':<12} Stav")
print("-" * 95)
for cip, pri, jme, pd, zd, zdroj, stav in vysledky:
zlom_str = str(zd) if zd else ""
print(f"{pri:<25} {jme:<20} {cip:<12} {str(pd):<15} {zlom_str:<12} {stav}")
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
06_nasledny_lekar.py
====================
Pro všechny ukončené pacienty (103) se dotáže VZP ke dni ukonceni+1
na jejich nového praktického lékaře (odbornost 001).
Výsledek je jeden ze tří stavů:
nenalezen → pacient u VZP neexistuje (zemřel / přestal být pojištěný)
bez_lekare → pacient existuje, ale nemá GP (dosud se nepřehlásil)
prehlasil → přehlásil se k novému lékaři (ukládáme ICP, jméno, datum)
Ukládá do tabulky seznam_pojistencu_nasledny_lekar.
Přeskočí pacienty, kteří tam už jsou (resumovatelný běh).
"""
import sys
import time
import xml.etree.ElementTree as ET
from datetime import date, timedelta
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny"))
from mysql_db import connect_mysql
from vzpb2b_client import VZPB2BClient
PFX_PATH = str(Path(__file__).resolve().parents[1] / "Certificates" / "picka.pfx")
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "09305000"
API_PAUSE = 2
CREATE_SQL = """
CREATE TABLE IF NOT EXISTS seznam_pojistencu_nasledny_lekar (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
cip VARCHAR(12) NOT NULL,
prijmeni VARCHAR(60) NOT NULL,
jmeno VARCHAR(40) NOT NULL,
datum_ukonceni DATE NOT NULL COMMENT 'Datum ukončení u nás',
datum_dotazu DATE NOT NULL COMMENT 'Dotaz k datu ukonceni+1',
stav_vzp VARCHAR(20) NOT NULL COMMENT 'nenalezen / bez_lekare / prehlasil',
stav_vyrizeni VARCHAR(10) NULL COMMENT 'stavVyrizeniPozadavku z VZP',
novy_icp VARCHAR(20) NULL,
novy_icz VARCHAR(20) NULL,
novy_nazev VARCHAR(200) NULL COMMENT 'nazevSZZ — jméno lékaře',
novy_ordinace VARCHAR(200) NULL COMMENT 'nazevICP — název ordinace',
datum_prehlaseni DATE NULL COMMENT 'datumRegistrace u nového lékaře',
dotazeno_dne DATE NOT NULL,
UNIQUE KEY uq_cip (cip)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COMMENT='Stav pojištěnce ke dni ukončení registrace u naší ordinace';
"""
NS = "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1"
def parse_nasledny(xml_text: str) -> dict:
"""
Vrátí dict s klíči: stav_vzp, stav_vyrizeni, novy_icp, novy_icz,
novy_nazev, novy_ordinace, datum_prehlaseni.
"""
try:
root = ET.fromstring(xml_text)
except ET.ParseError:
return {"stav_vzp": "chyba_xml", "stav_vyrizeni": None,
"novy_icp": None, "novy_icz": None, "novy_nazev": None,
"novy_ordinace": None, "datum_prehlaseni": None}
def find(el, tag):
e = el.find(f"{{{NS}}}{tag}")
return e.text.strip() if e is not None and e.text else None
stav_vyrizeni = find(root, "stavVyrizeniPozadavku")
# Hledám odbornost 001 s jiným lékařem
odbornosti = root.findall(f".//{{{NS}}}odbornost")
for odb in odbornosti:
# Vnější element — má ICZ
icz = find(odb, "ICZ")
icp = find(odb, "ICP")
if not icp:
continue
# Ověřím odbornost (vnořený subelement)
sub = odb.find(f"{{{NS}}}odbornost")
if sub is None:
continue
kod = find(sub, "kod")
if kod != "001":
continue
# Nalezen nový GP
dr = date.fromisoformat(find(odb, "datumRegistrace")) \
if find(odb, "datumRegistrace") else None
return {
"stav_vzp": "prehlasil",
"stav_vyrizeni": stav_vyrizeni,
"novy_icp": icp,
"novy_icz": icz,
"novy_nazev": find(odb, "nazevSZZ"),
"novy_ordinace": find(odb, "nazevICP"),
"datum_prehlaseni": dr,
}
# Žádná odbornost 001 nenalezena
if stav_vyrizeni == "0":
stav = "nenalezen"
elif stav_vyrizeni == "1":
stav = "bez_lekare"
else:
# Zkusím ještě — pokud jsou nějaké záznamy ale ne 001, je to bez_lekare
stav = "nenalezen" if not odbornosti else "bez_lekare"
return {"stav_vzp": stav, "stav_vyrizeni": stav_vyrizeni,
"novy_icp": None, "novy_icz": None, "novy_nazev": None,
"novy_ordinace": None, "datum_prehlaseni": None}
# ── Načtení ukončených pacientů ───────────────────────────────────────────────
conn = connect_mysql()
cur = conn.cursor()
cur.execute(CREATE_SQL)
# Ukončení z vzp_registrace_lekari
cur.execute("""
SELECT r.rc, r.prijmeni, r.jmeno, r.datum_ukonceni
FROM vzp_registrace_lekari r
INNER JOIN (
SELECT rc, MAX(k_datu) mk
FROM vzp_registrace_lekari
WHERE kod_odbornosti='001' AND ICP='09305001' AND ma_lekare=1
GROUP BY rc
) l ON r.rc=l.rc AND r.k_datu=l.mk
WHERE r.kod_odbornosti='001' AND r.ICP='09305001'
AND r.datum_ukonceni < '3000-01-01' AND r.datum_ukonceni < CURDATE()
""")
pacienti = [(r[0], r[1] or "", r[2] or "", r[3]) for r in cur.fetchall()]
# Doplním ze zlomů (13 nespárovaných)
cur.execute("""
SELECT cip, prijmeni, jmeno, zlom_datum
FROM seznam_pojistencu_zlomy
WHERE stav='ukončen' AND zlom_datum IS NOT NULL AND zlom_datum < CURDATE()
""")
for r in cur.fetchall():
if r[0] not in {p[0] for p in pacienti}:
pacienti.append((r[0], r[1], r[2], r[3]))
# Přeskočím již zpracované
cur.execute("SELECT cip FROM seznam_pojistencu_nasledny_lekar")
hotovi = {r[0] for r in cur.fetchall()}
ke_zpracovani = [(c, p, j, d) for c, p, j, d in pacienti if c not in hotovi]
print(f"Ukončených celkem: {len(pacienti)}")
print(f"Již zpracováno: {len(hotovi)}")
print(f"Ke zpracování: {len(ke_zpracovani)}")
if not ke_zpracovani:
print("Vše již zpracováno.")
cur.close(); conn.close(); sys.exit(0)
vzp = VZPB2BClient("prod", PFX_PATH, PFX_PASSWORD, icz=ICZ)
# ── Hlavní smyčka ─────────────────────────────────────────────────────────────
print()
sirka = max(len(f"{p} {j}") for _, p, j, _ in ke_zpracovani) + 2
for i, (cip, pri, jme, du) in enumerate(ke_zpracovani, 1):
datum_dotazu = du + timedelta(days=1)
jmeno_str = f"{pri} {jme}"
print(f"[{i:>3}/{len(ke_zpracovani)}] {jmeno_str:<{sirka}} ({cip}) k {datum_dotazu}", end=" ", flush=True)
try:
xml = vzp.registrace_lekare(cip, datum_dotazu.isoformat(), odbornosti=["001"])
time.sleep(API_PAUSE)
vysl = parse_nasledny(xml)
except Exception as e:
print(f"CHYBA: {e}")
vysl = {"stav_vzp": "chyba", "stav_vyrizeni": str(e),
"novy_icp": None, "novy_icz": None, "novy_nazev": None,
"novy_ordinace": None, "datum_prehlaseni": None}
# Výpis
stav = vysl["stav_vzp"]
if stav == "prehlasil":
print(f"→ přehlásil se: {vysl['novy_nazev']} (ICP {vysl['novy_icp']}, od {vysl['datum_prehlaseni']})")
elif stav == "bez_lekare":
print("→ bez nového lékaře")
elif stav == "nenalezen":
print("→ nenalezen (zemřel / nepojištěný)")
else:
print(f"{stav}")
cur.execute("""
INSERT INTO seznam_pojistencu_nasledny_lekar
(cip, prijmeni, jmeno, datum_ukonceni, datum_dotazu, stav_vzp,
stav_vyrizeni, novy_icp, novy_icz, novy_nazev, novy_ordinace,
datum_prehlaseni, dotazeno_dne)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
datum_ukonceni=VALUES(datum_ukonceni),
datum_dotazu=VALUES(datum_dotazu),
stav_vzp=VALUES(stav_vzp), stav_vyrizeni=VALUES(stav_vyrizeni),
novy_icp=VALUES(novy_icp), novy_icz=VALUES(novy_icz),
novy_nazev=VALUES(novy_nazev), novy_ordinace=VALUES(novy_ordinace),
datum_prehlaseni=VALUES(datum_prehlaseni),
dotazeno_dne=VALUES(dotazeno_dne)
""", (cip, pri, jme, du, datum_dotazu, stav,
vysl["stav_vyrizeni"], vysl["novy_icp"], vysl["novy_icz"],
vysl["novy_nazev"], vysl["novy_ordinace"], vysl["datum_prehlaseni"],
date.today()))
cur.close()
conn.close()
# ── Souhrn ────────────────────────────────────────────────────────────────────
print(f"\nHotovo. Zpracováno {len(ke_zpracovani)} pacientů.")
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
07_doplnit_zahajeni.py
======================
Pro nové pacienty (první dávka po 31.12.2024) kteří jsou již ukončeni
a nemají záznam v vzp_registrace_lekari pro naše ICP=09305001,
dotáže se VZP k datu jejich první dávky — tehdy tam ještě byli
a odpověď obsahuje datumZahajeni registrace u nás.
Výsledek uloží do vzp_registrace_lekari (stejná tabulka jako kdojelekar).
"""
import sys
import time
from datetime import date
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny"))
from mysql_db import connect_mysql
from vzpb2b_client import VZPB2BClient
PFX_PATH = str(Path(__file__).resolve().parents[1] / "Certificates" / "picka.pfx")
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "09305000"
NASA_ICP = "09305001"
API_PAUSE = 2
PRVNI_DAVKA = date(2024, 12, 1)
# ── Načtení kandidátů ─────────────────────────────────────────────────────────
conn = connect_mysql()
cur = conn.cursor()
# Noví pacienti (prvni_davka > 31.12.2024) bez záznamu v registrace pro naše ICP
cur.execute("""
SELECT
s.cip,
MIN(s.prijmeni) AS prijmeni,
MIN(s.jmeno) AS jmeno,
MIN(DATE(CONCAT(s.davka_rok,'-',LPAD(s.davka_mesic,2,'0'),'-',LPAD(s.davka_den,2,'0')))) AS prvni_davka
FROM seznam_pojistencu_davky s
WHERE s.pojistovna = '111'
GROUP BY s.cip
HAVING prvni_davka > %s
""", (PRVNI_DAVKA,))
vsichni_novi = {r[0]: (r[1], r[2], r[3]) for r in cur.fetchall()}
# Kteří z nich už mají záznam v registrace pro naše ICP
cur.execute("""
SELECT DISTINCT rc FROM vzp_registrace_lekari
WHERE ICP = %s AND kod_odbornosti = '001' AND ma_lekare = 1
""", (NASA_ICP,))
uz_maji = {r[0] for r in cur.fetchall()}
kandidati = {
cip: info
for cip, info in vsichni_novi.items()
if cip not in uz_maji
}
print(f"Noví pacienti celkem: {len(vsichni_novi)}")
print(f"Již mají datum zahájení: {len(uz_maji & vsichni_novi.keys())}")
print(f"Ke doplnění: {len(kandidati)}")
if not kandidati:
print("Vše je kompletní.")
cur.close(); conn.close(); sys.exit(0)
vzp = VZPB2BClient("prod", PFX_PATH, PFX_PASSWORD, icz=ICZ)
print()
sirka = max(len(f"{p} {j}") for p, j, _ in kandidati.values()) + 2
for i, (cip, (prijmeni, jmeno, prvni_davka)) in enumerate(kandidati.items(), 1):
print(f"[{i:>2}/{len(kandidati)}] {prijmeni+' '+jmeno:<{sirka}} ({cip}) k {prvni_davka}", end=" ", flush=True)
try:
xml = vzp.registrace_lekare(cip, prvni_davka.isoformat(), odbornosti=["001"])
time.sleep(API_PAUSE)
zaznamy = vzp.parse_registrace_lekare(xml)
except Exception as e:
print(f"CHYBA: {e}")
continue
nas_zaznam = next(
(z for z in zaznamy
if z.get("ma_lekare") and z.get("ICP") == NASA_ICP and z.get("kod_odbornosti") == "001"),
None
)
if not nas_zaznam:
print("→ nenalezen k tomuto datu")
continue
def to_date(s):
try:
return date.fromisoformat(s) if s else None
except ValueError:
return None
poj_el = nas_zaznam
cur.execute("""
INSERT INTO vzp_registrace_lekari
(rc, prijmeni, jmeno, k_datu, kod_odbornosti, ma_lekare,
ICZ, ICP, nazev_lekare, nazev_zzz,
poj_kod, poj_zkratka,
datum_registrace, datum_zahajeni, datum_ukonceni, stav_vyrizeni)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
datum_zahajeni = VALUES(datum_zahajeni),
datum_ukonceni = VALUES(datum_ukonceni),
nazev_lekare = VALUES(nazev_lekare),
nazev_zzz = VALUES(nazev_zzz)
""", (
cip, prijmeni, jmeno, prvni_davka, "001", 1,
nas_zaznam.get("ICZ"), nas_zaznam.get("ICP"),
nas_zaznam.get("nazev_lekare"), nas_zaznam.get("nazev_zzz"),
nas_zaznam.get("poj_kod"), nas_zaznam.get("poj_zkratka"),
to_date(nas_zaznam.get("datum_registrace")),
to_date(nas_zaznam.get("datum_zahajeni")),
to_date(nas_zaznam.get("datum_ukonceni")),
nas_zaznam.get("stav_vyrizeni"),
))
dz = nas_zaznam.get("datum_zahajeni") or "?"
du = nas_zaznam.get("datum_ukonceni") or "?"
print(f"→ zahájení: {dz} ukončení: {du}")
cur.close()
conn.close()
print(f"\nHotovo.")
@@ -0,0 +1,361 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
05_report_excel.py
==================
Kompletní Excel report všech VZP pojištěnců z dávek.
Kombinuje čtyři zdroje:
- seznam_pojistencu_davky → první/poslední dávka, zda v aktuální
- vzp_registrace_lekari → datum_ukonceni od VZP (989 pacientů)
- seznam_pojistencu_zlomy → bod zlomu pro 13 nespárovaných
- seznam_pojistencu_nasledny_lekar → nový lékař / nenalezen po ukončení
Listy:
1. Všichni pojištěnci kompletní přehled, řazeno příjmení
2. Aktivní stále registrováni
3. Ukončení registrace ukončena + nový lékař / osud
4. Nenalezeni bez dostatečných dat
"""
import sys
from datetime import date
from pathlib import Path
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "Knihovny"))
from mysql_db import connect_mysql
TODAY = date.today()
OUTPUT = Path(__file__).parent / "report_pojistenci.xlsx"
# ── Barvy ─────────────────────────────────────────────────────────────────────
C_HDR_DARK = "1F4E79"
C_HDR_MED = "2E75B6"
C_AKTIVNI = "E2EFDA" # zelená
C_UKONCEN = "FCE4D6" # lososová
C_NENALEZEN = "FFF2CC" # žlutá
C_ZEBRA = "F2F2F2"
C_TITLE_AKT = "375623"
C_TITLE_UKO = "C55A11"
C_TITLE_NEN = "7F6000"
THIN = Side(style="thin", color="BFBFBF")
def border():
return Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
def hdr(cell, text, bg=C_HDR_DARK, fg="FFFFFF", size=10):
cell.value = text
cell.font = Font(bold=True, color=fg, size=size)
cell.fill = PatternFill("solid", fgColor=bg)
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
cell.border = border()
def cell(ws, row, col, value, bg=None, bold=False, align="left", fmt=None, color="000000"):
c = ws.cell(row, col, value)
c.font = Font(bold=bold, size=10, color=color)
c.alignment = Alignment(horizontal=align, vertical="center")
c.border = border()
if bg:
c.fill = PatternFill("solid", fgColor=bg)
if fmt:
c.number_format = fmt
return c
def title_row(ws, row, text, ncols, bg, fg="FFFFFF", height=26):
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=ncols)
c = ws.cell(row, 1)
c.value = text
c.font = Font(bold=True, size=13, color=fg)
c.fill = PatternFill("solid", fgColor=bg)
c.alignment = Alignment(horizontal="center", vertical="center")
ws.row_dimensions[row].height = height
def autofit(ws, widths):
for i, w in enumerate(widths, 1):
ws.column_dimensions[get_column_letter(i)].width = w
# ── Načtení dat ───────────────────────────────────────────────────────────────
print("Načítám data z DB ...")
conn = connect_mysql()
cur = conn.cursor()
# 1. Ze seznam_pojistencu_davky: rozsah přítomnosti + počet dávek
cur.execute("""
SELECT
cip,
MIN(prijmeni) AS prijmeni,
MIN(jmeno) AS jmeno,
MIN(DATE(CONCAT(davka_rok,'-',LPAD(davka_mesic,2,'0'),'-',LPAD(davka_den,2,'0')))) AS prvni_davka,
MAX(DATE(CONCAT(davka_rok,'-',LPAD(davka_mesic,2,'0'),'-',LPAD(davka_den,2,'0')))) AS posledni_davka,
COUNT(DISTINCT CONCAT(davka_rok,davka_mesic)) AS pocet_davek
FROM seznam_pojistencu_davky
WHERE pojistovna='111'
GROUP BY cip
""")
seznam = {r[0]: {"prijmeni": r[1], "jmeno": r[2],
"prvni": r[3], "posledni": r[4], "pocet_davek": r[5]}
for r in cur.fetchall()}
# Nejnovější datum dávky (pro sloupec "v aktuální dávce")
cur.execute("""
SELECT MAX(DATE(CONCAT(davka_rok,'-',LPAD(davka_mesic,2,'0'),'-',LPAD(davka_den,2,'0'))))
FROM seznam_pojistencu_davky WHERE pojistovna='111'
""")
nejnovejsi_davka = cur.fetchone()[0]
cur.execute("""
SELECT DISTINCT cip FROM seznam_pojistencu_davky
WHERE pojistovna='111'
AND CONCAT(davka_rok,LPAD(davka_mesic,2,'0'),LPAD(davka_den,2,'0')) = (
SELECT MAX(CONCAT(davka_rok,LPAD(davka_mesic,2,'0'),LPAD(davka_den,2,'0')))
FROM seznam_pojistencu_davky WHERE pojistovna='111'
)
""")
v_aktualni = {r[0] for r in cur.fetchall()}
# 2. Z vzp_registrace_lekari: nejnovější záznam na pacienta (u nás, odb 001)
cur.execute("""
SELECT r.rc, r.datum_zahajeni, r.datum_ukonceni, r.k_datu
FROM vzp_registrace_lekari r
INNER JOIN (
SELECT rc, MAX(k_datu) AS max_k
FROM vzp_registrace_lekari
WHERE kod_odbornosti='001' AND ICP='09305001' AND ma_lekare=1
GROUP BY rc
) latest ON r.rc = latest.rc AND r.k_datu = latest.max_k
WHERE r.kod_odbornosti='001' AND r.ICP='09305001' AND r.ma_lekare=1
""")
registrace = {r[0]: {"zahajeni": r[1], "ukonceni": r[2], "k_datu": r[3]}
for r in cur.fetchall()}
# 3. Ze seznam_pojistencu_zlomy (13 nespárovaných)
cur.execute("SELECT cip, zlom_datum, zlom_zdroj, stav FROM seznam_pojistencu_zlomy")
zlomy = {r[0]: {"ukonceni": r[1], "zdroj": r[2], "stav": r[3]}
for r in cur.fetchall()}
# 4. Následný lékař po ukončení
cur.execute("""
SELECT cip, stav_vzp, novy_nazev, novy_ordinace, novy_icp, datum_prehlaseni
FROM seznam_pojistencu_nasledny_lekar
""")
nasledni = {r[0]: {"stav_vzp": r[1], "novy_nazev": r[2],
"novy_ordinace": r[3], "novy_icp": r[4],
"datum_prehlaseni": r[5]}
for r in cur.fetchall()}
cur.close()
conn.close()
# ── Sestavení řádků ───────────────────────────────────────────────────────────
print(f"Pacientů celkem: {len(seznam)}")
radky = []
for cip, s in seznam.items():
v_akt = cip in v_aktualni
if cip in registrace:
reg = registrace[cip]
du = reg["ukonceni"]
zdroj = f"vzp_registrace ({reg['k_datu']})"
elif cip in zlomy:
z = zlomy[cip]
du = z["ukonceni"]
zdroj = z["zdroj"]
else:
du = None
zdroj = ""
if du is None:
stav = "aktivní"
elif du.year >= 3000:
stav = "aktivní"
du = None # nezobrazujeme 3000-01-01
elif du >= TODAY:
stav = "aktivní"
else:
stav = "ukončen"
if zdroj == "" and not v_akt:
stav = "nenalezen"
# Následný lékař
nl = nasledni.get(cip)
if nl:
po_ukonceni = nl["stav_vzp"] # prehlasil / nenalezen / bez_lekare
novy_lekar = nl["novy_nazev"] or nl["novy_ordinace"] or ""
if nl["novy_icp"]:
novy_lekar += f" (ICP {nl['novy_icp']})"
datum_prehl = nl["datum_prehlaseni"]
else:
po_ukonceni = ""
novy_lekar = ""
datum_prehl = None
radky.append({
"cip": cip,
"prijmeni": s["prijmeni"],
"jmeno": s["jmeno"],
"prvni": s["prvni"],
"posledni": s["posledni"],
"pocet_davek": s["pocet_davek"],
"v_aktualni": v_akt,
"ukonceni": du,
"zdroj": zdroj,
"stav": stav,
"po_ukonceni": po_ukonceni,
"novy_lekar": novy_lekar,
"datum_prehl": datum_prehl,
})
DATUM_REGISTRACE_OD = date(2025, 1, 1)
radky.sort(key=lambda r: (r["prijmeni"], r["jmeno"]))
aktivni = [r for r in radky if r["stav"] == "aktivní"]
ukonceni = sorted([r for r in radky if r["stav"] == "ukončen"],
key=lambda r: r["ukonceni"] or date.min)
nenalezeni = [r for r in radky if r["stav"] == "nenalezen"]
# Noví: datum_zahajeni registrace u nás >= 01.01.2025, řazeno chronologicky
novi = sorted(
[r for r in radky
if registrace.get(r["cip"], {}).get("zahajeni") is not None
and registrace[r["cip"]]["zahajeni"] >= DATUM_REGISTRACE_OD],
key=lambda r: (registrace[r["cip"]]["zahajeni"], r["prijmeni"], r["jmeno"])
)
print(f" Aktivní: {len(aktivni)}")
print(f" Ukončení: {len(ukonceni)}")
print(f" Noví: {len(novi)}")
print(f" Nenalezeni: {len(nenalezeni)}")
# ── Excel ─────────────────────────────────────────────────────────────────────
SLOUPCE_ALL = ["Příjmení", "Jméno", "ČIP", "První\ndávka", "Poslední\ndávka",
"Počet\ndávek", "V aktuální\ndávce", "Ukončení\nregistrace",
"Stav", "Po ukončení", "Nový lékař / poznámka", "Datum\npřehlášení"]
WIDTHS_ALL = [24, 18, 13, 12, 13, 9, 10, 14, 10, 13, 40, 13]
SLOUPCE = SLOUPCE_ALL # alias pro funkci zapsat_list
WIDTHS = WIDTHS_ALL
NCOLS = len(SLOUPCE)
DATE_FMT = "DD.MM.YYYY"
def zapsat_list(ws, nadpis, bg_title, seznam_radku, stav_bg):
ws.freeze_panes = "A3"
title_row(ws, 1, nadpis, NCOLS, bg_title)
ws.row_dimensions[2].height = 30
for col, h in enumerate(SLOUPCE, 1):
hdr(ws.cell(2, col), h)
autofit(ws, WIDTHS)
PO_CLR = {"prehlasil": "375623", "nenalezen": "C55A11",
"bez_lekare": "7F6000", "": "000000"}
PO_TXT = {"prehlasil": "přehlásil se", "nenalezen": "nenalezen",
"bez_lekare": "bez lékaře", "": ""}
for ri, r in enumerate(seznam_radku, 3):
bg = stav_bg if ri % 2 == 0 else None
cell(ws, ri, 1, r["prijmeni"], bg)
cell(ws, ri, 2, r["jmeno"], bg)
cell(ws, ri, 3, r["cip"], bg, align="center")
cell(ws, ri, 4, r["prvni"], bg, align="center", fmt=DATE_FMT)
cell(ws, ri, 5, r["posledni"], bg, align="center", fmt=DATE_FMT)
cell(ws, ri, 6, r["pocet_davek"], bg, align="right")
akt_txt = "" if r["v_aktualni"] else ""
akt_clr = "375623" if r["v_aktualni"] else "C55A11"
cell(ws, ri, 7, akt_txt, bg, bold=True, align="center", color=akt_clr)
cell(ws, ri, 8, r["ukonceni"], bg, align="center", fmt=DATE_FMT)
cell(ws, ri, 9, r["stav"], bg, align="center", bold=True,
color=("375623" if r["stav"]=="aktivní" else
"C55A11" if r["stav"]=="ukončen" else "7F6000"))
po = r.get("po_ukonceni", "")
cell(ws, ri, 10, PO_TXT.get(po, po), bg, align="center", bold=bool(po),
color=PO_CLR.get(po, "000000"))
cell(ws, ri, 11, r.get("novy_lekar", ""), bg)
cell(ws, ri, 12, r.get("datum_prehl"), bg, align="center", fmt=DATE_FMT)
C_REGISTRACE = "DAEEF3" # světle modrá
C_TITLE_REG = "17375E"
SLOUPCE_REG = ["Příjmení", "Jméno", "ČIP", "Zahájení\nregistrace",
"Ukončení\nregistrace", "Počet\ndávek", "V aktuální\ndávce",
"Stav", "Po ukončení", "Nový lékař / poznámka", "Datum\npřehlášení"]
WIDTHS_REG = [24, 18, 13, 16, 14, 9, 10, 10, 13, 40, 13]
PO_CLR_REG = {"prehlasil": "375623", "nenalezen": "C55A11",
"bez_lekare": "7F6000", "": "000000"}
PO_TXT_REG = {"prehlasil": "přehlásil se", "nenalezen": "nenalezen",
"bez_lekare": "bez lékaře", "": ""}
def zapsat_registrace(ws, nadpis, seznam_radku):
ncols = len(SLOUPCE_REG)
ws.freeze_panes = "A3"
title_row(ws, 1, nadpis, ncols, C_TITLE_REG)
ws.row_dimensions[2].height = 30
for col, h in enumerate(SLOUPCE_REG, 1):
hdr(ws.cell(2, col), h, bg=C_TITLE_REG)
autofit(ws, WIDTHS_REG)
for ri, r in enumerate(seznam_radku, 3):
bg = C_REGISTRACE if ri % 2 == 0 else None
cell(ws, ri, 1, r["prijmeni"], bg)
cell(ws, ri, 2, r["jmeno"], bg)
cell(ws, ri, 3, r["cip"], bg, align="center")
zahajeni = registrace.get(r["cip"], {}).get("zahajeni")
cell(ws, ri, 4, zahajeni, bg, align="center", fmt=DATE_FMT)
cell(ws, ri, 5, r["ukonceni"], bg, align="center", fmt=DATE_FMT)
cell(ws, ri, 6, r["pocet_davek"],bg, align="right")
akt_txt = "" if r["v_aktualni"] else ""
akt_clr = "375623" if r["v_aktualni"] else "C55A11"
cell(ws, ri, 7, akt_txt, bg, bold=True, align="center", color=akt_clr)
cell(ws, ri, 8, r["stav"], bg, align="center", bold=True,
color=("375623" if r["stav"] == "aktivní" else "C55A11"))
po = r.get("po_ukonceni", "")
cell(ws, ri, 9, PO_TXT_REG.get(po, po), bg, align="center", bold=bool(po),
color=PO_CLR_REG.get(po, "000000"))
cell(ws, ri, 10, r.get("novy_lekar", ""), bg)
cell(ws, ri, 11, r.get("datum_prehl"), bg, align="center", fmt=DATE_FMT)
wb = openpyxl.Workbook()
# List 1 Všichni
ws1 = wb.active
ws1.title = "Všichni pojištěnci"
zapsat_list(ws1,
f"VZP pojištěnci — kompletní přehled ({TODAY.strftime('%d.%m.%Y')}) | "
f"celkem: {len(radky)} | aktivní: {len(aktivni)} | ukončení: {len(ukonceni)}",
C_HDR_DARK, radky, C_ZEBRA)
# List 2 Aktivní
ws2 = wb.create_sheet("Aktivní")
zapsat_list(ws2,
f"Aktivní pojištěnci ({TODAY.strftime('%d.%m.%Y')}) — celkem: {len(aktivni)}",
C_TITLE_AKT, aktivni, C_AKTIVNI)
# List 3 Ukončení
ws3 = wb.create_sheet("Ukončení")
zapsat_list(ws3,
f"Ukončená registrace — celkem: {len(ukonceni)}",
C_TITLE_UKO, ukonceni, C_UKONCEN)
# List 4 Registrace (noví po 31.12.2024)
ws4 = wb.create_sheet("Registrace")
zapsat_registrace(ws4,
f"Noví pojištěnci — datum zahájení registrace od 1.1.2025 — celkem: {len(novi)}",
novi)
# List 5 Nenalezeni
if nenalezeni:
ws5 = wb.create_sheet("Nenalezeni")
zapsat_list(ws5,
f"Bez dostatečných dat — celkem: {len(nenalezeni)}",
C_TITLE_NEN, nenalezeni, C_NENALEZEN)
wb.save(OUTPUT)
print(f"\nExcel uložen: {OUTPUT}")
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
"""
zadej_davku.py
==============
Odešle asynchronní požadavek na VZP B2B službu SeznamRegPojistencuB2B
(seznam zakapitovaných/registrovaných pojištěnců za dané období).
Použití:
python zadej_davku.py [mesic] [rok]
python zadej_davku.py 2 2025 # únor 2025
python zadej_davku.py # předchozí měsíc (výchozí)
Výstup:
- korelační ID zprávy (pro pozdější spárování asynchronní odpovědi)
- stavVyrizeniPozadavku=2 znamená "přijato, VZP zpracovává"
- finální odpověď přijde asynchronně na AS2 endpoint partnera
"""
import sys
import uuid
import argparse
from pathlib import Path
from datetime import date, timedelta
from requests_pkcs12 import Pkcs12Adapter
import requests
import xml.etree.ElementTree as ET
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "09305000"
ENV = "prod"
NS_VZP = "http://xmlns.gemsystem.cz/SeznamRegPojistencuB2B"
NS_COMMON = "http://xmlns.gemsystem.cz/CommonB2B"
NS_SOAP = "http://schemas.xmlsoap.org/soap/envelope/"
ENDPOINT_SIMU = "https://simu.b2b.vzp.cz/B2BProxy/HttpProxy/SIMUSeznamRegPojistencuB2B?sluzba=SIMUSeznamRegPojistencuB2B"
ENDPOINT_PROD = "https://prod.b2b.vzp.cz/B2BProxy/HttpProxy/SeznamRegPojistencuB2B"
# ── ARGUMENTY ────────────────────────────────────────────────────────────────
parser = argparse.ArgumentParser(description="Požadavek na seznam registrovaných pojištěnců VZP")
parser.add_argument("mesic", nargs="?", type=int, help="Měsíc (1-12)")
parser.add_argument("rok", nargs="?", type=int, help="Rok (např. 2025)")
parser.add_argument("--simu", action="store_true", help="Použít simulační prostředí")
parser.add_argument("--pdf", action="store_true", help="Výstup jako PDF (výchozí: text/plain)")
args = parser.parse_args()
# Výchozí: předchozí měsíc
if args.mesic and args.rok:
mesic, rok = args.mesic, args.rok
else:
prvni_tohoto = date.today().replace(day=1)
predchozi = prvni_tohoto - timedelta(days=1)
mesic, rok = predchozi.month, predchozi.year
format_vystupu = "application/pdf" if args.pdf else "text/plain"
endpoint = ENDPOINT_SIMU if args.simu else ENDPOINT_PROD
# ── KONTROLY ─────────────────────────────────────────────────────────────────
if not PFX_PATH.exists():
print(f"CHYBA: certifikát nenalezen: {PFX_PATH}")
sys.exit(1)
if not (1 <= mesic <= 12):
print(f"CHYBA: neplatný měsíc: {mesic}")
sys.exit(1)
# ── SESTAVENÍ POŽADAVKU ───────────────────────────────────────────────────────
id_zpravy = uuid.uuid4().hex[:12].upper()
soap = f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:soap="{NS_SOAP}"
xmlns:vzp="{NS_VZP}"
xmlns:com="{NS_COMMON}">
<soap:Header>
<com:idZpravy>{id_zpravy}</com:idZpravy>
<com:idSubjektu>
<com:icz>{ICZ}</com:icz>
</com:idSubjektu>
</soap:Header>
<soap:Body>
<vzp:seznamRegistrovanychPojistencuB2BPozadavek>
<vzp:idZpravy>{id_zpravy}</vzp:idZpravy>
<vzp:idSubjektu>{ICZ}</vzp:idSubjektu>
<vzp:typSubjektu>zp</vzp:typSubjektu>
<vzp:seznam>
<vzp:obdobiDavky>
<vzp:mesic>{mesic}</vzp:mesic>
<vzp:rok>{rok}</vzp:rok>
</vzp:obdobiDavky>
<vzp:formatVystupu>{format_vystupu}</vzp:formatVystupu>
</vzp:seznam>
</vzp:seznamRegistrovanychPojistencuB2BPozadavek>
</soap:Body>
</soap:Envelope>"""
# ── ODESLÁNÍ ─────────────────────────────────────────────────────────────────
print(f"Období: {mesic:02d}/{rok}")
print(f"Formát: {format_vystupu}")
print(f"Prostředí: {'SIMU' if args.simu else 'PROD'}")
print(f"IČZ: {ICZ}")
print(f"ID zprávy: {id_zpravy}")
print(f"Endpoint: {endpoint}")
print()
session = requests.Session()
session.mount("https://", Pkcs12Adapter(
pkcs12_filename=str(PFX_PATH),
pkcs12_password=PFX_PASSWORD
))
try:
resp = session.post(
endpoint,
data=soap.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": "process"},
timeout=30
)
except requests.RequestException as e:
print(f"CHYBA při spojení: {e}")
sys.exit(1)
print(f"HTTP status: {resp.status_code}")
print()
# ── PARSOVÁNÍ ODPOVĚDI ────────────────────────────────────────────────────────
if not resp.content:
print("→ Požadavek přijat (prázdná odpověď = asynchronní služba).")
print(f"→ VZP zpracuje a odešle výsledek na AS2 endpoint.")
print(f"→ ID zprávy pro spárování: {id_zpravy}")
else:
try:
root = ET.fromstring(resp.text)
NS = {"soap": NS_SOAP, "vzp": NS_VZP}
korelace = root.find(".//vzp:korelaceZpravy", NS)
text_odp = root.find(".//vzp:textOdpovedi", NS)
stav = root.find(".//vzp:stavVyrizeniPozadavku", NS)
print(f"Korelace zprávy: {korelace.text if korelace is not None else ''}")
print(f"Text odpovědi: {text_odp.text if text_odp is not None else ''}")
print(f"Stav vyřízení: {stav.text if stav is not None else ''}")
if stav is not None and stav.text == "2":
print()
print("→ VZP zpracovává — finální odpověď přijde asynchronně na AS2 endpoint.")
print(f"→ ID zprávy pro spárování: {id_zpravy}")
except ET.ParseError:
print("Nepodařilo se parsovat XML odpověď:")
print(resp.text)
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,135 @@
"""
Stahování seznamu registrovaných pojištěnců ČPZP.
Použij po 01_prihlaseni.py (ten uloží cpzp_cookies.json).
Co dělá:
- Načte cookies z cpzp_cookies.json
- Otevře prohlížeč jednou, projde všechny zadané měsíce
- Pro každý měsíc vyplní formulář, klikne Hledat, stáhne soubor
- Přeskočí měsíce kde soubor v cílovém adresáři už existuje
- Uloží jako: YYYY-MM-DD f205MMRR.123
NASTAVENÍ:
OD_MESIC / OD_ROK — první měsíc rozsahu
DO_MESIC / DO_ROK — poslední měsíc rozsahu (včetně)
"""
import glob
import json
import os
import sys
import time
from datetime import date
from playwright.sync_api import sync_playwright
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
from Knihovny.najdi_dropbox import get_dropbox_root
OD_MESIC = 12
OD_ROK = 2024
DO_MESIC = 3
DO_ROK = 2026
BASE_URL = "https://portal.cpzp.cz"
COOKIES_FILE = os.path.join(os.path.dirname(__file__), "..", "..", "StahováníZpráv", "205 ČPZP", "cpzp_cookies.json")
DEST_DIR = os.path.join(
get_dropbox_root(),
"Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "205 ČPZP",
)
def mesice_v_rozsahu(od_m, od_r, do_m, do_r):
"""Generuje (mesic, rok) od od_m/od_r do do_m/do_r včetně."""
m, r = od_m, od_r
while (r, m) <= (do_r, do_m):
yield m, r
m += 1
if m > 12:
m = 1
r += 1
def uz_stazeno(mesic: int, rok: int) -> bool:
"""Vrátí True pokud soubor pro daný měsíc/rok už existuje v DEST_DIR."""
mm = f"{mesic:02d}"
rr = str(rok)[-2:]
pattern = os.path.join(DEST_DIR, f"* f205{mm}{rr}.*")
return bool(glob.glob(pattern))
def stahni_mesic(page, mesic: int, rok: int) -> bool:
"""Stáhne soubor pro jeden měsíc. Vrátí True pokud staženo."""
today = date.today().strftime("%Y-%m-%d")
if uz_stazeno(mesic, rok):
print(f" [{mesic:02d}/{rok}] přeskočeno — soubor už existuje")
return False
# Vyplň formulář
inputs = page.query_selector_all("input[type=text]")
if len(inputs) < 2:
print(f" [{mesic:02d}/{rok}] CHYBA — inputy nenalezeny")
return False
inputs[0].fill(str(mesic))
inputs[1].fill(str(rok))
page.get_by_text("Hledat", exact=True).click()
page.wait_for_load_state("networkidle")
dl_selector = "a:has-text('Seznam registrovaných pojištěnců')"
if not page.query_selector(dl_selector):
print(f" [{mesic:02d}/{rok}] CHYBA — download odkaz nenalezen")
return False
with page.expect_download() as dl_info:
page.click(dl_selector)
download = dl_info.value
original_name = download.suggested_filename
dest_path = os.path.join(DEST_DIR, f"{today} {original_name}")
download.save_as(dest_path)
print(f" [{mesic:02d}/{rok}] OK — {os.path.basename(dest_path)}")
return True
def hlavni() -> None:
if not os.path.exists(COOKIES_FILE):
raise SystemExit(f"Soubor s cookies nenalezen: {COOKIES_FILE}\nNejdřív spusť 01_prihlaseni.py")
with open(COOKIES_FILE, encoding="utf-8") as f:
cookies = json.load(f)
os.makedirs(DEST_DIR, exist_ok=True)
mesice = list(mesice_v_rozsahu(OD_MESIC, OD_ROK, DO_MESIC, DO_ROK))
print(f"Celkem měsíců: {len(mesice)} ({OD_MESIC:02d}/{OD_ROK} {DO_MESIC:02d}/{DO_ROK})")
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
context.add_cookies(cookies)
page = context.new_page()
print("Otevírám stránku klientely...")
page.goto(f"{BASE_URL}/app/prohlizeni-klientely/")
page.wait_for_load_state("networkidle")
if "frmPrihlasCert" in page.content():
raise SystemExit("Cookies expirovala — nejdřív spusť 01_prihlaseni.py")
stazeno = 0
for mesic, rok in mesice:
if stahni_mesic(page, mesic, rok):
stazeno += 1
time.sleep(2)
browser.close()
print(f"\nHotovo: {stazeno} staženo, {len(mesice) - stazeno} přeskočeno.")
if __name__ == "__main__":
hlavni()
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -0,0 +1,756 @@
<!DOCTYPE html><html lang="cs"><head>
<meta charset="utf-8">
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<title>
Prohlížení klientely - E-přepážka ČPZP
</title>
<!-- Libraries -->
<script>
CPZP = {
settings : {
certificateLoginKey : 'Prohlášení:'+ String.fromCharCode(13, 10) + 'Tímto se přihlašuji k Portálu ČPZP'+ String.fromCharCode(13, 10) + ''+ String.fromCharCode(13, 10) + 'Okamžik vygenerování tohoto prohlášení: 03.05.2026 10:37:40',
maxHeightSelectDropDown : null
},
runtimeConfig: {
logged : '1',
loggedUser: 'Michaela Buzalková',
useCertificate : true },
signerIsLoaded : false
};
</script>
<script type="text/javascript" src="/app/js/util.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/SimpleEventBroker.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/json3.min.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/lib/jquery/jquery-3.7.1.min.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/lib/jquery/jquery-migrate-3.4.1.min.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/lib/jquery/jquery-ui-1.13.2.min.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/jquery.ui.datepicker-cs.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/jquery.nicefileinput.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/jquery.loadmask.min.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/jquery.timer.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/jquery.cookie.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/lib_java_sign.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/lib_signer_utf8.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/browser.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/menu.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/help.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/form.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/generate-js/analytics.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/toastr.min.js?v=1v2.162.0"></script>
<script type="text/javascript" src="/app/js/prohlizeniklientely/list.js?v=1v2.162.0"></script>
<link rel="stylesheet" href="/app/css/style.css?v=v2.162.0" type="text/css">
<link rel="stylesheet" href="/app/css/style-print.css?v=v2.162.0" type="text/css" media="print">
<link rel="stylesheet" href="/app/extcss/jquery-ui-1.13.2.min.css?v=v2.162.0" type="text/css">
<link rel="stylesheet" href="/app/css/all.css?v=v2.162.0" type="text/css">
<link rel="stylesheet" href="/app/css/solid.css?v=v2.162.0" type="text/css">
<link rel="stylesheet" href="/app/extcss/toastr.min.css?v=v2.162.0" type="text/css">
<link rel="shortcut icon" href="/images/favicon.ico?v=v2.162.0">
<script>
/* (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-46493716-1', 'cpzp.cz');
ga('send', 'pageview'); */
</script>
</head>
<body>
<div class="main">
<div class="header">
<div class="logo-info">
<div class="logo">
<a href="/" class="logo" alt="Česká průmyslová zdravotní pojištovna" title="Úvodní stránka"></a>
<div class="code">
<div class="text">kód pojišťovny: </div>
<div class="number">205</div>
</div>
</div>
<div class="info-panel">
<div><span>Uživatel:</span>Michaela Buzalková</div>
<div class="message-state"><span>Nepřečtených zpráv:</span><a href="/app/schranka/">0</a></div>
<div class="current-date" style="display: none"><span>Datum:</span>3.5.2026 10:40:30</div>
<div class="logout">
<a href="/app/logout/?new=1" onclick="sessionStorage.clear();">Odhlásit</a>
</div>
</div>
<div class="search-box">
<div class="head-line">Elektronická přepážka ČPZP</div>
<div class="message">Právě se nacházíte v Portálu ČPZP. Kliknutím zvolíte Portál:</div>
<div class="zone-href">
<a href="/app/redirect/?destination=ozp">OZP</a> | <a href="/app/redirect/?destination=rbp">RBP</a> | <a href="/app/redirect/?destination=vozp">VoZP ČR</a> | <a href="/app/redirect/?destination=zpskoda">ZPŠ</a> | <a href="/app/redirect/?destination=spol">Společná zóna</a>
</div>
</div>
<div class="clearboth"></div>
</div>
</div>
<div class="clearboth"></div>
<div class="menu" role="navigation" aria-label="hlavní menu">
<ul class="navigation">
<li class="menu-main-?vod" id="menu1">
<a href="/app/" class="level1">úvod</a>
</li>
<li class="menu-main-poji?t?nci" id="menu2">
<a href="/app/pojistenci-rozcestnik/" class="level1"><span>pojištěnci</span></a>
<ul>
<li class="menu-main-osobn?nastaven?">
<a href="/app/osobni-nastaveni-rozcestnik/" class="level2">Osobní nastavení<img src="/app/img/dot-white.png" style="float:right;margin:3px 0 0 0"></a>
<ul class="submenu2">
<li class="menu-main-zm?naadresyakontaktn?ch?daj?">
<a href="/app/zmena-adresy-a-kontaktnich-udaju/pojistenec/">Změna adresy a kontaktních údajů</a>
</li>
<li class="sub-disabled menu-main-bankovn???ty">
<a href="/app/bankovni-ucty/">Bankovní účty</a>
</li>
<li class="menu-main-mojeopr?vn?n?">
<a href="/app/prehled-opravneni/pojistenec/">Moje oprávnění</a>
</li>
</ul>
</li>
<li class="menu-main-preventivn?programy">
<a href="/app/preventivni-programy-rozcestnik/" class="level2">Preventivní programy<img src="/app/img/dot-white.png" style="float:right;margin:3px 0 0 0"></a>
<ul class="submenu2">
<li class="sub-disabled menu-main-registracedobonus+">
<a href="/app/bonus-plus-registrace/">Registrace do Bonus +</a>
</li>
<li class="sub-disabled menu-main-v?piskontabonus+">
<a href="/app/bonus-plus-vypis/">Výpis konta Bonus +</a>
</li>
<li class="sub-disabled menu-main-propl?cen?preventivn?chprogram?">
<a href="/app/proplaceni-preventivnich-programu/">Proplácení preventivních programů</a>
</li>
<li class="menu-main-informaceopreventivn?chprogramech">
<a href="https://www.cpzp.cz/programy/" target="_self">Informace o preventivních programech</a>
</li>
</ul>
</li>
<li class="menu-main-preventivn?prohl?dky">
<a href="/app/preventivni-prohlidky-rozcestnik/" class="level2">Preventivní prohlídky<img src="/app/img/dot-white.png" style="float:right;margin:3px 0 0 0"></a>
<ul class="submenu2">
<li class="sub-disabled menu-main-p?ehledpreventivn?chprohl?dek">
<a href="/app/preventivni-prohlidky/">Přehled preventivních prohlídek</a>
</li>
<li class="sub-disabled menu-main-zas?l?n?preventivn?chsms">
<a href="/app/prevence-v-mobilu/">Zasílání preventivních SMS</a>
</li>
</ul>
</li>
<li class="sub-disabled menu-main-??dostopr?kazpoji?t?nce">
<a href="/app/prukaz-pojistence/" class="level2">Žádost o průkaz pojištěnce</a>
</li>
<li class="sub-disabled menu-main-osobn???etzdravotn?p??e">
<a href="/app/osobni-ucet-pojistence/" class="level2">Osobní účet zdravotní péče</a>
</li>
<li class="sub-disabled menu-main-z?znamodlouhodob?mpobytuvcizin?">
<a href="/app/zaznam-o-dlouhodobem-pobytu-v-cizine/" class="level2">Záznam o dlouhodobém pobytu v cizině</a>
</li>
<li class="menu-main-pojistn?">
<a href="/app/pojistne-rozcestnik/" class="level2">Pojistné<img src="/app/img/dot-white.png" style="float:right;margin:3px 0 0 0"></a>
<ul class="submenu2">
<li class="sub-disabled menu-main-stavpojistn?ho">
<a href="/app/stav-pojistneho/">Stav pojistného<img src="/app/img/dot-white.png" style="float:right;margin:3px 0 0 0"></a>
<ul>
<li class="menu-main-??dostovr?cen?p?eplatk?">
<a href="/app/stav-pojistneho/preplatek">Žádost o vrácení přeplatků</a>
</li>
<li class="menu-main-??dostop?e??tov?n?platby">
<a href="/app/stav-pojistneho/platba">Žádost o přeúčtování platby</a>
</li>
<li class="menu-main-??dostopotvrzen?obezdlu?nosti">
<a href="/app/stav-pojistneho/bezdluznost">Žádost o potvrzení o bezdlužnosti</a>
</li>
<li class="menu-main-??dostov?pispohled?vekaz?vazk?">
<a href="/app/stav-pojistneho/vypis">Žádost o výpis pohledávek a závazků</a>
</li>
</ul>
</li>
<li class="sub-disabled menu-main-pl?tcipojistn?ho">
<a href="/app/zadost-o-prehled-platcu-pojistneho/">Plátci pojistného</a>
</li>
<li class="sub-disabled menu-main-osv?-pod?n?p?ehleduzarok">
<a href="/app/prehled-osvc/2025/">OSVČ - podání přehledu za rok</a>
</li>
<li class="sub-disabled menu-main-osv?-p?ehledp?ijat?chplateb">
<a href="/app/prehled-plateb-osvc/">OSVČ - přehled přijatých plateb</a>
</li>
<li class="sub-disabled menu-main-osv?-??dostosn??en?z?loh">
<a href="/app/snizeni-zaloh-osvc/">OSVČ - žádost o snížení záloh</a>
</li>
</ul>
</li>
</ul>
</li>
<li class="disabled menu-main-zam?stnavatel?" id="menu3">
<a href="/app/zamestnavatele-rozcestnik/" class="level1">zaměstnavatelé</a>
<ul>
<li class="sub-disabled menu-main-hromadn?ozn?men?zam?stnavatele">
<a href="/app/hromadne-oznameni-zamestnavatele/" class="level2">Hromadné oznámení zaměstnavatele</a>
</li>
<li class="sub-disabled menu-main-p?ehledoplatb?pojistn?ho">
<a href="/app/prehled-o-platbe-pojistneho/" class="level2">Přehled o platbě pojistného</a>
</li>
<li class="sub-disabled menu-main-??dostoseznamzam?stnanc?">
<a href="/app/zadost-o-seznam-zamestnancu/" class="level2">Žádost o seznam zaměstnanců</a>
</li>
<li class="sub-disabled menu-main-zm?naadresyakontaktn?ch?daj?">
<a href="/app/zmena-adresy-a-kontaktnich-udaju/zamestnavatel/" class="level2">Změna adresy a kontaktních údajů</a>
</li>
<li class="sub-disabled menu-main-stavpojistn?ho">
<a href="/app/zamestnavatele-stav-pojistneho/" class="level2">Stav pojistného</a>
</li>
<li class="sub-disabled menu-main-mojeopr?vn?n?">
<a href="/app/prehled-opravneni/zamestnavatel/" class="level2">Moje oprávnění</a>
</li>
</ul>
</li>
<li class="active menu-main-poskytovatel?zdrslu?eb" id="menu4">
<a href="/app/pzs-rozcestnik/" class="level1"><span>poskytovatelé zdr. služeb</span></a>
<ul>
<li class="sub-disabled menu-main-?pzppro">
<a href="/app/cpzp-pro/" class="level2">ČPZP PRO</a>
</li>
<li class="menu-main-schr?nkapzs">
<a href="/app/schranka-pzs/" class="level2">Schránka PZS</a>
</li>
<li class="menu-main-profilpzs">
<a href="/app/profil-pzs/" class="level2">Profil PZS</a>
</li>
<li class="menu-main-odesl?n?vy??tov?n?">
<a href="/app/odeslani-vyuctovani/" class="level2">Odeslání vyúčtování</a>
</li>
<li class="menu-main-odeslanivy??tov?n?antigenn?chtest?">
<a href="/app/odeslani-vyuctovani/antigenni-testy/" class="level2">Odeslani vyúčtování antigenních testů</a>
</li>
<li class="menu-main-odesl?n?registra?n?chl?stk?">
<a href="/app/registracni-listky/" class="level2">Odeslání registračních lístků</a>
</li>
<li class="menu-main-odesl?n?hromadn?sn??en?cen">
<a href="/app/hromadne-snizeni-cen/" class="level2">Odeslání hromadné snížení cen</a>
</li>
<li class="menu-main-odesl?n?l?ze?sk?chn?vrh?">
<a href="/app/lazenske-navrhy/" class="level2">Odeslání lázeňských návrhů</a>
</li>
<li class="menu-main-ov??en?poji?t?nce">
<a href="https://www.cpzp.cz/ehic/" target="_blank" class="level2">Ověření pojištěnce</a>
</li>
<li class="menu-main-ov??en?registruj?c?hopzs">
<a href="/app/overeni-registrujiciho-pzs/" class="level2">Ověření registrujícího PZS</a>
</li>
<li class="menu-main-prohl??en?/stornofaktur">
<a href="/app/prohlizeni-faktur/" class="level2">Prohlížení / Storno faktur</a>
</li>
<li class="menu-main-prohl??en?plateb">
<a href="/app/prohlizeni-plateb/" class="level2">Prohlížení plateb</a>
</li>
<li class="menu-main-prohl??en?vy??tov?n?zaobdob?">
<a href="/app/prohlizeni-vyuctovani/" class="level2">Prohlížení vyúčtování za období</a>
</li>
<li class="menu-main-prohl??en?zulp">
<a href="/app/prohlizeni-zulp/" class="level2">Prohlížení ZULP</a>
</li>
<li class="active menu-main-prohl??en?klientely">
<a href="/app/prohlizeni-klientely/" class="level2">Prohlížení klientely</a>
</li>
<li class="sub-disabled menu-main-pzs-osobn???etpoji?t?nce">
<a href="/app/pzs-osobni-ucet-pojistence/" class="level2">PZS - osobní účet pojištěnce</a>
</li>
<li class="sub-disabled menu-main-statistiky-n?kladyzdrslu?eb">
<a href="/app/dikap/statistika/" class="level2">Statistiky - náklady zdr. služeb</a>
</li>
<li class="menu-main-parametrydohodyocen?">
<a href="/app/dikap/dohody/" class="level2">Parametry dohody o ceně</a>
</li>
<li class="menu-main-mojeopr?vn?n?">
<a href="/app/prehled-opravneni/pzs/" class="level2">Moje oprávnění</a>
</li>
</ul>
</li>
<li class="menu-main-servis" id="menu5">
<a href="/app/servis-rozcestnik/" class="level1"><span>servis</span></a>
<ul>
<li class="menu-main-schr?nkaklienta">
<a href="/app/schranka/" class="level2">Schránka klienta</a>
</li>
<li class="menu-main-elektronick?podatelna">
<a href="/app/elektronicka-podatelna/" class="level2">Elektronická podatelna</a>
</li>
<li class="menu-main-p?ehledopr?vn?n?">
<a href="/app/prehled-opravneni/vse/" class="level2">Přehled oprávnění</a>
</li>
<li class="menu-main-p?ehledsouhlas?">
<a href="/app/prehled-souhlasu/" class="level2">Přehled souhlasů</a>
</li>
<li class="menu-main-aktiva?n?k?d(opr?vn?n?)">
<a href="/app/aktivacni-kod/" class="level2">Aktivační kód (oprávnění)</a>
</li>
<li class="menu-main-historiepod?n?">
<a href="/app/historie-podani/" class="level2">Historie podání</a>
</li>
<li class="menu-main-prohl??en?certifik?t?">
<a href="/app/prohlizeni-certifikatu/" class="level2">Prohlížení certifikátů</a>
</li>
<li class="menu-main-p?id?n?certifik?tu">
<a href="/app/pridani-certifikatu/" class="level2">Přidání certifikátu</a>
</li>
<li class="menu-main-zm?nakontaktn?ch?daj?">
<a href="/app/zmena-osobnich-udaju/" class="level2">Změna kontaktních údajů</a>
</li>
<li class="menu-main-zm?nasmsp?ihla?ov?n?">
<a href="/app/zmena-sms-konta/" class="level2">Změna SMS přihlašování</a>
</li>
<li class="menu-main-potvrzen?emailov?adresy">
<a href="/app/potvrzeni-emailu/" class="level2">Potvrzení emailové adresy</a>
</li>
</ul>
</li>
<li class="menu-main-informace" id="menu6">
<a href="/app/informace-rozcestnik/" class="level1"><span>informace</span></a>
<ul>
<li class="menu-main-aktuality">
<a href="/app/aktuality/" class="level2">aktuality</a>
</li>
<li class="menu-main-registracedoe-p?ep??ky">
<a href="/app/registrace-klienta/" class="level2">registrace do e-přepážky</a>
</li>
<li class="menu-main-jaksep?ihl?sit">
<a href="/app/clanek/jak-se-prihlasit/" class="level2">jak se přihlásit</a>
</li>
<li class="menu-main-?astokladen?ot?zky(faq)">
<a href="/app/clanek/casto-kladene-otazky/" class="level2">často kladené otázky (FAQ)</a>
</li>
<li class="menu-main-certifika?n?autority">
<a href="/app/clanek/certifikacni-autority/" class="level2">certifikační autority</a>
</li>
<li class="menu-main-ochranaosobn?ch?daj?">
<a href="/app/clanek/ochrana-osobnich-udaju/" class="level2">ochrana osobních údajů</a>
</li>
<li class="menu-main-e-podatelna?pzp">
<a href="/app/clanek/elektronicka-podatelna-cpzp/" class="level2">e-podatelna ČPZP</a>
</li>
<li class="menu-main-smlouvysposkytovatelizdravotn?chslu?eb">
<a href="/app/smlouvy-s-pzs/" class="level2">Smlouvy s poskytovateli zdravotních služeb</a>
</li>
</ul>
</li>
<li class="menu-main-n?pov?da" id="menu7">
<a href="/app/napoveda/" class="level1">nápověda</a>
</li>
<li class="menu-main-web?pzp" id="menu8">
<a href="http://www.cpzp.cz" class="level1">web ČPZP</a>
</li>
</ul> </div>
<div class="clearboth"></div>
<div class="content">
<div>
<div class="breadcrumb">
<a href="/app/">úvod</a><span class="separator"></span><a href="/app/pzs-rozcestnik/">poskytovatelé zdr. služeb</a><span class="separator"></span>Prohlížení klientely </div>
<div class="spacer" style="height: 20px;"></div>
<h1>Prohlížení klientely</h1>
<p>Aktuální klientela je zobrazována vždy až po interní uzávěrce pro Kapitační centrum, tzn. s cca měsíčním zpožděním oproti aktuálnímu datu.</p>
<form action="" method="post" name="prohlizeni-klientely" id="prohlizeni-klientely"><input type="hidden" name="csrf" value="8029b24e0b7c3607ed34303c892ff308" style=""><script>
CPZP.icpSort = '';
</script>
<fieldset>
<h3>Filtrování</h3>
<div class="field top">
<div class="label" style="width: 70px;">IČZ:</div>
<div class="input w600">
<select name="icz" id="icz"><option value="09305000">09305000 - MUDr. Michaela Buzalková</option></select> </div>
</div>
<div class="clearboth"></div>
<p></p>
<div class="field top" style="float: left; width: 46%">
<div class="label" style="width: 70px;">Měsíc:</div>
<div class="input w100">
<input name="mesic" type="text" id="mesic" value="3" style=""> </div>
<div class="clearboth"></div>
</div>
<div class="field top" style="float: left; width: 50%">
<div class="label" style="width: 70px;">Rok:</div>
<div class="input w200">
<input name="rok" type="text" id="rok" value="2026" style=""> </div>
<div class="clearboth"></div>
</div>
<div class="clearboth"></div>
<div class="field top" style="float: left; width: 46%">
<div class="label" style="width: 70px;">IČP:</div>
<div class="input w300">
<select name="icp" id="icp" class=""><option value="">---</option><option value="null"> - </option></select> </div>
</div>
<div class="field top" style="float: left; width: 50%">
<div class="label" style="width: 70px;">Řadit dle:</div>
<div class="input w200">
<select name="sortBy" id="sortBy"><option value="ICP_RC" selected="selected">IČP</option>
<option value="RC">Číslo pojištěnce</option>
<option value="PRIJMENI_JMENO">Příjmení a jméno</option></select> </div>
</div>
<div class="clearboth"></div>
<div class="top">
<div class="center">
<div class="red-btn">
<div class="wrap"><input name="submitbutton" type="submit" id="submitbutton" value="Hledat" class="" style=""></div>
</div>
</div>
</div>
<div class="clearboth"></div>
</fieldset>
</form><p></p>
<div class="action-panel navigation">
<a href="javascript:;" id="print-to-html" class="print-to-html-no-text x-href-btn" style="width: 47px;">tisknout sestavu</a>
<form action="" method="post" target="_blank" id="export-form" name="export-form">
<input type="hidden" name="icz" id="export-icz" style="">
<input type="hidden" name="icp" id="export-icp" style="">
<input type="hidden" name="mesic" id="export-mesic" style="">
<input type="hidden" name="rok" id="export-rok" style="">
<input type="hidden" name="sortBy" id="export-sortBy" style="">
<input type="hidden" name="csrf" value="8029b24e0b7c3607ed34303c892ff308" style=""> </form>
<a href="#" class="export-to-csv-no-text x-href-btn" style="width: 205px;" id="export-to-vzp">Seznam registrovaných pojištěnců ve formátu podle datového rozhraní VZP</a>
<div class="navigation">
&nbsp;&nbsp;Celkem 15 záznamů </div>
<div class="clearboth"></div>
</div>
<p></p>
<table class="list">
<thead>
<tr class="odd">
<th class="center">IČP</th>
<th class="right">Odbornost</th>
<th class="right">Číslo pojištěnce</th>
<th class="left">Příjmení</th>
<th class="left">Jméno</th>
<th class="center">Registrace od</th>
<th class="center">Registrace do</th>
</tr>
</thead>
<tbody>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">0105072528</td>
<td class="left">Vinický</td>
<td class="left">Ondřej</td>
<td class="center">01.03.2026</td>
<td class="center"></td>
</tr>
<tr class="odd">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">0301214925</td>
<td class="left">Štefanský</td>
<td class="left">Daniel</td>
<td class="center">01.05.2025</td>
<td class="center"></td>
</tr>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">0657650510</td>
<td class="left">Krehul</td>
<td class="left">Valeriia</td>
<td class="center">01.02.2026</td>
<td class="center"></td>
</tr>
<tr class="odd">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">435614435</td>
<td class="left">Strnadová</td>
<td class="left">Vítězslava</td>
<td class="center">01.07.2025</td>
<td class="center"></td>
</tr>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">446228471</td>
<td class="left">Feoktistova</td>
<td class="left">Natalia</td>
<td class="center">01.09.2018</td>
<td class="center"></td>
</tr>
<tr class="odd">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">6504141149</td>
<td class="left">Bečica</td>
<td class="left">Josef</td>
<td class="center">01.05.2010</td>
<td class="center"></td>
</tr>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">6510130792</td>
<td class="left">Šuhaj</td>
<td class="left">Petr</td>
<td class="center">01.06.2013</td>
<td class="center"></td>
</tr>
<tr class="odd">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">6758120446</td>
<td class="left">Bečicová</td>
<td class="left">Markéta</td>
<td class="center">01.05.2010</td>
<td class="center"></td>
</tr>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">6861010288</td>
<td class="left">Štefanská</td>
<td class="left">Renáta</td>
<td class="center">01.03.2025</td>
<td class="center"></td>
</tr>
<tr class="odd">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">7909054780</td>
<td class="left">Babáček</td>
<td class="left">Marek</td>
<td class="center">01.02.2017</td>
<td class="center"></td>
</tr>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">8509170802</td>
<td class="left">Neumann</td>
<td class="left">Jakub</td>
<td class="center">01.09.2015</td>
<td class="center"></td>
</tr>
<tr class="odd">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">8554125360</td>
<td class="left">Grygarová</td>
<td class="left">Jana</td>
<td class="center">01.04.2011</td>
<td class="center"></td>
</tr>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">9355042466</td>
<td class="left">Bečicová</td>
<td class="left">Tereza</td>
<td class="center">01.03.2012</td>
<td class="center"></td>
</tr>
<tr class="odd">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">9355071297</td>
<td class="left">Dobrohrušková</td>
<td class="left">Lucie</td>
<td class="center">01.11.2014</td>
<td class="center"></td>
</tr>
<tr class="even">
<td class="center">09305001</td>
<td class="right">001</td>
<td class="right">9651301253</td>
<td class="left">Kut Citores</td>
<td class="left">Markéta</td>
<td class="center">01.11.2021</td>
<td class="center"></td>
</tr>
</tbody>
</table>
<p></p>
<script>
$(document).ready(function() {
analyticsPzs($('#icz').val(), 'PZS_PROHLIZENI_KLIENTELY');
});
</script>
</div>
<div class="clearboth"></div>
</div>
</div>
<div class="footer">
<p class="copy">© ČPZP | Infocentrum: 810 800 000 | <a href="http://www.cpzp.cz/pobocky/">Pobočky ČPZP</a> | <a href="#" class="show-cookies">Cookies</a>
</p>
</div>
<section class="cookie-policy">
<article class="base cookie-container">
<div>
<h2>Povolení cookies</h2>
<p>V ČPZP používáme cookies a jiné technologie za účelem poskytování našich služeb, vylepšení
vašeho uživatelského zážitku, analýzy používání
našich stránek a při cílení reklamy. </p>
</div>
<div class="cookie-buttons">
<div class="red-btn disabled cookie-setup"><div class="wrap"><a style="color: rgb(19, 35, 57)" href="#">Nastavit cookies</a></div></div>
<div class="red-btn disabled cookie-deny"><div class="wrap"><a style="color: rgb(19, 35, 57)" href="#">Odmítnout vše kromě nutných</a></div></div>
<div class="red-btn cookie-accept"><div class="wrap"><a href="#">Přijmout vše</a></div></div>
</div>
</article>
<article class="detail cookie-container none">
<div class="cookie-detail">
<h2 style="grid-area: h">Nastavení cookies</h2>
<p style="grid-area: p1">V ČPZP používáme cookies a jiné technologie za účelem poskytování našich služeb, vylepšení
vašeho uživatelského zážitku, analýzy používání
našich stránek a při cílení reklamy. </p>
<p style="grid-area: p2">Vyberte vámi preferované povolení cookie, přičemž <b>základní jsou nezbytné pro fungování</b>, jiné můžeme používat jen s vaším souhlasem.
<br>
Vaše osobní údaje budou zpracovány a informace z vašeho zařízení (soubory cookie,
jidinečné identifikátory a další údaje zařízená) mohou být uchovávány.
<br>
Vaše preference můžete kdykoliv <b>změnit v dolní části naší webové stránky
s názvem Cookies</b>. Pro více informací o používání cookies prosím naštivte
<a href="/app/clanek/ochrana-osobnich-udaju/" target="__blank">Zásady ochrany osobních údajů</a>
.</p>
<div style="grid-area: b1; justify-self: center;" class="red-btn disabled cookie-deny"><div class="wrap"><a style="color: rgb(19, 35, 57)" href="#">Odmítnout vše kromě nutných</a></div></div>
<div style="grid-area: b2; justify-self: center;" class="red-btn cookie-accept-selected"><div class="wrap"><a href="#">Souhlasím a uložit nastavení</a></div></div>
</div>
<div class="cookie-options">
<label for="zakladni"><input type="checkbox" id="zakladni" value="1" checked="" disabled="" style="">Základní <span>Nezbytné pro správné fungování webu.</span></label>
<label for="analyticke"><input type="checkbox" id="analyticke" value="2" style="">Analytické <span>Umožňují měření výkonu webu a reklamních kampaní.</span></label>
<label for="preferencni"><input type="checkbox" id="preferencni" value="4" style="">Preferenční <span>Slouží k přizpůsobení potřeb a zájmů</span></label>
<label for="reklamni"><input type="checkbox" id="reklamni" value="8" style="">Reklamní <span>Slouží k zobrazení vhodného obsahu nebo reklamy, jak
na našich stránkách, tak na stránkách třetích subjektů.</span></label>
</div>
</article>
</section>
<div id="ie-warning-dialog" style="display: none;">
<p>
Používáte zastaralý prohlížeč Microsoft Internet Explorer. Z bezpečnostních a výkonových důvodů Vám důrazně doporučujeme přechod na modernější prohlížeč - např. Google Chrome, Microsoft Edge či Mozilla Firefox. Jedním z důvodů je i skutečnost, že společnost Microsoft již avizovala, že k 15.6.2022 ukončuje podporu Microsoft Internet Exploreru.
</p>
</div>
<script>
$(document).ready(function() {
const dialog = $("#ie-warning-dialog");
// MSIE for IE version <=10, trident/ for IE 11
if ((navigator.userAgent.indexOf('MSIE') > -1 || navigator.appVersion.indexOf('Trident/') > -1) && !sessionStorage.visited) {
dialog.dialog({
modal: true,
title: 'Upozornění na zastaralý prohlížeč',
show: {
effect: 'fold',
duration: 400
},
hide: {
effect: 'fold',
duration: 200
},
width: '50%',
buttons: {
'Zavřít': function () {
dialog.dialog('close');
}
}
});
sessionStorage.visited = true;
}
});
</script>
<script>
const CONSENT_KEY = 'cookieConsent';
function getCookie(cKey) {
const key = cKey + "=";
const decoded = decodeURIComponent(document.cookie);
const allCookies = decoded .split('; ');
let res;
allCookies.forEach(function (val) {
if (val.indexOf(key) === 0) res = val.substring(key.length);
})
return res;
}
function setCookie(cKey, value, duration) {
let date = new Date();
date.setTime(date.getTime() + (30 * 24 * 60 * 60 * 1000)); // days * hours * minutes * seconds * milliseconds
const expires = "expires=" + date.toUTCString();
document.cookie = cKey + "=" + value + "; " + expires + "; path=/";
}
function hideCookieConsent() {
if (!$('.cookie-policy .base').hasClass('none')) {
$('.cookie-policy .base').addClass('none');
}
if (!$('.cookie-policy > .detail').hasClass('none')) {
$('.cookie-policy > .detail').addClass('none');
}
if (!$('.cookie-policy').hasClass('none')) {
$('.cookie-policy').addClass('none');
}
}
function setConsentCheckboxes() {
const cookieConsent = parseInt(getCookie(CONSENT_KEY));
const binConsent = cookieConsent.toString(2);
if (binConsent.charAt(binConsent.length - 2) === '1') {
$("#analyticke").prop('checked', true);
}else {
$("#analyticke").prop('checked', false);
}
if (binConsent.charAt(binConsent.length - 3) === '1') {
$("#preferencni").prop('checked', true);
}else {
$("#preferencni").prop('checked', false);
}
if (binConsent.charAt(binConsent.length - 4) === '1') {
$("#reklamni").prop('checked', true);
}else {
$("#reklamni").prop('checked', false);
}
}
$(document).ready(function() {
const cookieConsent = getCookie(CONSENT_KEY);
if (!cookieConsent) {
$('.cookie-policy .base').removeClass('none');
$('.cookie-policy').removeClass('none');
}else {
const val = parseInt(cookieConsent);
const bin = val.toString(2);
if (bin.charAt(bin.length - 2) === '1') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-46493716-1', 'cpzp.cz');
ga('send', 'pageview');
}
}
$(document).on('click', '.show-cookies', function(e) {
e.preventDefault();
$('.cookie-policy .base').removeClass('none');
$('.cookie-policy').removeClass('none');
});
$(document).on('click', '.cookie-setup', function(e) {
e.preventDefault();
setConsentCheckboxes();
$('.cookie-policy .base').addClass('none');
$('.cookie-policy > .detail').removeClass('none');
});
$(document).on('click', '.cookie-accept', function(e) {
e.preventDefault();
setCookie(CONSENT_KEY, '15');
hideCookieConsent();
});
$(document).on('click', '.cookie-deny', function(e) {
e.preventDefault();
setCookie(CONSENT_KEY, '1');
hideCookieConsent();
});
$(document).on('click', '.cookie-accept-selected', function(e) {
e.preventDefault();
let consentValue = 1;
consentValue += $('#analyticke').is(":checked") ? parseInt($('#analyticke').val()) : 0;
consentValue += $('#preferencni').is(":checked") ? parseInt($('#preferencni').val()) : 0;
consentValue += $('#reklamni').is(":checked") ? parseInt($('#reklamni').val()) : 0;
setCookie(CONSENT_KEY, consentValue.toString());
hideCookieConsent();
});
});
</script>
<div id="x-portal-0" class="help-box" style="right: 10px;"><div class="help-box-wrap"><span>NÁPOVĚDA</span></div></div><div id="x-portal-1" class="help-tooltip" style="right: 10px;"><a hre="javascript:;"></a><div class="help-tooltip-content">Pokud si nevíte s touto funkcí rady, zkuste se podívat na naši nápovědu</div><div class="arrow bottom center"></div></div></body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -1,7 +1,7 @@
"""
Přihlásí se na VZP Point a stáhne nové zprávy.
Přihlásí se na VZP Point, stáhne nové zprávy a aktualizuje číselníky.
Kombinuje 01_prihlaseni.py + 03_stahuj_nove.py do jednoho spuštění.
Kombinuje 01_prihlaseni.py + 03_stahuj_nove.py + 01_stahni_ciselniky.py.
Přihlášení probíhá plně automaticky (Chrome auto-vybere certifikát).
POUŽITÍ:
@@ -13,23 +13,32 @@ import sys
import os
DIR = os.path.dirname(os.path.abspath(__file__))
CISELNIKY_SCRIPT = os.path.abspath(
os.path.join(DIR, "..", "..", "..", "Recepty", "StahovánízVZPWithClaude", "01_stahni_ciselniky.py")
)
def run(script: str) -> None:
result = subprocess.run(
[sys.executable, os.path.join(DIR, script)],
[sys.executable, script],
check=False,
)
if result.returncode != 0:
raise SystemExit(f"Skript {script} skončil s chybou (kód {result.returncode})")
raise SystemExit(f"Skript {os.path.basename(script)} skončil s chybou (kód {result.returncode})")
def main() -> None:
print("=== Přihlášení ===")
run("01_prihlaseni.py")
run(os.path.join(DIR, "01_prihlaseni.py"))
print("\n=== Stahování nových zpráv ===")
run("03_stahuj_nove.py")
run(os.path.join(DIR, "03_stahuj_nove.py"))
print("\n=== Stahování odeslaných podání ===")
run(os.path.join(DIR, "stahovanipodani.py"))
print("\n=== Stahování číselníků VZP ===")
run(CISELNIKY_SCRIPT)
if __name__ == "__main__":
@@ -0,0 +1,265 @@
"""
Stáhni odeslaná podání z VZP Point (sekce „Odeslaná podání").
Načte Bearer token ze stránky Desk/FormDashboard, pak volá REST API /api/desk/form.
Stahuje podání s přiloženým výsledkovým souborem — přeskočí ty, co už existují.
Použití: python stahovanipodani.py [--dry-run]
"""
import json
import os
import re
import sys
import time
import winreg
try:
import requests as req_lib
except ImportError:
print("Chybí requests: pip install requests")
sys.exit(1)
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
from Knihovny.najdi_dropbox import get_dropbox_root
DASHBOARD_URL = "https://point.vzp.cz/Desk/FormDashboard"
API_BASE = "https://point.vzp.cz/api/desk/form"
PAGE_SIZE = 50
CHROME_PROFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "chrome_profile"))
COOKIES_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "vzp_cookies.json"))
DOWNLOAD_DIR = os.path.join(
get_dropbox_root(),
"Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "111 VZP Podání"
)
DRY_RUN = False
def load_cookies(context) -> int:
if not os.path.exists(COOKIES_FILE):
return 0
try:
with open(COOKIES_FILE, "r", encoding="utf-8") as f:
cookies = json.load(f)
context.add_cookies(cookies)
return len(cookies)
except Exception:
return 0
def save_cookies(context) -> int:
try:
all_cookies = context.cookies()
vzp = [c for c in all_cookies if "vzp.cz" in c.get("domain", "")]
with open(COOKIES_FILE, "w", encoding="utf-8") as f:
json.dump(vzp, f, indent=2, ensure_ascii=False)
return len(vzp)
except Exception:
return 0
CERT_ISSUER_CN = "I.CA Public CA/RSA 06/2022"
def _set_chrome_cert_policy() -> None:
policy = json.dumps({
"pattern": "https://[*.]vzp.cz",
"filter": {"ISSUER": {"CN": CERT_ISSUER_CN}},
})
key_path = r"SOFTWARE\Policies\Google\Chrome\AutoSelectCertificateForUrls"
try:
key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, key_path)
winreg.SetValueEx(key, "1", 0, winreg.REG_SZ, policy)
winreg.CloseKey(key)
print(f" Chrome politika nastavena (issuer: {CERT_ISSUER_CN})")
except Exception as e:
print(f" Varování: nelze nastavit Chrome politiku: {e}")
def extract_bearer_token(page) -> str | None:
"""Extrahuje Bearer token z inline <script> tagu vloženého do HTML stránky."""
scripts = page.evaluate(
"() => Array.from(document.querySelectorAll('script:not([src])')).map(s => s.textContent)"
)
for text in scripts:
m = re.search(r'"bearerToken"\s*:\s*"([^"]+)"', text)
if m:
return m.group(1)
return None
def fetch_all_forms(token: str) -> list[dict]:
headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
all_items: list[dict] = []
page_num = 1
while True:
url = f"{API_BASE}?pageNumber={page_num}&pageSize={PAGE_SIZE}"
r = req_lib.get(url, headers=headers, timeout=30)
r.raise_for_status()
data = r.json()
items = data.get("items", [])
all_items.extend(items)
print(f" Stránka {page_num}: {len(items)} podání (celkem {len(all_items)})")
if not data.get("canLoadMore", False):
break
page_num += 1
return all_items
def parse_date(iso: str) -> str:
return iso[:10] if iso else "0000-00-00"
def download_file(token: str, form_id: int, file_id: str, dest: str) -> bool:
# Krok 1: získej publicUri z API
meta_url = f"{API_BASE}/{form_id}/result/{file_id}"
try:
r = req_lib.get(meta_url, headers={"Authorization": f"Bearer {token}"}, timeout=30)
r.raise_for_status()
public_uri = r.json().get("publicUri")
if not public_uri:
print(f" Chyba: odpověď neobsahuje publicUri")
return False
except Exception as e:
print(f" Chyba načítání publicUri: {e}")
return False
# Krok 2: stáhni soubor přímo z publicUri (bez auth hlavičky)
try:
r = req_lib.get(public_uri, stream=True, timeout=60)
r.raise_for_status()
with open(dest, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return True
except Exception as e:
print(f" Chyba stahování souboru: {e}")
return False
def main() -> None:
dry_run = DRY_RUN or "--dry-run" in sys.argv
if dry_run:
print("[dry-run] Pouze zobrazuji co by se stáhlo, nic nestahuju.\n")
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Chybí playwright: pip install playwright && playwright install chrome")
sys.exit(1)
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
_set_chrome_cert_policy()
token = None
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir=CHROME_PROFILE,
channel="chrome",
headless=False,
slow_mo=100,
ignore_https_errors=True,
accept_downloads=True,
args=["--force-renderer-accessibility"],
)
try:
loaded = load_cookies(context)
print(f"Cookies načtené z JSON: {loaded}")
page = context.new_page()
print("Naviguji na VZP Point Odeslaná podání...")
try:
page.goto(DASHBOARD_URL, wait_until="domcontentloaded", timeout=30_000)
except Exception as e:
print(f"Navigace: {e}")
if page.url.startswith("https://auth.vzp.cz/signin"):
print("Přihlašovací stránka — klikám na 'Certifikát'...")
cert_btn = page.locator("a, button").filter(has_text=re.compile(r"certifikát", re.I)).first
cert_btn.wait_for(state="visible", timeout=10_000)
cert_btn.click(no_wait_after=True)
print("Pokud se zobrazí dialog výběru certifikátu, vyberte ho ručně (max 60 s)...")
time.sleep(60)
page = context.new_page()
try:
page.goto(DASHBOARD_URL, wait_until="domcontentloaded", timeout=30_000)
except Exception as e:
print(f"Navigace po auth: {e}")
if not page.url.startswith("https://point.vzp.cz"):
print(f"Přihlášení selhalo. URL: {page.url}")
return
print("Přihlášení OK.")
page.wait_for_load_state("networkidle", timeout=15_000)
token = extract_bearer_token(page)
if token:
print("Bearer token načten.")
else:
print("Nepodařilo se načíst Bearer token ze stránky.")
finally:
saved = save_cookies(context)
print(f"Uloženo {saved} VZP cookies.")
context.close()
if not token:
sys.exit(1)
print("\nNačítám seznam podání...")
try:
forms = fetch_all_forms(token)
except Exception as e:
print(f"Chyba načítání podání: {e}")
sys.exit(1)
existing = set(os.listdir(DOWNLOAD_DIR))
print(f"\nV archivu: {len(existing)} souborů.")
print(f"Celkem podání v API: {len(forms)}\n")
downloaded = 0
skipped = 0
no_file = 0
for form in forms:
result = form.get("result") or {}
result_file = result.get("resultFile") or {}
file_id = result_file.get("fileId")
orig_name = result_file.get("name", "")
if not file_id or not orig_name:
no_file += 1
continue
date_str = parse_date(form.get("created", ""))
filename = f"{date_str} {orig_name}"
state = form.get("state", "")
if filename in existing:
print(f"{filename}")
skipped += 1
continue
size = result_file.get("size", 0)
print(f"{filename} ({size:,} B) [{state}]")
if dry_run:
downloaded += 1
continue
dest = os.path.join(DOWNLOAD_DIR, filename)
if download_file(token, form["id"], file_id, dest):
existing.add(filename)
downloaded += 1
print()
if dry_run:
print(f"[dry-run] Ke stažení: {downloaded}, přeskočeno: {skipped}, bez souboru: {no_file}")
else:
print(f"Staženo: {downloaded}, přeskočeno (již existovalo): {skipped}, bez souboru: {no_file}")
if __name__ == "__main__":
main()
@@ -1,7 +1,7 @@
[
{
"name": "PHPSESSID",
"value": "fdn1rc7eoss5u58vo9bmf7njr3",
"value": "jue2dfk7t4k34du7ngg4j706q1",
"domain": ".portal.cpzp.cz",
"path": "/",
"expires": -1,
@@ -1,7 +1,7 @@
[
{
"name": "JSESSIONID",
"value": "9299BF92B05EA8615D06B6EB22F22BE0",
"value": "FB03FE7B8D7D399AECCFFF71C433A2D5",
"domain": ".eforms.zpmvcr.cz",
"path": "/eforms",
"expires": -1,
@@ -13,8 +13,13 @@ import sys
import os
from datetime import datetime
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
from Knihovny.EmailMessagingGraph import send_mail
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
EMAIL_PRIJEMCE = "vladimir.buzalka@buzalka.cz"
POJISTOVNY = [
("111 VZP", "VZP"),
("205 ČPZP", "ČPZP"),
@@ -65,6 +70,28 @@ def main() -> None:
for nazev, stav in vysledky:
print(f" {nazev:<12} {stav}")
chyby = [n for n, s in vysledky if s != "OK" and s != "přeskočeno"]
ok = [n for n, s in vysledky if s == "OK"]
predmet = f"Pojišťovny {start.strftime('%d.%m.%Y')}"
predmet += "vše OK ✓" if not chyby else f"CHYBA: {', '.join(chyby)}"
radky = "".join(
f"<tr><td style='padding:4px 12px'>{n}</td>"
f"<td style='padding:4px 12px;color:{'green' if s == 'OK' else 'gray' if s == 'přeskočeno' else 'red'}'>"
f"{'' if s == 'OK' else s}</td></tr>"
for n, s in vysledky
)
body = (
f"<p>Stahování zpráv z pojišťoven dokončeno za {int(elapsed.total_seconds())} s.</p>"
f"<table border='0' cellspacing='0'>{radky}</table>"
f"<p style='color:gray;font-size:11px'>Spuštěno: {start.strftime('%Y-%m-%d %H:%M:%S')}</p>"
)
try:
send_mail(to=EMAIL_PRIJEMCE, subject=predmet, body=body, html=True)
print(f"\nEmail odeslán: {EMAIL_PRIJEMCE}")
except Exception as e:
print(f"\nEmail se nepodařilo odeslat: {e}")
if __name__ == "__main__":
main()
@@ -13,11 +13,12 @@ sys.path.insert(0, str(PROJECT_ROOT))
import time
import logging
from Knihovny.medicus_db import MedicusDB
import pymysql
import pymysql.cursors
from datetime import date
from Knihovny.medicus_db import get_medicus_db
from Knihovny.vzpb2b_client import VZPB2BClient
from Knihovny.mysql_db import connect_mysql
import pymysql
from datetime import date
# ==========================================
# LOGGING SETUP
@@ -46,7 +47,7 @@ def log_error(msg):
# ==========================================
# MYSQL CONNECTION
# ==========================================
mysql = connect_mysql()
mysql = connect_mysql(cursorclass=pymysql.cursors.DictCursor)
# ==========================================
# SAVE RESULT
@@ -84,14 +85,7 @@ def save_insurance_status(mysql_conn, rc, prijmeni, jmeno, k_datu, result, xml_t
# ==========================================
# CONFIGURATION
# ==========================================
# con = fdb.connect(
# host='192.168.1.10', database=r'm:\MEDICUS\data\medicus.FDB',
# user='sysdba', password='masterkey',charset='WIN1250')
HOST = "192.168.1.10"
DB_PATH = r"M:\Medicus\Data\Medicus.fdb"
PFX_PATH = Path(__file__).resolve().parent / "Certificates" / "picka.pfx"
# PFX_PATH = PROJECT_ROOT / "certificates" / "MBcert.pfx"
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASSWORD = "Vlado7309208104+"
ENV = "prod"
@@ -105,7 +99,7 @@ if not PFX_PATH.exists():
# ==========================================
# INIT CONNECTIONS
# ==========================================
db = MedicusDB(HOST, DB_PATH)
db = get_medicus_db()
vzp = VZPB2BClient(
ENV,
str(PFX_PATH), # <-- important: pass as string
@@ -127,7 +121,7 @@ today = date.today()
# ==========================================
with mysql.cursor() as cur:
cur.execute("SELECT DISTINCT rc FROM vzp_stav_pojisteni WHERE k_datu = %s", (today,))
already_checked = {row[0] for row in cur.fetchall()}
already_checked = {row["rc"] for row in cur.fetchall()}
patients_to_check = [
(rc, prijmeni, jmeno)
@@ -32,7 +32,7 @@ MYSQL_CONFIG = {
"autocommit": True
}
PFX_PATH = script_location / "Certificates" / "MBcert.pfx"
PFX_PATH = script_location.parent / "Certificates" / "MBcert.pfx"
PFX_PASSWORD = "Vlado7309208104++"
ENV = "prod"
@@ -0,0 +1,265 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
batch_stav0_20250101.py
========================
Zpracuje 50 pacientů, kteří k 1. 1. 2025 vrátili stavVyrizeniPozadavku=0
na dotaz registrace lékaře (= VZP pojištěnce nenašla).
Pro každého:
1. Dotáže VZP stavPojisteni k 2025-01-01 → uloží do vzp_stav_pojisteni.
2. Pokud stav != '1' a != '4', binárně hledá zlom pojištění
a uloží do vzp_sledovani_pojisteni.
Resumovatelný — pacienty, kteří už mají záznam v vzp_stav_pojisteni
k 2025-01-01, přeskočí.
"""
import sys
import time
from pathlib import Path
from datetime import date, timedelta
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from Knihovny.mysql_db import connect_mysql
from Knihovny.vzpb2b_client import VZPB2BClient
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
K_DATU = date(2025, 1, 1)
API_DELAY = 1.5
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "00000000"
DIC = "00000000"
ENV = "prod"
# ── PACIENTI ─────────────────────────────────────────────────────────────────
PACIENTI = [
("415513130", "Rohlíková", "Marie"),
("420622031", "Hamerník", "Josef"),
("430127082", "Anderle", "Václav"),
("435625017", "Kafková", "Marie"),
("436005111", "Plzáková", "Ivana"),
("445624103", "Kloučková", "Vlasta"),
("446116017", "Strnadová", "Dagmar"),
("456016085", "Kubcová", "Anna"),
("485627038", "Poustková", "Jiřina"),
("506109148", "Holubcová", "Svatava"),
("6008040247", "Šulc", "Jiří"),
("6054574130", "Přibová", "Darina"),
("6102694070", "Elouchefoun", "Aziz"),
("6654251978", "Svozilová", "Ivana"),
("6808292018", "Moudrý", "Jiří"),
("6853222079", "Milatová", "Martina"),
("6909154934", "Novák", "Petr"),
("7056764319", "Michlíková", "Anna"),
("7157734210", "Moudry Molloy", "Joanne"),
("7309300449", "Vojáček", "Aleš"),
("7410540709", "Torres blanco", "Jose maria"),
("7459303599", "Noháčová", "Kateřina"),
("7651090106", "Matějková", "Jana"),
("7803744597", "Barrell", "Peter"),
("7855080420", "Vondřičková", "Zuzana"),
("7908030427", "Smetana", "Libor"),
("7957312099", "Nimeřická", "Michaela"),
("7961794126", "Tomyshynets", "Halyna"),
("8005291404", "Hanzl", "František"),
("8156013041", "Maršíková", "Simona"),
("8157544241", "Jarošová", "Zuzana"),
("8203120299", "Otčenášek", "Vojtěch"),
("8253215223", "Slavíková", "Kateřina"),
("8301070558", "Kříž", "Michal"),
("8352210438", "Bartáková", "Jana"),
("8412175123", "Mičulka", "Jan"),
("8454664262", "Feoktistová", "Irina"),
("8462150147", "Říhová", "Markéta"),
("8503120417", "Šindelář", "Tomáš"),
("8552170517", "Slabá", "Gabriela"),
("8558150227", "Horáková", "Lucie"),
("8652034380", "Kopová", "Jana"),
("8754280403", "Jindrová", "Helena"),
("8755075153", "Babjáková", "Jana"),
("8910584023", "Pham Van", "Duy"),
("8953010330", "Špatná", "Markéta"),
("8956039037", "Slavíková", "Zuzana"),
("9002025956", "Banáš", "Martin"),
("9010262448", "Bečica", "Marek"),
("9811040305", "Sidej", "Natan"),
]
# ── INIT ──────────────────────────────────────────────────────────────────────
vzp = VZPB2BClient(ENV, str(PFX_PATH), PFX_PASSWORD, icz=ICZ, dic=DIC)
mysql = connect_mysql()
call_count = 0
today = date.today()
# ── HELPERS ───────────────────────────────────────────────────────────────────
def check_stav(rc: str, check_date: date) -> str:
global call_count
if call_count > 0:
time.sleep(API_DELAY)
call_count += 1
print(f" [{call_count}] {check_date.isoformat()} ...", end=" ", flush=True)
xml = vzp.stav_pojisteni(rc=rc, k_datu=check_date.isoformat())
stav = vzp.parse_stav_pojisteni(xml)["stav"]
print(f"stav = {stav!r}")
return stav
def uloz_stav(rc, prijmeni, jmeno, k_datu, stav):
xml = vzp.stav_pojisteni(rc=rc, k_datu=k_datu.isoformat())
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_stav_pojisteni (rc, prijmeni, jmeno, k_datu, stav, response_xml)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE stav=VALUES(stav), response_xml=VALUES(response_xml)
""", (rc, prijmeni, jmeno, k_datu, stav, xml))
def najdi_zlom(rc: str, last_ok_mysql) -> tuple:
if last_ok_mysql:
low = last_ok_mysql
high = today
print(f" MySQL last stav='1': {low} → [{low}{high}]")
else:
print(" MySQL: žádný stav='1' — hledám zpětně po rocích ...")
prev_probe = today
low = high = None
for n in range(1, 21):
y = today.year - n
try:
probe = date(y, today.month, today.day)
except ValueError:
probe = date(y, today.month, today.day - 1)
stav = check_stav(rc, probe)
if stav == "1":
low = probe
high = prev_probe
print(f" Nalezeno stav='1' k {low} → [{low}{high}]")
break
prev_probe = probe
if low is None:
print(" NELZE: stav='1' nenalezen ani 20 let zpět.")
return None, None
stav_low = check_stav(rc, low)
stav_high = check_stav(rc, high)
if stav_low != "1":
print(f" NELZE: dolní mez {low} má stav='{stav_low}'.")
return None, None
if stav_high == "1":
print(f" INFO: horní mez {high} má stav='1' — aktuálně pojištěn.")
return None, None
print(f" Binární hledání ({(high - low).days} dní) ...")
while (high - low).days > 1:
mid = low + timedelta(days=(high - low).days // 2)
stav = check_stav(rc, mid)
if stav == "1":
low = mid
else:
high = mid
return low, high # insured_to, uninsured_from
# ── RESUME: načti již hotové ──────────────────────────────────────────────────
with mysql.cursor() as cur:
cur.execute("SELECT rc FROM vzp_stav_pojisteni WHERE k_datu = %s", (K_DATU,))
hotove = {row[0] for row in cur.fetchall()}
zbyvaji = [(rc, p, j) for rc, p, j in PACIENTI if rc not in hotove]
print(f"Celkem pacientů: {len(PACIENTI)}, již hotovo: {len(hotove)}, zbývá: {len(zbyvaji)}")
print(f"API prodleva: {API_DELAY}s | K_DATU: {K_DATU}\n")
print("=" * 60)
# ── HLAVNÍ SMYČKA ─────────────────────────────────────────────────────────────
vysledky = []
for i, (rc, prijmeni, jmeno) in enumerate(zbyvaji, 1):
print(f"\n[{i}/{len(zbyvaji)}] {prijmeni} {jmeno} (RC: {rc})")
# Krok 1: stav k K_DATU
if call_count > 0:
time.sleep(API_DELAY)
call_count += 1
print(f" [{call_count}] {K_DATU.isoformat()} ...", end=" ", flush=True)
xml_hist = vzp.stav_pojisteni(rc=rc, k_datu=K_DATU.isoformat())
stav_hist = vzp.parse_stav_pojisteni(xml_hist)["stav"]
print(f"stav = {stav_hist!r}")
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_stav_pojisteni (rc, prijmeni, jmeno, k_datu, stav, response_xml)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE stav=VALUES(stav), response_xml=VALUES(response_xml)
""", (rc, prijmeni, jmeno, K_DATU, stav_hist, xml_hist))
# Krok 2: zlom (jen pokud stav != '1' a != '4')
insured_to = uninsured_from = None
if stav_hist in ("1", "4"):
print(f" → pojištěn (stav={stav_hist!r}), zlom se nehledá")
else:
# Zkontroluj watchlist
with mysql.cursor() as cur:
cur.execute("SELECT insured_to, uninsured_from FROM vzp_sledovani_pojisteni WHERE rc = %s", (rc,))
existuje = cur.fetchone()
if existuje:
insured_to, uninsured_from = existuje
print(f" → již ve watchlistu: insured_to={insured_to}, uninsured_from={uninsured_from}")
else:
with mysql.cursor() as cur:
cur.execute(
"SELECT MAX(k_datu) FROM vzp_stav_pojisteni WHERE rc = %s AND stav = '1'",
(rc,)
)
r = cur.fetchone()
last_ok = r[0] if r and r[0] else None
insured_to, uninsured_from = najdi_zlom(rc, last_ok)
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_sledovani_pojisteni
(rc, prijmeni, jmeno, insured_to, uninsured_from,
aktualni_stav, prvni_detekce, posledni_kontrola)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
insured_to=VALUES(insured_to),
uninsured_from=VALUES(uninsured_from),
aktualni_stav=VALUES(aktualni_stav),
posledni_kontrola=VALUES(posledni_kontrola)
""", (rc, prijmeni, jmeno, insured_to, uninsured_from,
stav_hist, today, today))
vysledky.append({
"rc": rc, "prijmeni": prijmeni, "jmeno": jmeno,
"stav_20250101": stav_hist,
"insured_to": insured_to,
"uninsured_from": uninsured_from,
})
# ── SOUHRN ────────────────────────────────────────────────────────────────────
mysql.close()
print(f"\n{'=' * 60}")
print(f" SOUHRN — {len(vysledky)} zpracovaných pacientů")
print(f"{'=' * 60}")
print(f" {'Příjmení':<20} {'Jméno':<14} {'Stav':>5} {'Pojištěn do':<13} {'Nepoj. od'}")
print(f" {'-'*58}")
for v in vysledky:
ins = str(v["insured_to"]) if v["insured_to"] else "-"
uni = str(v["uninsured_from"]) if v["uninsured_from"] else "-"
print(f" {v['prijmeni']:<20} {v['jmeno']:<14} {v['stav_20250101']:>5} {ins:<13} {uni}")
print(f"\nCelkem VZP dotazů: {call_count}")
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Najde přesný den zlomu pojištění pro konkrétního pacienta.
Algoritmus:
1. Z MySQL vezme MAX(k_datu) WHERE stav='1' pro daného pacienta.
Pokud neexistuje, prochází zpět po 1 roce a hledá první stav='1'.
2. Binárním hledáním najde přesný den:
low = poslední den kdy byl stav='1'
high = první den kdy byl stav!='1'
"""
import sys
import time
from pathlib import Path
from datetime import date, timedelta
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from Knihovny.mysql_db import connect_mysql
from Knihovny.vzpb2b_client import VZPB2BClient
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
RC = "500208129"
PRIJMENI = "Zuzák"
JMENO = "Viktor"
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "00000000"
DIC = "00000000"
ENV = "prod"
API_DELAY = 2 # sekundy mezi dotazy na VZP
# ── INIT VZP ──────────────────────────────────────────────────────────────────
if not PFX_PATH.exists():
print(f"CHYBA: PFX certifikat nenalezen: {PFX_PATH}")
sys.exit(1)
vzp = VZPB2BClient(ENV, str(PFX_PATH), PFX_PASSWORD, icz=ICZ, dic=DIC)
call_count = 0
def check_stav(rc: str, check_date: date) -> str | None:
global call_count
if call_count > 0:
time.sleep(API_DELAY)
call_count += 1
print(f" [{call_count}] VZP dotaz k {check_date.isoformat()} ...", end=" ", flush=True)
xml = vzp.stav_pojisteni(rc=rc, k_datu=check_date.isoformat())
stav = vzp.parse_stav_pojisteni(xml)["stav"]
print(f"stav = {stav!r}")
return stav
# ── KROK 1: DOLNÍ MEZ ─────────────────────────────────────────────────────────
print(f"\nHledám zlom pojištění pro {PRIJMENI} {JMENO} (RC: {RC})\n")
print("── Krok 1: dolní mez ──────────────────────────────────────────────────")
mysql = connect_mysql()
with mysql.cursor() as cur:
cur.execute(
"SELECT MAX(k_datu) FROM vzp_stav_pojisteni WHERE rc = %s AND stav = '1'",
(RC,)
)
row = cur.fetchone()
mysql.close()
today = date.today()
last_ok = row[0] if row and row[0] else None
if last_ok:
low = last_ok
high = today
print(f" MySQL: poslední stav='1' k datu {low}")
print(f" → binary search [{low}{high}]")
else:
print(" MySQL: žádný záznam se stavem='1' — hledám zpětně po rocích ...")
prev_probe = today
low = high = None
for n in range(1, 21):
y = today.year - n
try:
probe = date(y, today.month, today.day)
except ValueError:
probe = date(y, today.month, today.day - 1)
stav = check_stav(RC, probe)
if stav == "1":
low = probe
high = prev_probe
print(f"\n Nalezeno stav='1' k {low}")
print(f" → binary search [{low}{high}]")
break
prev_probe = probe
if low is None:
print("CHYBA: Stav='1' nenalezen ani 20 let zpatky. Nelze urcit zlom.")
sys.exit(1)
# ── KROK 2: OVĚŘENÍ HRANIC ────────────────────────────────────────────────────
print("\n── Krok 2: ověření hranic ─────────────────────────────────────────────")
stav_low = check_stav(RC, low)
stav_high = check_stav(RC, high)
if stav_low != "1":
print(f"CHYBA: Dolni mez {low} ma stav='{stav_low}' (ocekavam '1'). Nelze pokracovat.")
sys.exit(1)
if stav_high == "1":
print(f"INFO: Horni mez {high} ma stav='1' — pacient je aktualne pojisten, zadny zlom nenalezen.")
sys.exit(0)
print(f" OK {low} -> '{stav_low}' | {high} -> '{stav_high}' — rozsah v poradku")
# ── KROK 3: BINÁRNÍ HLEDÁNÍ ───────────────────────────────────────────────────
print(f"\n── Krok 3: binární hledání ({(high - low).days} dní v rozsahu) ─────────")
while (high - low).days > 1:
mid = low + timedelta(days=(high - low).days // 2)
stav = check_stav(RC, mid)
if stav == "1":
low = mid
else:
high = mid
# ── VÝSLEDEK ──────────────────────────────────────────────────────────────────
print(f"\n{'=' * 55}")
print(f" VYSLEDEK - {PRIJMENI} {JMENO} (RC: {RC})")
print(f"{'=' * 55}")
print(f" Posledni den POJISTEN : {low}")
print(f" Prvni den BEZ pojisteni: {high}")
print(f" Celkem VZP dotazu : {call_count}")
print(f"{'=' * 55}\n")
@@ -0,0 +1,364 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
zkontroluj_a_odesli_zlomy.py
=============================
Ucel:
Sleduje registrovane pacienty, kteri maji problematicky stav pojisteni
(stav "X" = pojistovna nenalezena / nepojisten). Detekuje zmeny stavu
a odesila email s vysledky. Spousti se ad hoc nebo planovane.
Zavislosti:
Knihovny/vzpb2b_client.py -- VZP B2B SOAP API klient (mTLS pres picka.pfx)
Knihovny/mysql_db.py -- pripojeni k MySQL 192.168.1.76, DB medevio
Knihovny/EmailMessagingGraph.py -- odesilani emailu pres Microsoft Graph
MySQL tabulky:
vzp_stav_pojisteni -- denni zaznamy VZP dotazu (plni FinalSaveInsuranceScript)
vzp_sledovani_pojisteni -- watchlist pacientu se stavem X (spravuje tento skript)
vzp_sledovani_zmeny -- log vsech detektvanych zmen stavu (spravuje tento skript)
Logika:
FAZE 1 -- Re-overeni watchlistu
Pro kazdeho pacienta v tabulce vzp_sledovani_pojisteni zavola VZP API
k dnesnemu datumu. Pokud se stav zmenil (X->1, 1->X apod.), zapise
zmenu do vzp_sledovani_zmeny a aktualizuje aktualni_stav.
FAZE 2 -- Novi pacienti
Z tabulky vzp_stav_pojisteni vybere pacienty, jejichz POSLEDNI zaznam
ma stav NOT IN ('1','4') a jeste nejsou ve watchlistu.
Pro kazdeho:
a) Hleda dolni mez (posledni datum stav='1'):
- Nejprve MAX(k_datu WHERE stav='1') z MySQL.
- Pokud neni, prochazi zpetne po 1 roce az najde stav='1' (max 20 let).
b) Binarnim hledanim zpresni na konkretni den zlomu:
low = posledni den pojisten (stav='1')
high = prvni den bez pojisteni (stav!='1')
c) Vlozi pacienta do vzp_sledovani_pojisteni.
FAZE 3 -- Email
Vzdy odesle email na vladimir.buzalka@buzalka.cz.
Struktura emailu:
[NAHORE] Novi pacienti pridani toto spusteni (s datem zlomu)
[DOLE] Vsechny historicke zmeny z vzp_sledovani_zmeny, razene
podle datum_zmeny DESC
Poznamky:
- Mezi kazdym VZP API volanim je 2s prodleva (API_DELAY).
- Stav '4' (cizinec bez plneho naroku) se povazuje za OK, nesledi se.
- Tabulky se vytvori automaticky pri prvnim spusteni (CREATE TABLE IF NOT EXISTS).
- Kolace vsech tabulek: utf8mb4_unicode_ci (shoduje se s vzp_stav_pojisteni).
"""
import sys
import time
from pathlib import Path
from datetime import date, timedelta
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from Knihovny.mysql_db import connect_mysql
from Knihovny.vzpb2b_client import VZPB2BClient
from Knihovny.EmailMessagingGraph import send_mail
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
EMAIL_PRIJEMCE = "vladimir.buzalka@buzalka.cz"
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "00000000"
DIC = "00000000"
ENV = "prod"
API_DELAY = 2
# ── INIT ──────────────────────────────────────────────────────────────────────
if not PFX_PATH.exists():
print(f"CHYBA: PFX certifikat nenalezen: {PFX_PATH}")
sys.exit(1)
vzp = VZPB2BClient(ENV, str(PFX_PATH), PFX_PASSWORD, icz=ICZ, dic=DIC)
call_count = 0
today = date.today()
# ── HELPERS ───────────────────────────────────────────────────────────────────
def check_stav(rc: str, check_date: date) -> str | None:
global call_count
if call_count > 0:
time.sleep(API_DELAY)
call_count += 1
print(f" [{call_count}] {check_date.isoformat()} ...", end=" ", flush=True)
xml = vzp.stav_pojisteni(rc=rc, k_datu=check_date.isoformat())
stav = vzp.parse_stav_pojisteni(xml)["stav"]
print(f"stav = {stav!r}")
return stav
def najdi_zlom(rc: str, last_ok_mysql) -> tuple:
"""Vrati (insured_to, uninsured_from) nebo (None, None) pri selhani."""
if last_ok_mysql:
low = last_ok_mysql
high = today
print(f" MySQL last stav='1': {low} -> [{low} ... {high}]")
else:
print(" MySQL: zadny stav='1' — hledam zpetne po rocich ...")
prev_probe = today
low = high = None
for n in range(1, 21):
y = today.year - n
try:
probe = date(y, today.month, today.day)
except ValueError:
probe = date(y, today.month, today.day - 1)
stav = check_stav(rc, probe)
if stav == "1":
low = probe
high = prev_probe
print(f" Nalezeno stav='1' k {low} -> [{low} ... {high}]")
break
prev_probe = probe
if low is None:
print(" NELZE: stav='1' nenalezen ani 20 let zpet.")
return None, None
stav_low = check_stav(rc, low)
stav_high = check_stav(rc, high)
if stav_low != "1":
print(f" NELZE: dolni mez {low} ma stav='{stav_low}'.")
return None, None
if stav_high == "1":
print(f" INFO: horni mez {high} ma stav='1' — bez zlomu.")
return None, None
while (high - low).days > 1:
mid = low + timedelta(days=(high - low).days // 2)
stav = check_stav(rc, mid)
if stav == "1":
low = mid
else:
high = mid
return low, high
# ── MYSQL: INIT TABULEK ───────────────────────────────────────────────────────
mysql = connect_mysql()
with mysql.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS vzp_sledovani_pojisteni (
rc VARCHAR(20) NOT NULL PRIMARY KEY,
prijmeni VARCHAR(100),
jmeno VARCHAR(100),
insured_to DATE,
uninsured_from DATE,
aktualni_stav VARCHAR(10),
prvni_detekce DATE NOT NULL,
posledni_kontrola DATE NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS vzp_sledovani_zmeny (
id INT AUTO_INCREMENT PRIMARY KEY,
rc VARCHAR(20) NOT NULL,
prijmeni VARCHAR(100),
jmeno VARCHAR(100),
datum_zmeny DATE NOT NULL,
stav_pred VARCHAR(10),
stav_po VARCHAR(10),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_rc (rc),
INDEX idx_datum (datum_zmeny)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
""")
# ── FAZE 1: RE-OVERENI WATCHLISTU ─────────────────────────────────────────────
print(f"\n== Faze 1: Re-overeni watchlistu ({today}) ==")
with mysql.cursor() as cur:
cur.execute("""
SELECT rc, prijmeni, jmeno, aktualni_stav
FROM vzp_sledovani_pojisteni
ORDER BY prijmeni, jmeno
""")
watchlist = cur.fetchall()
print(f"Watchlist: {len(watchlist)} pacientu\n")
tato_zmeny = [] # zmeny detekované toto spusteni
for rc, prijmeni, jmeno, stav_pred in watchlist:
print(f" {prijmeni} {jmeno} (RC: {rc}) aktualni={stav_pred!r}")
stav_po = check_stav(rc, today)
with mysql.cursor() as cur:
cur.execute(
"UPDATE vzp_sledovani_pojisteni SET aktualni_stav=%s, posledni_kontrola=%s WHERE rc=%s",
(stav_po, today, rc)
)
if stav_po != stav_pred:
print(f" *** ZMENA: {stav_pred!r} -> {stav_po!r} ***")
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_sledovani_zmeny (rc, prijmeni, jmeno, datum_zmeny, stav_pred, stav_po)
VALUES (%s, %s, %s, %s, %s, %s)
""", (rc, prijmeni, jmeno, today, stav_pred, stav_po))
tato_zmeny.append((rc, prijmeni, jmeno, stav_pred, stav_po))
# ── FAZE 2: NOVI PACIENTI S X ─────────────────────────────────────────────────
print(f"\n== Faze 2: Novi pacienti s X ==")
with mysql.cursor() as cur:
cur.execute("""
SELECT rc, prijmeni, jmeno, stav
FROM (
SELECT rc, prijmeni, jmeno, stav,
ROW_NUMBER() OVER (PARTITION BY rc ORDER BY k_datu DESC) AS rn
FROM vzp_stav_pojisteni
) t
WHERE rn = 1
AND stav NOT IN ('1', '4')
AND rc NOT IN (SELECT rc FROM vzp_sledovani_pojisteni)
ORDER BY prijmeni, jmeno
""")
novi_raw = cur.fetchall()
print(f"Novych pacientu: {len(novi_raw)}\n")
novi = []
for rc, prijmeni, jmeno, stav in novi_raw:
print(f" Novy: {prijmeni} {jmeno} (RC: {rc}) stav={stav!r}")
with mysql.cursor() as cur:
cur.execute(
"SELECT MAX(k_datu) FROM vzp_stav_pojisteni WHERE rc = %s AND stav = '1'",
(rc,)
)
r = cur.fetchone()
last_ok = r[0] if r and r[0] else None
insured_to, uninsured_from = najdi_zlom(rc, last_ok)
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_sledovani_pojisteni
(rc, prijmeni, jmeno, insured_to, uninsured_from, aktualni_stav, prvni_detekce, posledni_kontrola)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
""", (rc, prijmeni, jmeno, insured_to, uninsured_from, stav, today, today))
novi.append({
"rc": rc,
"prijmeni": prijmeni,
"jmeno": jmeno,
"stav": stav,
"insured_to": str(insured_to) if insured_to else "nezjisteno",
"uninsured_from": str(uninsured_from) if uninsured_from else "nezjisteno",
})
print()
# ── FAZE 3: NACTENI VSECH HISTORICKYCH ZMEN ──────────────────────────────────
with mysql.cursor() as cur:
cur.execute("""
SELECT rc, prijmeni, jmeno, datum_zmeny, stav_pred, stav_po
FROM vzp_sledovani_zmeny
ORDER BY datum_zmeny DESC, created_at DESC
""")
vsechny_zmeny = cur.fetchall()
mysql.close()
# ── SESTAVENI EMAILU ──────────────────────────────────────────────────────────
# --- Sekce 1: NOVI ---
if novi:
radky = "".join(f"""
<tr>
<td>{p['prijmeni']} {p['jmeno']}</td>
<td>{p['rc']}</td>
<td style="color:#c0392b;font-weight:bold">{p['stav']}</td>
<td>{p['insured_to']}</td>
<td style="color:#c0392b">{p['uninsured_from']}</td>
</tr>""" for p in novi)
sekce_novi = f"""
<h2 style="background:#c0392b;color:white;padding:8px 12px;margin-top:0">
Novi pacienti pridani do sledovani &mdash; {len(novi)}
</h2>
<table border="1" cellpadding="6" cellspacing="0"
style="border-collapse:collapse;border-color:#ccc;width:100%;margin-bottom:24px">
<thead style="background:#922b21;color:white">
<tr>
<th>Pacient</th><th>RC</th><th>Stav</th>
<th>Posledni den pojisten</th><th>Prvni den bez pojisteni</th>
</tr>
</thead>
<tbody>{radky}</tbody>
</table>"""
else:
sekce_novi = """
<h2 style="background:#27ae60;color:white;padding:8px 12px;margin-top:0">
Zadni novi pacienti
</h2>"""
# --- Sekce 2: VSECHNY HISTORICKE ZMENY ---
if vsechny_zmeny:
radky = ""
for rc, prijmeni, jmeno, datum_zmeny, stav_pred, stav_po in vsechny_zmeny:
# Zvyraznit radky z tohoto spusteni
je_dnes = (datum_zmeny == today)
bg = "#fff9e6" if je_dnes else "white"
barva = "#c0392b" if (stav_po or "") != "1" else "#27ae60"
radky += f"""
<tr style="background:{bg}">
<td>{prijmeni} {jmeno}</td>
<td>{rc}</td>
<td>{datum_zmeny}</td>
<td style="font-weight:bold">{stav_pred or '?'}</td>
<td style="color:{barva};font-weight:bold">{stav_po or '?'}</td>
</tr>"""
sekce_zmeny = f"""
<h2 style="background:#2c3e50;color:white;padding:8px 12px">
Vsechny zmeny stavu pojisteni &mdash; {len(vsechny_zmeny)} zaznam(u), razeno od nejnovejiho
</h2>
<p style="font-size:12px;color:#888">Zbarvene radky = dnesni spusteni</p>
<table border="1" cellpadding="6" cellspacing="0"
style="border-collapse:collapse;border-color:#ccc;width:100%">
<thead style="background:#2c3e50;color:white">
<tr>
<th>Pacient</th><th>RC</th><th>Datum zmeny</th>
<th>Stav pred</th><th>Stav po</th>
</tr>
</thead>
<tbody>{radky}</tbody>
</table>"""
else:
sekce_zmeny = """
<h2 style="background:#888;color:white;padding:8px 12px">
Zadne zmeny stavu v historii
</h2>"""
body = f"""<html><body style="font-family:Arial,sans-serif;font-size:14px;max-width:900px">
<h1 style="border-bottom:2px solid #2c3e50;padding-bottom:8px">
Sledovani pojisteni &mdash; {today.strftime('%d. %m. %Y')}
</h1>
{sekce_novi}
{sekce_zmeny}
<p style="color:#aaa;font-size:11px;margin-top:32px">
Celkem VZP dotazu: {call_count} &nbsp;|&nbsp; {today}
</p>
</body></html>"""
predmet = f"Pojisteni {today.strftime('%d.%m.%Y')} {len(novi)} novych, {len(tato_zmeny)} zmen dnes"
print(f"\nOdesilam email na {EMAIL_PRIJEMCE} ...")
send_mail(to=EMAIL_PRIJEMCE, subject=predmet, body=body, html=True)
print(f"Email odeslan.")
print(f"Hotovo. VZP dotazu celkem: {call_count}")
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
zkontroluj_rc_jednorazove.py
=============================
Jednorázový skript pro libovolné RC:
1. Dotáže VZP na stav pojištění k zadanému K_DATU.
2. Uloží do vzp_stav_pojisteni.
3. Pokud stav != '1' a != '4', spustí binární hledání zlomu
a uloží do vzp_sledovani_pojisteni.
"""
import sys
import time
from pathlib import Path
from datetime import date, timedelta
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from Knihovny.mysql_db import connect_mysql
from Knihovny.vzpb2b_client import VZPB2BClient
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
RC = "430127082"
PRIJMENI = "Anderle"
JMENO = "Václav"
K_DATU = date(2025, 1, 1) # datum pro první dotaz
PFX_PATH = Path(__file__).resolve().parent.parent / "Certificates" / "picka.pfx"
PFX_PASSWORD = "Vlado7309208104+"
ICZ = "00000000"
DIC = "00000000"
ENV = "prod"
API_DELAY = 2
# ── INIT ──────────────────────────────────────────────────────────────────────
vzp = VZPB2BClient(ENV, str(PFX_PATH), PFX_PASSWORD, icz=ICZ, dic=DIC)
mysql = connect_mysql()
call_count = 0
today = date.today()
def check_stav(rc: str, check_date: date) -> str:
global call_count
if call_count > 0:
time.sleep(API_DELAY)
call_count += 1
print(f" [{call_count}] VZP dotaz k {check_date.isoformat()} ...", end=" ", flush=True)
xml = vzp.stav_pojisteni(rc=rc, k_datu=check_date.isoformat())
stav = vzp.parse_stav_pojisteni(xml)["stav"]
print(f"stav = {stav!r}")
return stav
def uloz_stav(rc, prijmeni, jmeno, k_datu, stav, xml):
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_stav_pojisteni
(rc, prijmeni, jmeno, k_datu, stav, response_xml)
VALUES (%s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE stav=VALUES(stav), response_xml=VALUES(response_xml)
""", (rc, prijmeni, jmeno, k_datu, stav, xml))
def najdi_zlom(rc, last_ok_mysql):
if last_ok_mysql:
low = last_ok_mysql
high = today
print(f" MySQL last stav='1': {low} -> [{low} ... {high}]")
else:
print(" MySQL: žádný stav='1' — hledám zpětně po rocích ...")
prev_probe = today
low = high = None
for n in range(1, 21):
y = today.year - n
try:
probe = date(y, today.month, today.day)
except ValueError:
probe = date(y, today.month, today.day - 1)
stav = check_stav(rc, probe)
if stav == "1":
low = probe
high = prev_probe
print(f" Nalezeno stav='1' k {low} -> [{low} ... {high}]")
break
prev_probe = probe
if low is None:
print(" NELZE: stav='1' nenalezen ani 20 let zpět.")
return None, None
stav_low = check_stav(rc, low)
stav_high = check_stav(rc, high)
if stav_low != "1":
print(f" NELZE: dolní mez {low} má stav='{stav_low}'.")
return None, None
if stav_high == "1":
print(f" INFO: horní mez {high} má stav='1' — pacient je aktuálně pojištěn, žádný zlom.")
return None, None
print(f" Binární hledání v rozsahu {(high - low).days} dní ...")
while (high - low).days > 1:
mid = low + timedelta(days=(high - low).days // 2)
stav = check_stav(rc, mid)
if stav == "1":
low = mid
else:
high = mid
return low, high # insured_to, uninsured_from
# ── KROK 1: Dotaz k zadanému datu ─────────────────────────────────────────────
print(f"\n{PRIJMENI} {JMENO} (RC: {RC})")
print(f"── Krok 1: stav pojištění k {K_DATU} ─────────────────────────────────")
xml_hist = vzp.stav_pojisteni(rc=RC, k_datu=K_DATU.isoformat())
stav_hist = vzp.parse_stav_pojisteni(xml_hist)["stav"]
call_count += 1
print(f" stav k {K_DATU}: {stav_hist!r}")
uloz_stav(RC, PRIJMENI, JMENO, K_DATU, stav_hist, xml_hist)
print(f" Uloženo do vzp_stav_pojisteni (k_datu={K_DATU})")
# ── KROK 2: Pokud není pojištěn, najdi zlom ───────────────────────────────────
if stav_hist in ("1", "4"):
print(f"\nStav '{stav_hist}' = pojištěn (nebo cizinec), zlom se nehledá.")
else:
print(f"\nStav '{stav_hist}' = nepojištěn/nenalezen")
print(f"── Krok 2: hledám zlom pojištění ─────────────────────────────────────")
# Zkontroluj, zda je pacient již ve watchlistu
with mysql.cursor() as cur:
cur.execute("SELECT insured_to, uninsured_from FROM vzp_sledovani_pojisteni WHERE rc = %s", (RC,))
existuje = cur.fetchone()
if existuje:
print(f" Pacient již ve watchlistu: insured_to={existuje[0]}, uninsured_from={existuje[1]}")
print(f" Přeskakuji hledání zlomu.")
else:
with mysql.cursor() as cur:
cur.execute(
"SELECT MAX(k_datu) FROM vzp_stav_pojisteni WHERE rc = %s AND stav = '1'",
(RC,)
)
r = cur.fetchone()
last_ok = r[0] if r and r[0] else None
insured_to, uninsured_from = najdi_zlom(RC, last_ok)
with mysql.cursor() as cur:
cur.execute("""
INSERT INTO vzp_sledovani_pojisteni
(rc, prijmeni, jmeno, insured_to, uninsured_from,
aktualni_stav, prvni_detekce, posledni_kontrola)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
insured_to=VALUES(insured_to),
uninsured_from=VALUES(uninsured_from),
aktualni_stav=VALUES(aktualni_stav),
posledni_kontrola=VALUES(posledni_kontrola)
""", (RC, PRIJMENI, JMENO, insured_to, uninsured_from,
stav_hist, today, today))
print(f"\n{'=' * 50}")
print(f" VÝSLEDEK — {PRIJMENI} {JMENO} (RC: {RC})")
print(f"{'=' * 50}")
print(f" Stav k {K_DATU} : {stav_hist!r}")
print(f" Poslední den pojištěn : {insured_to or 'nezjištěno'}")
print(f" První den bez pojištění : {uninsured_from or 'nezjištěno'}")
print(f" Celkem VZP dotazů : {call_count}")
print(f"{'=' * 50}\n")
mysql.close()
print(f"Hotovo. Celkem VZP dotazů: {call_count}")
+39 -5
View File
@@ -1,6 +1,32 @@
import socket
import fdb
def get_medicus_connection():
"""
Připojí se k Firebird medicus.fdb podle názvu počítače.
Vrátí fdb.Connection nebo vyhodí RuntimeError pro neznámý počítač.
"""
computer_name = socket.gethostname().upper()
dsn_map = {
"LEKAR": r"localhost:M:\medicus\data\medicus.fdb",
"SESTRA": r"192.168.1.10:m:\medicus\data\medicus.fdb",
"LENOVO": r"192.168.1.10:m:\medicus\data\medicus.fdb",
}
dsn = dsn_map.get(computer_name, r"localhost:c:\medicus 3\data\medicus.fdb")
print(f"[medicus_db] Připojuji se jako {computer_name}{dsn}")
return fdb.connect(dsn=dsn, user="SYSDBA", password="masterkey", charset="win1250")
def get_medicus_db():
"""Vrátí MedicusDB instanci s připojením podle názvu počítače."""
conn = get_medicus_connection()
instance = object.__new__(MedicusDB)
instance.conn = conn
instance.cur = conn.cursor()
return instance
class MedicusDB:
def __init__(self, host, db_path, user="SYSDBA", password="masterkey", charset="WIN1250"):
@@ -44,13 +70,21 @@ class MedicusDB:
kar.prijmeni,
kar.jmeno,
kar.poj
FROM registr
JOIN kar ON registr.idpac = kar.idpac
WHERE registr.datum_zruseni IS NULL
AND registr.priznak IN ('A','D','V')
FROM kar
WHERE kar.vyrazen = 'N'
AND kar.rodcis IS NOT NULL
AND kar.rodcis <> ''
AND kar.vyrazen <> 'A'
AND EXISTS (
SELECT r.id FROM registr r
JOIN icp i ON r.idicp = i.idicp
WHERE r.idpac = kar.idpac
AND r.datum <= CURRENT_DATE
AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= CURRENT_DATE)
AND r.priznak IN ('V','D','A')
AND i.icp = '09305001'
AND i.odb = '001'
)
ORDER BY kar.prijmeni, kar.rodcis
"""
if as_dict:
return self.query_dict(sql)
+3 -1
View File
@@ -11,7 +11,7 @@ _LOCAL_HOSTS = {"lekar", "sestra", "lenovo"}
def connect_mysql(user="root", password="Vlado9674+", database="medevio",
port=3306, charset="utf8mb4", autocommit=True):
port=3306, charset="utf8mb4", autocommit=True, cursorclass=None):
"""
Připojí se k MySQL. Na lokálních stanicích (lekar/sestra/lenovo) použije
127.0.0.1 přímo, jinak zkusí 192.168.1.76 a pak 127.0.0.1 jako fallback.
@@ -22,6 +22,8 @@ def connect_mysql(user="root", password="Vlado9674+", database="medevio",
params = dict(port=port, user=user, password=password,
database=database, charset=charset, autocommit=autocommit)
if cursorclass is not None:
params["cursorclass"] = cursorclass
last_error = None
for host in candidates:
+90
View File
@@ -162,6 +162,96 @@ class VZPB2BClient:
print("HTTP:", resp.status_code)
return resp.text
def registrace_lekare(self, rc: str, k_datu: str = None,
odbornosti: list = None) -> str:
"""
Calls RegistracePojistencePZSB2B — vrátí registrující lékaře pojištěnce.
odbornosti: seznam kódů, např. ["001","002","014"]. None = bez filtru (vrátí vše).
"""
service = "RegistracePojistencePZSB2B"
endpoint = self._build_endpoint(service)
if not k_datu:
k_datu = date.today().isoformat()
ns = "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1"
odb_xml = ""
if odbornosti:
kody = "".join(f"<ns1:kodOdbornosti>{k}</ns1:kodOdbornosti>"
for k in odbornosti)
odb_xml = f"<ns1:seznamOdbornosti>{kody}</ns1:seznamOdbornosti>"
soap = f"""<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns1:registracePojistencePZSB2B xmlns:ns1="{ns}">
<ns1:cisloPojistence>{rc}</ns1:cisloPojistence>
<ns1:kDatu>{k_datu}</ns1:kDatu>
{odb_xml}
</ns1:registracePojistencePZSB2B>
</soap:Body>
</soap:Envelope>"""
resp = self.session.post(
endpoint,
data=soap.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": "process"},
timeout=30,
)
return resp.text
def parse_registrace_lekare(self, xml_text: str) -> list[dict]:
"""
Parsuje odpověď RegistracePojistencePZSB2B.
Vrátí seznam diktů — jeden na odbornost (i prázdné = ma_lekare=False).
"""
import xml.etree.ElementTree as ET
NS = {
"soap": "http://schemas.xmlsoap.org/soap/envelope/",
"rp": "http://xmlns.gemsystem.cz/B2B/RegistracePojistencePZSB2B/1",
}
root = ET.fromstring(xml_text)
stav_el = root.find(".//rp:stavVyrizeniPozadavku", NS)
stav_vyrizeni = stav_el.text.strip() if stav_el is not None and stav_el.text else None
# Jen vnější <odbornost> elementy (ty s ICZ), ne vnořené subelementy
results = []
for it in root.findall(".//rp:seznamOdbornosti/rp:odbornost", NS):
def g(tag):
el = it.find(f"rp:{tag}", NS)
return el.text.strip() if el is not None and el.text else None
odb = it.find("rp:odbornost", NS)
if odb is not None:
# Záznam s lékařem
kod = odb.find("rp:kod", NS)
naz = odb.find("rp:nazev", NS)
poj = it.find("rp:zdravotniPojistovna", NS)
results.append({
"ma_lekare": True,
"kod_odbornosti": kod.text.strip() if kod is not None and kod.text else None,
"nazev_odbornosti": naz.text.strip() if naz is not None and naz.text else None,
"ICZ": g("ICZ"),
"ICP": g("ICP"),
"nazev_lekare": g("nazevICP"),
"nazev_zzz": g("nazevSZZ"),
"poj_kod": poj.find("rp:kod", NS).text.strip() if poj is not None and poj.find("rp:kod", NS) is not None else None,
"poj_zkratka": poj.find("rp:zkratka", NS).text.strip() if poj is not None and poj.find("rp:zkratka", NS) is not None else None,
"datum_registrace": g("datumRegistrace"),
"datum_zahajeni": g("datumZahajeni"),
"datum_ukonceni": g("datumUkonceni"),
"stav_vyrizeni": stav_vyrizeni,
})
else:
# Prázdný placeholder — pacient nemá lékaře v této odbornosti
# (VZP vrací element bez ICZ/ICP — ignorujeme, zaznamená skript sám)
pass
return results
def parse_stav_pojisteni(self, xml_text: str):
"""
Parses stavPojisteniB2B SOAP response into a Python dict.
+1
View File
@@ -0,0 +1 @@
ANTHROPIC_API_KEY=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
+16
View File
@@ -0,0 +1,16 @@
# Virtual environment
.venv/
# Python
__pycache__/
*.pyc
# PyCharm / IDE
.idea/
# Claude worktrees
.claude/worktrees/
# OS
.DS_Store
Thumbs.db
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Fetch Medevio pending (ACTIVE) patient requests and return a pandas DataFrame.
Reads Bearer token from token.txt (single line, token only).
"""
import requests
import pandas as pd
import time
from pathlib import Path
from typing import List, Dict, Any
# CONFIG ---------------------------------------------------------------------
TOKEN_FILE = str(Path(__file__).resolve().parent.parent / "token.txt") # centralized token
GRAPHQL_URL = "https://app.medevio.cz/graphql"
CLINIC_SLUG = "mudr-buzalkova" # adjust if needed
LOCALE = "cs"
PAGE_SIZE = 50 # how many items to request per page
REQUEST_WAIT = 0.2 # seconds between requests to be polite
# ---------------------------------------------------------------------------
GRAPHQL_QUERY = r"""
query ClinicLegacyRequestList_ListPatientRequestsForClinic(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requests: listPatientRequestsForClinic(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
id
createdAt
dueDate
displayTitle(locale: $locale)
doneAt
removedAt
priority
evaluationResult(locale: $locale) {
fields {
name
value
}
}
clinicId
extendedPatient {
id
identificationNumber
kind
name
note
owner { name surname }
key
status
surname
type
user { id name surname }
isUnknownPatient
}
lastMessage {
createdAt
id
readAt
sender { id name surname clinicId }
text
}
queue { id name }
reservations { id canceledAt done start }
tags(onlyImportant: true) { id name color icon }
priceWhenCreated
currencyWhenCreated
}
}
"""
def read_token(path: str) -> str:
with open(path, "r", encoding="utf-8") as f:
t = f.read().strip()
if t.startswith("Bearer "):
t = t.split(" ", 1)[1]
return t
def fetch_requests(token: str,
clinic_slug: str = CLINIC_SLUG,
locale: str = LOCALE,
page_size: int = PAGE_SIZE) -> List[Dict[str, Any]]:
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
all_items: List[Dict[str, Any]] = []
offset = 0
while True:
variables = {
"clinicSlug": clinic_slug,
"queueId": None,
"queueAssignment": "ANY",
"state": "ACTIVE",
"pageInfo": {"first": page_size, "offset": offset},
"locale": locale,
}
payload = {"query": GRAPHQL_QUERY, "variables": variables, "operationName": "ClinicLegacyRequestList_ListPatientRequestsForClinic"}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=30)
r.raise_for_status()
js = r.json()
# Basic error handling
if "errors" in js:
raise RuntimeError(f"GraphQL returned errors: {js['errors']}")
items = js.get("data", {}).get("requests", [])
if not items:
break
all_items.extend(items)
# If fewer than requested, we are at the end
if len(items) < page_size:
break
offset += page_size
time.sleep(REQUEST_WAIT)
return all_items
def flatten_item(item: Dict[str, Any]) -> Dict[str, Any]:
patient = item.get("extendedPatient") or {}
last_msg = item.get("lastMessage") or {}
queue = item.get("queue") or {}
# evaluationResult fields -> map of name:value (if exists)
eval_map = {}
eval_block = item.get("evaluationResult") or {}
for fld in (eval_block.get("fields") or []):
name = fld.get("name")
value = fld.get("value")
if name:
eval_map[name] = value
flat = {
"id": item.get("id"),
"createdAt": item.get("createdAt"),
"dueDate": item.get("dueDate"),
"displayTitle": item.get("displayTitle"),
"doneAt": item.get("doneAt"),
"removedAt": item.get("removedAt"),
"priority": item.get("priority"),
"clinicId": item.get("clinicId"),
"patient_id": patient.get("id"),
"patient_identificationNumber": patient.get("identificationNumber"),
"patient_name": patient.get("name"),
"patient_surname": patient.get("surname"),
"patient_status": patient.get("status"),
"lastMessage_id": last_msg.get("id"),
"lastMessage_createdAt": last_msg.get("createdAt"),
"lastMessage_text": last_msg.get("text"),
"queue_id": queue.get("id"),
"queue_name": queue.get("name"),
"priceWhenCreated": item.get("priceWhenCreated"),
"currencyWhenCreated": item.get("currencyWhenCreated"),
}
# merge evaluation fields (if any) prefixed by "eval_"
for k, v in eval_map.items():
flat[f"eval_{k}"] = v
return flat
def to_dataframe(items: List[Dict[str, Any]]) -> pd.DataFrame:
rows = [flatten_item(it) for it in items]
df = pd.DataFrame(rows)
# try parsing dates
for col in ("createdAt", "dueDate", "doneAt", "lastMessage_createdAt", "removedAt"):
if col in df.columns:
df[col] = pd.to_datetime(df[col], errors="coerce")
return df
def main():
token = read_token(TOKEN_FILE)
print("Fetching pending (ACTIVE) requests from Medevio...")
items = fetch_requests(token)
print(f"Fetched {len(items)} items.")
df = to_dataframe(items)
pd.set_option("display.max_rows", 20)
pd.set_option("display.max_colwidth", 160)
print(df.head(50))
# optionally save
df.to_excel("medevio_pending_requests.xlsx", index=False)
print("Saved medevio_pending_requests.xlsx")
if __name__ == "__main__":
main()
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from pathlib import Path
import requests # 👈 this is new
# --- Settings ----------------------------------------------------
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
SHOW_FULL_TOKEN = False # set True if you want to print the full token
# -----------------------------------------------------------------
GRAPHQL_QUERY = r"""
query ClinicLegacyRequestList_ListPatientRequestsForClinic(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requests: listPatientRequestsForClinic(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
id
createdAt
dueDate
displayTitle(locale: $locale)
doneAt
removedAt
priority
evaluationResult(locale: $locale) { fields { name value } }
clinicId
extendedPatient {
id
identificationNumber
kind
name
surname
status
isUnknownPatient
}
lastMessage { id text createdAt }
queue { id name }
reservations { id canceledAt done start }
tags(onlyImportant: true) { id name color icon }
priceWhenCreated
currencyWhenCreated
}
}
"""
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"state": "ACTIVE", # pending / nevyřízené
"pageInfo": {"first": 30, "offset": 0},
"locale": "cs",
}
payload = {
"operationName": "ClinicLegacyRequestList_ListPatientRequestsForClinic",
"query": GRAPHQL_QUERY,
"variables": variables,
}
# === Actually call Medevio API ==================================
print("📡 Querying Medevio GraphQL API...\n")
url = "https://api.medevio.cz/graphql"
r = requests.post(url, json=payload, headers=headers)
print(f"HTTP status: {r.status_code}\n")
# --- Try to decode JSON
try:
data = r.json()
print("=== Raw JSON response ===")
print(json.dumps(data, indent=2, ensure_ascii=False))
except Exception as e:
print("❌ Failed to decode JSON:", e)
print("Raw text:\n", r.text)
if __name__ == "__main__":
main()
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from pathlib import Path
import requests
# --- Settings ----------------------------------------------------
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
# -----------------------------------------------------------------
GRAPHQL_QUERY = r"""
query ClinicLegacyRequestList_ListPatientRequestsForClinic(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requests: listPatientRequestsForClinic(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
id
displayTitle(locale: $locale)
extendedPatient {
name
surname
identificationNumber
}
}
}
"""
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"state": "ACTIVE", # pending / nevyřízené
"pageInfo": {"first": 30, "offset": 0},
"locale": "cs",
}
payload = {
"operationName": "ClinicLegacyRequestList_ListPatientRequestsForClinic",
"query": GRAPHQL_QUERY,
"variables": variables,
}
url = "https://api.medevio.cz/graphql"
print("📡 Querying Medevio GraphQL API...\n")
r = requests.post(url, json=payload, headers=headers)
print(f"HTTP status: {r.status_code}\n")
# --- Parse JSON safely
try:
data = r.json()
except Exception as e:
print("❌ Failed to decode JSON:", e)
print("Raw text:\n", r.text)
return
requests_data = data.get("data", {}).get("requests", [])
if not requests_data:
print("⚠️ No requests found or invalid response.")
return
print(f"📋 Found {len(requests_data)} active requests:\n")
for req in requests_data:
patient = req.get("extendedPatient", {})
print(f"- {patient.get('surname','')} {patient.get('name','')} "
f"({patient.get('identificationNumber','')}) "
f"{req.get('displayTitle','')} [ID: {req.get('id')}]")
if __name__ == "__main__":
main()
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from pathlib import Path
import requests
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
# --- Try including `updatedAt` field directly ---
GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
pageInfo: $pageInfo
) {
count
patientRequests {
id
createdAt
updatedAt # 👈 TESTUJEME, jestli Medevio toto pole podporuje
doneAt
removedAt
displayTitle(locale: $locale)
lastMessage { createdAt }
extendedPatient {
name
surname
identificationNumber
}
}
}
}
"""
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": 3, "offset": 0},
"locale": "cs",
}
payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
url = "https://api.medevio.cz/graphql"
print("📡 Querying Medevio GraphQL API (testing `updatedAt` field)...\n")
r = requests.post(url, json=payload, headers=headers)
print(f"HTTP status: {r.status_code}\n")
try:
data = r.json()
except Exception as e:
print("❌ Failed to parse JSON:", e)
print("Raw text:\n", r.text)
return
print("=== JSON response ===")
print(json.dumps(data, indent=2, ensure_ascii=False))
# Quick check: did it return an error message about updatedAt?
errors = data.get("errors")
if errors:
print("\n⚠️ Medevio returned GraphQL error:")
for e in errors:
print(f"{e.get('message')}")
else:
print("\n✅ No errors, `updatedAt` might exist in schema!")
if __name__ == "__main__":
main()
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
from pathlib import Path
import requests
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug
queueId: $queueId
queueAssignment: $queueAssignment
pageInfo: $pageInfo
) {
count
patientRequests {
id
createdAt
doneAt
displayTitle(locale: $locale)
extendedPatient {
name
surname
identificationNumber
}
}
}
}
"""
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
# 👇 state zcela vynechán
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": 10, "offset": 0},
"locale": "cs",
}
payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
url = "https://api.medevio.cz/graphql"
print("📡 Querying Medevio GraphQL API (no state argument)...\n")
r = requests.post(url, json=payload, headers=headers)
print(f"HTTP status: {r.status_code}\n")
try:
data = r.json()
print("=== JSON response ===")
print(json.dumps(data, indent=2, ensure_ascii=False))
except Exception as e:
print("❌ Failed to parse JSON:", e)
print("Raw text:\n", r.text)
if __name__ == "__main__":
main()
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import time
import pymysql
import requests
from pathlib import Path
from datetime import datetime
# ================================
# 🔧 CONFIGURATION
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 100
STATES = ["ACTIVE", "DONE"] # optionally add "REMOVED"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$pageInfo: PageInfo!,
$locale: Locale!,
$state: PatientRequestState
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
pageInfo: $pageInfo,
state: $state
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
}
}
}
"""
# ================================
# 🔑 TOKEN
# ================================
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
# ================================
# 🕒 DATETIME CONVERSION
# ================================
def to_mysql_dt(iso_str):
"""Convert ISO 8601 (with Z) to MySQL DATETIME."""
if not iso_str:
return None
try:
dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return None
# ================================
# 💾 UPSERT TO MYSQL
# ================================
def upsert(conn, r):
p = (r.get("extendedPatient") or {})
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r.get("id"),
r.get("displayTitle"),
to_mysql_dt(r.get("createdAt")),
to_mysql_dt(r.get("updatedAt")),
to_mysql_dt(r.get("doneAt")),
to_mysql_dt(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# 📡 FETCH ONE BATCH
# ================================
def fetch_batch(headers, state, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
"state": state,
}
payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json().get("data", {}).get("requestsResponse", {})
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# 🧠 MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
total_downloaded = 0
total_upserted = 0
for state in STATES:
print(f"\n📡 STATE = {state}")
offset = 0
state_total = None
while True:
batch, count_total = fetch_batch(headers, state, offset)
if state_total is None:
state_total = count_total
print(f" • Total from server: {state_total}")
if not batch:
break
print(f" • Offset {offset:>5}: got {len(batch)}")
for r in batch:
upsert(conn, r)
total_upserted += 1
total_downloaded += len(batch)
offset += BATCH_SIZE
if offset >= state_total:
break
time.sleep(0.4) # respect API
conn.close()
print(f"\n✅ Done. Downloaded {total_downloaded} items, upserted {total_upserted} rows (states: {', '.join(STATES)}).")
if __name__ == "__main__":
main()
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import time
import pymysql
import requests
from pathlib import Path
from datetime import datetime
# ================================
# 🔧 CONFIGURATION
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 1000
STATES = ["ACTIVE", "DONE"] # optionally add "REMOVED"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$pageInfo: PageInfo!,
$locale: Locale!,
$state: PatientRequestState
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
pageInfo: $pageInfo,
state: $state
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
}
}
}
"""
# ================================
# 🔑 TOKEN
# ================================
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
# ================================
# 🕒 DATETIME CONVERSION
# ================================
def to_mysql_dt(iso_str):
"""Convert ISO 8601 (with Z) to MySQL DATETIME."""
if not iso_str:
return None
try:
dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return None
# ================================
# 💾 UPSERT TO MYSQL
# ================================
def upsert_many(conn, batch):
"""Upsert multiple records in one commit."""
if not batch:
return
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = []
for r in batch:
p = (r.get("extendedPatient") or {})
vals.append((
r.get("id"),
r.get("displayTitle"),
to_mysql_dt(r.get("createdAt")),
to_mysql_dt(r.get("updatedAt")),
to_mysql_dt(r.get("doneAt")),
to_mysql_dt(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
))
with conn.cursor() as cur:
cur.executemany(sql, vals)
conn.commit()
def upsert(conn, r):
p = (r.get("extendedPatient") or {})
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r.get("id"),
r.get("displayTitle"),
to_mysql_dt(r.get("createdAt")),
to_mysql_dt(r.get("updatedAt")),
to_mysql_dt(r.get("doneAt")),
to_mysql_dt(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# 📡 FETCH ONE BATCH
# ================================
def fetch_batch(headers, state, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
"state": state,
}
payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json().get("data", {}).get("requestsResponse", {})
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# 🧠 MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
total_downloaded = 0
total_upserted = 0
for state in STATES:
print(f"\n📡 STATE = {state}")
offset = 0
state_total = None
while True:
batch, count_total = fetch_batch(headers, state, offset)
if state_total is None:
state_total = count_total
print(f" • Total from server: {state_total}")
if not batch:
break
print(f" • Offset {offset:>5}: got {len(batch)}")
# Perform one efficient upsert for the entire batch
upsert_many(conn, batch)
total_upserted += len(batch)
total_downloaded += len(batch)
offset += BATCH_SIZE
if offset >= state_total:
break
time.sleep(10) # respect API
conn.close()
print(f"\n✅ Done. Downloaded {total_downloaded} items, upserted {total_upserted} rows (states: {', '.join(STATES)}).")
if __name__ == "__main__":
main()
@@ -0,0 +1,92 @@
import requests
import json
from pathlib import Path
# === Nastavení ===
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
REQUEST_ID = "092a0c63-28be-4c6b-ab3b-204e1e2641d4"
OUTPUT_DIR = Path(r"u:\Dropbox\!!!Days\Downloads Z230\Medevio_přílohy")
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2(
$requestId: UUID!,
$isDoctor: Boolean!
) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient @include(if: $isDoctor)
}
}
}
"""
variables = {
"isDoctor": True,
"requestId": REQUEST_ID,
}
headers = {
"Authorization": f"Bearer {read_token(TOKEN_PATH)}",
"Content-Type": "application/json",
"Accept": "application/json",
}
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
print("📡 Querying Medevio API for attachments...\n")
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
print(f"HTTP status: {r.status_code}\n")
data = r.json()
records = data.get("data", {}).get("patientRequestMedicalRecords", [])
if not records:
print("⚠️ No attachments found.")
exit()
# === Uložení ===
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print(f"📂 Saving {len(records)} attachments to: {OUTPUT_DIR}\n")
for rec in records:
med = rec.get("medicalRecord", {})
url = med.get("downloadUrl")
name = med.get("description", med.get("id")) or "unknown.pdf"
if not url:
print(f"❌ Skipped {name} (no download URL)")
continue
safe_name = name.replace("/", "_").replace("\\", "_")
out_path = OUTPUT_DIR / safe_name
print(f"⬇️ Downloading: {safe_name}")
try:
file_data = requests.get(url, timeout=30)
file_data.raise_for_status()
out_path.write_bytes(file_data.content)
print(f"✅ Saved: {out_path.name} ({len(file_data.content)/1024:.1f} KB)")
except Exception as e:
print(f"❌ Error saving {safe_name}: {e}")
print("\n🎉 Done!")
+59
View File
@@ -0,0 +1,59 @@
import requests
import json
from pathlib import Path
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
REQUEST_ID = "092a0c63-28be-4c6b-ab3b-204e1e2641d4"
CLINIC_SLUG = "mudr-buzalkova"
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2(
$requestId: UUID!,
) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient
}
}
}
"""
variables = {
"requestId": REQUEST_ID,
}
headers = {
"Authorization": f"Bearer {read_token(TOKEN_PATH)}",
"Content-Type": "application/json",
"Accept": "application/json",
}
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
print("📡 Querying Medevio API...\n")
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
print(f"HTTP status: {r.status_code}\n")
print(json.dumps(r.json(), indent=2, ensure_ascii=False))
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os,zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
import shutil
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BASE_DIR = Path(r"u:\Dropbox\ordinace\Dokumentace_ke_zpracování\Medevio_přílohy")
BASE_DIR.mkdir(parents=True, exist_ok=True)
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2(
$requestId: UUID!,
) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient
}
}
}
"""
def short_crc8(uuid_str: str) -> str:
"""Return deterministic 8-char hex string from any input string (CRC32)."""
return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}"
def extract_filename_from_url(url: str) -> str:
"""Extracts filename from S3-style URL (between last '/' and first '?')."""
try:
filename = url.split("/")[-1].split("?")[0]
return filename
except Exception:
return "unknown_filename"
def safe_rename(src: Path, dst: Path, retries: int = 5, delay: float = 3.0):
"""Rename a folder with retries to avoid Dropbox/OneDrive sync lock issues."""
for attempt in range(1, retries + 1):
try:
src.rename(dst)
return # success
except PermissionError as e:
print(f" ⚠️ Rename attempt {attempt}/{retries} failed ({e}) — waiting {delay}s...")
time.sleep(delay)
except Exception as e:
print(f" ❌ Unexpected rename error: {e}")
break
print(f" 🚫 Failed to rename '{src}''{dst}' after {retries} attempts.")
# ==============================
# 🔑 TOKEN
# ==============================
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
# ==============================
# 💾 DOWNLOAD FILE
# ==============================
def download_file(url: str, out_path: Path):
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "wb") as f:
f.write(r.content)
print(f" 💾 Saved: {out_path.relative_to(BASE_DIR)}")
except Exception as e:
print(f" ⚠️ Failed to download {out_path.name}: {e}")
# ==============================
# 📡 FETCH ATTACHMENTS
# ==============================
def fetch_attachments(headers, request_id):
variables = {
"requestId": request_id,
}
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
if r.status_code != 200:
print(f"❌ HTTP {r.status_code}")
return []
data = r.json().get("data", {}).get("patientRequestMedicalRecords", [])
return data
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
cur.execute("""
SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt
FROM pozadavky
WHERE displayTitle = 'Odeslat lékařskou zprávu'
""")
rows = cur.fetchall()
print(f"📋 Found {len(rows)} 'Odeslat lékařskou zprávu' requests")
for i, row in enumerate(rows, 1):
req_id = row["id"]
print(req_id)
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
jmeno = row.get("pacient_jmeno") or ""
created = row.get("createdAt")
created_date = None
if created:
try:
created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S").strftime("%Y-%m-%d")
except Exception:
created_date = "unknown"
patient_dir = BASE_DIR / f"{prijmeni}, {jmeno}" / created_date
print(f"\n[{i}/{len(rows)}] 📂 {patient_dir.relative_to(BASE_DIR)}")
attachments = fetch_attachments(headers, req_id)
# print(attachments)
if not attachments:
print(" ⚠️ No attachments")
continue
# vytvoř krátký CRC32 hash z UUID
uuid_short = short_crc8(str(req_id))
# Dočasná složka bez počtu
temp_dir = BASE_DIR / f"{prijmeni}, {jmeno}" / f"{created_date} {uuid_short}"
temp_dir.mkdir(parents=True, exist_ok=True)
for a in attachments:
m = a.get("medicalRecord") or {}
# fname = m.get("description") or f"{m.get('id')}.bin"
url = m.get("downloadUrl")
fname = extract_filename_from_url(url)
if url:
out_path = temp_dir / fname
download_file(url, out_path)
# Po stažení všech příloh spočítej skutečné soubory
real_count = len([f for f in temp_dir.iterdir() if f.is_file()])
# Přejmenuj složku na finální název s počtem
final_dir = temp_dir.parent / f"{temp_dir.name} ({real_count})"
if real_count != 0:
safe_rename(temp_dir, final_dir)
print(f" 📎 Saved {real_count} attachments → {final_dir.relative_to(BASE_DIR)}")
else:
print(f" ⚠️ No attachments for {temp_dir.name}")
temp_dir.rmdir() # smaž prázdnou složku
conn.close()
print("\n✅ Done!")
if __name__ == "__main__":
main()
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download all 'Odeslat lékařskou zprávu' attachments from Medevio API
and store them (including binary content) directly into MySQL table `medevio_downloads`.
Each attachment (PDF, image, etc.) is fetched once and saved as LONGBLOB.
Duplicate protection is ensured via UNIQUE KEY on `attachment_id`.
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient
}
}
}
"""
# ==============================
# 🧮 HELPERS
# ==============================
def short_crc8(uuid_str: str) -> str:
"""Return deterministic 8-char hex string from any input string (CRC32)."""
return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}"
def extract_filename_from_url(url: str) -> str:
"""Extracts filename from S3-style URL (between last '/' and first '?')."""
try:
return url.split("/")[-1].split("?")[0]
except Exception:
return "unknown_filename"
def read_token(p: Path) -> str:
"""Read Bearer token from file."""
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
# ==============================
# 📡 FETCH ATTACHMENTS
# ==============================
def fetch_attachments(headers, request_id):
variables = {"requestId": request_id}
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
data = r.json().get("data", {}).get("patientRequestMedicalRecords", [])
return data
# ==============================
# 💾 SAVE TO MYSQL (with skip)
# ==============================
def insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids):
attachment_id = a.get("id")
if attachment_id in existing_ids:
print(f" ⏭️ Skipping already downloaded attachment {attachment_id}")
return
url = m.get("downloadUrl")
if not url:
print(" ⚠️ No download URL")
return
filename = extract_filename_from_url(url)
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
content = r.content
except Exception as e:
print(f" ⚠️ Failed to download {url}: {e}")
return
file_size = len(content)
attachment_type = a.get("attachmentType")
content_type = m.get("contentType")
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type, filename,
content_type, file_size, pacient_jmeno, pacient_prijmeni,
created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
attachment_type,
filename,
content_type,
file_size,
jmeno,
prijmeni,
created_date,
content
))
print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)")
existing_ids.add(attachment_id) # add to skip list
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
print("📦 Loading list of already downloaded attachments...")
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
print(f"✅ Found {len(existing_ids)} attachments already saved.")
with conn.cursor() as cur:
cur.execute("""
SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt
FROM pozadavky
WHERE displayTitle = 'Odeslat lékařskou zprávu'
""")
rows = cur.fetchall()
print(f"📋 Found {len(rows)} 'Odeslat lékařskou zprávu' requests")
for i, row in enumerate(rows, 1):
req_id = row["id"]
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
jmeno = row.get("pacient_jmeno") or ""
created = row.get("createdAt")
try:
created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S")
except Exception:
created_date = None
print(f"\n[{i}/{len(rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
attachments = fetch_attachments(headers, req_id)
if not attachments:
print(" ⚠️ No attachments")
continue
with conn.cursor() as cur:
for a in attachments:
m = a.get("medicalRecord") or {}
insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids)
conn.commit()
print(f"{len(attachments)} attachments saved for {prijmeni}, {jmeno}")
time.sleep(0.5) # be nice to the API
conn.close()
print("\n✅ Done! All attachments stored in MySQL table `medevio_downloads`.")
# ==============================
if __name__ == "__main__":
main()
@@ -0,0 +1,208 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Fetch communication threads (messages) from Medevio API
for pozadavky where communicationprocessed IS NULL or outdated,
optionally filtered by creation date.
Stores results in MySQL table `medevio_messages`.
"""
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ✅ Optional: Only process requests created after this date
# Leave empty ("") to process all
CREATED_AFTER = "2025-11-09" # 🕓 Adjust freely, or set to "" for no limit
# ==============================
# 🔐 TOKEN
# ==============================
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
headers = {
"Authorization": f"Bearer {read_token(TOKEN_PATH)}",
"Content-Type": "application/json",
}
# ==============================
# 🧩 GRAPHQL QUERY
# ==============================
GRAPHQL_QUERY = """
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
messages: listMessages(
patientRequestId: $requestId
updatedSince: $updatedSince
) {
id
createdAt
text
updatedAt
readAt
sender { id name surname clinicId }
medicalRecord { downloadUrl description contentType }
}
}
"""
# ==============================
# 🧮 HELPERS
# ==============================
def normalize_ts(ts: str):
"""Convert ISO 8601 string to MySQL DATETIME format."""
if not ts:
return None
ts = ts.replace("T", " ").replace("Z", "")
if "." in ts:
ts = ts.split(".")[0]
return ts
# ==============================
# 📡 FETCH MESSAGES
# ==============================
def fetch_messages(request_id):
payload = {
"operationName": "UseMessages_ListMessages",
"variables": {"requestId": request_id, "updatedSince": None},
"query": GRAPHQL_QUERY,
}
r = requests.post(GRAPHQL_URL, headers=headers, json=payload, timeout=30)
if r.status_code != 200:
print(f"❌ HTTP {r.status_code}: {r.text}")
return []
return r.json().get("data", {}).get("messages", []) or []
# ==============================
# 💾 CREATE TABLE IF NEEDED
# ==============================
def ensure_table_exists(conn):
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS medevio_messages (
id VARCHAR(64) PRIMARY KEY,
request_id VARCHAR(64),
sender_name VARCHAR(255),
sender_id VARCHAR(64),
sender_clinic_id VARCHAR(64),
text TEXT,
created_at DATETIME NULL,
read_at DATETIME NULL,
updated_at DATETIME NULL,
attachment_url TEXT,
attachment_description TEXT,
attachment_content_type VARCHAR(128),
inserted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
""")
conn.commit()
# ==============================
# 💾 INSERT MESSAGE
# ==============================
def insert_message(cur, req_id, msg):
sender = msg.get("sender") or {}
medrec = msg.get("medicalRecord") or {}
cur.execute("""
REPLACE INTO medevio_messages (
id, request_id, sender_name, sender_id, sender_clinic_id, text,
created_at, read_at, updated_at,
attachment_url, attachment_description, attachment_content_type
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
""", (
msg.get("id"),
req_id,
f"{sender.get('name','')} {sender.get('surname','')}".strip(),
sender.get("id"),
sender.get("clinicId"),
msg.get("text"),
normalize_ts(msg.get("createdAt")),
normalize_ts(msg.get("readAt")),
normalize_ts(msg.get("updatedAt")),
medrec.get("downloadUrl"),
medrec.get("description"),
medrec.get("contentType")
))
# ==============================
# 🧠 MAIN
# ==============================
def main():
conn = pymysql.connect(**DB_CONFIG)
ensure_table_exists(conn)
with conn.cursor() as cur:
sql = """
SELECT id, createdAt, updatedAt, communicationprocessed
FROM pozadavky
WHERE (communicationprocessed IS NULL OR communicationprocessed < updatedAt)
"""
if CREATED_AFTER:
sql += " AND createdAt >= %s"
cur.execute(sql, (CREATED_AFTER,))
else:
cur.execute(sql)
rows = cur.fetchall()
if not rows:
print("✅ No pending communication updates.")
return
print(f"📋 Found {len(rows)} requests needing communication check.")
for i, row in enumerate(rows, 1):
req_id = row["id"]
print(f"\n[{i}/{len(rows)}] 🔍 Fetching communication for {req_id} ...")
messages = fetch_messages(req_id)
print(f" 💬 {len(messages)} messages found.")
# Update timestamp even if none found
with conn.cursor() as cur:
if messages:
for msg in messages:
insert_message(cur, req_id, msg)
cur.execute("""
UPDATE pozadavky
SET communicationprocessed = NOW()
WHERE id = %s
""", (req_id,))
conn.commit()
print(f" ✅ Processed {len(messages)} messages for {req_id}")
time.sleep(0.5) # avoid hammering the API
conn.close()
print("\n✅ All communication threads processed and timestamps updated.")
# ==============================
if __name__ == "__main__":
main()
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download and store Medevio questionnaires (userNote + eCRF) for all patient requests.
Uses the verified working query "GetPatientRequest2".
"""
import json
import requests
import pymysql
from datetime import datetime
from pathlib import Path
import time
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
from datetime import datetime
def fix_datetime(dt_str):
"""Convert ISO 8601 string with 'Z' or ms into MySQL DATETIME format."""
if not dt_str:
return None
try:
# Remove trailing Z and parse flexible ISO format
return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", ""))
except Exception:
return None
# ✅ Optional: limit which requests to process
CREATED_AFTER = "2025-11-09" # set "" to disable
# ==============================
# 🧮 HELPERS
# ==============================
def read_token(p: Path) -> str:
"""Read Bearer token from file."""
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
GRAPHQL_QUERY = r"""
query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) {
request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) {
id
displayTitle(locale: $locale)
createdAt
updatedAt
userNote
eventType
extendedPatient(clinicSlug: $clinicSlug) {
name
surname
identificationNumber
}
ecrfFilledData(locale: $locale) {
name
groups {
label
fields {
name
label
type
value
}
}
}
}
}
"""
def fetch_questionnaire(headers, request_id, clinic_slug):
"""Fetch questionnaire for given request ID."""
payload = {
"operationName": "GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": {
"requestId": request_id,
"clinicSlug": clinic_slug,
"locale": "cs",
},
}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40)
if r.status_code != 200:
print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}")
return None
return r.json().get("data", {}).get("request")
def insert_questionnaire(cur, req):
"""Insert questionnaire data into MySQL."""
if not req:
return
patient = req.get("extendedPatient") or {}
ecrf_data = req.get("ecrfFilledData")
created_at = fix_datetime(req.get("createdAt"))
updated_at = fix_datetime(req.get("updatedAt"))
cur.execute("""
INSERT INTO medevio_questionnaires (
request_id, patient_name, patient_surname, patient_identification,
created_at, updated_at, user_note, ecrf_json
)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
updated_at = VALUES(updated_at),
user_note = VALUES(user_note),
ecrf_json = VALUES(ecrf_json),
updated_local = NOW()
""", (
req.get("id"),
patient.get("name"),
patient.get("surname"),
patient.get("identificationNumber"),
created_at,
updated_at,
req.get("userNote"),
json.dumps(ecrf_data, ensure_ascii=False),
))
print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}")
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
sql = """
SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed
FROM pozadavky
WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt)
"""
if CREATED_AFTER:
sql += " AND createdAt >= %s"
cur.execute(sql, (CREATED_AFTER,))
else:
cur.execute(sql)
rows = cur.fetchall()
print(f"📋 Found {len(rows)} requests needing questionnaire check.")
for i, row in enumerate(rows, 1):
req_id = row["id"]
print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...")
req = fetch_questionnaire(headers, req_id, CLINIC_SLUG)
if not req:
print(" ⚠️ No questionnaire data found.")
continue
with conn.cursor() as cur:
insert_questionnaire(cur, req)
cur.execute("UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
time.sleep(0.4) # polite pacing
conn.close()
print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.")
# ==============================
if __name__ == "__main__":
main()
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pandas as pd
import pymysql
from datetime import datetime, timedelta
# ================================
# ⚙️ CONFIGURATION
# ================================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
# kam uložit výstup
OUTPUT_DIR = r"U:\Dropbox\!!!Days\Downloads Z230"
DAYS_BACK = 700 # posledních X dní
# ================================
# 📘 SQL dotaz
# ================================
SQL = f"""
SELECT
m.id AS Message_ID,
m.request_id AS Request_ID,
m.created_at AS Datum_vytvoření,
m.sender_name AS Odesílatel,
m.text AS Text_zprávy,
m.pacient_jmeno AS Pacient_jméno,
m.pacient_prijmeni AS Pacient_příjmení,
m.pacient_rodnecislo AS Rodné_číslo
FROM medevio_messages m
WHERE m.created_at >= NOW() - INTERVAL {DAYS_BACK} DAY
ORDER BY m.created_at DESC;
"""
# ================================
# 🧠 MAIN
# ================================
def main():
conn = pymysql.connect(**DB_CONFIG)
df = pd.read_sql(SQL, conn)
conn.close()
today = datetime.now().strftime("%Y-%m-%d")
output_path = f"{OUTPUT_DIR}\\Medevio_messages_report_{today}.xlsx"
df.to_excel(output_path, index=False)
print(f"✅ Export hotov: {output_path}")
print(f"📄 Počet řádků: {len(df)}")
if __name__ == "__main__":
main()
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from datetime import datetime
from pathlib import Path
import time, socket
# ===============================
# ⚙️ CONFIG
# ===============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetMessages(
$clinicSlug: String!,
$requestId: ID!
) {
clinicRequestDetail_GetPatientRequestMessages(
clinicSlug: $clinicSlug,
requestId: $requestId
) {
id
text
createdAt
sender {
id
name
}
extendedPatient {
name
surname
identificationNumber
}
}
}
"""
# ===============================
# 🔑 Token reader
# ===============================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
# ===============================
# 🕒 Helper
# ===============================
def to_mysql_dt(iso_str):
if not iso_str:
return None
try:
dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return None
# ===============================
# 💾 Upsert
# ===============================
def upsert_message(conn, msg, request_id):
s = msg.get("sender") or {}
p = msg.get("extendedPatient") or {}
sql = """
INSERT INTO medevio_messages (
id, request_id, sender_name, sender_id, text, created_at,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
text=VALUES(text),
created_at=VALUES(created_at),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
msg.get("id"),
request_id,
s.get("name"),
s.get("id"),
msg.get("text"),
to_mysql_dt(msg.get("createdAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ===============================
# 📡 Fetch messages for one request
# ===============================
def fetch_messages(headers, request_id):
payload = {
"operationName": "ClinicRequestDetail_GetMessages",
"query": GRAPHQL_QUERY,
"variables": {"clinicSlug": CLINIC_SLUG, "requestId": request_id},
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json().get("data", {}).get("clinicRequestDetail_GetPatientRequestMessages", [])
return data
# ===============================
# 🧠 Main
# ===============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
cur = conn.cursor()
# vezmeme všechny request_id z tabulky pozadavky
cur.execute("SELECT id FROM pozadavky ORDER BY updatedAt DESC")
request_ids = [r["id"] for r in cur.fetchall()]
print(f"📋 Found {len(request_ids)} požadavků.")
for i, rid in enumerate(request_ids, 1):
try:
msgs = fetch_messages(headers, rid)
for msg in msgs:
upsert_message(conn, msg, rid)
print(f"[{i}/{len(request_ids)}] {rid}{len(msgs)} zpráv uloženo.")
time.sleep(0.4)
except Exception as e:
print(f"❌ Chyba při načítání {rid}: {e}")
conn.close()
print("\n✅ Hotovo, všechny zprávy synchronizovány.")
if __name__ == "__main__":
main()
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from pathlib import Path
from datetime import datetime
import time
# ================================
# ⚙️ CONFIGURATION
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 100
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestGrid_ListPatientRequestsForClinic2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$pageInfo: PageInfo!,
$locale: Locale!,
$state: PatientRequestState
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
pageInfo: $pageInfo,
state: $state
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
}
}
}
"""
# ================================
# 🔑 TOKEN
# ================================
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
# ================================
# 📡 FETCH FUNCTION
# ================================
def fetch_requests(headers, state, offset=0):
"""Fetch a batch of patient requests for a given state."""
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
"state": state,
}
payload = {
"operationName": "ClinicRequestGrid_ListPatientRequestsForClinic2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
for attempt in range(3): # up to 3 attempts
try:
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
r.raise_for_status()
resp = r.json().get("data", {}).get("requestsResponse", {})
return resp.get("patientRequests", []), resp.get("count", 0)
except requests.exceptions.RequestException as e:
print(f"⚠️ Chyba při načítání (pokus {attempt+1}/3): {e}")
time.sleep(5)
return [], 0
# ================================
# 💾 UPDATE ALL MESSAGES BY PATIENT DATA
# ================================
def update_all_messages(conn, patient):
"""Update all messages belonging to this request with patient data."""
p = patient.get("extendedPatient") or {}
if not p:
return 0
sql = """
UPDATE medevio_messages
SET pacient_jmeno=%s,
pacient_prijmeni=%s,
pacient_rodnecislo=%s
WHERE request_id=%s
"""
vals = (p.get("name"), p.get("surname"), p.get("identificationNumber"), patient.get("id"))
with conn.cursor() as cur:
cur.execute(sql, vals)
affected = cur.rowcount
conn.commit()
return affected
# ================================
# 🧠 MAIN
# ================================
def process_state(conn, headers, state):
print(f"\n=== 🟦 Zpracovávám {state} požadavky ===")
offset = 0
total_processed = 0
total_updated = 0
while True:
batch, total_count = fetch_requests(headers, state, offset)
if not batch:
break
print(f"📦 Dávka od offsetu {offset} ({len(batch)} záznamů z {total_count})")
for r in batch:
updated = update_all_messages(conn, r)
total_processed += 1
total_updated += updated
if updated:
print(f"{r.get('id')}{updated} zpráv aktualizováno")
offset += BATCH_SIZE
if offset >= total_count:
break
time.sleep(0.4)
print(f"{state}: zpracováno {total_processed} požadavků, aktualizováno {total_updated} zpráv.")
return total_processed, total_updated
# ================================
# 🚀 ENTRY POINT
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
print(f"\n=== Medevio mass patient sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
total_p, total_u = process_state(conn, headers, "ACTIVE")
done_p, done_u = process_state(conn, headers, "DONE")
conn.close()
print("\n=== 🧾 SOUHRN ===")
print(f"ACTIVE: {total_p} požadavků, {total_u} zpráv aktualizováno")
print(f"DONE: {done_p} požadavků, {done_u} zpráv aktualizováno")
print("===========================================")
print(f"CELKEM: {total_p + done_p} požadavků, {total_u + done_u} zpráv aktualizováno ✅")
if __name__ == "__main__":
main()
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download all attachments for pozadavky where attachmentsProcessed IS NULL
and (optionally) createdAt is newer than a configurable cutoff date.
Store them in MySQL table `medevio_downloads`, and update pozadavky.attachmentsProcessed = NOW().
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ✅ Optional: Only process requests created after this date
# Leave empty ("") to process all
CREATED_AFTER = "2025-01-01" # 🕓 Adjust freely, or set to "" for no limit
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient
}
}
}
"""
# ==============================
# 🧮 HELPERS
# ==============================
def short_crc8(uuid_str: str) -> str:
"""Return deterministic 8-char hex string from any input string (CRC32)."""
return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}"
def extract_filename_from_url(url: str) -> str:
"""Extracts filename from S3-style URL (between last '/' and first '?')."""
try:
return url.split("/")[-1].split("?")[0]
except Exception:
return "unknown_filename"
def read_token(p: Path) -> str:
"""Read Bearer token from file."""
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
# ==============================
# 📡 FETCH ATTACHMENTS
# ==============================
def fetch_attachments(headers, request_id):
variables = {"requestId": request_id}
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
data = r.json().get("data", {}).get("patientRequestMedicalRecords", [])
return data
# ==============================
# 💾 SAVE TO MYSQL (with skip)
# ==============================
def insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids):
attachment_id = a.get("id")
if attachment_id in existing_ids:
print(f" ⏭️ Skipping already downloaded attachment {attachment_id}")
return False
url = m.get("downloadUrl")
if not url:
print(" ⚠️ No download URL")
return False
filename = extract_filename_from_url(url)
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
content = r.content
except Exception as e:
print(f" ⚠️ Failed to download {url}: {e}")
return False
file_size = len(content)
attachment_type = a.get("attachmentType")
content_type = m.get("contentType")
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type, filename,
content_type, file_size, pacient_jmeno, pacient_prijmeni,
created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
attachment_type,
filename,
content_type,
file_size,
jmeno,
prijmeni,
created_date,
content
))
existing_ids.add(attachment_id)
print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)")
return True
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
print("📦 Loading list of already downloaded attachments...")
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
print(f"✅ Found {len(existing_ids)} attachments already saved.")
# ✅ Dynamic SQL with optional createdAt filter
sql = """
SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt
FROM pozadavky
WHERE attachmentsProcessed IS NULL
"""
params = []
if CREATED_AFTER:
sql += " AND createdAt >= %s"
params.append(CREATED_AFTER)
with conn.cursor() as cur:
cur.execute(sql, params)
rows = cur.fetchall()
print(f"📋 Found {len(rows)} pozadavky to process (attachmentsProcessed IS NULL"
+ (f", created >= {CREATED_AFTER}" if CREATED_AFTER else "") + ")")
for i, row in enumerate(rows, 1):
time.sleep(1) # polite API delay
req_id = row["id"]
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
jmeno = row.get("pacient_jmeno") or ""
created = row.get("createdAt")
try:
created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S")
except Exception:
created_date = None
print(f"\n[{i}/{len(rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
attachments = fetch_attachments(headers, req_id)
if not attachments:
print(" ⚠️ No attachments found")
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
continue
with conn.cursor() as cur:
for a in attachments:
m = a.get("medicalRecord") or {}
insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date, existing_ids)
conn.commit()
# ✅ mark processed
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
print(f"{len(attachments)} attachments processed for {prijmeni}, {jmeno}")
time.sleep(0.3) # polite API delay
conn.close()
print("\n✅ Done! All new attachments processed and pozadavky updated.")
# ==============================
if __name__ == "__main__":
main()
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from pathlib import Path
from datetime import datetime, timezone
import time
from dateutil import parser
# Force UTF-8 output even under Windows Task Scheduler
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
# Python < 3.7 fallback (not needed for you, but safe)
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ================================
# 🔧 CONFIGURATION
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 100
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ⭐ NOVÝ TESTOVANÝ DOTAZ obsahuje lastMessage.createdAt
GRAPHQL_QUERY = r"""
query ClinicRequestList2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
lastMessage {
createdAt
}
}
}
}
"""
# ================================
# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL)
# ================================
def to_mysql_dt_utc(iso_str):
"""
Parse Medevio timestamps safely.
Treat timestamps WITHOUT timezone as UTC.
Convert to local time before saving to MySQL.
"""
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str)
# If tz is missing → assume UTC
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# Convert to local timezone
dt_local = dt.astimezone()
return dt_local.strftime("%Y-%m-%d %H:%M:%S")
except:
return None
# ================================
# 🔑 TOKEN
# ================================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
return tok.split(" ", 1)[1]
return tok
# ================================
# 💾 UPSERT (včetně správného updatedAt)
# ================================
def upsert(conn, r):
p = r.get("extendedPatient") or {}
# raw timestamps z API nyní přes nový parser
api_updated = to_mysql_dt_utc(r.get("updatedAt"))
last_msg = r.get("lastMessage") or {}
msg_updated = to_mysql_dt_utc(last_msg.get("createdAt"))
# nejnovější změna
def max_dt(a, b):
if a and b:
return max(a, b)
return a or b
final_updated = max_dt(api_updated, msg_updated)
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r.get("id"),
r.get("displayTitle"),
to_mysql_dt_utc(r.get("createdAt")),
final_updated,
to_mysql_dt_utc(r.get("doneAt")),
to_mysql_dt_utc(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# 📡 FETCH ACTIVE PAGE
# ================================
def fetch_active(headers, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
"state": "ACTIVE",
}
payload = {
"operationName": "ClinicRequestList2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json().get("data", {}).get("requestsResponse", {})
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# 🧠 MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
offset = 0
total_processed = 0
total_count = None
while True:
batch, count = fetch_active(headers, offset)
if total_count is None:
total_count = count
print(f"📡 Celkem ACTIVE v Medevio: {count}")
if not batch:
break
for r in batch:
upsert(conn, r)
total_processed += len(batch)
print(f"{total_processed}/{total_count} ACTIVE processed")
if offset + BATCH_SIZE >= count:
break
offset += BATCH_SIZE
time.sleep(0.4)
conn.close()
print("\n✅ ACTIVE sync hotovo!\n")
# ================================
if __name__ == "__main__":
main()
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from pathlib import Path
from datetime import datetime
from dateutil import parser
# ================================
# 🔧 CONFIGURATION
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
LIMIT = 500 # batch size / number of records
FULL_DOWNLOAD = False # 🔥 TOGGLE: False = last X, True = ALL batches
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ⭐ Query with lastMessage
GRAPHQL_QUERY = r"""
query ClinicRequestList2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
lastMessage {
createdAt
}
}
}
}
"""
# ================================
# TOKEN
# ================================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
# ================================
# DATETIME PARSER (UTC → MySQL)
# ================================
def to_mysql_dt(iso_str):
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str) # ISO8601 → aware datetime (UTC)
dt = dt.astimezone() # convert to local timezone
return dt.strftime("%Y-%m-%d %H:%M:%S")
except:
return None
# ================================
# UPSERT REQUEST
# ================================
def upsert(conn, r):
p = r.get("extendedPatient") or {}
api_updated = to_mysql_dt(r.get("updatedAt"))
last_msg = r.get("lastMessage") or {}
msg_at = to_mysql_dt(last_msg.get("createdAt"))
def max_dt(a, b):
if a and b:
return max(a, b)
return a or b
final_updated = max_dt(api_updated, msg_at)
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r["id"],
r.get("displayTitle"),
to_mysql_dt(r.get("createdAt")),
final_updated,
to_mysql_dt(r.get("doneAt")),
to_mysql_dt(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# FETCH DONE REQUESTS (one batch)
# ================================
def fetch_done(headers, offset):
vars = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": LIMIT, "offset": offset},
"locale": "cs",
"state": "DONE",
}
payload = {
"operationName": "ClinicRequestList2",
"query": GRAPHQL_QUERY,
"variables": vars,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json()["data"]["requestsResponse"]
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
print(f"\n=== Sync CLOSED requests @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
offset = 0
total_count = None
total_processed = 0
while True:
batch, count = fetch_done(headers, offset)
if total_count is None:
total_count = count
print(f"📡 Total DONE in Medevio: {count}")
if not batch:
break
print(f" • Processing batch offset={offset} size={len(batch)}")
for r in batch:
upsert(conn, r)
total_processed += len(batch)
if not FULL_DOWNLOAD:
# process only last LIMIT records
break
# FULL DOWNLOAD → fetch next batch
offset += LIMIT
if offset >= count:
break
conn.close()
print(f"\n✅ DONE — {total_processed} requests synced.\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download and store Medevio questionnaires (userNote + eCRF) for all patient requests.
Uses the verified working query "GetPatientRequest2".
"""
import json
import requests
import pymysql
from datetime import datetime
from pathlib import Path
import time
import sys
# Force UTF-8 output even under Windows Task Scheduler
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
# Python < 3.7 fallback (not needed for you, but safe)
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ==============================
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
# ==============================
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc.lower().startswith("utf"):
# strip emoji + anything above BMP
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
# final ASCII fallback
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ==============================
# 🕒 DATETIME FIXER
# ==============================
def fix_datetime(dt_str):
"""Convert ISO 8601 string with 'Z' or ms into MySQL DATETIME format."""
if not dt_str:
return None
try:
return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", ""))
except Exception:
return None
# Optional filter
CREATED_AFTER = "2025-01-01"
# ==============================
# 🧮 HELPERS
# ==============================
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
return tok.split(" ", 1)[1]
return tok
GRAPHQL_QUERY = r"""
query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) {
request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) {
id
displayTitle(locale: $locale)
createdAt
updatedAt
userNote
eventType
extendedPatient(clinicSlug: $clinicSlug) {
name
surname
identificationNumber
}
ecrfFilledData(locale: $locale) {
name
groups {
label
fields {
name
label
type
value
}
}
}
}
}
"""
def fetch_questionnaire(headers, request_id, clinic_slug):
"""Fetch questionnaire for given request ID."""
payload = {
"operationName": "GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": {
"requestId": request_id,
"clinicSlug": clinic_slug,
"locale": "cs",
},
}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40)
if r.status_code != 200:
safe_print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}")
return None
return r.json().get("data", {}).get("request")
def insert_questionnaire(cur, req):
"""Insert questionnaire data into MySQL."""
if not req:
return
patient = req.get("extendedPatient") or {}
ecrf_data = req.get("ecrfFilledData")
created_at = fix_datetime(req.get("createdAt"))
updated_at = fix_datetime(req.get("updatedAt"))
cur.execute("""
INSERT INTO medevio_questionnaires (
request_id, created_at, updated_at, user_note, ecrf_json
)
VALUES (%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
updated_at = VALUES(updated_at),
user_note = VALUES(user_note),
ecrf_json = VALUES(ecrf_json),
updated_local = NOW()
""", (
req.get("id"),
created_at,
updated_at,
req.get("userNote"),
json.dumps(ecrf_data, ensure_ascii=False),
))
safe_print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}")
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
# load list of requests
with conn.cursor() as cur:
sql = """
SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed
FROM pozadavky
WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt)
"""
if CREATED_AFTER:
sql += " AND createdAt >= %s"
cur.execute(sql, (CREATED_AFTER,))
else:
cur.execute(sql)
rows = cur.fetchall()
safe_print(f"📋 Found {len(rows)} requests needing questionnaire check.")
# process each one
for i, row in enumerate(rows, 1):
req_id = row["id"]
safe_print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...")
req = fetch_questionnaire(headers, req_id, CLINIC_SLUG)
if not req:
safe_print(" ⚠️ No questionnaire data found.")
continue
with conn.cursor() as cur:
insert_questionnaire(cur, req)
cur.execute(
"UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s",
(req_id,)
)
conn.commit()
time.sleep(0.6)
conn.close()
safe_print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.")
# ==============================
if __name__ == "__main__":
main()
@@ -0,0 +1,287 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Stáhne konverzaci pro požadavky, kde:
messagesProcessed IS NULL OR messagesProcessed < updatedAt.
Vloží do medevio_conversation a přílohy do medevio_downloads.
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
import sys
# Force UTF-8 output even under Windows Task Scheduler
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
# Python < 3.7 fallback (not needed for you, but safe)
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ==============================
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
# ==============================
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc or not enc.lower().startswith("utf"):
# strip emoji + characters outside BMP for Task Scheduler (CP1250)
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
# fallback pure ASCII
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY_MESSAGES = r"""
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
id
createdAt
updatedAt
readAt
text
type
sender {
id
name
surname
clinicId
}
medicalRecord {
id
description
contentType
url
downloadUrl
token
createdAt
updatedAt
}
}
}
"""
# ==============================
# ⏱ DATETIME PARSER
# ==============================
def parse_dt(s):
if not s:
return None
try:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
except:
pass
try:
return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S")
except:
return None
# ==============================
# 🔐 TOKEN
# ==============================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
return tok.replace("Bearer ", "")
# ==============================
# 📡 FETCH MESSAGES
# ==============================
def fetch_messages(headers, request_id):
payload = {
"operationName": "UseMessages_ListMessages",
"query": GRAPHQL_QUERY_MESSAGES,
"variables": {"requestId": request_id, "updatedSince": None},
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
return r.json().get("data", {}).get("messages", []) or []
# ==============================
# 💾 SAVE MESSAGE
# ==============================
def insert_message(cur, req_id, msg):
sender = msg.get("sender") or {}
sender_name = " ".join(
x for x in [sender.get("name"), sender.get("surname")] if x
) or None
sql = """
INSERT INTO medevio_conversation (
id, request_id,
sender_name, sender_id, sender_clinic_id,
text, created_at, read_at, updated_at,
attachment_url, attachment_description, attachment_content_type
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
sender_name = VALUES(sender_name),
sender_id = VALUES(sender_id),
sender_clinic_id = VALUES(sender_clinic_id),
text = VALUES(text),
created_at = VALUES(created_at),
read_at = VALUES(read_at),
updated_at = VALUES(updated_at),
attachment_url = VALUES(attachment_url),
attachment_description = VALUES(attachment_description),
attachment_content_type = VALUES(attachment_content_type)
"""
mr = msg.get("medicalRecord") or {}
cur.execute(sql, (
msg.get("id"),
req_id,
sender_name,
sender.get("id"),
sender.get("clinicId"),
msg.get("text"),
parse_dt(msg.get("createdAt")),
parse_dt(msg.get("readAt")),
parse_dt(msg.get("updatedAt")),
mr.get("downloadUrl") or mr.get("url"),
mr.get("description"),
mr.get("contentType")
))
# ==============================
# 💾 DOWNLOAD MESSAGE ATTACHMENT
# ==============================
def insert_download(cur, req_id, msg, existing_ids):
mr = msg.get("medicalRecord") or {}
attachment_id = mr.get("id")
if not attachment_id:
return
if attachment_id in existing_ids:
return
url = mr.get("downloadUrl") or mr.get("url")
if not url:
return
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
data = r.content
except Exception as e:
safe_print(f"⚠️ Failed to download: {e}")
return
filename = url.split("/")[-1].split("?")[0]
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type,
filename, content_type, file_size, created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
"MESSAGE_ATTACHMENT",
filename,
mr.get("contentType"),
len(data),
parse_dt(msg.get("createdAt")),
data
))
existing_ids.add(attachment_id)
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
# ---- Load existing attachments
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
safe_print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
# ---- Select pozadavky needing message sync
sql = """
SELECT id
FROM pozadavky
WHERE messagesProcessed IS NULL
OR messagesProcessed < updatedAt
"""
with conn.cursor() as cur:
cur.execute(sql)
requests_to_process = cur.fetchall()
safe_print(f"📋 Found {len(requests_to_process)} pozadavků requiring message sync.\n")
# ---- Process each record
for idx, row in enumerate(requests_to_process, 1):
req_id = row["id"]
safe_print(f"[{idx}/{len(requests_to_process)}] Processing {req_id}")
messages = fetch_messages(headers, req_id)
with conn.cursor() as cur:
for msg in messages:
insert_message(cur, req_id, msg)
insert_download(cur, req_id, msg, existing_ids)
conn.commit()
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
safe_print(f"{len(messages)} messages saved\n")
time.sleep(0.25)
conn.close()
safe_print("🎉 Done!")
if __name__ == "__main__":
main()
@@ -0,0 +1,293 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Delta sync Medevio communication.
Stáhne pouze zprávy změněné po messagesProcessed pro každý požadavek.
"""
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
import sys
# ==============================
# UTF-8 SAFE OUTPUT
# ==============================
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc.lower().startswith("utf"):
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# CONFIG
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY_MESSAGES = r"""
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
messages: listMessages(
patientRequestId: $requestId,
updatedSince: $updatedSince
) {
id
createdAt
updatedAt
readAt
text
type
sender {
id
name
surname
clinicId
}
medicalRecord {
id
description
contentType
url
downloadUrl
createdAt
updatedAt
}
}
}
"""
# ==============================
# HELPERS
# ==============================
def parse_dt(s):
if not s:
return None
try:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
except Exception:
return None
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
return tok.replace("Bearer ", "")
# ==============================
# FETCH MESSAGES (DELTA)
# ==============================
def fetch_messages(headers, request_id, updated_since):
payload = {
"operationName": "UseMessages_ListMessages",
"query": GRAPHQL_QUERY_MESSAGES,
"variables": {
"requestId": request_id,
"updatedSince": updated_since,
},
}
r = requests.post(
"https://api.medevio.cz/graphql",
json=payload,
headers=headers,
timeout=30
)
if r.status_code != 200:
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
j = r.json()
if "errors" in j:
safe_print(f"❌ GraphQL error for {request_id}: {j['errors']}")
return []
return j.get("data", {}).get("messages", []) or []
# ==============================
# INSERT MESSAGE
# ==============================
def insert_message(cur, req_id, msg):
sender = msg.get("sender") or {}
sender_name = " ".join(
x for x in [sender.get("name"), sender.get("surname")] if x
) or None
mr = msg.get("medicalRecord") or {}
sql = """
INSERT INTO medevio_conversation (
id, request_id,
sender_name, sender_id, sender_clinic_id,
text, created_at, read_at, updated_at,
attachment_url, attachment_description, attachment_content_type
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
sender_name = VALUES(sender_name),
sender_id = VALUES(sender_id),
sender_clinic_id = VALUES(sender_clinic_id),
text = VALUES(text),
created_at = VALUES(created_at),
read_at = VALUES(read_at),
updated_at = VALUES(updated_at),
attachment_url = VALUES(attachment_url),
attachment_description = VALUES(attachment_description),
attachment_content_type = VALUES(attachment_content_type)
"""
cur.execute(sql, (
msg.get("id"),
req_id,
sender_name,
sender.get("id"),
sender.get("clinicId"),
msg.get("text"),
parse_dt(msg.get("createdAt")),
parse_dt(msg.get("readAt")),
parse_dt(msg.get("updatedAt")),
mr.get("downloadUrl") or mr.get("url"),
mr.get("description"),
mr.get("contentType")
))
# ==============================
# INSERT ATTACHMENT (DEDUP)
# ==============================
def insert_download(cur, req_id, msg, existing_ids):
mr = msg.get("medicalRecord") or {}
attachment_id = mr.get("id")
if not attachment_id or attachment_id in existing_ids:
return
url = mr.get("downloadUrl") or mr.get("url")
if not url:
return
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
data = r.content
except Exception as e:
safe_print(f"⚠️ Attachment download failed: {e}")
return
filename = url.split("/")[-1].split("?")[0]
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type,
filename, content_type, file_size, created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
"MESSAGE_ATTACHMENT",
filename,
mr.get("contentType"),
len(data),
parse_dt(msg.get("createdAt")),
data
))
existing_ids.add(attachment_id)
# ==============================
# MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
# existing attachments
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {r["attachment_id"] for r in cur.fetchall()}
# select requests needing sync
with conn.cursor() as cur:
cur.execute("""
SELECT id, messagesProcessed
FROM pozadavky
WHERE messagesProcessed IS NULL
OR messagesProcessed < updatedAt
""")
rows = cur.fetchall()
safe_print(f"📋 Found {len(rows)} requests for message delta-sync\n")
for i, row in enumerate(rows, 1):
req_id = row["id"]
updated_since = row["messagesProcessed"]
if updated_since:
updated_since = updated_since.replace(microsecond=0).isoformat() + "Z"
safe_print(f"[{i}/{len(rows)}] {req_id}")
messages = fetch_messages(headers, req_id, updated_since)
if not messages:
safe_print(" ⏭ No new messages")
else:
with conn.cursor() as cur:
for msg in messages:
insert_message(cur, req_id, msg)
insert_download(cur, req_id, msg, existing_ids)
conn.commit()
safe_print(f"{len(messages)} new/updated messages")
with conn.cursor() as cur:
cur.execute(
"UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s",
(req_id,)
)
conn.commit()
time.sleep(0.25)
conn.close()
safe_print("\n🎉 Delta message sync DONE")
# ==============================
if __name__ == "__main__":
main()
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download all attachments for pozadavky where attachmentsProcessed IS NULL
and (optionally) createdAt is newer than a cutoff date.
Store them in MySQL table `medevio_downloads`, and update pozadavky.attachmentsProcessed.
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
import sys
# Force UTF-8 output even under Windows Task Scheduler
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
# Python < 3.7 fallback (not needed for you, but safe)
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ==============================
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
# ==============================
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc or not enc.lower().startswith("utf"):
# strip emoji + characters outside BMP
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
# ASCII fallback
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
CREATED_AFTER = "2024-12-01" # optional filter
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient
}
}
}
"""
# ==============================
# 🧮 HELPERS
# ==============================
def extract_filename_from_url(url: str) -> str:
try:
return url.split("/")[-1].split("?")[0]
except:
return "unknown_filename"
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
# ==============================
# 📡 FETCH ATTACHMENTS
# ==============================
def fetch_attachments(headers, request_id):
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": {"requestId": request_id},
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
safe_print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
return r.json().get("data", {}).get("patientRequestMedicalRecords", [])
# ==============================
# 💾 SAVE TO MYSQL
# ==============================
def insert_download(cur, req_id, a, m, created_date, existing_ids):
attachment_id = a.get("id")
if attachment_id in existing_ids:
safe_print(f" ⏭️ Already downloaded {attachment_id}")
return False
url = m.get("downloadUrl")
if not url:
safe_print(" ⚠️ Missing download URL")
return False
filename = extract_filename_from_url(url)
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
content = r.content
except Exception as e:
safe_print(f" ⚠️ Download failed {url}: {e}")
return False
file_size = len(content)
attachment_type = a.get("attachmentType")
content_type = m.get("contentType")
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type,
filename, content_type, file_size,
created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
attachment_type,
filename,
content_type,
file_size,
created_date,
content,
))
existing_ids.add(attachment_id)
safe_print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)")
return True
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
# Load existing attachments
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
safe_print(f"{len(existing_ids)} attachments already saved.")
# Build query for pozadavky
sql = """
SELECT id, pacient_prijmeni, pacient_jmeno, createdAt, updatedAt, attachmentsProcessed
FROM pozadavky
WHERE attachmentsProcessed IS NULL
OR updatedAt > attachmentsProcessed
"""
params = []
if CREATED_AFTER:
sql += " AND createdAt >= %s"
params.append(CREATED_AFTER)
with conn.cursor() as cur:
cur.execute(sql, params)
req_rows = cur.fetchall()
safe_print(f"📋 Found {len(req_rows)} pozadavky to process.")
# Process each pozadavek
for i, row in enumerate(req_rows, 1):
req_id = row["id"]
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
jmeno = row.get("pacient_jmeno") or ""
created_date = row.get("createdAt") or datetime.now()
safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
attachments = fetch_attachments(headers, req_id)
if not attachments:
safe_print(" ⚠️ No attachments found")
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
continue
with conn.cursor() as cur:
for a in attachments:
m = a.get("medicalRecord") or {}
insert_download(cur, req_id, a, m, created_date, existing_ids)
conn.commit()
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
safe_print(f" ✅ Done ({len(attachments)} attachments)")
time.sleep(0.3)
conn.close()
safe_print("\n🎯 All attachments processed.")
# ==============================
if __name__ == "__main__":
main()
@@ -0,0 +1,252 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import pymysql
import re
from pathlib import Path
from datetime import datetime
import time
import sys
# Force UTF-8 output even under Windows Task Scheduler
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
# Python < 3.7 fallback (not needed for you, but safe)
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ==============================
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
# ==============================
def safe_print(text: str = ""):
enc = sys.stdout.encoding or ""
if not enc.lower().startswith("utf"):
# Strip emoji and characters outside BMP for Task Scheduler
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
# ASCII fallback
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
def sanitize_name(name: str) -> str:
"""Replace invalid filename characters with underscore."""
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
def make_abbrev(title: str) -> str:
if not title:
return ""
words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title)
abbr = ""
for w in words:
if w.isdigit():
abbr += w
else:
abbr += w[0]
return abbr.upper()
# ==============================
# 🧹 DELETE UNEXPECTED FILES
# ==============================
def clean_folder(folder: Path, valid_files: set):
if not folder.exists():
return
for f in folder.iterdir():
if f.is_file():
if f.name.startswith(""):
continue
sanitized = sanitize_name(f.name)
if sanitized not in valid_files:
safe_print(f"🗑️ Removing unexpected file: {f.name}")
try:
f.unlink()
except Exception as e:
safe_print(f"⚠️ Could not delete {f}: {e}")
# ==============================
# 📦 DB CONNECTION
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
cur_blob = conn.cursor()
safe_print("🔍 Loading metadata from DB (FAST)…")
cur_meta.execute("""
SELECT d.id AS download_id,
d.request_id,
d.filename,
d.created_at,
p.updatedAt AS req_updated_at,
p.pacient_jmeno AS jmeno,
p.pacient_prijmeni AS prijmeni,
p.displayTitle
FROM medevio_downloads d
JOIN pozadavky p ON d.request_id = p.id
ORDER BY p.updatedAt DESC
""")
rows = cur_meta.fetchall()
safe_print(f"📋 Found {len(rows)} attachment records.\n")
# ==============================
# 🧠 MAIN LOOP WITH PROGRESS
# ==============================
unique_request_ids = []
seen = set()
for r in rows:
req_id = r["request_id"]
if req_id not in seen:
unique_request_ids.append(req_id)
seen.add(req_id)
total_requests = len(unique_request_ids)
safe_print(f"🔄 Processing {total_requests} unique requests...\n")
processed_requests = set()
current_index = 0
for r in rows:
req_id = r["request_id"]
if req_id in processed_requests:
continue
processed_requests.add(req_id)
current_index += 1
percent = (current_index / total_requests) * 100
safe_print(f"\n[ {percent:5.1f}% ] Processing request {current_index} / {total_requests}{req_id}")
# ========== FETCH VALID FILENAMES ==========
cur_meta.execute(
"SELECT filename FROM medevio_downloads WHERE request_id=%s",
(req_id,)
)
valid_files = {sanitize_name(row["filename"]) for row in cur_meta.fetchall()}
# ========== BUILD FOLDER NAME ==========
updated_at = r["req_updated_at"] or datetime.now()
date_str = updated_at.strftime("%Y-%m-%d")
prijmeni = sanitize_name(r["prijmeni"] or "Unknown")
jmeno = sanitize_name(r["jmeno"] or "")
title = r.get("displayTitle") or ""
abbr = make_abbrev(title)
clean_folder_name = sanitize_name(
f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}"
)
# ========== DETECT EXISTING FOLDER ==========
existing_folder = None
for f in BASE_DIR.iterdir():
if f.is_dir() and req_id in f.name:
existing_folder = f
break
main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name
# ========== MERGE DUPLICATES ==========
possible_dups = [
f for f in BASE_DIR.iterdir()
if f.is_dir() and req_id in f.name and f != main_folder
]
for dup in possible_dups:
safe_print(f"♻️ Merging duplicate folder: {dup.name}")
clean_folder(dup, valid_files)
main_folder.mkdir(parents=True, exist_ok=True)
for f in dup.iterdir():
if f.is_file():
target = main_folder / f.name
if not target.exists():
f.rename(target)
shutil.rmtree(dup, ignore_errors=True)
# ========== CLEAN MAIN FOLDER ==========
clean_folder(main_folder, valid_files)
# ========== DOWNLOAD MISSING FILES ==========
added_new_file = False
main_folder.mkdir(parents=True, exist_ok=True)
for filename in valid_files:
dest_plain = main_folder / filename
dest_marked = main_folder / ("" + filename)
if dest_plain.exists() or dest_marked.exists():
continue
added_new_file = True
cur_blob.execute(
"SELECT file_content FROM medevio_downloads "
"WHERE request_id=%s AND filename=%s",
(req_id, filename)
)
row = cur_blob.fetchone()
if not row:
continue
content = row[0]
if not content:
continue
with open(dest_plain, "wb") as f:
f.write(content)
safe_print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}")
# ========== REMOVE ▲ FLAG IF NEW FILES ADDED ==========
if added_new_file and "" in main_folder.name:
new_name = main_folder.name.replace("", "").strip()
new_path = main_folder.parent / new_name
if new_path != main_folder:
try:
main_folder.rename(new_path)
safe_print(f"🔄 Folder flag ▲ removed → {new_name}")
main_folder = new_path
except Exception as e:
safe_print(f"⚠️ Could not rename folder: {e}")
safe_print("\n🎯 Export complete.\n")
cur_blob.close()
cur_meta.close()
conn.close()
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import pymysql
import re
from pathlib import Path
from datetime import datetime
from collections import defaultdict
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
# ==============================
# 🔧 HELPERS
# ==============================
def sanitize_name(name: str) -> str:
"""Replace invalid Windows filename characters."""
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
def make_abbrev(title: str) -> str:
"""Create abbreviation from title."""
if not title:
return ""
words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title)
abbr = ""
for w in words:
if w.isdigit():
abbr += w
else:
abbr += w[0]
return abbr.upper()
def clean_folder(folder: Path, valid_files: set):
"""Remove unexpected files except ▲ files."""
if not folder.exists():
return
for f in folder.iterdir():
if f.is_file():
if f.name.startswith(""):
continue
sanitized = sanitize_name(f.name)
if sanitized not in valid_files:
print(f"🗑️ Removing unexpected file: {f.name}")
try:
f.unlink()
except Exception as e:
print(f"⚠️ Could not delete {f}: {e}")
# ==============================
# 📦 DB CONNECTION
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
cur_blob = conn.cursor()
print("🔍 Loading only requests with NEW attachments…")
cur_meta.execute("""
SELECT
p.id AS request_id,
p.displayTitle,
p.pacient_jmeno,
p.pacient_prijmeni,
p.updatedAt,
p.attachmentsProcessed,
d.filename,
d.created_at
FROM pozadavky p
JOIN medevio_downloads d ON d.request_id = p.id
LEFT JOIN (
SELECT request_id, MAX(created_at) AS last_attachment_ts
FROM medevio_downloads
GROUP BY request_id
) x ON x.request_id = p.id
WHERE p.attachmentsProcessed IS NULL
OR p.attachmentsProcessed < x.last_attachment_ts
ORDER BY p.updatedAt DESC;
""")
rows = cur_meta.fetchall()
print(f"📋 Found {len(rows)} attachment rows belonging to requests needing processing.\n")
# ==============================
# 🧠 PREPARE REQUEST GROUPING
# ==============================
grouped = defaultdict(list)
for r in rows:
grouped[r["request_id"]].append(r)
unique_request_ids = list(grouped.keys())
total_requests = len(unique_request_ids)
print(f"🔄 Processing {total_requests} requests needing updates…\n")
# ==============================
# 🧠 MAIN LOOP
# ==============================
index = 0
for req_id in unique_request_ids:
index += 1
pct = (index / total_requests) * 100
print(f"\n[ {pct:5.1f}% ] Processing request {index}/{total_requests}{req_id}")
req_rows = grouped[req_id]
first = req_rows[0]
# Build folder name
updated_at = first["updatedAt"] or datetime.now()
date_str = updated_at.strftime("%Y-%m-%d")
prijmeni = sanitize_name(first["pacient_prijmeni"] or "Unknown")
jmeno = sanitize_name(first["pacient_jmeno"] or "")
abbr = make_abbrev(first["displayTitle"])
desired_folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}")
# Detect existing folder for request
main_folder = None
for f in BASE_DIR.iterdir():
if f.is_dir() and req_id in f.name:
main_folder = f
break
if not main_folder:
main_folder = BASE_DIR / desired_folder_name
main_folder.mkdir(parents=True, exist_ok=True)
# Build valid filename set
valid_files = {sanitize_name(r["filename"]) for r in req_rows}
# Clean unexpected non-▲ files
clean_folder(main_folder, valid_files)
# Track if ANY new files were downloaded
added_new_file = False
# DOWNLOAD MISSING FILES
for r in req_rows:
filename = sanitize_name(r["filename"])
dest_plain = main_folder / filename
dest_flag = main_folder / ("" + filename)
# Skip if file already exists (plain or ▲)
if dest_plain.exists() or dest_flag.exists():
continue
# Fetch content
cur_blob.execute("""
SELECT file_content
FROM medevio_downloads
WHERE request_id=%s AND filename=%s
""", (req_id, r["filename"]))
row = cur_blob.fetchone()
if not row or not row[0]:
continue
with open(dest_plain, "wb") as f:
f.write(row[0])
print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}")
added_new_file = True
# ------------------------------------
# 🟦 FOLDER ▲ LOGIC (IMPORTANT)
# ------------------------------------
if added_new_file:
# If folder contains ▲ in its name → remove it
if "" in main_folder.name:
new_name = main_folder.name.replace("", "").strip()
new_path = main_folder.parent / new_name
try:
main_folder.rename(new_path)
print(f"🔄 Folder flag ▲ removed → {new_name}")
main_folder = new_path
except Exception as e:
print(f"⚠️ Could not rename folder: {e}")
else:
# NO new files → NEVER rename folder
pass
# Mark request as processed
cur_meta.execute(
"UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id=%s",
(req_id,)
)
conn.commit()
# ==============================
# 🏁 DONE
# ==============================
print("\n🎯 Export complete.\n")
cur_blob.close()
cur_meta.close()
conn.close()
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import pymysql
import re
from pathlib import Path
from datetime import datetime
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
def sanitize_name(name: str) -> str:
"""Replace invalid filename characters with underscore."""
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
def make_abbrev(title: str) -> str:
"""Create abbreviation from displayTitle."""
if not title:
return ""
words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title)
abbr = ""
for w in words:
abbr += w if w.isdigit() else w[0]
return abbr.upper()
# ==============================
# 🧹 DELETE UNEXPECTED FILES
# ==============================
def clean_folder(folder: Path, valid_files: set):
if not folder.exists():
return
for f in folder.iterdir():
if f.is_file():
if f.name.startswith(""):
continue
sanitized = sanitize_name(f.name)
if sanitized not in valid_files:
print(f"🗑️ Removing unexpected file: {f.name}")
try:
f.unlink()
except Exception as e:
print(f"⚠️ Could not delete {f}: {e}")
# ==============================
# 📦 DB CONNECTION
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
cur_blob = conn.cursor()
print("🔍 Loading ALL metadata without file_content…")
# ⭐ Load ALL metadata once (NO BLOBs)
cur_meta.execute("""
SELECT
d.request_id,
d.filename,
d.created_at,
p.updatedAt AS req_updated_at,
p.pacient_jmeno AS jmeno,
p.pacient_prijmeni AS prijmeni,
p.displayTitle
FROM medevio_downloads d
JOIN pozadavky p ON d.request_id = p.id
ORDER BY p.updatedAt DESC;
""")
rows = cur_meta.fetchall()
print(f"📋 Found {len(rows)} metadata rows.\n")
# ==============================
# 🧠 PRE-GROUP METADATA
# ==============================
# Build dictionary: request_id → all metadata rows for that request
grouped = {}
for row in rows:
grouped.setdefault(row["request_id"], []).append(row)
unique_request_ids = list(grouped.keys())
total_requests = len(unique_request_ids)
print(f"🔄 Processing {total_requests} unique requests…\n")
# ==============================
# 🧠 MAIN LOOP
# ==============================
for idx, req_id in enumerate(unique_request_ids, start=1):
pct = (idx / total_requests) * 100
req_rows = grouped[req_id]
first = req_rows[0]
print(f"\n[ {pct:5.1f}% ] Processing request {idx}/{total_requests}{req_id}")
# ======================
# Build folder name
# ======================
updated_at = first["req_updated_at"] or datetime.now()
date_str = updated_at.strftime("%Y-%m-%d")
prijmeni = sanitize_name(first["prijmeni"] or "Unknown")
jmeno = sanitize_name(first["jmeno"] or "")
abbr = make_abbrev(first["displayTitle"] or "")
clean_folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}")
# Detect existing folder
existing_folder = None
for f in BASE_DIR.iterdir():
if f.is_dir() and req_id in f.name:
existing_folder = f
break
main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name
main_folder.mkdir(parents=True, exist_ok=True)
# ======================
# Valid files for this request
# ======================
valid_files = {sanitize_name(r["filename"]) for r in req_rows}
# Clean unexpected files
clean_folder(main_folder, valid_files)
# ======================
# DOWNLOAD MISSING FILES → only now load BLOBs
# ======================
added_new_file = False
for r in req_rows:
filename = sanitize_name(r["filename"])
dest_plain = main_folder / filename
dest_marked = main_folder / ("" + filename)
if dest_plain.exists() or dest_marked.exists():
continue
added_new_file = True
# ⭐ Load BLOB only when needed
cur_blob.execute("""
SELECT file_content
FROM medevio_downloads
WHERE request_id=%s AND filename=%s
""", (req_id, r["filename"]))
row = cur_blob.fetchone()
if not row or not row[0]:
continue
with open(dest_plain, "wb") as f:
f.write(row[0])
print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}")
# ======================
# Folder-level ▲ logic
# ======================
if added_new_file and "" in main_folder.name:
new_name = main_folder.name.replace("", "").strip()
new_path = main_folder.parent / new_name
try:
main_folder.rename(new_path)
main_folder = new_path
print(f"🔄 Folder flag ▲ removed → {new_name}")
except Exception as e:
print(f"⚠️ Could not rename folder: {e}")
cur_blob.close()
cur_meta.close()
conn.close()
print("\n🎯 Export complete.\n")
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import pymysql
import re
from pathlib import Path
from datetime import datetime
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
def sanitize_name(name: str) -> str:
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
def clean_folder(folder: Path, valid_files: set):
"""Remove files that do NOT exist in MySQL for this request."""
if not folder.exists():
return
for f in folder.iterdir():
if f.is_file() and sanitize_name(f.name) not in valid_files:
print(f"🗑️ Removing unexpected file: {f.name}")
try:
f.unlink()
except Exception as e:
print(f"⚠️ Cannot delete {f}: {e}")
# ==============================
# 📥 LOAD EVERYTHING IN ONE QUERY
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur = conn.cursor(pymysql.cursors.DictCursor)
print("📥 Loading ALL metadata + BLOBs with ONE MySQL query…")
cur.execute("""
SELECT
d.id AS download_id,
d.request_id,
d.filename,
d.file_content,
p.updatedAt AS req_updated_at,
p.pacient_jmeno AS jmeno,
p.pacient_prijmeni AS prijmeni
FROM medevio_downloads d
JOIN pozadavky p ON d.request_id = p.id
ORDER BY p.updatedAt DESC, d.created_at ASC
""")
rows = cur.fetchall()
print(f"📦 Loaded {len(rows)} total file rows.\n")
conn.close()
# ==============================
# 🔄 ORGANIZE ROWS PER REQUEST
# ==============================
requests = {} # req_id → list of file dicts
for r in rows:
req_id = r["request_id"]
if req_id not in requests:
requests[req_id] = []
requests[req_id].append(r)
print(f"📌 Unique requests: {len(requests)}\n")
# ==============================
# 🧠 MAIN LOOP SAME LOGIC AS BEFORE
# ==============================
for req_id, filelist in requests.items():
# ========== GET UPDATEDAT (same logic) ==========
any_row = filelist[0]
updated_at = any_row["req_updated_at"] or datetime.now()
date_str = updated_at.strftime("%Y-%m-%d")
prijmeni = sanitize_name(any_row["prijmeni"] or "Unknown")
jmeno = sanitize_name(any_row["jmeno"] or "")
folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} {req_id}")
main_folder = BASE_DIR / folder_name
# ========== VALID FILES ==========
valid_files = {sanitize_name(r["filename"]) for r in filelist}
# ========== FIND OLD FOLDERS ==========
possible_dups = [
f for f in BASE_DIR.iterdir()
if f.is_dir() and req_id in f.name and f != main_folder
]
# ========== MERGE OLD FOLDERS ==========
for dup in possible_dups:
print(f"♻️ Merging folder: {dup.name}")
clean_folder(dup, valid_files)
main_folder.mkdir(parents=True, exist_ok=True)
for f in dup.iterdir():
if f.is_file():
target = main_folder / f.name
if not target.exists():
f.rename(target)
shutil.rmtree(dup, ignore_errors=True)
# ========== CLEAN MAIN FOLDER ==========
main_folder.mkdir(parents=True, exist_ok=True)
clean_folder(main_folder, valid_files)
# ========== SAVE FILES (fast now) ==========
for r in filelist:
filename = sanitize_name(r["filename"])
dest = main_folder / filename
if dest.exists():
continue
content = r["file_content"]
if not content:
continue
with open(dest, "wb") as f:
f.write(content)
print(f"💾 Saved: {dest.relative_to(BASE_DIR)}")
print("\n🎯 Export complete.\n")
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import importlib.util
import sys
from pathlib import Path
# Load FunctionsLoader
FUNCTIONS_LOADER_PATH = Path(r"C:\Reporting\Functions\FunctionsLoader.py")
spec = importlib.util.spec_from_file_location("FunctionsLoader", FUNCTIONS_LOADER_PATH)
FunctionsLoader = importlib.util.module_from_spec(spec)
sys.modules["FunctionsLoader"] = FunctionsLoader
spec.loader.exec_module(FunctionsLoader)
"""
Spustí všechny PRAVIDELNÉ skripty v daném pořadí:
0) PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py
1) PRAVIDELNE_1_ReadLast300DonePozadavku.py
2) PRAVIDELNE_2_ReadPoznamky.py
3) PRAVIDELNE_3_StahniKomunikaci.py
4) PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py
5) PRAVIDELNE_5_SaveToFileSystem incremental.py
"""
import time, socket
for _ in range(30):
try:
socket.create_connection(("192.168.1.76", 3307), timeout=3).close()
break
except OSError:
time.sleep(10)
import sys
import subprocess
from pathlib import Path
from datetime import datetime
# složka, kde leží tento skript i všechny PRAVIDELNE_*.py
BASE_DIR = Path(__file__).resolve().parent
SCRIPTS_IN_ORDER = [
"PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py",
"PRAVIDELNE_1_ReadLast300DonePozadavku.py",
"PRAVIDELNE_2_ReadPoznamky.py",
"PRAVIDELNE_3_StahniKomunikaci.py",
"PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py",
"PRAVIDELNE_5_SaveToFileSystem incremental.py", # má mezeru v názvu, ale v listu je to OK
]
LOG_FILE = BASE_DIR / "PRAVIDELNE_log.txt"
def log(msg: str):
"""Zapíše zprávu do log souboru i na konzoli."""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {msg}"
print(line)
try:
with LOG_FILE.open("a", encoding="utf-8") as f:
f.write(line + "\n")
except Exception:
# log nesmí shodit běh
pass
def main():
log("=== START pravidelného běhu ===")
for script_name in SCRIPTS_IN_ORDER:
script_path = BASE_DIR / script_name
if not script_path.exists():
log(f"❌ Skript nenalezen: {script_path}")
continue
log(f"▶ Spouštím: {script_path.name}")
# spustíme stejným interpretem, kterým běží tento orchestr
try:
result = subprocess.run(
[sys.executable, str(script_path)],
cwd=str(BASE_DIR),
capture_output=True,
text=True,
encoding="utf-8",
errors="ignore", # NEZKAZÍ SE, NEZBOŘÍ SE, PROSTĚ IGNORUJE CP1252 NEZÁKONNÉ BYTES
)
except Exception as e:
log(f" 💥 Chyba při spouštění {script_path.name}: {e}")
continue
# vypíšeme návratový kód
log(f" ↳ return code: {result.returncode}")
# pokud něco skript vypsal na stderr, logneme
if result.stderr:
log(f" ⚠ stderr {script_path.name}:\n{result.stderr.strip()}")
# stdout můžeš podle chuti také logovat (někdy je toho moc):
# if result.stdout:
# log(f" stdout {script_path.name}:\n{result.stdout.strip()}")
log("=== KONEC pravidelného běhu ===\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,29 @@
[2025-12-01 06:37:41] === START pravidelného běhu ===
[2025-12-01 06:37:42] ▶ Spouštím: PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py
[2025-12-01 06:37:44] ↳ PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py return code: 0
[2025-12-01 06:37:44] ▶ Spouštím: PRAVIDELNE_1_ReadLast300DonePozadavku.py
[2025-12-01 06:37:48] ↳ PRAVIDELNE_1_ReadLast300DonePozadavku.py return code: 0
[2025-12-01 06:37:48] ▶ Spouštím: PRAVIDELNE_2_ReadPoznamky.py
[2025-12-01 06:37:49] ↳ PRAVIDELNE_2_ReadPoznamky.py return code: 0
[2025-12-01 06:37:50] ▶ Spouštím: PRAVIDELNE_3_StahniKomunikaci.py
[2025-12-01 06:37:51] ↳ PRAVIDELNE_3_StahniKomunikaci.py return code: 0
[2025-12-01 06:37:52] ▶ Spouštím: PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py
[2025-12-01 06:37:53] ↳ PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py return code: 0
[2025-12-01 06:37:53] ▶ Spouštím: PRAVIDELNE_5_SaveToFileSystem incremental.py
[2025-12-01 06:38:42] ↳ PRAVIDELNE_5_SaveToFileSystem incremental.py return code: 0
[2025-12-01 06:38:43] === KONEC pravidelného běhu ===
[2025-12-02 07:04:34] === START pravidelného běhu ===
[2025-12-02 07:04:34] ▶ Spouštím: PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py
[2025-12-02 07:04:35] ↳ return code: 0
[2025-12-02 07:04:35] ▶ Spouštím: PRAVIDELNE_1_ReadLast300DonePozadavku.py
[2025-12-02 07:04:39] ↳ return code: 0
[2025-12-02 07:04:39] ▶ Spouštím: PRAVIDELNE_2_ReadPoznamky.py
[2025-12-02 07:04:40] ↳ return code: 0
[2025-12-02 07:04:40] ▶ Spouštím: PRAVIDELNE_3_StahniKomunikaci.py
[2025-12-02 07:04:40] ↳ return code: 0
[2025-12-02 07:04:40] ▶ Spouštím: PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py
[2025-12-02 07:04:40] ↳ return code: 0
[2025-12-02 07:04:40] ▶ Spouštím: PRAVIDELNE_5_SaveToFileSystem incremental.py
[2025-12-02 07:05:28] ↳ return code: 0
[2025-12-02 07:05:28] === KONEC pravidelného běhu ===
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Orchestrator for all PRAVIDELNE scripts in exact order.
"""
import time, socket
for _ in range(30):
try:
socket.create_connection(("192.168.1.76", 3307), timeout=3).close()
break
except OSError:
time.sleep(10)
import sys
import subprocess
from pathlib import Path
from datetime import datetime
# =====================================================================
# Import EXACT Functions.py from: C:\Reporting\Fio\Functions.py
# This bypasses all other Functions.py files in the system.
# =====================================================================
import importlib.util
FUNCTIONS_FILE = Path(r"C:\Reporting\Fio\Functions.py")
spec = importlib.util.spec_from_file_location("Functions_FIO", FUNCTIONS_FILE)
Functions_FIO = importlib.util.module_from_spec(spec)
sys.modules["Functions_FIO"] = Functions_FIO
spec.loader.exec_module(Functions_FIO)
# correct WhatsApp function
SendWhatsAppMessage = Functions_FIO.SendWhatsAppMessage
# =====================================================================
# General Orchestrator Settings
# =====================================================================
# folder where orchestrator + sub-scripts live
BASE_DIR = Path(__file__).resolve().parent
SCRIPTS_IN_ORDER = [
"PRAVIDELNE_0_READ_ALL_ACTIVE_POZADAVKY.py",
"PRAVIDELNE_1_ReadLast300DonePozadavku.py",
"PRAVIDELNE_2_ReadPoznamky.py",
"PRAVIDELNE_3_StahniKomunikaci.py",
"PRAVIDELNE_4_StahniPrilohyUlozDoMySQL.py",
"PRAVIDELNE_5_SaveToFileSystem incremental.py",
]
LOG_FILE = BASE_DIR / "PRAVIDELNE_log.txt"
# =====================================================================
# Logging + WhatsApp wrappers
# =====================================================================
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {msg}"
print(line)
try:
with LOG_FILE.open("a", encoding="utf-8") as f:
f.write(line + "\n")
except:
pass
def whatsapp_notify(text: str):
"""WhatsApp message wrapper — never allowed to crash orchestrator"""
try:
SendWhatsAppMessage(text)
except:
pass
# =====================================================================
# Main orchestrator
# =====================================================================
def main():
log("=== START pravidelného běhu ===")
whatsapp_notify("🏁 *PRAVIDELNÉ skripty: START*")
for script_name in SCRIPTS_IN_ORDER:
script_path = BASE_DIR / script_name
if not script_path.exists():
err = f"❌ Skript nenalezen: {script_path}"
log(err)
whatsapp_notify(err)
continue
log(f"▶ Spouštím: {script_path.name}")
whatsapp_notify(f"▶ *Spouštím:* {script_path.name}")
try:
result = subprocess.run(
[sys.executable, str(script_path)],
cwd=str(BASE_DIR),
capture_output=True,
text=True,
encoding="utf-8",
errors="ignore",
)
except Exception as e:
err = f"💥 Chyba při spouštění {script_path.name}: {e}"
log(err)
whatsapp_notify(err)
continue
# return code
rc_msg = f"{script_path.name} return code: {result.returncode}"
log(rc_msg)
whatsapp_notify(rc_msg)
# stderr (warnings/errors)
if result.stderr:
err_msg = f"⚠ stderr v {script_path.name}:\n{result.stderr.strip()}"
log(err_msg)
whatsapp_notify(err_msg)
log("=== KONEC pravidelného běhu ===")
whatsapp_notify("✅ *PRAVIDELNÉ skripty: KONEC*\n")
# =====================================================================
# Entry point
# =====================================================================
if __name__ == "__main__":
main()
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download all 'Odeslat lékařskou zprávu' attachments from Medevio API
and store them (including binary content) directly into MySQL table `medevio_downloads`.
Each attachment (PDF, image, etc.) is fetched once and saved as LONGBLOB.
Duplicate protection is ensured via UNIQUE KEY on `attachment_id`.
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient
}
}
}
"""
# ==============================
# 🧮 HELPERS
# ==============================
def short_crc8(uuid_str: str) -> str:
"""Return deterministic 8-char hex string from any input string (CRC32)."""
return f"{zlib.crc32(uuid_str.encode('utf-8')) & 0xffffffff:08x}"
def extract_filename_from_url(url: str) -> str:
"""Extracts filename from S3-style URL (between last '/' and first '?')."""
try:
return url.split("/")[-1].split("?")[0]
except Exception:
return "unknown_filename"
def read_token(p: Path) -> str:
"""Read Bearer token from file."""
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
tok = tok.split(" ", 1)[1]
return tok
# ==============================
# 📡 FETCH ATTACHMENTS
# ==============================
def fetch_attachments(headers, request_id):
variables = {"requestId": request_id}
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
print(f"❌ HTTP {r.status_code} for request {request_id}")
return []
data = r.json().get("data", {}).get("patientRequestMedicalRecords", [])
return data
# ==============================
# 💾 SAVE TO MYSQL
# ==============================
def insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date):
url = m.get("downloadUrl")
if not url:
print(" ⚠️ No download URL")
return
try:
r = requests.get(url, timeout=45)
r.raise_for_status()
content = r.content
except Exception as e:
print(f" ⚠️ Failed to download {url}: {e}")
return
file_size = len(content)
filename = extract_filename_from_url(url)
attachment_id = a.get("id")
attachment_type = a.get("attachmentType")
content_type = m.get("contentType")
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type, filename,
content_type, file_size, pacient_jmeno, pacient_prijmeni,
created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
attachment_type,
filename,
content_type,
file_size,
jmeno,
prijmeni,
created_date,
content
))
print(f" 💾 Saved {filename} ({file_size/1024:.1f} kB)")
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
cur.execute("""
SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno, createdAt
FROM pozadavky
WHERE displayTitle = 'Odeslat lékařskou zprávu'
""")
rows = cur.fetchall()
print(f"📋 Found {len(rows)} 'Odeslat lékařskou zprávu' requests")
for i, row in enumerate(rows, 1):
req_id = row["id"]
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
jmeno = row.get("pacient_jmeno") or ""
created = row.get("createdAt")
try:
created_date = datetime.strptime(str(created), "%Y-%m-%d %H:%M:%S")
except Exception:
created_date = None
print(f"\n[{i}/{len(rows)}] 🧾 {prijmeni}, {jmeno} ({req_id})")
attachments = fetch_attachments(headers, req_id)
if not attachments:
print(" ⚠️ No attachments")
continue
with conn.cursor() as cur:
for a in attachments:
m = a.get("medicalRecord") or {}
insert_download(cur, req_id, a, m, jmeno, prijmeni, created_date)
conn.commit()
print(f"{len(attachments)} attachments saved for {prijmeni}, {jmeno}")
time.sleep(0.5) # be nice to the API
conn.close()
print("\n✅ Done! All attachments stored in MySQL table `medevio_downloads`.")
# ==============================
if __name__ == "__main__":
main()
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from pathlib import Path
from datetime import datetime
from dateutil import parser
import time
import sys
# ================================
# UTF-8 SAFE OUTPUT (Windows friendly)
# ================================
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc.lower().startswith("utf"):
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ================================
# 🔧 CONFIG
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 500
STATES = ["ACTIVE", "DONE"] # explicitně jinak API vrací jen ACTIVE
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestList2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
lastMessage {
createdAt
}
}
}
}
"""
# ================================
# TOKEN
# ================================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
return tok.split(" ", 1)[1]
return tok
# ================================
# DATETIME PARSER
# ================================
def to_mysql_dt(iso_str):
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo)
return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return None
# ================================
# UPSERT
# ================================
def upsert(conn, r):
p = r.get("extendedPatient") or {}
api_updated = to_mysql_dt(r.get("updatedAt"))
msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt"))
final_updated = max(filter(None, [api_updated, msg_updated]), default=None)
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r.get("id"),
r.get("displayTitle"),
to_mysql_dt(r.get("createdAt")),
final_updated,
to_mysql_dt(r.get("doneAt")),
to_mysql_dt(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# FETCH PAGE (per state)
# ================================
def fetch_state(headers, state, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"state": state,
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
}
payload = {
"operationName": "ClinicRequestList2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json()["data"]["requestsResponse"]
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
grand_total = 0
for state in STATES:
safe_print(f"\n🔁 STATE = {state}")
offset = 0
total = None
processed = 0
while True:
batch, count = fetch_state(headers, state, offset)
if total is None:
total = count
safe_print(f"📡 {state}: celkem {total}")
if not batch:
break
for r in batch:
upsert(conn, r)
processed += len(batch)
safe_print(f"{processed}/{total}")
offset += BATCH_SIZE
if offset >= count:
break
time.sleep(0.4)
grand_total += processed
conn.close()
safe_print(f"\n✅ HOTOVO celkem zpracováno {grand_total} požadavků\n")
# ================================
if __name__ == "__main__":
main()
@@ -0,0 +1,279 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Fetches messages from Medevio API.
Modes:
- Incremental (default): Only requests where messagesProcessed IS NULL or < updatedAt
- Full resync (--full): Fetches ALL messages for ALL pozadavky
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
import argparse
# ==============================
# 🔧 CONFIGURATION
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY_MESSAGES = r"""
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
id
createdAt
updatedAt
readAt
text
type
sender {
id
name
surname
clinicId
}
medicalRecord {
id
description
contentType
url
downloadUrl
token
createdAt
updatedAt
}
}
}
"""
# ==============================
# ⏱ DATETIME PARSER
# ==============================
def parse_dt(s):
if not s:
return None
try:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
except:
pass
try:
return datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S")
except:
return None
# ==============================
# 🔐 TOKEN
# ==============================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
return tok.replace("Bearer ", "")
# ==============================
# 📡 FETCH MESSAGES
# ==============================
def fetch_messages(headers, request_id):
payload = {
"operationName": "UseMessages_ListMessages",
"query": GRAPHQL_QUERY_MESSAGES,
"variables": {"requestId": request_id, "updatedSince": None},
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
if r.status_code != 200:
print("❌ HTTP", r.status_code, "for request", request_id)
return []
return r.json().get("data", {}).get("messages", []) or []
# ==============================
# 💾 SAVE MESSAGE
# ==============================
def insert_message(cur, req_id, msg):
sender = msg.get("sender") or {}
sender_name = " ".join(
x for x in [sender.get("name"), sender.get("surname")] if x
) or None
sql = """
INSERT INTO medevio_conversation (
id, request_id,
sender_name, sender_id, sender_clinic_id,
text, created_at, read_at, updated_at,
attachment_url, attachment_description, attachment_content_type
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
sender_name = VALUES(sender_name),
sender_id = VALUES(sender_id),
sender_clinic_id = VALUES(sender_clinic_id),
text = VALUES(text),
created_at = VALUES(created_at),
read_at = VALUES(read_at),
updated_at = VALUES(updated_at),
attachment_url = VALUES(attachment_url),
attachment_description = VALUES(attachment_description),
attachment_content_type = VALUES(attachment_content_type)
"""
mr = msg.get("medicalRecord") or {}
cur.execute(sql, (
msg.get("id"),
req_id,
sender_name,
sender.get("id"),
sender.get("clinicId"),
msg.get("text"),
parse_dt(msg.get("createdAt")),
parse_dt(msg.get("readAt")),
parse_dt(msg.get("updatedAt")),
mr.get("downloadUrl") or mr.get("url"),
mr.get("description"),
mr.get("contentType")
))
# ==============================
# 💾 DOWNLOAD MESSAGE ATTACHMENT
# ==============================
def insert_download(cur, req_id, msg, existing_ids):
mr = msg.get("medicalRecord") or {}
attachment_id = mr.get("id")
if not attachment_id:
return
if attachment_id in existing_ids:
return # skip duplicates
url = mr.get("downloadUrl") or mr.get("url")
if not url:
return
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
data = r.content
except Exception as e:
print("⚠️ Failed to download:", e)
return
filename = url.split("/")[-1].split("?")[0]
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type,
filename, content_type, file_size, created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
file_content = VALUES(file_content),
file_size = VALUES(file_size),
downloaded_at = NOW()
""", (
req_id,
attachment_id,
"MESSAGE_ATTACHMENT",
filename,
mr.get("contentType"),
len(data),
parse_dt(msg.get("createdAt")),
data
))
existing_ids.add(attachment_id)
# ==============================
# 🧠 MAIN
# ==============================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--full", action="store_true", help="Load messages for ALL pozadavky")
# Force full mode ON
args = parser.parse_args(args=["--full"])
# args = parser.parse_args()
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
# ---- Load existing attachments
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
print(f"📦 Already downloaded attachments: {len(existing_ids)}\n")
# ---- Select pozadavky to process
with conn.cursor() as cur:
if args.full:
print("🔁 FULL REFRESH MODE: Fetching messages for ALL pozadavky!\n")
cur.execute("SELECT id FROM pozadavky")
else:
print("📥 Incremental mode: Only syncing updated pozadavky.\n")
cur.execute("""
SELECT id FROM pozadavky
WHERE messagesProcessed IS NULL
OR messagesProcessed < updatedAt
""")
requests_to_process = cur.fetchall()
# =================================
# ⏩ SKIP FIRST 3100 AS YESTERDAY
# =================================
SKIP = 3100
if len(requests_to_process) > SKIP:
print(f"⏩ Skipping first {SKIP} pozadavky (already processed yesterday).")
requests_to_process = requests_to_process[SKIP:]
else:
print("⚠️ Not enough pozadavky to skip!")
print(f"📋 Requests to process: {len(requests_to_process)}\n")
# ---- Process each request
for idx, row in enumerate(requests_to_process, 1):
req_id = row["id"]
print(f"[{idx}/{len(requests_to_process)}] Processing {req_id}")
messages = fetch_messages(headers, req_id)
with conn.cursor() as cur:
for msg in messages:
insert_message(cur, req_id, msg)
insert_download(cur, req_id, msg, existing_ids)
conn.commit()
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
print(f"{len(messages)} messages saved\n")
time.sleep(0.25)
conn.close()
print("🎉 Done!")
if __name__ == "__main__":
main()
@@ -0,0 +1 @@
{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]}
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from pathlib import Path
from datetime import datetime, timezone
import time
from dateutil import parser
import sys
# Force UTF-8 output
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ================================
# 🔧 CONFIGURATION
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 100
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ⭐ NOVÝ TESTOVANÝ DOTAZ obsahuje lastMessage.createdAt
GRAPHQL_QUERY = r"""
query ClinicRequestList2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
lastMessage {
createdAt
}
}
}
}
"""
# ================================
# 🧿 SAFE DATETIME PARSER (ALWAYS UTC → LOCAL)
# ================================
def to_mysql_dt_utc(iso_str):
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt_local = dt.astimezone()
return dt_local.strftime("%Y-%m-%d %H:%M:%S")
except:
return None
# ================================
# 🔑 TOKEN
# ================================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
return tok.split(" ", 1)[1]
return tok
# ================================
# 💾 UPSERT
# ================================
def upsert(conn, r):
p = r.get("extendedPatient") or {}
api_updated = to_mysql_dt_utc(r.get("updatedAt"))
last_msg = r.get("lastMessage") or {}
msg_updated = to_mysql_dt_utc(last_msg.get("createdAt"))
def max_dt(a, b):
if a and b:
return max(a, b)
return a or b
final_updated = max_dt(api_updated, msg_updated)
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r.get("id"),
r.get("displayTitle"),
to_mysql_dt_utc(r.get("createdAt")),
final_updated,
to_mysql_dt_utc(r.get("doneAt")),
to_mysql_dt_utc(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# 📡 FETCH ACTIVE PAGE
# ================================
def fetch_active(headers, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
"state": "ACTIVE",
}
payload = {
"operationName": "ClinicRequestList2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json().get("data", {}).get("requestsResponse", {})
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# 🧠 MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
print(f"\n=== Sync ACTIVE požadavků @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
offset = 0
total_processed = 0
total_count = None
while True:
batch, count = fetch_active(headers, offset)
if total_count is None:
total_count = count
print(f"📡 Celkem ACTIVE v Medevio: {count}")
if not batch:
break
for r in batch:
upsert(conn, r)
total_processed += len(batch)
print(f"{total_processed}/{total_count} ACTIVE processed")
if offset + BATCH_SIZE >= count:
break
offset += BATCH_SIZE
time.sleep(0.4)
conn.close()
print("\n✅ ACTIVE sync hotovo!\n")
if __name__ == "__main__":
main()
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import pymysql
import requests
from pathlib import Path
from datetime import datetime
from dateutil import parser
import time
import sys
# ================================
# UTF-8 SAFE OUTPUT (Windows friendly)
# ================================
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc.lower().startswith("utf"):
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ================================
# 🔧 CONFIG
# ================================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
BATCH_SIZE = 500
STATES = ["ACTIVE", "DONE"] # explicitně jinak API vrací jen ACTIVE
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = r"""
query ClinicRequestList2(
$clinicSlug: String!,
$queueId: String,
$queueAssignment: QueueAssignmentFilter!,
$state: PatientRequestState,
$pageInfo: PageInfo!,
$locale: Locale!
) {
requestsResponse: listPatientRequestsForClinic2(
clinicSlug: $clinicSlug,
queueId: $queueId,
queueAssignment: $queueAssignment,
state: $state,
pageInfo: $pageInfo
) {
count
patientRequests {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
extendedPatient {
name
surname
identificationNumber
}
lastMessage {
createdAt
}
}
}
}
"""
# ================================
# TOKEN
# ================================
def read_token(path: Path) -> str:
tok = path.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
return tok.split(" ", 1)[1]
return tok
# ================================
# DATETIME PARSER
# ================================
def to_mysql_dt(iso_str):
if not iso_str:
return None
try:
dt = parser.isoparse(iso_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=datetime.now().astimezone().tzinfo)
return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S")
except Exception:
return None
# ================================
# UPSERT
# ================================
def upsert(conn, r):
p = r.get("extendedPatient") or {}
api_updated = to_mysql_dt(r.get("updatedAt"))
msg_updated = to_mysql_dt((r.get("lastMessage") or {}).get("createdAt"))
final_updated = max(filter(None, [api_updated, msg_updated]), default=None)
sql = """
INSERT INTO pozadavky (
id, displayTitle, createdAt, updatedAt, doneAt, removedAt,
pacient_jmeno, pacient_prijmeni, pacient_rodnecislo
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
displayTitle=VALUES(displayTitle),
updatedAt=VALUES(updatedAt),
doneAt=VALUES(doneAt),
removedAt=VALUES(removedAt),
pacient_jmeno=VALUES(pacient_jmeno),
pacient_prijmeni=VALUES(pacient_prijmeni),
pacient_rodnecislo=VALUES(pacient_rodnecislo)
"""
vals = (
r.get("id"),
r.get("displayTitle"),
to_mysql_dt(r.get("createdAt")),
final_updated,
to_mysql_dt(r.get("doneAt")),
to_mysql_dt(r.get("removedAt")),
p.get("name"),
p.get("surname"),
p.get("identificationNumber"),
)
with conn.cursor() as cur:
cur.execute(sql, vals)
conn.commit()
# ================================
# FETCH PAGE (per state)
# ================================
def fetch_state(headers, state, offset):
variables = {
"clinicSlug": CLINIC_SLUG,
"queueId": None,
"queueAssignment": "ANY",
"state": state,
"pageInfo": {"first": BATCH_SIZE, "offset": offset},
"locale": "cs",
}
payload = {
"operationName": "ClinicRequestList2",
"query": GRAPHQL_QUERY,
"variables": variables,
}
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers)
r.raise_for_status()
data = r.json()["data"]["requestsResponse"]
return data.get("patientRequests", []), data.get("count", 0)
# ================================
# MAIN
# ================================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
safe_print(f"\n=== FULL Medevio READ-ALL sync @ {datetime.now():%Y-%m-%d %H:%M:%S} ===")
grand_total = 0
for state in STATES:
safe_print(f"\n🔁 STATE = {state}")
offset = 0
total = None
processed = 0
while True:
batch, count = fetch_state(headers, state, offset)
if total is None:
total = count
safe_print(f"📡 {state}: celkem {total}")
if not batch:
break
for r in batch:
upsert(conn, r)
processed += len(batch)
safe_print(f"{processed}/{total}")
offset += BATCH_SIZE
if offset >= count:
break
time.sleep(0.4)
grand_total += processed
conn.close()
safe_print(f"\n✅ HOTOVO celkem zpracováno {grand_total} požadavků\n")
# ================================
if __name__ == "__main__":
main()
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download and store Medevio questionnaires (userNote + eCRF) for all patient requests.
Uses the verified working query "GetPatientRequest2".
"""
import json
import requests
import pymysql
from datetime import datetime
from pathlib import Path
import time
import sys
# Force UTF-8 output even under Windows Task Scheduler
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ==============================
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
# ==============================
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc.lower().startswith("utf"):
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# 🔧 CONFIGURATION (UPDATED TO 192.168.1.50)
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
# ==============================
# 🕒 DATETIME FIXER
# ==============================
def fix_datetime(dt_str):
if not dt_str:
return None
try:
return datetime.fromisoformat(dt_str.replace("Z", "").replace("+00:00", ""))
except Exception:
return None
# Optional filter
CREATED_AFTER = "2025-01-01"
# ==============================
# 🧮 HELPERS
# ==============================
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
if tok.startswith("Bearer "):
return tok.split(" ", 1)[1]
return tok
GRAPHQL_QUERY = r"""
query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) {
request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) {
id
displayTitle(locale: $locale)
createdAt
updatedAt
userNote
eventType
extendedPatient(clinicSlug: $clinicSlug) {
name
surname
identificationNumber
}
ecrfFilledData(locale: $locale) {
name
groups {
label
fields {
name
label
type
value
}
}
}
}
}
"""
def fetch_questionnaire(headers, request_id, clinic_slug):
payload = {
"operationName": "GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": {
"requestId": request_id,
"clinicSlug": clinic_slug,
"locale": "cs",
},
}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=40)
if r.status_code != 200:
safe_print(f"❌ HTTP {r.status_code} for {request_id}: {r.text}")
return None
return r.json().get("data", {}).get("request")
def insert_questionnaire(cur, req):
if not req:
return
patient = req.get("extendedPatient") or {}
ecrf_data = req.get("ecrfFilledData")
created_at = fix_datetime(req.get("createdAt"))
updated_at = fix_datetime(req.get("updatedAt"))
cur.execute("""
INSERT INTO medevio_questionnaires (
request_id, created_at, updated_at, user_note, ecrf_json
)
VALUES (%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
updated_at = VALUES(updated_at),
user_note = VALUES(user_note),
ecrf_json = VALUES(ecrf_json),
updated_local = NOW()
""", (
req.get("id"),
created_at,
updated_at,
req.get("userNote"),
json.dumps(ecrf_data, ensure_ascii=False),
))
safe_print(f" 💾 Stored questionnaire for {patient.get('surname','')} {patient.get('name','')}")
# ==============================
# 🧠 MAIN
# ==============================
def main():
token = read_token(TOKEN_PATH)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
conn = pymysql.connect(**DB_CONFIG)
# load list of requests from the table we just filled
with conn.cursor() as cur:
sql = """
SELECT id, pacient_jmeno, pacient_prijmeni, createdAt, updatedAt, questionnaireprocessed
FROM pozadavky
WHERE (questionnaireprocessed IS NULL OR questionnaireprocessed < updatedAt)
"""
if CREATED_AFTER:
sql += " AND createdAt >= %s"
cur.execute(sql, (CREATED_AFTER,))
else:
cur.execute(sql)
rows = cur.fetchall()
safe_print(f"📋 Found {len(rows)} requests needing questionnaire check.")
for i, row in enumerate(rows, 1):
req_id = row["id"]
safe_print(f"\n[{i}/{len(rows)}] 🔍 Fetching questionnaire for {req_id} ...")
req = fetch_questionnaire(headers, req_id, CLINIC_SLUG)
if not req:
safe_print(" ⚠️ No questionnaire data found.")
continue
with conn.cursor() as cur:
insert_questionnaire(cur, req)
cur.execute(
"UPDATE pozadavky SET questionnaireprocessed = NOW() WHERE id = %s",
(req_id,)
)
conn.commit()
time.sleep(0.6)
conn.close()
safe_print("\n✅ Done! All questionnaires stored in MySQL table `medevio_questionnaires`.")
if __name__ == "__main__":
main()
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
import sys
# UTF-8 SAFE OUTPUT
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
pass
# ==============================
# CONFIG (.50)
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY_MESSAGES = r"""
query UseMessages_ListMessages($requestId: String!, $updatedSince: DateTime) {
messages: listMessages(patientRequestId: $requestId, updatedSince: $updatedSince) {
id createdAt updatedAt readAt text type
sender { id name surname clinicId }
medicalRecord { id description contentType url downloadUrl createdAt updatedAt }
}
}
"""
def parse_dt(s):
if not s: return None
try: return datetime.fromisoformat(s.replace("Z", "+00:00"))
except: return None
def read_token(path: Path) -> str:
return path.read_text(encoding="utf-8").strip().replace("Bearer ", "")
def main():
token = read_token(TOKEN_PATH)
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
conn = pymysql.connect(**DB_CONFIG)
# 1. Seznam již stažených příloh (prevence duplicit)
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {r["attachment_id"] for r in cur.fetchall()}
# 2. Seznam požadavků k synchronizaci
with conn.cursor() as cur:
cur.execute("""
SELECT id, messagesProcessed FROM pozadavky
WHERE messagesProcessed IS NULL OR messagesProcessed < updatedAt
""")
rows = cur.fetchall()
print(f"📋 Počet požadavků k synchronizaci zpráv: {len(rows)}")
for i, row in enumerate(rows, 1):
req_id = row["id"]
updated_since = row["messagesProcessed"]
if updated_since:
updated_since = updated_since.replace(microsecond=0).isoformat() + "Z"
print(f"[{i}/{len(rows)}] Synchronizuji: {req_id}")
payload = {
"operationName": "UseMessages_ListMessages",
"query": GRAPHQL_QUERY_MESSAGES,
"variables": {"requestId": req_id, "updatedSince": updated_since}
}
try:
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
messages = r.json().get("data", {}).get("messages", []) or []
if messages:
with conn.cursor() as cur:
for msg in messages:
# Uložení zprávy
sender = msg.get("sender") or {}
sender_name = " ".join(filter(None, [sender.get("name"), sender.get("surname")]))
mr = msg.get("medicalRecord") or {}
cur.execute("""
INSERT INTO medevio_conversation (
id, request_id, sender_name, sender_id, sender_clinic_id,
text, created_at, read_at, updated_at,
attachment_url, attachment_description, attachment_content_type
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
ON DUPLICATE KEY UPDATE
text = VALUES(text), updated_at = VALUES(updated_at), read_at = VALUES(read_at)
""", (
msg.get("id"), req_id, sender_name, sender.get("id"), sender.get("clinicId"),
msg.get("text"), parse_dt(msg.get("createdAt")), parse_dt(msg.get("readAt")),
parse_dt(msg.get("updatedAt")), mr.get("downloadUrl") or mr.get("url"),
mr.get("description"), mr.get("contentType")
))
# Uložení přílohy (pokud existuje a nemáme ji)
attachment_id = mr.get("id")
if attachment_id and attachment_id not in existing_ids:
url = mr.get("downloadUrl") or mr.get("url")
if url:
att_r = requests.get(url, timeout=30)
if att_r.status_code == 200:
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type,
filename, content_type, file_size, created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
""", (
req_id, attachment_id, "MESSAGE_ATTACHMENT",
url.split("/")[-1].split("?")[0], mr.get("contentType"),
len(att_r.content), parse_dt(msg.get("createdAt")), att_r.content
))
existing_ids.add(attachment_id)
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
else:
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET messagesProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
time.sleep(0.3)
except Exception as e:
print(f" ❌ Chyba u {req_id}: {e}")
conn.close()
print("\n🎉 Delta sync zpráv a příloh DOKONČEN")
if __name__ == "__main__":
main()
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Download all attachments for pozadavky where attachmentsProcessed IS NULL
Store them in MySQL table `medevio_downloads` on 192.168.1.50.
"""
import zlib
import json
import requests
import pymysql
from pathlib import Path
from datetime import datetime
import time
import sys
# Force UTF-8 output
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
pass
# ==============================
# 🛡 SAFE PRINT
# ==============================
def safe_print(text: str):
enc = sys.stdout.encoding or ""
if not enc or not enc.lower().startswith("utf"):
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# 🔧 CONFIGURATION (.50)
# ==============================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
CLINIC_SLUG = "mudr-buzalkova"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
CREATED_AFTER = "2024-12-01"
GRAPHQL_QUERY = r"""
query ClinicRequestDetail_GetPatientRequest2($requestId: UUID!) {
patientRequestMedicalRecords: listMedicalRecordsForPatientRequest(
attachmentTypes: [ECRF_FILL_ATTACHMENT, MESSAGE_ATTACHMENT, PATIENT_REQUEST_ATTACHMENT]
patientRequestId: $requestId
pageInfo: {first: 100, offset: 0}
) {
attachmentType
id
medicalRecord {
contentType
description
downloadUrl
id
url
visibleToPatient
}
}
}
"""
def extract_filename_from_url(url: str) -> str:
try:
return url.split("/")[-1].split("?")[0]
except:
return "unknown_filename"
def read_token(p: Path) -> str:
tok = p.read_text(encoding="utf-8").strip()
return tok.split(" ", 1)[1] if tok.startswith("Bearer ") else tok
def main():
token = read_token(TOKEN_PATH)
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
conn = pymysql.connect(**DB_CONFIG)
# 1. Načtení ID již stažených příloh
with conn.cursor() as cur:
cur.execute("SELECT attachment_id FROM medevio_downloads")
existing_ids = {row["attachment_id"] for row in cur.fetchall()}
safe_print(f"✅ V databázi již máme {len(existing_ids)} příloh.")
# 2. Výběr požadavků ke zpracování
sql = "SELECT id, pacient_prijmeni, pacient_jmeno, createdAt FROM pozadavky WHERE attachmentsProcessed IS NULL"
params = []
if CREATED_AFTER:
sql += " AND createdAt >= %s"
params.append(CREATED_AFTER)
with conn.cursor() as cur:
cur.execute(sql, params)
req_rows = cur.fetchall()
safe_print(f"📋 Počet požadavků ke stažení příloh: {len(req_rows)}")
for i, row in enumerate(req_rows, 1):
req_id = row["id"]
prijmeni = row.get("pacient_prijmeni") or "Neznamy"
created_date = row.get("createdAt") or datetime.now()
safe_print(f"\n[{i}/{len(req_rows)}] 🧾 {prijmeni} ({req_id})")
payload = {
"operationName": "ClinicRequestDetail_GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": {"requestId": req_id},
}
try:
r = requests.post("https://api.medevio.cz/graphql", json=payload, headers=headers, timeout=30)
attachments = r.json().get("data", {}).get("patientRequestMedicalRecords", [])
if attachments:
with conn.cursor() as cur:
for a in attachments:
m = a.get("medicalRecord") or {}
att_id = a.get("id")
if att_id in existing_ids:
continue
url = m.get("downloadUrl")
if url:
att_r = requests.get(url, timeout=30)
if att_r.status_code == 200:
content = att_r.content
filename = extract_filename_from_url(url)
cur.execute("""
INSERT INTO medevio_downloads (
request_id, attachment_id, attachment_type,
filename, content_type, file_size,
created_at, file_content
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
""", (req_id, att_id, a.get("attachmentType"), filename,
m.get("contentType"), len(content), created_date, content))
existing_ids.add(att_id)
safe_print(f" 💾 Uloženo: {filename} ({len(content) / 1024:.1f} kB)")
conn.commit()
# Označíme jako zpracované i když nebyly nalezeny žádné přílohy
with conn.cursor() as cur:
cur.execute("UPDATE pozadavky SET attachmentsProcessed = NOW() WHERE id = %s", (req_id,))
conn.commit()
time.sleep(0.3)
except Exception as e:
print(f" ❌ Chyba u {req_id}: {e}")
conn.close()
safe_print("\n🎯 Všechny přílohy byly zpracovány.")
if __name__ == "__main__":
main()
@@ -0,0 +1,232 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import pymysql
import re
from pathlib import Path
from datetime import datetime
from collections import defaultdict
import time
import sys
# Force UTF-8 output even under Windows Task Scheduler
import sys
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except AttributeError:
# Python < 3.7 fallback (not needed for you, but safe)
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
# ==============================
# 🛡 SAFE PRINT FOR CP1250 / EMOJI
# ==============================
def safe_print(text: str = ""):
enc = sys.stdout.encoding or ""
if not enc.lower().startswith("utf"):
# Strip emoji and characters outside BMP for Task Scheduler
text = ''.join(ch for ch in text if ord(ch) < 65536)
try:
print(text)
except UnicodeEncodeError:
# ASCII fallback
text = ''.join(ch for ch in text if ord(ch) < 128)
print(text)
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
def sanitize_name(name: str) -> str:
"""Replace invalid filename characters with underscore."""
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
def make_abbrev(title: str) -> str:
if not title:
return ""
words = re.findall(r"[A-Za-zÁ-Žá-ž0-9]+", title)
abbr = ""
for w in words:
if w.isdigit():
abbr += w
else:
abbr += w[0]
return abbr.upper()
# ==============================
# 🧹 DELETE UNEXPECTED FILES
# ==============================
def clean_folder(folder: Path, valid_files: set):
if not folder.exists():
return
for f in folder.iterdir():
if f.is_file():
if f.name.startswith(""):
continue
sanitized = sanitize_name(f.name)
if sanitized not in valid_files:
safe_print(f"🗑️ Removing unexpected file: {f.name}")
try:
f.unlink()
except Exception as e:
safe_print(f"⚠️ Could not delete {f}: {e}")
# ==============================
# 📦 DB CONNECTION
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
cur_blob = conn.cursor()
safe_print("🔍 Loading metadata from DB (FAST)…")
cur_meta.execute("""
SELECT d.id AS download_id,
d.request_id,
d.filename,
d.created_at,
p.updatedAt AS req_updated_at,
p.pacient_jmeno AS jmeno,
p.pacient_prijmeni AS prijmeni,
p.displayTitle
FROM medevio_downloads d
JOIN pozadavky p ON d.request_id = p.id
WHERE p.updatedAt >= DATE_SUB(NOW(), INTERVAL 14 DAY)
ORDER BY p.updatedAt DESC
""")
rows = cur_meta.fetchall()
safe_print(f"📋 Found {len(rows)} attachment records.\n")
# ==============================
# 🧠 MAIN LOOP WITH PROGRESS
# ==============================
# Group rows by request_id in Python — avoids N extra SELECT filename queries
rows_by_request = defaultdict(list)
for r in rows:
rows_by_request[r["request_id"]].append(r)
total_requests = len(rows_by_request)
safe_print(f"🔄 Processing {total_requests} unique requests...\n")
# Pre-index BASE_DIR once — avoids iterdir() called twice per request
folder_list = [(f, f.name) for f in BASE_DIR.iterdir() if f.is_dir()]
for current_index, (req_id, req_rows) in enumerate(rows_by_request.items(), 1):
percent = (current_index / total_requests) * 100
safe_print(f"\n[ {percent:5.1f}% ] Processing request {current_index} / {total_requests}{req_id}")
# ========== VALID FILENAMES from already-loaded rows ==========
# original filename → sanitized name (needed for DB query later)
file_map = {sanitize_name(r["filename"]): r["filename"] for r in req_rows}
valid_files = set(file_map.keys())
# ========== BUILD FOLDER NAME ==========
r = req_rows[0]
updated_at = r["req_updated_at"] or datetime.now()
date_str = updated_at.strftime("%Y-%m-%d")
prijmeni = sanitize_name(r["prijmeni"] or "Unknown")
jmeno = sanitize_name(r["jmeno"] or "")
title = r.get("displayTitle") or ""
abbr = make_abbrev(title)
clean_folder_name = sanitize_name(
f"{date_str} {prijmeni}, {jmeno} [{abbr}] {req_id}"
)
# ========== DETECT EXISTING FOLDER from pre-built index ==========
req_id_str = str(req_id)
matching = [f for f, name in folder_list if req_id_str in name]
existing_folder = matching[0] if matching else None
main_folder = existing_folder if existing_folder else BASE_DIR / clean_folder_name
# ========== MERGE DUPLICATES ==========
possible_dups = [f for f, name in folder_list if req_id_str in name and f != main_folder]
for dup in possible_dups:
safe_print(f"♻️ Merging duplicate folder: {dup.name}")
clean_folder(dup, valid_files)
main_folder.mkdir(parents=True, exist_ok=True)
for f in dup.iterdir():
if f.is_file():
target = main_folder / f.name
if not target.exists():
f.rename(target)
shutil.rmtree(dup, ignore_errors=True)
# ========== CLEAN MAIN FOLDER ==========
clean_folder(main_folder, valid_files)
# ========== DOWNLOAD MISSING FILES (batch blob fetch per request) ==========
main_folder.mkdir(parents=True, exist_ok=True)
added_new_file = False
missing_san = [
fn for fn in valid_files
if not (main_folder / fn).exists() and not (main_folder / ("" + fn)).exists()
]
if missing_san:
# Fetch all missing blobs in a single query instead of one per file
missing_orig = [file_map[fn] for fn in missing_san]
placeholders = ",".join(["%s"] * len(missing_orig))
cur_blob.execute(
f"SELECT filename, file_content FROM medevio_downloads "
f"WHERE request_id=%s AND filename IN ({placeholders})",
[req_id] + missing_orig,
)
for blob_filename, content in cur_blob.fetchall():
if not content:
continue
dest_plain = main_folder / sanitize_name(blob_filename)
with open(dest_plain, "wb") as fh:
fh.write(content)
safe_print(f"💾 Wrote: {dest_plain.relative_to(BASE_DIR)}")
added_new_file = True
# ========== REMOVE ▲ FLAG IF NEW FILES ADDED ==========
if added_new_file and "" in main_folder.name:
new_name = main_folder.name.replace("", "").strip()
new_path = main_folder.parent / new_name
if new_path != main_folder:
try:
main_folder.rename(new_path)
safe_print(f"🔄 Folder flag ▲ removed → {new_name}")
main_folder = new_path
except Exception as e:
safe_print(f"⚠️ Could not rename folder: {e}")
safe_print("\n🎯 Export complete.\n")
cur_blob.close()
cur_meta.close()
conn.close()
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import shutil
import pymysql
import re
from pathlib import Path
from datetime import datetime
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
def sanitize_name(name: str) -> str:
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
def clean_folder(folder: Path, valid_files: set):
"""Remove files that do NOT exist in MySQL for this request."""
if not folder.exists():
return
for f in folder.iterdir():
if f.is_file() and sanitize_name(f.name) not in valid_files:
print(f"🗑️ Removing unexpected file: {f.name}")
try:
f.unlink()
except Exception as e:
print(f"⚠️ Cannot delete {f}: {e}")
# ==============================
# 📥 LOAD EVERYTHING IN ONE QUERY
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur = conn.cursor(pymysql.cursors.DictCursor)
print("📥 Loading ALL metadata + BLOBs with ONE MySQL query…")
cur.execute("""
SELECT
d.id AS download_id,
d.request_id,
d.filename,
d.file_content,
p.updatedAt AS req_updated_at,
p.pacient_jmeno AS jmeno,
p.pacient_prijmeni AS prijmeni
FROM medevio_downloads d
JOIN pozadavky p ON d.request_id = p.id
ORDER BY p.updatedAt DESC, d.created_at ASC
""")
rows = cur.fetchall()
print(f"📦 Loaded {len(rows)} total file rows.\n")
conn.close()
# ==============================
# 🔄 ORGANIZE ROWS PER REQUEST
# ==============================
requests = {} # req_id → list of file dicts
for r in rows:
req_id = r["request_id"]
if req_id not in requests:
requests[req_id] = []
requests[req_id].append(r)
print(f"📌 Unique requests: {len(requests)}\n")
# ==============================
# 🧠 MAIN LOOP SAME LOGIC AS BEFORE
# ==============================
for req_id, filelist in requests.items():
# ========== GET UPDATEDAT (same logic) ==========
any_row = filelist[0]
updated_at = any_row["req_updated_at"] or datetime.now()
date_str = updated_at.strftime("%Y-%m-%d")
prijmeni = sanitize_name(any_row["prijmeni"] or "Unknown")
jmeno = sanitize_name(any_row["jmeno"] or "")
folder_name = sanitize_name(f"{date_str} {prijmeni}, {jmeno} {req_id}")
main_folder = BASE_DIR / folder_name
# ========== VALID FILES ==========
valid_files = {sanitize_name(r["filename"]) for r in filelist}
# ========== FIND OLD FOLDERS ==========
possible_dups = [
f for f in BASE_DIR.iterdir()
if f.is_dir() and req_id in f.name and f != main_folder
]
# ========== MERGE OLD FOLDERS ==========
for dup in possible_dups:
print(f"♻️ Merging folder: {dup.name}")
clean_folder(dup, valid_files)
main_folder.mkdir(parents=True, exist_ok=True)
for f in dup.iterdir():
if f.is_file():
target = main_folder / f.name
if not target.exists():
f.rename(target)
shutil.rmtree(dup, ignore_errors=True)
# ========== CLEAN MAIN FOLDER ==========
main_folder.mkdir(parents=True, exist_ok=True)
clean_folder(main_folder, valid_files)
# ========== SAVE FILES (fast now) ==========
for r in filelist:
filename = sanitize_name(r["filename"])
dest = main_folder / filename
if dest.exists():
continue
content = r["file_content"]
if not content:
continue
with open(dest, "wb") as f:
f.write(content)
print(f"💾 Saved: {dest.relative_to(BASE_DIR)}")
print("\n🎯 Export complete.\n")
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import zlib
import pymysql
import re
from pathlib import Path
from datetime import datetime
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
def sanitize_name(name: str) -> str:
"""Replace invalid filename characters with underscore."""
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
# ==============================
# 📦 STREAMING EXPORT WITH TRIANGLE CHECK
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
cur_blob = conn.cursor()
cur_meta.execute("""
SELECT id, request_id, attachment_id, filename, pacient_jmeno,
pacient_prijmeni, created_at, downloaded_at
FROM medevio_downloads
WHERE file_content IS NOT NULL;
""")
rows = cur_meta.fetchall()
print(f"📋 Found {len(rows)} records to check/export")
skipped, exported = 0, 0
for r in rows:
try:
created = r["created_at"] or r["downloaded_at"] or datetime.now()
date_str = created.strftime("%Y-%m-%d")
prijmeni = sanitize_name(r["pacient_prijmeni"] or "Unknown")
jmeno = sanitize_name(r["pacient_jmeno"] or "")
crc = f"{zlib.crc32(r['request_id'].encode('utf-8')) & 0xFFFFFFFF:08X}"
# Base (non-triangle) and processed (triangle) folder variants
base_folder = sanitize_name(f"{date_str} {prijmeni}, {jmeno} {crc}")
tri_folder = sanitize_name(f"{date_str}{prijmeni}, {jmeno} {crc}")
base_path = BASE_DIR / base_folder
tri_path = BASE_DIR / tri_folder
filename = sanitize_name(r["filename"] or f"unknown_{r['id']}.bin")
file_path_base = base_path / filename
file_path_tri = tri_path / filename
# 🟡 Skip if exists in either version
if file_path_base.exists() or file_path_tri.exists():
skipped += 1
found_in = "" if file_path_tri.exists() else ""
print(f"⏭️ Skipping existing{found_in}: {filename}")
continue
# Make sure base folder exists before saving
base_path.mkdir(parents=True, exist_ok=True)
# 2️⃣ Fetch blob
cur_blob.execute("SELECT file_content FROM medevio_downloads WHERE id = %s", (r["id"],))
blob = cur_blob.fetchone()[0]
if blob:
with open(file_path_base, "wb") as f:
f.write(blob)
exported += 1
print(f"✅ Saved: {file_path_base.relative_to(BASE_DIR)}")
else:
print(f"⚠️ No content for id={r['id']}")
except Exception as e:
print(f"❌ Error for id={r['id']}: {e}")
cur_blob.close()
cur_meta.close()
conn.close()
print(f"\n🎯 Export complete — {exported} new files saved, {skipped} skipped.\n")
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import zlib
import pymysql
import re
from pathlib import Path
from datetime import datetime
# ==============================
# ⚙️ CONFIGURATION
# ==============================
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
}
BASE_DIR = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
BASE_DIR.mkdir(parents=True, exist_ok=True)
def sanitize_name(name: str) -> str:
"""Replace invalid filename characters with underscore."""
return re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name).strip()
# ==============================
# 📦 EXPORT WITH JOIN TO POZADAVKY
# ==============================
conn = pymysql.connect(**DB_CONFIG)
cur_meta = conn.cursor(pymysql.cursors.DictCursor)
cur_blob = conn.cursor()
# 🎯 JOIN medevio_downloads → pozadavky
cur_meta.execute("""
SELECT d.id, d.request_id, d.attachment_id, d.filename,
d.created_at, d.downloaded_at,
p.pacient_jmeno AS jmeno,
p.pacient_prijmeni AS prijmeni
FROM medevio_downloads d
JOIN pozadavky p ON d.request_id = p.id
WHERE d.file_content IS NOT NULL;
""")
rows = cur_meta.fetchall()
print(f"📋 Found {len(rows)} records to check/export")
skipped, exported = 0, 0
for r in rows:
try:
created = r["created_at"] or r["downloaded_at"] or datetime.now()
date_str = created.strftime("%Y-%m-%d")
# 👍 Now always correct from pozadavky
prijmeni = sanitize_name(r["prijmeni"] or "Unknown")
jmeno = sanitize_name(r["jmeno"] or "")
# 🔥 Full request_id for folder identification
full_req_id = sanitize_name(r["request_id"])
# Folder names (normal and triangle)
base_folder = f"{date_str} {prijmeni}, {jmeno} {full_req_id}"
tri_folder = f"{date_str}{prijmeni}, {jmeno} {full_req_id}"
base_folder = sanitize_name(base_folder)
tri_folder = sanitize_name(tri_folder)
base_path = BASE_DIR / base_folder
tri_path = BASE_DIR / tri_folder
filename = sanitize_name(r["filename"] or f"unknown_{r['id']}.bin")
file_path_base = base_path / filename
file_path_tri = tri_path / filename
# 🟡 Skip if file already exists
if file_path_base.exists() or file_path_tri.exists():
skipped += 1
found_in = "" if file_path_tri.exists() else ""
print(f"⏭️ Skipping existing{found_in}: {filename}")
continue
# Ensure directory exists
base_path.mkdir(parents=True, exist_ok=True)
# 2️⃣ Fetch blob content
cur_blob.execute(
"SELECT file_content FROM medevio_downloads WHERE id = %s",
(r["id"],)
)
blob = cur_blob.fetchone()[0]
if blob:
with open(file_path_base, "wb") as f:
f.write(blob)
exported += 1
print(f"✅ Saved: {file_path_base.relative_to(BASE_DIR)}")
else:
print(f"⚠️ No content for id={r['id']}")
except Exception as e:
print(f"❌ Error for id={r['id']}: {e}")
cur_blob.close()
cur_meta.close()
conn.close()
print(f"\n🎯 Export complete — {exported} new files saved, {skipped} skipped.\n")
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
from pathlib import Path
import sys
# UTF-8 safety
try:
sys.stdout.reconfigure(encoding='utf-8')
except:
pass
# === CONFIG ===
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
REQUEST_ID = "e17536c4-ed22-4242-ada5-d03713e0b7ac" # požadavek který sledujeme
def read_token(path: Path) -> str:
t = path.read_text().strip()
if t.startswith("Bearer "):
return t.split(" ", 1)[1]
return t
# === QUERY ===
QUERY = r"""
query ClinicRequestNotes_Get($patientRequestId: String!) {
notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {
id
content
createdAt
updatedAt
createdBy {
id
name
surname
}
}
}
"""
def run_query(request_id, token):
payload = {
"operationName": "ClinicRequestNotes_Get",
"query": QUERY,
"variables": {"patientRequestId": request_id},
}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers)
r.raise_for_status()
return r.json()
def main():
token = read_token(TOKEN_PATH)
print(f"🔍 Čtu interní klinické poznámky k požadavku {REQUEST_ID} ...\n")
data = run_query(REQUEST_ID, token)
notes = data.get("data", {}).get("notes", [])
if not notes:
print("📭 Žádné klinické poznámky nejsou uložené.")
return
print(f"📌 Nalezeno {len(notes)} poznámek:\n")
for n in notes:
print("──────────────────────────────")
print(f"🆔 ID: {n['id']}")
print(f"👤 Vytvořil: {n['createdBy']['surname']} {n['createdBy']['name']}")
print(f"📅 createdAt: {n['createdAt']}")
print(f"🕒 updatedAt: {n['updatedAt']}")
print("📝 Obsah:")
print(n['content'])
print("")
if __name__ == "__main__":
main()
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
from pathlib import Path
import sys
# UTF-8 handling
try:
sys.stdout.reconfigure(encoding='utf-8')
except:
pass
# === CONFIG ===
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
REQUEST_ID = "e17536c4-ed22-4242-ada5-d03713e0b7ac" # požadavek
NOTE_PREPEND_TEXT = "🔥 NOVÝ TESTOVACÍ ŘÁDEK\n" # text, který se přidá NA ZAČÁTEK
# === Helpers ===
def read_token(p: Path) -> str:
t = p.read_text().strip()
if t.startswith("Bearer "):
return t.split(" ", 1)[1]
return t
# === Queries ===
QUERY_GET_NOTES = r"""
query ClinicRequestNotes_Get($patientRequestId: String!) {
notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {
id
content
createdAt
updatedAt
createdBy {
id
name
surname
}
}
}
"""
MUTATION_UPDATE_NOTE = r"""
mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) {
updateClinicPatientRequestNote(noteInput: $noteInput) {
id
}
}
"""
# === Core functions ===
def gql(query, variables, token):
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
payload = {"query": query, "variables": variables}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers)
r.raise_for_status()
return r.json()
def get_internal_note(request_id, token):
data = gql(QUERY_GET_NOTES, {"patientRequestId": request_id}, token)
notes = data.get("data", {}).get("notes", [])
return notes[0] if notes else None
def update_internal_note(note_id, new_content, token):
variables = {"noteInput": {"id": note_id, "content": new_content}}
return gql(MUTATION_UPDATE_NOTE, variables, token)
# === MAIN ===
def main():
token = read_token(TOKEN_PATH)
print(f"🔍 Načítám interní poznámku pro požadavek {REQUEST_ID}...\n")
note = get_internal_note(REQUEST_ID, token)
if not note:
print("❌ Nebyla nalezena žádná interní klinická poznámka!")
return
note_id = note["id"]
old_content = note["content"] or ""
print("📄 Původní obsah:")
print(old_content)
print("────────────────────────────\n")
# ===============================
# PREPEND new text
# ===============================
new_content = NOTE_PREPEND_TEXT + old_content
print("📝 Nový obsah který odešlu:")
print(new_content)
print("────────────────────────────\n")
# UPDATE
result = update_internal_note(note_id, new_content, token)
print(f"✅ Hotovo! Poznámka {note_id} aktualizována.")
print(result)
if __name__ == "__main__":
main()
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
import mysql.connector
from pathlib import Path
import sys
from datetime import datetime
# UTF-8 handling
try:
sys.stdout.reconfigure(encoding='utf-8')
except:
pass
# === KONFIGURACE ===
# --- Medevio API ---
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
# --- ZPRACOVÁNÍ ---
# Zadejte počet požadavků ke zpracování.
# 0 znamená zpracovat VŠECHNY nesynchronizované požadavky.
PROCESS_LIMIT = 10 # <-- Používáme PROCESS_LIMIT
# --- MySQL DB ---
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3307,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
}
# === Helpers ===
def read_token(p: Path) -> str:
"""Načte Bearer token z textového souboru."""
t = p.read_text().strip()
if t.startswith("Bearer "):
return t.split(" ", 1)[1]
return t
# === DB Funkce ===
def get_requests_to_process_from_db(limit):
"""
Získá seznam požadavků (ID, Titul, Jméno, Příjmení) k synchronizaci z MySQL.
Použije LIMIT, pokud limit > 0.
"""
if limit == 0:
print("🔍 Připojuji se k MySQL a hledám **VŠECHNY** nesynchronizované požadavky...")
else:
print(f"🔍 Připojuji se k MySQL a hledám **{limit}** nesynchronizovaných požadavků...")
requests_list = []
conn = None
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor()
# Základní SQL dotaz
query = """
SELECT id, displayTitle, pacient_jmeno, pacient_prijmeni
FROM pozadavky
WHERE doneAt IS NULL
AND noteSyncedAt IS NULL
ORDER BY updatedAt DESC
"""
# Podmíněné přidání LIMIT klauzule
if limit > 0:
query += f"LIMIT {limit};"
else:
query += ";"
cursor.execute(query)
results = cursor.fetchall()
for result in results:
request_id, display_title, jmeno, prijmeni = result
requests_list.append({
"id": request_id,
"displayTitle": display_title,
"jmeno": jmeno,
"prijmeni": prijmeni
})
cursor.close()
if requests_list:
print(f"✅ Nalezeno {len(requests_list)} požadavků ke zpracování.")
else:
print("❌ Nebyl nalezen žádný nesynchronizovaný otevřený požadavek v DB.")
return requests_list
except mysql.connector.Error as err:
print(f"❌ Chyba při připojení/dotazu MySQL: {err}")
return []
finally:
if conn and conn.is_connected():
conn.close()
def update_db_sync_time(request_id, conn):
"""Aktualizuje sloupec noteSyncedAt v tabulce pozadavky. Používá existující připojení."""
cursor = conn.cursor()
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
update_query = """
UPDATE pozadavky
SET noteSyncedAt = %s
WHERE id = %s;
"""
cursor.execute(update_query, (current_time, request_id))
conn.commit()
cursor.close()
print(f" (DB: Čas synchronizace pro {request_id} uložen)")
# === GraphQL Operace (Beze Změny) ===
QUERY_GET_NOTE = r"""
query ClinicRequestNotes_Get($patientRequestId: String!) {
notes: getClinicPatientRequestNotes(requestId: $patientRequestId) {
id
content
}
}
"""
MUTATION_UPDATE_NOTE = r"""
mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) {
updateClinicPatientRequestNote(noteInput: $noteInput) {
id
}
}
"""
MUTATION_CREATE_NOTE = r"""
mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) {
createClinicPatientRequestNote(noteInput: $noteInput) {
id
}
}
"""
def gql(query, variables, token):
"""Obecná funkce pro volání GraphQL endpointu."""
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
payload = {"query": query, "variables": variables}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers)
r.raise_for_status()
return r.json()
def get_internal_note(request_id, token):
"""Získá jedinou interní poznámku (obsah a ID) pro daný požadavek."""
data = gql(QUERY_GET_NOTE, {"patientRequestId": request_id}, token)
notes = data.get("data", {}).get("notes", [])
return notes[0] if notes else None
def update_internal_note(note_id, new_content, token):
"""Aktualizuje obsah poznámky v Medeviu."""
variables = {"noteInput": {"id": note_id, "content": new_content}}
return gql(MUTATION_UPDATE_NOTE, variables, token)
def create_internal_note(request_id, content, token):
"""Vytvoří novou interní poznámku k požadavku v Medeviu."""
variables = {"noteInput": {"requestId": request_id, "content": content}}
return gql(MUTATION_CREATE_NOTE, variables, token)
# === MAIN ===
def main():
token = read_token(TOKEN_PATH)
# 1. Získat seznam ID požadavků ke zpracování (používáme PROCESS_LIMIT)
requests_to_process = get_requests_to_process_from_db(PROCESS_LIMIT)
if not requests_to_process:
return
# Pro update DB time otevřeme připojení jednou a použijeme ho v cyklu
conn = mysql.connector.connect(**DB_CONFIG)
print("\n=============================================")
print(f"START ZPRACOVÁNÍ {len(requests_to_process)} POŽADAVKŮ")
print("=============================================\n")
for idx, request in enumerate(requests_to_process, 1):
request_id = request["id"]
print(
f"[{idx}/{len(requests_to_process)}] Zpracovávám požadavek: {request['prijmeni']} {request['jmeno']} (ID: {request_id})")
# 2. Vytvořit text, který chceme přidat/vytvořit
prepend_text = f"ID: {request_id}\n"
# 3. Pokusit se získat existující interní poznámku z Medevia
note = get_internal_note(request_id, token)
medevio_update_success = False
if note:
# A) POZNÁMKA EXISTUJE -> AKTUALIZOVAT
note_id = note["id"]
old_content = note["content"] or ""
new_content = prepend_text + old_content
try:
# Odeslání aktualizace
update_internal_note(note_id, new_content, token)
print(f" (Medevio: Poznámka {note_id} **aktualizována**.)")
medevio_update_success = True
except requests.exceptions.HTTPError as e:
print(f" ❌ Chyba při aktualizaci Medevio API: {e}")
else:
# B) POZNÁMKA NEEXISTUJE -> VYTVOŘIT
new_content = prepend_text.strip()
try:
# Odeslání vytvoření
result = create_internal_note(request_id, new_content, token)
new_note_id = result.get("data", {}).get("createClinicPatientRequestNote", {}).get("id", "N/A")
print(f" (Medevio: Nová poznámka {new_note_id} **vytvořena**.)")
medevio_update_success = True
except requests.exceptions.HTTPError as e:
print(f" ❌ Chyba při vytváření Medevio API: {e}")
# 4. AKTUALIZACE ČASOVÉHO RAZÍTKA V DB
if medevio_update_success:
update_db_sync_time(request_id, conn)
print("---------------------------------------------")
# Uzavřeme připojení k DB po dokončení cyklu
if conn and conn.is_connected():
conn.close()
print("\n✅ Všechny požadavky zpracovány. Připojení k DB uzavřeno.")
if __name__ == "__main__":
main()
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Full Medevio Report:
- Agenda (API, next 30 days)
- Otevřené požadavky (MySQL)
- Merged (Agenda + Open, deduplicated)
- Vaccine sheets (from merged data)
"""
import re
import json
import pymysql
import requests
import pandas as pd
from pathlib import Path
from datetime import datetime
from dateutil import parser, tz
from dateutil.relativedelta import relativedelta
from openpyxl import load_workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
from openpyxl.utils.dataframe import dataframe_to_rows
# ==================== CONFIG ====================
GRAPHQL_URL = "https://api.medevio.cz/graphql"
CALENDAR_ID = "144c4e12-347c-49ca-9ec0-8ca965a4470d"
CLINIC_SLUG = "mudr-buzalkova"
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
EXPORT_DIR = Path(r"u:\Dropbox\Ordinace\Reporty")
EXPORT_DIR.mkdir(exist_ok=True, parents=True)
# Delete previous reports
for old in EXPORT_DIR.glob("* Agenda + Požadavky.xlsx"):
old.unlink()
print(f"🗑️ Deleted old report: {old.name}")
timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
xlsx_path = EXPORT_DIR / f"{timestamp} Agenda + Požadavky.xlsx"
# ==================== LOAD TOKEN ====================
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
gateway_token = TOKEN_PATH.read_text(encoding="utf-8").strip()
headers = {
"content-type": "application/json",
"authorization": f"Bearer {gateway_token}",
"origin": "https://my.medevio.cz",
"referer": "https://my.medevio.cz/",
}
# ==================== STYLING ====================
widths = {1: 11, 2: 13, 3: 45, 4: 30, 5: 15, 6: 15, 7: 30, 8: 15, 9: 37, 10: 37}
header_fill = PatternFill("solid", fgColor="FFFF00")
alt_fill = PatternFill("solid", fgColor="F2F2F2")
thin_border = Border(
left=Side(style="thin", color="000000"),
right=Side(style="thin", color="000000"),
top=Side(style="thin", color="000000"),
bottom=Side(style="thin", color="000000"),
)
REQUEST_URL_TEMPLATE = "https://my.medevio.cz/mudr-buzalkova/klinika/pozadavky?pozadavek={}"
link_font = Font(color="0563C1", underline="single")
def format_ws(ws, df):
"""Apply unified formatting to a worksheet."""
# Find Request_ID column index (1-based)
req_id_col = None
columns = list(df.columns)
if "Request_ID" in columns:
req_id_col = columns.index("Request_ID") + 1
for col_idx in range(1, len(df.columns) + 1):
col_letter = get_column_letter(col_idx)
cell = ws.cell(row=1, column=col_idx)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.fill = header_fill
cell.value = str(cell.value).upper()
cell.border = thin_border
ws.column_dimensions[col_letter].width = widths.get(col_idx, 20)
for r_idx, row in enumerate(ws.iter_rows(min_row=2, max_row=ws.max_row), start=2):
for cell in row:
cell.border = thin_border
if r_idx % 2 == 0:
cell.fill = alt_fill
# Add hyperlink to Request_ID cells
if req_id_col and cell.column == req_id_col and cell.value:
cell.hyperlink = REQUEST_URL_TEMPLATE.format(cell.value)
cell.font = link_font
ws.freeze_panes = "A2"
ws.auto_filter.ref = ws.dimensions
# ==================== 1️⃣ LOAD AGENDA (API) ====================
print("📡 Querying Medevio API for agenda...")
dnes = datetime.utcnow().date()
since = datetime.combine(dnes, datetime.min.time())
until = since + relativedelta(months=1)
payload = {
"operationName": "ClinicAgenda_ListClinicReservations",
"variables": {
"calendarIds": [CALENDAR_ID],
"clinicSlug": CLINIC_SLUG,
"since": since.isoformat() + "Z",
"until": until.isoformat() + "Z",
"locale": "cs",
"emptyCalendarIds": False,
},
"query": """query ClinicAgenda_ListClinicReservations(
$calendarIds: [UUID!], $clinicSlug: String!,
$locale: Locale!, $since: DateTime!, $until: DateTime!,
$emptyCalendarIds: Boolean!
) {
reservations: listClinicReservations(
clinicSlug: $clinicSlug, calendarIds: $calendarIds,
since: $since, until: $until
) @skip(if: $emptyCalendarIds) {
id start end note done color
request {
id displayTitle(locale: $locale)
extendedPatient {
name surname dob insuranceCompanyObject { shortName }
}
}
}
}""",
}
r = requests.post(GRAPHQL_URL, headers=headers, data=json.dumps(payload))
r.raise_for_status()
resp = r.json()
if "errors" in resp or "data" not in resp:
print("❌ API response:")
print(json.dumps(resp, indent=2, ensure_ascii=False))
raise SystemExit("API call failed - check token or query.")
reservations = resp["data"]["reservations"]
rows = []
for r in reservations:
req = r.get("request") or {}
patient = req.get("extendedPatient") or {}
insurance = patient.get("insuranceCompanyObject") or {}
try:
start_dt = parser.isoparse(r.get("start")).astimezone(tz.gettz("Europe/Prague"))
end_dt = parser.isoparse(r.get("end")).astimezone(tz.gettz("Europe/Prague"))
except Exception:
start_dt = end_dt = None
date_str = start_dt.strftime("%Y-%m-%d") if start_dt else ""
time_interval = (
f"{start_dt.strftime('%H:%M')}-{end_dt.strftime('%H:%M')}"
if start_dt and end_dt
else ""
)
rows.append(
{
"Date": date_str,
"Time": time_interval,
"Title": req.get("displayTitle") or "",
"Patient": f"{patient.get('surname','')} {patient.get('name','')}".strip(),
"DOB": patient.get("dob") or "",
"Insurance": insurance.get("shortName") or "",
"Note": r.get("note") or "",
"Color": r.get("color") or "",
"Request_ID": req.get("id") or "",
"Reservation_ID": r.get("id"),
}
)
df_agenda = pd.DataFrame(rows).sort_values(["Date", "Time"])
print(f"✅ Loaded {len(df_agenda)} agenda rows.")
# ==================== 2️⃣ LOAD OPEN REQUESTS (MySQL) ====================
print("📡 Loading open requests from MySQL...")
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
cur.execute(
"""
SELECT
id AS Request_ID,
displayTitle AS Title,
pacient_prijmeni AS Pacient_Prijmeni,
pacient_jmeno AS Pacient_Jmeno,
pacient_rodnecislo AS DOB,
createdAt AS Created
FROM pozadavky
WHERE doneAt IS NULL AND removedAt IS NULL
ORDER BY createdAt DESC
"""
)
rows = cur.fetchall()
conn.close()
df_open = pd.DataFrame(rows)
if not df_open.empty:
df_open["Patient"] = (
df_open["Pacient_Prijmeni"].fillna("")
+ " "
+ df_open["Pacient_Jmeno"].fillna("")
).str.strip()
df_open["Date"] = df_open["Created"].astype(str).str[:10]
df_open["Time"] = ""
df_open["Insurance"] = ""
df_open["Note"] = ""
df_open["Color"] = ""
df_open["Reservation_ID"] = ""
df_open = df_open[
[
"Date",
"Time",
"Title",
"Patient",
"DOB",
"Insurance",
"Note",
"Color",
"Request_ID",
"Reservation_ID",
]
]
print(f"✅ Loaded {len(df_open)} open requests.")
# ==================== 3️⃣ MERGE + DEDUPLICATE ====================
print("🟢 Merging and deduplicating (Agenda preferred)...")
df_agenda["Source"] = "Agenda"
df_open["Source"] = "Open"
df_merged = pd.concat([df_agenda, df_open], ignore_index=True).fillna("")
df_merged = df_merged.sort_values(["Source"], ascending=[True])
# drop duplicates — prefer Agenda if same Request_ID or same (Patient+Title)
df_merged = df_merged.drop_duplicates(
subset=["Request_ID", "Patient", "Title"], keep="first"
)
df_merged = df_merged.drop(columns=["Source"], errors="ignore")
df_merged = df_merged.sort_values(["Date", "Time"], na_position="last").reset_index(
drop=True
)
print(f"✅ Total merged rows after deduplication: {len(df_merged)}")
# ==================== 4️⃣ WRITE BASE SHEETS ====================
with pd.ExcelWriter(xlsx_path, engine="openpyxl") as writer:
df_agenda.to_excel(writer, sheet_name="Agenda", index=False)
df_open.to_excel(writer, sheet_name="Otevřené požadavky", index=False)
df_merged.to_excel(writer, sheet_name="Merged", index=False)
wb = load_workbook(xlsx_path)
for name, df_ref in [
("Agenda", df_agenda),
("Otevřené požadavky", df_open),
("Merged", df_merged),
]:
ws = wb[name]
format_ws(ws, df_ref)
# ==================== 5️⃣ VACCINE SHEETS (from MERGED) ====================
VACCINE_SHEETS = {
"Chřipka": ["očkování", "chřipka"],
"COVID": ["očkování", "covid"],
"Pneumokok": ["očkování", "pneumo"],
"Hep A": ["očkování", "žloutenka a"],
"Hep B": ["očkování", "žloutenka b"],
"Hep A+B": ["očkování", "žloutenka a+b"],
"Klíšťovka": ["očkování", "klíšť"],
}
def kw_pattern(kw):
return rf"(?<!\w){re.escape(kw)}(?!\s*\+\s*\w)"
for sheet_name, keywords in VACCINE_SHEETS.items():
mask = pd.Series(True, index=df_merged.index)
title_series = df_merged["Title"].fillna("")
for kw in keywords:
pattern = kw_pattern(kw)
mask &= title_series.str.contains(pattern, flags=re.IGNORECASE, regex=True)
filtered_df = df_merged[mask].copy()
if filtered_df.empty:
print(f"️ No matches for '{sheet_name}'")
continue
ws_new = wb.create_sheet(title=sheet_name)
for r in dataframe_to_rows(filtered_df, index=False, header=True):
ws_new.append(r)
format_ws(ws_new, filtered_df)
print(f"🟡 Created sheet '{sheet_name}' ({len(filtered_df)} rows).")
# ==================== SAVE ====================
wb.save(xlsx_path)
print(f"📘 Exported full merged report:\n{xlsx_path}")
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Check one request in MySQL."""
import pymysql
import json
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
REQUEST_ID = "6b46b5a8-b080-4821-86b0-39adabeec86b"
conn = pymysql.connect(**DB_CONFIG)
with conn.cursor() as cur:
cur.execute("SELECT * FROM pozadavky WHERE id = %s", (REQUEST_ID,))
row = cur.fetchone()
conn.close()
if row:
# Convert datetime objects to strings for JSON serialization
for k, v in row.items():
if hasattr(v, 'isoformat'):
row[k] = v.isoformat()
print(json.dumps(row, indent=2, ensure_ascii=False, default=str))
else:
print(f"Not found: {REQUEST_ID}")
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Quick check: fetch one request from Medevio API and print all fields."""
import json
import requests
from pathlib import Path
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
GRAPHQL_URL = "https://api.medevio.cz/graphql"
CLINIC_SLUG = "mudr-buzalkova"
REQUEST_ID = "6b46b5a8-b080-4821-86b0-39adabeec86b"
token = TOKEN_PATH.read_text(encoding="utf-8").strip()
headers = {
"content-type": "application/json",
"authorization": f"Bearer {token}",
"origin": "https://my.medevio.cz",
"referer": "https://my.medevio.cz/",
}
# Query with as many fields as possible
QUERY = """
query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!, $locale: Locale!) {
request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) {
id
displayTitle(locale: $locale)
createdAt
updatedAt
doneAt
removedAt
userNote
eventType
extendedPatient(clinicSlug: $clinicSlug) {
name
surname
dob
identificationNumber
insuranceCompanyObject { shortName }
}
ecrfFilledData(locale: $locale) {
name
groups {
label
fields { name label type value }
}
}
}
}
"""
payload = {
"operationName": "GetPatientRequest2",
"query": QUERY,
"variables": {
"requestId": REQUEST_ID,
"clinicSlug": CLINIC_SLUG,
"locale": "cs",
},
}
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=30)
print(json.dumps(r.json(), indent=2, ensure_ascii=False))
@@ -0,0 +1 @@
{"cookies": [{"name": "gateway-access-token", "value": "YwBgkf8McREDKs7vCZj0EZD2fJsuV8RyDPtYx7WiDoz0nFJ9kxId8kcNEPBLFSwM+Tiz80+SOdFwo+oj", "domain": "my.medevio.cz", "path": "/", "expires": 1763372319, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "aws-waf-token", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c6zAAAA:OYwXLY5OyitSQPl5v2oIlS+hIxsrb5LxV4VjCyE2gJCFFE5PQu+0Zbxse2ZIofrNv5QKs0TYUDTmxPhZyTr9Qtjnq2gsVQxWHXzrbebv3Z7RbzB63u6Ymn3Fo8IbDev3CfCNcNuxCKltFEXLqSCjI2vqNY+7HZkgQBIqy2wMgzli3aSLq0w8lWYtZzyyot7q8RPXWMGTfaBUo2reY0SOSffm9rAivE9PszNfPid71CvNrGAAoxRbwb25eVujlyIcDVWe5vZ9Iw==", "domain": ".my.medevio.cz", "path": "/", "expires": 1761125920, "httpOnly": false, "secure": true, "sameSite": "Lax"}], "origins": [{"origin": "https://my.medevio.cz", "localStorage": [{"name": "awswaf_token_refresh_timestamp", "value": "1760780309860"}, {"name": "awswaf_session_storage", "value": "b6a1d4eb-4350-40e5-8e52-1f5f9600fbb8:CgoAr9pC8c+zAAAA:+vw//1NzmePjPpbGCJzUB+orCRivtJd098DbDX4AnABiGRw/+ql6ShqvFY4YdCY7w2tegb5mEPBdAmc4sNi22kNR9BuEoAgCUiMhkU1AZWfzM51zPfTh7SveCrREZ7xdvxcqKPMmfVLRYX5E4+UWh22z/LKQ7+d9VERp3J+wWCUW3dFFirkezy3N7b2FVjTlY/RxsZwhejQziTG/L3CkIFFP3mOReNgBvDpj7aKoM1knY4IL4TZ8E7zNv3nTsvzACLYvnUutVOUcofN1TfOzwZshSKsEXsMzrQn8PzLccX1jM5VSzce7gfEzl0zSPsT8NB3Sna+rhMIttDNYgvbW1HsfG2LIeKMR27Zf8hkslDRVVkcU/Kp2jLOEdhhrBKGjKY2o9/uX3NExdzh5MEKQSSRtmue01BpWYILPH23rMsz4YSmF+Ough5OeQoC95rkcYwVXMhwvUN9Zfp9UZ4xCNfFUex5dOrg9aJntYRnaceeocGUttNI5AdT0i3+osV6XHXzKxeqO8zLCS9BIsCzxaHfdqqem5DorMceuGKz+QqksatIQAA=="}, {"name": "Application.Intl.locale", "value": "cs"}, {"name": "Password.prefill", "value": "{\"username\":\"vladimir.buzalka@buzalka.cz\",\"type\":\"email\"}"}]}]}
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Sync open requests: checks each request marked as open in MySQL (doneAt IS NULL
AND removedAt IS NULL) against the Medevio API. If the API shows the request is
closed (doneAt) or removed (removedAt), updates MySQL accordingly.
"""
import json
import sys
import time
import requests
import pymysql
from pathlib import Path
from datetime import datetime
# ==============================
# UTF-8 output (Windows friendly)
# ==============================
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except AttributeError:
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
# ==============================
# DRY RUN - set to True to only print what would be updated, False to actually update
# ==============================
DRY_RUN = False
# ==============================
# CONFIG
# ==============================
GRAPHQL_URL = "https://api.medevio.cz/graphql"
CLINIC_SLUG = "mudr-buzalkova"
TOKEN_PATH = Path(__file__).resolve().parent.parent / "token.txt"
gateway_token = TOKEN_PATH.read_text(encoding="utf-8").strip()
headers = {
"content-type": "application/json",
"authorization": f"Bearer {gateway_token}",
"origin": "https://my.medevio.cz",
"referer": "https://my.medevio.cz/",
}
DB_CONFIG = {
"host": "192.168.1.76",
"port": 3306,
"user": "root",
"password": "Vlado9674+",
"database": "medevio",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
GRAPHQL_QUERY = """
query GetPatientRequest2($requestId: UUID!, $clinicSlug: String!) {
request: getPatientRequest2(patientRequestId: $requestId, clinicSlug: $clinicSlug) {
id
doneAt
removedAt
updatedAt
}
}
"""
def fix_datetime(dt_str):
if not dt_str:
return None
try:
return datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
except Exception:
return None
def fetch_request(request_id):
payload = {
"operationName": "GetPatientRequest2",
"query": GRAPHQL_QUERY,
"variables": {
"requestId": request_id,
"clinicSlug": CLINIC_SLUG,
},
}
for attempt in range(3):
try:
r = requests.post(GRAPHQL_URL, json=payload, headers=headers, timeout=30)
break
except (requests.ConnectionError, requests.Timeout, requests.exceptions.RequestException) as e:
print(f" ⚠️ Attempt {attempt+1}/3 failed: {e}")
time.sleep(2)
else:
print(f" ❌ Connection failed after 3 attempts for {request_id}")
return None
if r.status_code != 200:
print(f" ❌ HTTP {r.status_code} for {request_id}")
return None
data = r.json()
if "errors" in data:
print(f" ❌ API error for {request_id}: {data['errors']}")
return None
return data.get("data", {}).get("request")
# ==============================
# MAIN
# ==============================
conn = pymysql.connect(**DB_CONFIG)
# 1) Read all open requests from MySQL
with conn.cursor() as cur:
cur.execute(
"SELECT id, displayTitle, pacient_prijmeni, pacient_jmeno "
"FROM pozadavky WHERE doneAt IS NULL AND removedAt IS NULL"
)
open_requests = cur.fetchall()
mode = "DRY RUN" if DRY_RUN else "LIVE"
print(f"🔧 Mode: {mode}")
print(f"📋 Found {len(open_requests)} open requests in MySQL.\n")
updated = 0
errors = 0
for i, req in enumerate(open_requests, 1):
rid = req["id"]
name = f"{req.get('pacient_prijmeni', '')} {req.get('pacient_jmeno', '')}".strip()
title = req.get("displayTitle", "")
print(f"[{i}/{len(open_requests)}] {name} {title} ({rid})")
api_data = fetch_request(rid)
if api_data is None:
errors += 1
continue
api_done = api_data.get("doneAt")
api_removed = api_data.get("removedAt")
api_updated = api_data.get("updatedAt")
if api_done or api_removed:
done_dt = fix_datetime(api_done)
removed_dt = fix_datetime(api_removed)
updated_dt = fix_datetime(api_updated)
status = "DONE" if api_done else "REMOVED"
if DRY_RUN:
print(f" 🔍 Would update → {status} (doneAt={api_done}, removedAt={api_removed})")
else:
with conn.cursor() as cur:
cur.execute(
"UPDATE pozadavky SET doneAt = %s, removedAt = %s, updatedAt = %s WHERE id = %s",
(done_dt, removed_dt, updated_dt, rid),
)
conn.commit()
print(f" ✅ Updated → {status}")
updated += 1
else:
print(f" ⏳ Still open")
# Be gentle with the API
time.sleep(1)
conn.close()
print(f"\n{'='*50}")
print(f"📊 Total open in MySQL: {len(open_requests)}")
print(f"✅ Updated (closed/removed): {updated}")
print(f"⏳ Still open: {len(open_requests) - updated - errors}")
print(f"❌ Errors: {errors}")
@@ -0,0 +1,46 @@
import os
from pathlib import Path
# Define the target directory
target_path = Path(r"U:\Dropbox\Ordinace\Dokumentace_ke_zpracování\MP")
def rename_folders():
# Ensure the path exists
if not target_path.exists():
print(f"Error: The path {target_path} does not exist.")
return
# Iterate through items in the directory
for folder in target_path.iterdir():
# Only process directories
if folder.is_dir():
original_name = folder.name
# Check if name starts with the triangle
if original_name.startswith(""):
# 1. Remove the triangle from the start
name_without_tri = original_name[1:]
# 2. Prepare the name to be at least 10 chars long
# (so the triangle can sit at index 10 / position 11)
clean_name = name_without_tri.ljust(10)
# 3. Construct new name: first 10 chars + triangle + the rest
new_name = clean_name[:10] + "" + clean_name[10:]
# Remove trailing spaces if the original name was short
# but you don't want extra spaces at the very end
new_name = new_name.rstrip()
new_folder_path = folder.parent / new_name
try:
print(f"Renaming: '{original_name}' -> '{new_name}'")
folder.rename(new_folder_path)
except Exception as e:
print(f"Could not rename {original_name}: {e}")
if __name__ == "__main__":
rename_folders()
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Compress PDF — output DPI and JPEG quality are chosen automatically
based on the detected resolution of the source PDF.
Usage: python compress_pdf.py <input.pdf> [output.pdf]
python compress_pdf.py (processes all PDFs in current folder)
Output filename: original_name (139 kB).pdf
"""
import sys
import fitz
from pathlib import Path
# ==============================
# COMPRESSION TABLE
# Detected source DPI -> (output DPI, JPEG quality)
# Rows are evaluated top-to-bottom; first match wins.
# ==============================
#
# src_dpi_min src_dpi_max out_dpi jpeg_quality
COMPRESSION_TABLE = [
( 0, 99, 72, 60), # very low res — already small, compress hard
( 100, 149, 100, 70), # low res
( 150, 249, 150, 80), # standard scan (our tested sweet spot)
( 250, 399, 150, 80), # good scan — downsample to 150 is fine
( 400, 599, 200, 85), # high res scan
( 600, 9999, 150, 80), # very high res / professional scan
]
def detect_source_dpi(src: fitz.Document) -> int:
"""Estimate source DPI from the largest image on the first page."""
page = src[0]
images = page.get_images(full=True)
if not images:
return 150 # no raster images — use default
# Find the largest image by pixel area
best = max(images, key=lambda img: img[2] * img[3]) # width * height
img_w_px, img_h_px = best[2], best[3]
# Page size in inches (1 point = 1/72 inch)
page_w_in = page.rect.width / 72.0
page_h_in = page.rect.height / 72.0
dpi_x = img_w_px / page_w_in if page_w_in else 0
dpi_y = img_h_px / page_h_in if page_h_in else 0
return round((dpi_x + dpi_y) / 2)
def pick_settings(source_dpi: int) -> tuple[int, int]:
for min_dpi, max_dpi, out_dpi, quality in COMPRESSION_TABLE:
if min_dpi <= source_dpi <= max_dpi:
return out_dpi, quality
# fallback to last row
return COMPRESSION_TABLE[-1][2], COMPRESSION_TABLE[-1][3]
def compress(input_path: Path, output_path: Path = None):
src = fitz.open(input_path)
source_dpi = detect_source_dpi(src)
out_dpi, jpeg_quality = pick_settings(source_dpi)
print(f" zdroj ~{source_dpi} DPI -> komprese {out_dpi} DPI / JPEG q{jpeg_quality}")
zoom = out_dpi / 72.0
mat = fitz.Matrix(zoom, zoom)
out_doc = fitz.open()
for page in src:
pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB)
img_bytes = pix.tobytes("jpeg", jpg_quality=jpeg_quality)
img_doc = fitz.open("pdf", fitz.open("jpeg", img_bytes).convert_to_pdf())
rect = page.rect
new_page = out_doc.new_page(width=rect.width, height=rect.height)
new_page.show_pdf_page(new_page.rect, img_doc, 0)
src.close()
tmp = input_path.with_suffix(".tmp.pdf")
out_doc.save(tmp, deflate=True, garbage=4)
out_doc.close()
size_kb = round(tmp.stat().st_size / 1024)
if output_path is None:
output_path = input_path.parent / f"{input_path.stem} ({size_kb} kB).pdf"
if output_path.exists():
output_path.unlink()
tmp.rename(output_path)
orig_kb = round(input_path.stat().st_size / 1024)
saving = (1 - size_kb / orig_kb) * 100
print(f" {input_path.name} -> {output_path.name} (bylo {orig_kb} kB, uspora {saving:.0f}%)")
if __name__ == "__main__":
if len(sys.argv) >= 2:
inp = Path(sys.argv[1])
out = Path(sys.argv[2]) if len(sys.argv) >= 3 else None
compress(inp, out)
else:
folder = Path(__file__).parent
pdfs = [p for p in folder.glob("*.pdf") if not p.name.endswith(").pdf") and p.stem != Path(__file__).stem]
if not pdfs:
print("Zadne PDF k zpracovani.")
for pdf in pdfs:
compress(pdf)
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Compress a PDF into multiple variants at different DPI / JPEG quality settings.
Uses PyMuPDF (fitz) — renders each page as JPEG image, saves back as PDF.
"""
import sys
import fitz # PyMuPDF
from pathlib import Path
INPUT = Path(r"u:\Medevio\50 Různé testy\MinimizeOptimizePDF\afd1823b-8277-44a2-84e1-db89a0ccd134.pdf")
OUT_DIR = INPUT.parent
VARIANTS = [
# (label, dpi, jpeg_quality)
("300dpi_q90", 300, 90),
("200dpi_q85", 200, 85),
("150dpi_q80", 150, 80),
("120dpi_q75", 120, 75),
("96dpi_q70", 96, 70),
("72dpi_q60", 72, 60),
]
src = fitz.open(INPUT)
original_size = INPUT.stat().st_size
print(f"Originál: {INPUT.name} ({original_size / 1024:.0f} KB)\n")
print(f"{'Varianta':<20} {'DPI':>5} {'Kvalita':>8} {'Velikost':>12} {'Úspora':>8}")
print("-" * 58)
for label, dpi, quality in VARIANTS:
out_path = OUT_DIR / f"{INPUT.stem}_{label}.pdf"
zoom = dpi / 72.0
mat = fitz.Matrix(zoom, zoom)
out_doc = fitz.open()
for page in src:
pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB)
img_bytes = pix.tobytes("jpeg", jpg_quality=quality)
# Create a new PDF page with the same physical dimensions
img_doc = fitz.open("pdf", fitz.open("jpeg", img_bytes).convert_to_pdf())
# Scale page back to original size
rect = page.rect
new_page = out_doc.new_page(width=rect.width, height=rect.height)
new_page.show_pdf_page(new_page.rect, img_doc, 0)
out_doc.save(out_path, deflate=True, garbage=4)
out_doc.close()
size = out_path.stat().st_size
size_kb = round(size / 1024)
final_path = OUT_DIR / f"{INPUT.stem}_{label} ({size_kb} kB).pdf"
out_path.rename(final_path)
saving = (1 - size / original_size) * 100
print(f"{label:<20} {dpi:>5} {quality:>8} {size_kb:>9} kB {saving:>7.0f}%")
src.close()
print("\nHotovo.")
+46
View File
@@ -0,0 +1,46 @@
# 60 ScansProcessing
Agent pro zpracování naskenovaných lékařských zpráv (PDF i JPG/PNG).
## Skripty
### `extract_patient_info.py` — hlavní agent
Spuštění: `python extract_patient_info.py` (bez argumentů = celá složka ToProcess)
**Workflow:**
1. Načte soubory z `ToProcess/`
2. Claude Vision API (sonnet-4-6) extrahuje: jméno, RČ, datum, typ dokumentu, poznámku, navržený název, rotaci
3. Ověří pacienta v Medicus Firebird (tabulka KAR, pole RODCIS/PRIJMENI/JMENO)
4. Fuzzy matching RČ při nenalezení: vynechání cifry + záměna podobných (0↔8, 1↔7, 5↔6, 3↔8) + checksum /11
5. Upozorní na duplicitu v `U:\Dropbox\Ordinace\Dokumentace_zpracovaná\`
6. Interaktivní schválení / oprava názvu
7. JPG/PNG → skutečné PDF (správná orientace, DPI=150, quality=80)
8. Přesun do `Processed/`, smazání z `ToProcess/`
9. Opravy názvů se ukládají do `corrections.json` jako few-shot příklady
**Formát názvu souboru:**
`{RČ} {YYYY-MM-DD} {Příjmení}, {Jméno} [{typ dokumentu}] [{poznámka}].pdf`
Příklady typů: `LZ chirurgie`, `LZ kardiologie`, `Laboratoř`, `CT břicha`, `kolonoskopie`, `poukaz FT`
### `jpg_to_pdf.py` — konverze obrázku na PDF
```
python jpg_to_pdf.py soubor.jpg [vystup.pdf] [rotace_ccw]
```
- Opravuje EXIF orientaci
- Rotace: 0 / 90 / 180 / 270 (CCW)
- A4, DPI=150, quality=80, bez okrajů
- Používá se i interně z `extract_patient_info.py`
## Složky
| Složka | Účel |
|---|---|
| `ToProcess/` | Sem se házejí nové skeny (PDF, JPG, PNG) |
| `Processed/` | Správně pojmenované PDF po schválení |
| `U:\Dropbox\Ordinace\Dokumentace_zpracovaná\` | Finální archiv |
## Konfigurace
- API klíč: `U:\Medevio\.env``ANTHROPIC_API_KEY`
- Medicus: `localhost:c:\medicus 3\data\medicus.fdb` (Firebird, SYSDBA)
- Few-shot korekce: `corrections.json`
@@ -0,0 +1,577 @@
"""
Agent pro extrakci a pojmenování naskenovaných PDF lékařských zpráv.
- Claude Vision API — bez OCR, správná čeština s diakritikou
- Ověření pacienta proti Medicus (KAR), fuzzy matching RČ
- Interaktivní schválení / oprava názvu
- Few-shot learning z uložených korekcí
"""
import base64
import gc
import io
import json
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
# Windows: nastav stdout/stderr na UTF-8
if sys.platform == "win32":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
import anthropic
from pdf2image import convert_from_path
sys.path.insert(0, str(Path(__file__).parent.parent))
from Knihovny.najdi_dropbox import get_dropbox_root
from Knihovny.najdi_medicus import get_medicus_config
POPPLER_PATH = r"C:/Poppler/Library/bin"
CORRECTIONS_FILE = Path(__file__).parent / "corrections.json"
_DROPBOX = Path(get_dropbox_root())
TO_PROCESS = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\KeZpracování"
PROCESSED = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\Zpracováno"
DOKUMENTACE = _DROPBOX / r"Ordinace\Dokumentace_zpracovaná"
# ─── Konfigurace ──────────────────────────────────────────────────────────────
def _load_env():
env_path = Path(__file__).parent.parent / ".env"
if env_path.exists():
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
os.environ[k.strip()] = v.strip()
_load_env()
# ─── Korekce (few-shot příklady) ──────────────────────────────────────────────
def load_corrections() -> list[dict]:
if CORRECTIONS_FILE.exists():
return json.loads(CORRECTIONS_FILE.read_text(encoding="utf-8"))
return []
def save_correction(original: str, corrected: str):
corrections = load_corrections()
for c in corrections:
if c["original"] == original and c["corrected"] == corrected:
return
corrections.append({"original": original, "corrected": corrected})
CORRECTIONS_FILE.write_text(
json.dumps(corrections, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f" ✓ Korekce uložena ({len(corrections)} celkem)")
def build_corrections_prompt() -> str:
corrections = load_corrections()
if not corrections:
return ""
lines = ["Příklady korekcí z minulých běhů (uč se z nich):"]
for c in corrections[-10:]:
lines.append(f' - špatně: "{c["original"]}"')
lines.append(f' správně: "{c["corrected"]}"')
return "\n".join(lines) + "\n\n"
# ─── Kontrola duplicit ───────────────────────────────────────────────────────
def check_duplicates(rc: str, datum: str) -> list[str]:
"""
Hledá v Dokumentace_zpracovaná soubory se stejným RČ a datem.
Vrátí seznam názvů nalezených souborů.
"""
if not DOKUMENTACE.exists():
return []
prefix = f"{rc} {datum}"
return [f.name for f in DOKUMENTACE.iterdir() if f.name.startswith(prefix)]
# ─── Medicus ověření ──────────────────────────────────────────────────────────
def _medicus_connect():
try:
import fdb
cfg = get_medicus_config()
return fdb.connect(
dsn=cfg.dsn,
user="SYSDBA", password="masterkey", charset="win1250"
)
except Exception as e:
print(f" [Medicus] Nepřipojeno: {e}")
return None
def _lookup_by_rc(cur, rc_digits: str) -> dict | None:
"""Přesné vyhledání podle RČ (bez lomítka)."""
cur.execute(
"SELECT IDPAC, PRIJMENI, JMENO, RODCIS FROM KAR "
"WHERE REPLACE(RODCIS, '/', '') = ?",
(rc_digits,)
)
row = cur.fetchone()
if row:
return {"idpac": row[0], "prijmeni": row[1].strip(), "jmeno": row[2].strip(), "rodcis": row[3].strip()}
return None
def _rc_candidates(rc: str) -> list[str]:
"""
Generuje kandidáty RČ pro fuzzy matching:
- vynechání každé cifry (OCR přečetlo znak navíc)
- vložení nuly na každou pozici (OCR přehlédlo nulu v sekvenci 00)
- záměna podobně vypadajících číslic na každé pozici
Vrátí unikátní seznam kandidátů bez původního RČ.
"""
similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"}
candidates = set()
# Vynechání jedné cifry (OCR přečetlo znak navíc)
for i in range(len(rc)):
candidates.add(rc[:i] + rc[i+1:])
# Vložení nuly na každou pozici (nejčastější chyba: sekvence 00 přečtena jako 0)
for i in range(len(rc) + 1):
candidates.add(rc[:i] + "0" + rc[i:])
# Záměna podobné cifry na každé pozici
for i, ch in enumerate(rc):
if ch in similar:
candidates.add(rc[:i] + similar[ch] + rc[i+1:])
candidates.discard(rc)
candidates = {c for c in candidates if len(c) in (9, 10)}
return sorted(candidates)
def _rc_checksum_ok(rc: str) -> bool:
"""Ověří dělitelnost 11 pro 10místná RČ (platí pro narozené po 1.1.1954)."""
digits = re.sub(r"\D", "", rc)
if len(digits) == 10:
return int(digits) % 11 == 0
return True # 9místná RČ nemají checksum
def verify_patient(rc_raw: str) -> dict:
"""
Ověří pacienta v Medicus.
Vrací:
status: "ok" | "fuzzy" | "not_found" | "offline"
patient: dict nebo None
rc_corrected: opravené RČ (pokud fuzzy) nebo None
"""
rc = re.sub(r"\D", "", rc_raw or "")
if not rc:
return {"status": "not_found", "patient": None, "rc_corrected": None}
con = _medicus_connect()
if con is None:
return {"status": "offline", "patient": None, "rc_corrected": None}
try:
cur = con.cursor()
# 1. Přesná shoda
patient = _lookup_by_rc(cur, rc)
if patient:
return {"status": "ok", "patient": patient, "rc_corrected": None}
# 2. Fuzzy matching — zkus kandidáty, preferuj ty s platným checksumem
candidates = _rc_candidates(rc)
matches = []
for cand in candidates:
p = _lookup_by_rc(cur, cand)
if p:
matches.append((cand, p))
if not matches:
return {"status": "not_found", "patient": None, "rc_corrected": None}
# Seřaď: platný checksum na prvním místě
matches.sort(key=lambda x: (0 if _rc_checksum_ok(x[0]) else 1))
best_rc, best_patient = matches[0]
return {"status": "fuzzy", "patient": best_patient, "rc_corrected": best_rc, "all_matches": matches}
finally:
con.close()
# ─── PDF → obrázek ────────────────────────────────────────────────────────────
def pdf_to_images(pdf_path: str) -> list:
return convert_from_path(pdf_path, poppler_path=POPPLER_PATH, dpi=300)
def image_to_base64(image) -> str:
buf = io.BytesIO()
image.save(buf, format="JPEG", quality=95)
return base64.standard_b64encode(buf.getvalue()).decode("utf-8")
# ─── Extrakce Claude Vision ───────────────────────────────────────────────────
def extract_patient_info(pdf_path: str) -> dict:
pdf_path = Path(pdf_path)
if not pdf_path.exists():
raise FileNotFoundError(f"Soubor nenalezen: {pdf_path}")
print(f"\nNačítám: {pdf_path.name}")
suffix = pdf_path.suffix.lower()
if suffix in (".jpg", ".jpeg", ".png"):
from PIL import Image
img = Image.open(pdf_path)
image_b64 = image_to_base64(img)
img.close()
else:
images = pdf_to_images(str(pdf_path))
image_b64 = image_to_base64(images[0])
del images
gc.collect()
prompt = (
build_corrections_prompt() +
"Toto je naskenovaná lékařská zpráva v češtině. "
"Vrať JSON s těmito poli:\n"
"- \"jmeno\": celé jméno pacienta (příjmení + jméno + případný titul)\n"
"- \"rodne_cislo\": rodné číslo pacienta BEZ lomítka (pouze číslice)\n"
"- \"datum_zpravy\": datum zprávy ve formátu YYYY-MM-DD\n"
"- \"typ_dokumentu\": typ dokumentu — "
"\"LZ {oddělení}\" = ambulantní/lékařská zpráva (např. \"LZ chirurgie\", \"LZ kardiologie\", \"LZ plicní\", \"LZ ORL\"); "
"\"PZ {oddělení}\" = propouštěcí zpráva z hospitalizace (např. \"PZ interna\", \"PZ neurologie\"). "
"Jiné typy: \"Laboratoř\", \"CT břicha\", \"MRI páteře\", \"kolonoskopie\", "
"\"operační protokol oční\", \"poukaz FT\", \"diagnostická mamografie\" atd.\n"
"- \"poznamka\": krátká klinická poznámka česky, max 80 znaků. "
"DŮLEŽITÉ: pokud zpráva obsahuje sekci \"Závěr:\" nebo \"Závěr vyšetření:\", "
"použij VÝHRADNĚ obsah této sekce — je nejdůležitější. "
"Teprve pokud závěr chybí, shrň obsah z celé zprávy.\n"
"- \"nazev_souboru\": název souboru ve formátu "
"\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" "
"(jméno bez titulu, RČ bez lomítka)\n"
"- \"rotace\": o kolik stupňů CCW je třeba otočit obrázek aby byl text čitelně na výšku nebo šířku "
"(hodnoty: 0, 90, 180, 270). Pokud je text již správně orientovaný, vrať 0.\n\n"
"Pokud pole nenajdeš, použij null. Nepiš nic jiného než JSON."
)
print(" Volám Claude Vision API...")
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=400,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
{"type": "text", "text": prompt},
],
}],
)
usage = response.usage
cost_input = usage.input_tokens * 3 / 1_000_000
cost_output = usage.output_tokens * 15 / 1_000_000
print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${cost_input + cost_output:.4f}")
raw = response.content[0].text.strip()
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
try:
return json.loads(raw.strip())
except json.JSONDecodeError:
print(f" VAROVÁNÍ: nelze parsovat JSON: {raw!r}")
return {"nazev_souboru": None, "raw": raw}
# ─── Interaktivní schválení ───────────────────────────────────────────────────
def sanitize_filename(name: str) -> str:
return re.sub(r'[<>:"/\\|?*]', '', name)
def _open_preview(root, pdf_path: Path):
"""Otevře náhledové okno PDF/obrázku jako Toplevel. Pracuje s temp kopií — žádné zamykání originálu."""
import tkinter as tk
import tempfile
import shutil as _shutil
try:
from PIL import Image, ImageTk
import fitz
except ImportError:
return
# Temp kopie — prohlížeč nikdy nesahá na originál
tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix))
_shutil.copy2(pdf_path, tmp)
suffix = pdf_path.suffix.lower()
if suffix in (".jpg", ".jpeg", ".png"):
pil_pages = [Image.open(tmp)]
doc = None
else:
try:
doc = fitz.open(str(tmp))
except Exception:
tmp.unlink(missing_ok=True)
return
pil_pages = []
def render(n) -> Image.Image:
if doc is not None:
page = doc[n]
zoom = min(700 / page.rect.width, (sh - 150) / page.rect.height)
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
else:
img = pil_pages[0].copy()
img.thumbnail((700, sh - 150), Image.LANCZOS)
return img
def on_close():
try:
if doc:
doc.close()
except Exception:
pass
tmp.unlink(missing_ok=True)
win.destroy()
page_count = len(doc) if doc else 1
sh = root.winfo_screenheight()
current = [0]
photo_ref = [None]
win = tk.Toplevel(root)
win.title(pdf_path.name)
win.attributes("-topmost", True)
win.resizable(False, False)
win.protocol("WM_DELETE_WINDOW", on_close)
lbl_img = tk.Label(win)
lbl_img.pack()
frame_nav = tk.Frame(win)
frame_nav.pack(pady=4)
lbl_page = tk.Label(frame_nav, font=("Segoe UI", 9))
lbl_page.pack(side="left", padx=10)
def show(n):
current[0] = n
img = render(n)
photo_ref[0] = ImageTk.PhotoImage(img)
lbl_img.config(image=photo_ref[0])
lbl_page.config(text=f"Strana {n + 1} / {page_count}")
btn_prev.config(state="normal" if n > 0 else "disabled")
btn_next.config(state="normal" if n < page_count - 1 else "disabled")
btn_prev = tk.Button(frame_nav, text="◄ Předchozí",
command=lambda: show(current[0] - 1))
btn_prev.pack(side="left")
btn_next = tk.Button(frame_nav, text="Další ►",
command=lambda: show(current[0] + 1))
btn_next.pack(side="left")
show(0)
win.update_idletasks()
win.geometry(f"+0+0")
def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None:
"""
Spustí rename_dialog.py jako subprocess — vyhneme se Tkinter konfliktům s PyCharm.
Vrátí finální název (s .pdf) nebo None = přeskočit.
"""
import tempfile
data = {"nazev": nazev, "info_lines": info_lines}
tmp = Path(tempfile.mktemp(suffix=".json"))
tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
dialog_script = Path(__file__).parent / "rename_dialog.py"
try:
proc = subprocess.run(
[sys.executable, str(dialog_script), str(tmp)],
capture_output=True, text=True, encoding="utf-8",
)
output = proc.stdout.strip()
if output:
return json.loads(output).get("value")
return None
finally:
tmp.unlink(missing_ok=True)
def print_verification(verif: dict, rc_from_scan: str):
"""Vypíše výsledek ověření proti Medicus."""
status = verif["status"]
patient = verif.get("patient")
if status == "ok":
print(f" ✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
elif status == "fuzzy":
rc_corr = verif["rc_corrected"]
print(f" ⚠ Medicus: RČ ze skenu '{rc_from_scan}' nenalezeno")
print(f" → Nalezen podobný pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
print(f" → Pravděpodobná oprava RČ: {rc_from_scan}{rc_corr} (OCR chyba)")
if len(verif.get("all_matches", [])) > 1:
print(f" → Další shody: {[m[0] for m in verif['all_matches'][1:]]}")
elif status == "not_found":
print(f" ✗ Medicus: RČ '{rc_from_scan}' nenalezeno ani při fuzzy hledání")
elif status == "offline":
print(f" — Medicus: nedostupný (offline), ověření přeskočeno")
def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool:
"""
Otevře tkinter dialog pro schválení / opravu názvu.
Schválený soubor přesune do Processed/ a smaže z ToProcess/.
"""
rc = re.sub(r"\D", "", verif["patient"]["rodcis"] if verif.get("patient") else info.get("rodne_cislo") or "")
datum = info.get("datum_zpravy") or ""
duplicity = check_duplicates(rc, datum)
# Oprava RČ při fuzzy matchi
nazev = info.get("nazev_souboru")
if verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev:
rc_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
nazev = nazev.replace(rc_scan, verif["rc_corrected"], 1)
print(f" → Název aktualizován s opraveným RČ")
# Sestavení info řádků pro dialog
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
status = verif["status"]
patient = verif.get("patient")
info_lines = []
if status == "ok":
info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
elif status == "fuzzy":
info_lines.append(f"⚠ RČ ze skenu '{rc_from_scan}' → opraveno na {verif['rc_corrected']}")
info_lines.append(f" Pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
elif status == "not_found":
info_lines.append(f"✗ RČ '{rc_from_scan}' nenalezeno v Medicus")
else:
info_lines.append("— Medicus nedostupný (offline)")
if duplicity:
info_lines.append(f"⚠ DUPLICITA: {', '.join(duplicity)}")
print()
print("" * 70)
if nazev:
print(f" Navržený název: {nazev}")
print(" Otevírám dialog...")
odpoved = _rename_dialog(nazev or "", info_lines)
if odpoved is None:
print(" Přeskočeno.")
return False
if not odpoved.endswith(".pdf"):
odpoved += ".pdf"
final_name = sanitize_filename(odpoved)
if nazev and nazev != final_name:
save_correction(nazev, final_name)
if not final_name or final_name == ".pdf":
print(" Název je prázdný, přeskakuji.")
return False
dest = PROCESSED / final_name
if dest.exists():
print(f" VAROVÁNÍ: '{final_name}' již existuje v Processed, přeskakuji.")
return False
if pdf_path.suffix.lower() in (".jpg", ".jpeg", ".png"):
from jpg_to_pdf import image_to_pdf
image_to_pdf(pdf_path, dest, rotate_ccw=info.get("rotace") or 0)
else:
shutil.copy2(pdf_path, dest)
pdf_path.unlink()
print(f" ✓ Uloženo: Processed/{final_name}")
return True
# ─── Hlavní logika ────────────────────────────────────────────────────────────
def _start_preview_process(pdf_path: Path):
"""
Otevře náhled PDF jako samostatný subprocess (žádné tkinter threading problémy).
Pracuje s temp kopií — originál zůstane volný.
Vrátí funkci close() pro ukončení procesu.
"""
import tempfile
import shutil as _shutil
tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix))
_shutil.copy2(pdf_path, tmp)
viewer = Path(__file__).parent / "preview_viewer.py"
proc = subprocess.Popen(
[sys.executable, str(viewer), str(tmp), "--delete-on-close"],
)
def close():
try:
proc.terminate()
proc.wait(timeout=3)
except Exception:
pass
try:
tmp.unlink(missing_ok=True)
except Exception:
pass
return close
def process_file(pdf_path: Path):
close_preview = _start_preview_process(pdf_path)
try:
info = extract_patient_info(str(pdf_path))
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...")
verif = verify_patient(rc_from_scan)
print_verification(verif, rc_from_scan)
interactive_rename(pdf_path, info, verif)
finally:
close_preview()
def process_folder(folder: Path):
pdf_files = sorted(f for f in folder.iterdir()
if f.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png"))
if not pdf_files:
print(f"Žádná PDF nenalezena v: {folder}")
return
print(f"Nalezeno {len(pdf_files)} PDF soubor(ů).\n")
for pdf_file in pdf_files:
try:
process_file(pdf_file)
except Exception as e:
print(f" CHYBA: {e}")
print("\nHotovo.")
if __name__ == "__main__":
if len(sys.argv) > 1:
target = Path(sys.argv[1])
else:
target = TO_PROCESS
PROCESSED.mkdir(exist_ok=True)
TO_PROCESS.mkdir(exist_ok=True)
if target.is_file() and target.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png"):
process_file(target)
elif target.is_dir():
process_folder(target)
else:
print("Použití: python extract_patient_info.py [soubor.pdf nebo složka]")
sys.exit(1)

Some files were not shown because too many files have changed in this diff Show More