Compare commits
43 Commits
7a7c35f778
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 06b1f87107 | |||
| 15f70988dc | |||
| 0fe37c2434 | |||
| 7a4847e1cc | |||
| 4f13f075ff | |||
| a5a4b7c349 | |||
| 14accd3d78 | |||
| 4112b5d3d4 | |||
| ffb3db1e07 | |||
| 417cf86b2d | |||
| 194ac6c62e | |||
| eed6e192f1 | |||
| 804dce8794 | |||
| 371eed9971 | |||
| d013e43d34 | |||
| 88602cb406 | |||
| 1b904e3da0 | |||
| 2e929f1d77 | |||
| b58232b7d4 | |||
| daad4adeab | |||
| a9c143ba24 | |||
| a1b9c93506 | |||
| 3c3a12d5a6 | |||
| 4aee1a05bd | |||
| b1f246bc54 | |||
| 6cff5f1b91 | |||
| ef5d837f34 | |||
| 4c81529718 | |||
| c98001ae93 | |||
| 4f3c774469 | |||
| 7ec3fcedea | |||
| 47c4789a06 | |||
| 1f9d7bbe78 | |||
| 2447b4cf8e | |||
| 78ed84209c | |||
| 0bfa9c48e4 | |||
| 718d27aad5 | |||
| e2c61eddb9 | |||
| 9812d48ce9 | |||
| c29ff51209 | |||
| add3b46223 | |||
| 5785ceecbc | |||
| 365fcd16ba |
@@ -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) |
|
||||
|
||||
@@ -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()
|
||||
Binary file not shown.
@@ -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">
|
||||
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()
|
||||
|
||||
+8
-14
@@ -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)
|
||||
+1
-1
@@ -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 — {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 — {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 — {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} | {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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ANTHROPIC_API_KEY=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
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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!")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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\"}"}]}]}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
+261
@@ -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.")
|
||||
@@ -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
Reference in New Issue
Block a user