notebookvb

This commit is contained in:
Vladimir Buzalka
2026-06-18 05:32:36 +02:00
parent 0beaffec45
commit 19036b58cc
11 changed files with 1662 additions and 2 deletions
@@ -0,0 +1,87 @@
# VZP (111) — Stahování seznamu registrovaných pojištěnců
## Co skript dělá
`StahniSeznamPojistencuVZP.py` (Playwright + Chrome):
1. **Přihlásí se** certifikátem na VZP Point (auto-výběr cert z Windows store)
2. Projde **ODESLANÁ PODÁNÍ** (řazeno od nejnovějšího) a najde podání typu
„Seznam registrovaných pojištěnců"
3. Stahuje **přiložené datové dávky** `F111MMRR.nnn` (CP852) do
`…\Zúčtovací zprávy\SeznamyPojištěnců\` od nejnovějšího a **zastaví se na první
už stažené dávce** (inkrementálně — starší jsou stažené, nejde hluboko do minulosti).
4. **Podá novou žádost** o výpis (datové rozhraní) za nejnovější dostupné období
(zjištěno z configu) — výsledek dorazí do ODESLANÝCH PODÁNÍ a stáhne se příště.
Dávky pak zpracovává `Insurance/SeznamPojistencu/01_parse_seznam_dg_tool.py`.
## Platforma — ODLIŠNÁ
VZP běží na **point.vzp.cz** (VZP Point), NE portalzp.cz ani eforms. Login je
certifikátem přes Chrome — politika `AutoSelectCertificateForUrls` vybere cert
automaticky (issuer `I.CA Public CA/RSA 06/2022`), bez NMSigneru. Plně Playwright.
## Jak se seznam získává
VZP seznam **není** samočinná zpráva — musí se **požádat podáním**:
- NOVÉ PODÁNÍ → „Seznam registrovaných pojištěnců ke dni"
- **Formát výstupu = „Datové rozhraní"** (NE „PDF"!) + období (měsíc/rok)
- VZP požadavek zpracuje (~minuty) a výsledek = datová dávka III-1.1.2,
stažitelná z detailu zpracovaného podání (sloupec „Přiložený soubor").
> Pozn.: pokud se zvolí formát „PDF", výsledkem je PDF (p…pdf), které parser neumí.
> Vždy volit „Datové rozhraní".
## Formát dávky (III-1.1.2)
Soubor `F111MMRR.nnn`, pevná šířka, **CP852**. Hlavička typ H:
`H09305001` (IČP) + počet + RRMMDD. Věty typu I: příjmení, jméno, číslo poj.,
datum registrace, kód pojišťovny. (Detaily v `SeznamPojistencu/01_parse_seznam_dg_tool.py`.)
## Stažení dávky z detailu podání
Detail `/Desk/Form/Detail/{id}` → záložka „Výsledky zpracování" → odkaz s názvem
`F111MMRR.nnn` (href="#", JS handler). Stahuje se Playwright klikem
(`expect_download` + `dispatch_event('click')`) — žádná přímá URL.
## Podání žádosti (REST API — bez podpisu!)
Podání jde čistě přes REST API Pointu (Bearer token z inline `"bearerToken"` na dashboardu),
**žádný elektronický podpis** — autentizace stačí přes session + token. Tři kroky:
1. **Config** (zjištění období): `GET /api/desk/draft/form65/config`
`periodLimits {from, until}` + `defaultModel.period {month, year}`.
Podává se za **nejnovější dostupné období** (`until` / `defaultModel`), ne za kalendářní
měsíc (ten portál odmítne — HTTP 400 při publish).
2. **Vytvoř koncept**: `POST /api/desk/draft/form65/{partnerId}`
body `{"outputFormat":"Text","period":{"month":M,"year":Y}}``{"draftId":"...","state":"Verified"}`
- `outputFormat:"Text"` = **Datové rozhraní** (NE "Pdf"!)
- partnerId = `3197807` (subjekt MUDr. Buzalková)
3. **Publikuj**: `POST /api/desk/draft/form65/{draftId}/publish` (prázdné tělo)
`{"formId": <id odeslaného podání>}`
Token se čte stejně jako v `StahováníZpráv/111 VZP/stahovanipodani.py`.
### Jak bylo zjištěno
Formulář Form65 je React SPA s custom comboboxem, který nešel proklikat headless ani
naslepo. Odchyceno tak, že uživatel podal jedno podání ručně a do stránky byl vložen
háček ukládající fetch/XHR do `localStorage` (přežije přesměrování) — z toho se vyčetly
přesné endpointy a payloady.
## Soubory
| Soubor | Popis |
|--------|-------|
| `StahniSeznamPojistencuVZP.py` | Login + stažení datových dávek z podání |
## Parametry
- **IČP**: 09305001, **IČZ**: 09305000 (MUDr. Michaela Buzalková)
- **Login**: certifikát ve Windows store (sdílený profil `StahováníZpráv/111 VZP/chrome_profile`)
## Stav
Hotovo a otestováno (17.06.2026): login ✓, backfill 23 dávek `F111….0NN` (všechny `H09305001`),
inkrementální běh zastaví na první už stažené dávce ✓, **podání žádosti přes REST API ✓**
(auto období z configu = 04/2026, create+publish → formId). Download i podání plně automatické.
@@ -0,0 +1,322 @@
"""
Stahování seznamu registrovaných pojištěnců VZP (111) — VZP Point (Playwright).
VZP běží na ODLIŠNÉ platformě (point.vzp.cz) — ne portalzp.cz, ne eforms:
- login: certifikát přes Chrome (auto-výběr z Windows store, politika
AutoSelectCertificateForUrls), Playwright. Bez NMSigneru.
- seznam: požaduje se podáním "Seznam registrovaných pojištěnců" s formátem
výstupu "Datové rozhraní". Výsledek = datová dávka III-1.1.2
(soubor F111MMRR.nnn, CP852, hlavička H09305001), stažitelná
z detailu zpracovaného podání.
Tento skript STAHUJE výsledky už zpracovaných podání "Seznam registrovaných
pojištěnců" (datová dávka) do složky SeznamyPojištěnců.
Podání žádosti (NOVÉ PODÁNÍ) zatím dělá uživatel ručně na portálu — viz NOTES.md.
Soubory dávek pak zpracovává Insurance/SeznamPojistencu/01_parse_seznam_dg_tool.py.
"""
import json
import os
import re
import sys
import time
import winreg
from pathlib import Path
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
from Knihovny.najdi_dropbox import get_dropbox_root
POINT_URL = "https://point.vzp.cz"
DASHBOARD_URL = f"{POINT_URL}/Desk/FormDashboard"
INBOX_URL = f"{POINT_URL}/Inbox/Message"
# Sdílené s VZP skriptem pro stahování zpráv
STAHUJ_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "StahováníZpráv", "111 VZP"))
CHROME_PROFILE = os.path.join(STAHUJ_DIR, "chrome_profile")
COOKIES_FILE = os.path.join(STAHUJ_DIR, "vzp_cookies.json")
DEST_DIR = os.path.join(
get_dropbox_root(),
"Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "SeznamyPojištěnců",
)
CERT_ISSUER_CN = "I.CA Public CA/RSA 06/2022"
# Název podání i přílohy
PODANI_NAZEV = "Seznam registrovaných pojištěnců"
DAVKA_RE = re.compile(r"^F\d{7}\.\d+$") # F111MMRR.nnn
# Podání žádosti (REST API, ověřeno odchytem)
PARTNER_ID = "3197807" # subjekt MUDr. Buzalková (partnerId z formuláře Form65)
OUTPUT_FORMAT = "Text" # "Text" = Datové rozhraní (NE "Pdf"!)
# Období podávané žádosti se zjistí automaticky z configu (nejnovější dostupné, viz
# config.defaultModel / periodLimits.until). Pro ruční přepsání nastav OVERRIDE_OBDOBI
# na (měsíc, rok), jinak ponech None.
OVERRIDE_OBDOBI: tuple[int, int] | None = None
# Kolikrát max. kliknout 'Načíst další' při hledání podání (dashboard míchá typy).
# Stahování se stejně zastaví na první už stažené dávce, takže do minulosti nejde hluboko.
MAX_LOADS = 8
def _set_chrome_cert_policy() -> None:
policy = json.dumps({"pattern": "https://[*.]vzp.cz",
"filter": {"ISSUER": {"CN": CERT_ISSUER_CN}}})
try:
key = winreg.CreateKey(winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Policies\Google\Chrome\AutoSelectCertificateForUrls")
winreg.SetValueEx(key, "1", 0, winreg.REG_SZ, policy)
winreg.CloseKey(key)
except Exception as e:
print(f" Varování: nelze nastavit Chrome politiku: {e}")
def _load_cookies(context) -> int:
if not os.path.exists(COOKIES_FILE):
return 0
try:
with open(COOKIES_FILE, encoding="utf-8") as f:
context.add_cookies(json.load(f))
return 1
except Exception:
return 0
def _save_cookies(context) -> None:
try:
vzp = [c for c in context.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)
except Exception:
pass
def prihlaseni(context):
"""Zajistí přihlášení na VZP Point. Vrátí přihlášenou page."""
_load_cookies(context)
page = context.new_page()
page.goto(DASHBOARD_URL, wait_until="domcontentloaded", timeout=30_000)
if page.url.startswith("https://auth.vzp.cz/signin"):
print("Přihlašuji certifikátem...")
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)
try:
page.wait_for_url("https://point.vzp.cz/**", timeout=60_000)
except Exception:
pass
if not page.url.startswith(POINT_URL):
raise RuntimeError(f"Přihlášení selhalo. URL: {page.url}")
print("Přihlášení OK.")
_save_cookies(context)
return page
def _bearer_token(page) -> str:
"""Vytáhne Bearer token z inline <script> na stránce VZP Point."""
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)
raise RuntimeError("bearerToken nenalezen na stránce")
def zjisti_obdobi(page) -> tuple[int, int]:
"""Vrátí nejnovější dostupné období (měsíc, rok) z configu formuláře Form65."""
token = _bearer_token(page)
cfg = page.evaluate(
"""async (token) => {
const r = await fetch('/api/desk/draft/form65/config',
{headers:{'Authorization':'Bearer '+token, 'Accept':'application/json'}});
return await r.json();
}""",
token,
)
period = (cfg.get("defaultModel") or {}).get("period") \
or (cfg.get("periodLimits") or {}).get("until") or {}
return int(period["month"]), int(period["year"])
def podej_zadost(page, mesic: int, rok: int) -> int | None:
"""Podá žádost 'Seznam registrovaných pojištěnců' (datové rozhraní) za období mesic/rok.
Vytvoří koncept (POST .../form65/{partnerId}) a publikuje ho
(POST .../form65/{draftId}/publish). Vrátí formId odeslaného podání nebo None.
"""
token = _bearer_token(page)
res = page.evaluate(
"""async ({token, partner, fmt, mesic, rok}) => {
const h = {'Authorization':'Bearer '+token,
'Content-Type':'application/json', 'Accept':'application/json'};
const r1 = await fetch('/api/desk/draft/form65/'+partner, {
method:'POST', headers:h,
body: JSON.stringify({outputFormat: fmt, period: {month: mesic, year: rok}})});
let j1=null; try { j1 = await r1.json(); } catch(e){}
if (!r1.ok || !j1 || !j1.draftId)
return {ok:false, step:'create', status:r1.status, body: JSON.stringify(j1)};
const r2 = await fetch('/api/desk/draft/form65/'+j1.draftId+'/publish', {
method:'POST', headers:h});
let j2=null; try { j2 = await r2.json(); } catch(e){}
return {ok: r2.ok, step:'publish', status:r2.status,
formId: j2 && j2.formId, state: j1.state};
}""",
{"token": token, "partner": PARTNER_ID, "fmt": OUTPUT_FORMAT, "mesic": mesic, "rok": rok},
)
if res.get("ok"):
print(f" OK — podání odesláno, formId: {res.get('formId')} (stav konceptu: {res.get('state')})")
return res.get("formId")
print(f" Podání selhalo ({res.get('step')}, HTTP {res.get('status')}): {res.get('body','')[:200]}")
return None
def _seznam_podani_v_dom(page) -> list[dict]:
"""Vrátí podání 'Seznam registrovaných pojištěnců' aktuálně načtená v DOMu (pořadí = nejnovější první)."""
podani = page.evaluate(r"""() => {
return Array.from(document.querySelectorAll('a[href*="/Desk/Form/Detail/"]'))
.map(a => ({ text: (a.innerText || a.title || '').replace(/\s+/g, ' ').trim(),
href: a.getAttribute('href') }))
.filter(x => /Seznam registrovaných pojištěnců/i.test(x.text));
}""")
seen, out = set(), []
for p in podani:
if p["href"] in seen:
continue
seen.add(p["href"])
out.append(p)
return out
def _nacti_dalsi(page) -> bool:
"""Klikne 'Načíst další záznamy'. Vrátí True pokud tlačítko existovalo."""
clicked = page.evaluate("""() => {
const a = Array.from(document.querySelectorAll('a,button'))
.find(e => /Načíst další/i.test(e.innerText || ''));
if (a) { a.scrollIntoView(); a.click(); return true; }
return false;
}""")
if clicked:
page.wait_for_timeout(1500)
return clicked
def stahni_davku_z_podani(page, href: str, already: set) -> tuple[int, bool]:
"""Otevře detail podání a stáhne přiloženou datovou dávku (F...).
Vrátí (počet_stažených, narazil_na_uz_stazenou). Druhý příznak je True, pokud
má podání dávku, kterou už máme v archivu — signál, že jsme dorazili do už
stažené minulosti a stahování lze ukončit.
"""
page.goto(POINT_URL + href, wait_until="networkidle", timeout=40_000)
page.wait_for_timeout(2000)
fnames = page.evaluate(r"""() => Array.from(document.querySelectorAll('a'))
.map(a => (a.innerText || '').trim())
.filter(t => /^F\d{7}\.\d+$/.test(t))""")
fnames = list(dict.fromkeys(fnames))
downloaded = 0
hit_existing = False
for fname in fnames:
if fname in already or os.path.exists(os.path.join(DEST_DIR, fname)):
print(f" [stop] dávka už stažena: {fname}")
hit_existing = True
continue
link = page.locator("a", has_text=fname).first
try:
with page.expect_download(timeout=30_000) as di:
link.dispatch_event("click")
body = di.value
target = os.path.join(DEST_DIR, fname)
body.save_as(target)
with open(target, "rb") as fh:
head = fh.read(9)
if not head.decode("cp852", errors="ignore").startswith("H09305001"):
print(f" POZOR: {fname} nemá hlavičku H09305001 (přesto uloženo)")
print(f" OK: {fname}")
already.add(fname)
downloaded += 1
time.sleep(0.5)
except Exception as e:
print(f" Chyba při stahování {fname}: {e}")
return downloaded, hit_existing
def hlavni() -> None:
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Chybí playwright: pip install playwright && playwright install chrome")
sys.exit(1)
os.makedirs(DEST_DIR, exist_ok=True)
_set_chrome_cert_policy()
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:
page = prihlaseni(context)
already = set(os.listdir(DEST_DIR))
print(f"V archivu: {len(already)} souborů.\n")
# Nasbírej podání 'Seznam...' — ODESLANÁ PODÁNÍ řadí od nejnovějšího.
# Dashboard míchá typy podání, proto je potřeba pár 'Načíst další'.
page.goto(DASHBOARD_URL, wait_until="networkidle", timeout=40_000)
page.wait_for_timeout(2500)
for _ in range(MAX_LOADS):
if not _nacti_dalsi(page):
break
podani = _seznam_podani_v_dom(page)
print(f"Nalezeno podání '{PODANI_NAZEV}': {len(podani)}\n")
# Stahuj od nejnovějšího; jakmile narazíš na už staženou dávku, skonči
# (starší jsou všechny stažené — není třeba jít hlouběji do minulosti).
celkem = 0
for pdn in podani:
print(f"Podání: {pdn['text']}")
dl, hit_existing = stahni_davku_z_podani(page, pdn["href"], already)
celkem += dl
if hit_existing:
print("Dosaženo už stažené dávky — končím (starší jsou stažené).")
break
print(f"\nStaženo nových dávek: {celkem}")
# Podání žádosti o nový výpis (datové rozhraní) za zvolené období.
# Výsledek dorazí do ODESLANÝCH PODÁNÍ a stáhne se při příštím spuštění.
page.goto(DASHBOARD_URL, wait_until="networkidle", timeout=40_000)
page.wait_for_timeout(2000)
mesic, rok = OVERRIDE_OBDOBI if OVERRIDE_OBDOBI else zjisti_obdobi(page)
print(f"\n=== Podávám žádost za období {mesic:02d}/{rok} ===")
podej_zadost(page, mesic, rok)
print("\nHotovo.")
finally:
_save_cookies(context)
context.close()
if __name__ == "__main__":
hlavni()
@@ -0,0 +1,87 @@
# VoZP (201) — Stahování seznamu registrovaných pojištěnců
## Co skript dělá
`StahniSeznamPojistencuVoZP.py` provede v jednom spuštění:
1. **Přihlásí se** certifikátem na portál VoZP (čistý Python, bez NMSigneru)
— uloží cookies do sdíleného `StahováníZpráv/201 VoZP/vozp_cookies.json`
2. **Stáhne nové výpisy** ze schránky `vypis-registrovanych-pacientu-praktickeho-lekare`
— stahuje soubory s hlavičkou `H09305001` (PDF protokoly se přeskočí)
— ukládá do `…\Zúčtovací zprávy\SeznamyPojištěnců\` (Dropbox)
— po stahování se **znovu přihlásí** (Playwright invaliduje requests session)
3. **Podá žádost** o aktuální výpis (datové rozhraní)
## Platforma
VoZP běží na stejné platformě jako **ZPŠ, OZP, RBP** (portalzp.cz / json-api).
Login identický, jen `BASE_URL = https://portal.vozp.cz`.
## Schránka a stažení
Schránka má **vlastní URL** (ne `schranky-vypis-pojistencu-v-kapitaci` jako OZP/RBP):
`/app/vypis-registrovanych-pacientu-praktickeho-lekare`
Stažení přílohy: GET `/html/prehled-zprav-ve-schrankach/zobrazit-prilohu?zprava_id={fileId}`
`fileId` z `onclick="SchrPolOpenFile(<id>)"`. Datové soubory `f201MMRR.001`, hlavička `H09305001`.
Ve schránce bývá i PDF protokol — header checkem se přeskočí.
## Podání žádosti
Formulář `106-zadost-o-vypis` je **nejjednodušší** — jen IČZ + Třídění, žádné datum ani typ.
Výpis je aktuální snímek registrovaných pacientů. Pro datový soubor se volí třídění = `d`
(Datové rozhraní). Žádný stav.json.
POST `https://portal.vozp.cz/json-api/formular-schranky/106-zadost-o-vypis/ulozit-formular`
Body: `{"schrXml": "...", "schrSign": "-----BEGIN PKCS7-----...", "schrFiles": []}`
### XML žádosti (řádky `\r\n`)
```xml
<SchrankaZadost NazevSchranky="SEZNAM_KAP" NazevFiltru="SEZNAM_KAP">
<PolozkaFiltru Nazev="nicoz">-109305000</PolozkaFiltru>
<PolozkaFiltru Nazev="trideni">d</PolozkaFiltru>
</SchrankaZadost>
```
| Položka | Hodnota | Význam |
|---------|---------|--------|
| `nicoz` | `-109305000` | **interní ID** položky IČZ (zobrazené IČZ = 09305000). Pozor: záporné! Ověřeno. |
| `trideni` | `d` | `p`=příjmení, `i`=IČP+příjmení, `r`=rodná čísla, **`d`=Datové rozhraní** (datový soubor) |
### Podpis XML
PKCS7/SHA-256, **bez** certifikátu (`NoCerts`) — stejně jako ZPŠ/OZP/RBP.
## Jak byly endpointy zjištěny
Odposlechem reálného podání v Chrome (MCP) — `data-xml-*` atributy + odchycený XHR na
`ulozit-formular`. První ostré podání: **ref. 179776197** (17.06.2026).
## Srovnání platformy portalzp.cz
| | ZPŠ (209) | OZP (207) | RBP (213) | VoZP (201) |
|--|-----------|-----------|-----------|------------|
| Schránka | schranka-vypis-… | schranky-vypis-… | schranky-vypis-… | vypis-registrovanych-pacientu-… |
| Formulář | 29-… | 108-… | 110-… | 106-… |
| Schránka/filtr | VypisPojKap / ZZ_VYP_REG | SEZNAM_KAP | VypisPojKap / ZZ_VYP_REG | SEZNAM_KAP |
| IČZ položka | icz=25520 | nicoz=13074913 | icz=933189 | nicoz=-109305000 |
| datum | poslední den měsíce | — | Ke dni (dnešek) | — |
| typ/trideni | razeni+typ=soubor | trideni=p+typ=soubor | razeni+typ=soubor | trideni=d (Datové rozhraní) |
## Soubory
| Soubor | Popis |
|--------|-------|
| `StahniSeznamPojistencuVoZP.py` | Hlavní skript — stažení výpisů + podání žádosti |
| `log_podani.json` | Historie podání s referenčními čísly |
## Parametry
- **IČZ**: 09305000 (IČP: 09305001, MUDr. Michaela Buzalková), interní ID `-109305000`
- **Certifikát**: `Insurance/Certificates/MBQualifiedCert.pfx`
## Stav
Hotovo a otestováno (17.06.2026): login ✓, stažení ✓ (3 datové soubory, PDF přeskočeno),
podání ✓ (ref. 179776197). Výpis z prvního podání dorazí do schránky.
@@ -0,0 +1,408 @@
"""
Stahování seznamu registrovaných pojištěnců VoZP (201) — čistý Python, bez NMSigneru.
VoZP běží na stejné platformě jako ZPŠ/OZP/RBP (portalzp.cz / json-api), s rozdíly:
- schránka: /app/vypis-registrovanych-pacientu-praktickeho-lekare
- formulář: 106-zadost-o-vypis
- filtr XML: NazevSchranky = NazevFiltru = "SEZNAM_KAP" (jako OZP)
- položky: nicoz (interní ID = -109305000), trideni (p/i/r/d)
trideni="d" = Datové rozhraní → datový soubor f201MMRR.001
- BEZ pole "datum" a BEZ pole "typ" — výpis je aktuální snímek registrovaných pacientů.
Co skript dělá v jednom spuštění:
1. Přihlásí se certifikátem (uloží cookies pro Playwright)
2. Stáhne nové výpisy ze schránky (soubory s hlavičkou H09305001)
3. Znovu se přihlásí (Playwright invaliduje requests session)
4. Podá jednu žádost o aktuální výpis (datové rozhraní)
Log podání: log_podani.json — seznam { ref_cislo, podano_kdy }
"""
import json
import os
import re
import sys
import time
from datetime import datetime
from pathlib import Path
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import pkcs7, pkcs12
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
from Knihovny.najdi_dropbox import get_dropbox_root
PFX_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "Certificates", "MBQualifiedCert.pfx"))
PFX_PASSWORD = b"Vlado7309208104++"
BASE_URL = "https://portal.vozp.cz"
CHALLENGE_URL = f"{BASE_URL}/json-api/prihlaseni/prihlasovaci-zprava"
CERTLOGIN_URL = f"{BASE_URL}/json-api/prihlaseni/prihlaseni-certifikatem"
SUBMIT_URL = f"{BASE_URL}/json-api/formular-schranky/106-zadost-o-vypis/ulozit-formular"
VYPIS_URL = f"{BASE_URL}/app/vypis-registrovanych-pacientu-praktickeho-lekare"
DOWNLOAD_URL = f"{BASE_URL}/html/prehled-zprav-ve-schrankach/zobrazit-prilohu"
# Hodnoty filtru (ověřeno odchytem reálného podání na portálu)
ICZ_INTERNAL = "-109305000" # IČZ 09305000 — interní ID položky "nicoz"
TRIDENI = "d" # p=příjmení, i=IČP+příjmení, r=rodná čísla, d=Datové rozhraní
# Hlavička platného výpisu pojištěnců (IČP 09305001 = MUDr. Buzalková)
HLAVICKA = "H09305001"
LOG_FILE = os.path.join(os.path.dirname(__file__), "log_podani.json")
# Sdílené soubory s VoZP skriptem pro stahování zpráv
STAHUJ_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "StahováníZpráv", "201 VoZP"))
COOKIES_FILE = os.path.join(STAHUJ_DIR, "vozp_cookies.json")
CHROME_PROFILE = os.path.join(STAHUJ_DIR, "chrome_profile")
DOWNLOAD_DIR = os.path.join(
get_dropbox_root(),
"Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "SeznamyPojištěnců",
)
# ---------------------------------------------------------------------------
# Přihlášení
# ---------------------------------------------------------------------------
def prihlaseni() -> requests.Session:
"""Přihlásí se certifikátem, vrátí autentizovanou session. Uloží cookies pro Playwright."""
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"X-Requested-With": "XMLHttpRequest",
"Origin": BASE_URL,
"Referer": BASE_URL + "/",
})
r = session.get(f"{BASE_URL}/app/prihlaseni")
r.raise_for_status()
session.cookies.set("pzp_sign", "CERT", domain="portal.vozp.cz", path="/")
r = session.post(CHALLENGE_URL, json={"login_sign": "CERT"},
headers={"Content-Type": "application/json; charset=UTF-8"})
r.raise_for_status()
zprava = r.json()["data"]["zprava"]
with open(PFX_PATH, "rb") as f:
private_key, cert, _ = pkcs12.load_key_and_certificates(f.read(), PFX_PASSWORD)
podpis = (
pkcs7.PKCS7SignatureBuilder()
.set_data(zprava.encode("utf-8"))
.add_signer(cert, private_key, hashes.SHA256())
.sign(serialization.Encoding.PEM, [pkcs7.PKCS7Options.DetachedSignature])
.decode("ascii").strip()
)
r = session.post(CERTLOGIN_URL, json={"zprava": zprava, "podpis": podpis},
headers={"Content-Type": "application/json; charset=UTF-8"})
r.raise_for_status()
data = r.json()["data"]
if not data.get("prihlasen"):
raise RuntimeError(f"Přihlášení selhalo: {r.json().get('errMsg', '')}")
print("Přihlášení úspěšné!")
cookies = [
{
"name": c.name,
"value": c.value,
"domain": c.domain if c.domain.startswith(".") else "." + c.domain,
"path": c.path or "/",
"expires": int(c.expires) if c.expires else -1,
"secure": bool(c.secure),
"httpOnly": False,
"sameSite": "Lax",
}
for c in session.cookies
]
with open(COOKIES_FILE, "w", encoding="utf-8") as f:
json.dump(cookies, f, indent=2, ensure_ascii=False)
return session
# ---------------------------------------------------------------------------
# Stahování z výpisové schránky
# ---------------------------------------------------------------------------
def safe_filename(name: str) -> str:
return re.sub(r'[\\/:*?"<>|]', "_", name).strip()
def parse_date(date_str: str) -> str:
try:
return datetime.strptime(date_str.strip()[:19], "%d.%m.%Y %H:%M:%S").strftime("%Y-%m-%d")
except Exception:
try:
return datetime.strptime(date_str.strip()[:10], "%d.%m.%Y").strftime("%Y-%m-%d")
except Exception:
return "0000-00-00"
def parse_row(cells: list) -> dict:
"""Z buněk řádku schránky vytvoří popis a cílový název souboru."""
date_raw = cells[1].strip() if len(cells) > 1 else ""
desc_raw = cells[2].strip() if len(cells) > 2 else ""
fname_raw = cells[3].strip() if len(cells) > 3 else ""
desc_lines = [l.strip() for l in desc_raw.split("\n") if l.strip()]
if len(desc_lines) >= 3:
description = desc_lines[2]
elif len(desc_lines) >= 2:
description = desc_lines[1]
else:
description = desc_lines[0] if desc_lines else ""
description = description[:80]
fname_match = re.match(r'^(.+?)\s*\(\d{2}\.\d{2}\.\d{4}\)\s*$', fname_raw)
original = fname_match.group(1).strip() if fname_match else fname_raw.split("(")[0].strip()
orig_path = Path(original)
stem = orig_path.stem or "zprava"
ext = orig_path.suffix or ""
date_iso = parse_date(date_raw)
name = f"{date_iso} {safe_filename(description)} ({safe_filename(stem)}){ext}"
if len(name) > 240:
name = f"{date_iso} ({safe_filename(stem)}){ext}"
return {"date": date_iso, "desc": description, "original": original, "filename": name}
def stahni_nove_vypisy() -> int:
"""Stáhne nové výpisy z výpisové schránky. Vrátí počet stažených souborů."""
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Chybí playwright: pip install playwright && playwright install chrome")
return 0
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
with open(COOKIES_FILE, encoding="utf-8") as f:
cookies = json.load(f)
downloaded = 0
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,
)
try:
context.add_cookies(cookies)
page = context.new_page()
page.goto(f"{VYPIS_URL}/", wait_until="domcontentloaded", timeout=30_000)
if "prihlaseni" in page.url or "login" in page.url.lower():
print("Session v prohlížeči expirovala — stahování přeskočeno")
return 0
print("Prohlížeč přihlášen OK\n")
already = set(os.listdir(DOWNLOAD_DIR))
print(f"V archivu: {len(already)} souborů.\n")
page_num = 1
seen_ids: set = set()
while True:
url = f"{VYPIS_URL}/stranka-{page_num}"
print(f" Stránka {page_num}: {url}")
try:
page.goto(url, wait_until="domcontentloaded", timeout=30_000)
except Exception as e:
print(f" Navigace selhala: {e}")
break
page.wait_for_load_state("networkidle", timeout=15_000)
data = page.evaluate("""() => {
const rows = [];
for (const tr of document.querySelectorAll('table tr')) {
const cells = Array.from(tr.querySelectorAll('td')).map(td => td.innerText.trim());
if (cells.length < 4) continue;
const dlLink = tr.querySelector('a[onclick*="SchrPolOpenFile"]');
if (!dlLink) continue;
const mFile = dlLink.getAttribute('onclick').match(/\\d+/);
rows.push({ cells, fileId: mFile ? mFile[0] : null });
}
return rows;
}""")
rows = [r for r in data if r["fileId"]]
if not rows:
print(f" Stránka {page_num} — žádné řádky, konec schránky.")
break
current_ids = {r["fileId"] for r in rows}
if current_ids & seen_ids:
print(f" Stránka {page_num} — opakující se obsah, konec schránky.")
break
seen_ids.update(current_ids)
print(f" Nalezeno {len(rows)} zpráv.")
stop = False
for row in rows:
info = parse_row(row["cells"])
target = os.path.join(DOWNLOAD_DIR, info["filename"])
if info["filename"] in already or os.path.exists(target):
print(f" [stop] Nalezena již stažená zpráva: {info['filename']}")
stop = True
break
dl_url = f"{DOWNLOAD_URL}?zprava_id={row['fileId']}"
try:
r = context.request.get(dl_url, headers={"Referer": VYPIS_URL}, timeout=30_000)
if not r.ok:
print(f" HTTP {r.status} příloha (id={row['fileId']})")
else:
body = r.body()
if not body[:len(HLAVICKA)].decode("ascii", errors="ignore").startswith(HLAVICKA):
print(f" přeskočeno (není výpis pojištěnců): {info['filename']}")
else:
with open(target, "wb") as fh:
fh.write(body)
print(f" OK: {info['filename']}")
already.add(info["filename"])
downloaded += 1
except Exception as e:
print(f" Chyba příloha (id={row['fileId']}): {e}")
time.sleep(1.0)
if stop:
break
page_num += 1
finally:
context.close()
return downloaded
# ---------------------------------------------------------------------------
# Sestavení XML a podpis žádosti
# ---------------------------------------------------------------------------
def build_xml() -> str:
"""Sestaví XML žádosti o aktuální výpis registrovaných pacientů (datové rozhraní)."""
return (
f'<SchrankaZadost NazevSchranky="SEZNAM_KAP" NazevFiltru="SEZNAM_KAP">\r\n'
f'<PolozkaFiltru Nazev="nicoz">{ICZ_INTERNAL}</PolozkaFiltru>\r\n'
f'<PolozkaFiltru Nazev="trideni">{TRIDENI}</PolozkaFiltru>\r\n'
f'</SchrankaZadost>'
)
def sign_xml(xml: str) -> str:
"""Podepíše XML certifikátem (PKCS7 detached, bez certifikátu — server cert v podpisu odmítá)."""
with open(PFX_PATH, "rb") as f:
private_key, cert, _ = pkcs12.load_key_and_certificates(f.read(), PFX_PASSWORD)
pem = (
pkcs7.PKCS7SignatureBuilder()
.set_data(xml.encode("utf-8"))
.add_signer(cert, private_key, hashes.SHA256())
.sign(serialization.Encoding.PEM, [pkcs7.PKCS7Options.DetachedSignature, pkcs7.PKCS7Options.NoCerts])
.decode("ascii")
)
return pem.replace("\r\n", "\n").replace("\n", "\r\n")
def odeslat_zadost(session: requests.Session) -> str | None:
"""Odešle podepsanou žádost o aktuální výpis. Vrátí referenční číslo nebo None."""
xml = build_xml()
podpis = sign_xml(xml)
payload = {"schrXml": xml, "schrSign": podpis, "schrFiles": []}
r = session.post(SUBMIT_URL, json=payload, headers={
"Content-Type": "application/json; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Referer": BASE_URL + "/",
})
r.raise_for_status()
try:
resp = r.json()
except Exception:
print(f" Odpověď není JSON: {r.text[:300]}")
return None
resp_str = json.dumps(resp, ensure_ascii=False)
if resp.get("errMsg") or resp.get("error"):
print(f" Chyba od serveru: {resp.get('errMsg') or resp.get('error')}")
return None
m = re.search(r'\b(1[5-9]\d{7})\b', resp_str)
ref = m.group(1) if m else None
if ref:
print(f" OK — ref. číslo: {ref}")
else:
print(f" Odpověď (bez ref. čísla): {resp_str[:300]}")
return ref or ("OK" if r.ok else None)
# ---------------------------------------------------------------------------
# Log
# ---------------------------------------------------------------------------
def uloz_log(ref_cislo: str) -> None:
log = []
if os.path.exists(LOG_FILE):
with open(LOG_FILE, encoding="utf-8") as f:
log = json.load(f)
log.append({
"ref_cislo": ref_cislo,
"podano_kdy": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
})
with open(LOG_FILE, "w", encoding="utf-8") as f:
json.dump(log, f, indent=2, ensure_ascii=False)
# ---------------------------------------------------------------------------
# Hlavní funkce
# ---------------------------------------------------------------------------
def hlavni() -> None:
# 1. Přihlášení — uloží cookies pro Playwright
prihlaseni()
# 2. Stažení nových výpisů z výpisové schránky
print("\n=== Stahování nových výpisů ===")
stazeno = stahni_nove_vypisy()
print(f"Staženo: {stazeno} souborů.\n")
# 3. Znovu přihlásit — Playwright mohl invalidovat předchozí session
print("=== Znovu přihlašuji před podáním ===")
session = prihlaseni()
# 4. Podání žádosti o aktuální výpis
print("=== Podávám žádost o aktuální výpis (datové rozhraní) ===")
ref = odeslat_zadost(session)
if ref:
uloz_log(ref)
print(f"\nHotovo — žádost podána, ref: {ref}")
else:
print("\nPodání selhalo — žádost nebyla zaevidována.")
if __name__ == "__main__":
hlavni()
@@ -0,0 +1,6 @@
[
{
"ref_cislo": "179776533",
"podano_kdy": "2026-06-17 05:48:36"
}
]
@@ -0,0 +1,64 @@
# ZPMVČR (211) — Stahování seznamu registrovaných pojištěnců
## Co skript dělá
`StahniSeznamPojistencuZPMVCR.py` (čistý Python, requests + bs4):
1. **Přihlásí se** PIN + heslem (POST formulář, bez certifikátu / NMSigneru)
2. **Projde stránkovaný přehled** všech registrací pro IČP 09305001
3. **Uloží CSV** do `…\Zúčtovací zprávy\SeznamyPojištěnců\`
## Platforma — ODLIŠNÁ od ostatních
ZPMVČR běží na **eforms.zpmvcr.cz**, NE na portalzp.cz. Žádné certifikáty, žádné schránky,
žádné datové rozhraní .001. Login je PIN + heslo.
## Zásadní rozdíl: NENÍ datový soubor
Ostatní pojišťovny dávají datový soubor (.001 / F-soubor). ZPMVČR **nemá** ekvivalent:
- EP2 sekce (`dokumenty_ke_stazeni/ep2`) je prázdná — *"nebylo stahování dokumentů nastaveno"*.
- Jediný zdroj seznamu je **HTML přehled** na stránce `registrovani_pojistenci`,
který se musí naparsovat → proto výstupem je **CSV**, ne datový soubor.
## Přihlášení
POST `https://eforms.zpmvcr.cz/eforms/ekomunikace`
Pole: `pin` (9023895287), `pin2` (prázdné), `pwd` (heslo).
## Stažení seznamu
POST `https://eforms.zpmvcr.cz/eforms/smluvni_zdravotnicke_zarizeni/registrovani_pojistenci`
| Pole | Hodnota | Význam |
|------|---------|--------|
| `icp` | `09305001` | IČP (nebo "Vše") |
| `arztart` | `` (prázdné = Vše) | odbornost D/G/P/S |
| `mesic` / `rok` | aktuální měsíc/rok | období |
| `registrace` | `3` | 1=platné, 2=neplatné, **3=všechny** |
| `tridit` | `1` | 1=příjmení, 2=číslo pojištěnce |
| `vyhledat` | `Vyhledat` | submit |
Výsledek je **stránkovaný** (~20 řádků/strana). Další strany: POST + pole `page=N`.
Řádky v HTML: `<tr class="c1|c2">`, hodnoty za `<span class="responsiveColumn">Label:</span>`.
Hláška "Přehled ... (celkem N)" udává očekávaný počet (kontrola úplnosti).
## CSV výstup
Soubor `YYYY-MM-DD 211 ZPMVČR vsechny registrace.csv`, kódování utf-8-sig (Excel),
oddělovač `;`. Sloupce: Číslo pojištěnce; Titul; Příjmení; Jméno; Registrace od; Registrace do.
## Soubory
| Soubor | Popis |
|--------|-------|
| `StahniSeznamPojistencuZPMVCR.py` | Hlavní skript — login + scrape přehledu → CSV |
## Parametry
- **IČP**: 09305001 (MUDr. Michaela Buzalková)
- **Login**: PIN 9023895287 + heslo (v kódu, stejně jako StahováníZpráv/211)
## Stav
Hotovo a otestováno (17.06.2026): login ✓, staženo 172 registrací (9 stran, sedí s "celkem 172"),
CSV uloženo. Volba uživatele: VŠECHNY registrace (registrace=3).
@@ -0,0 +1,174 @@
"""
Stahování seznamu registrovaných pojištěnců ZPMVČR (211) — čistý Python (requests + bs4).
ZPMVČR běží na ODLIŠNÉ platformě (eforms.zpmvcr.cz) — ne portalzp.cz:
- login: PIN + heslo (POST formulář), bez certifikátu a bez NMSigneru
- seznam: NENÍ datový soubor jako u ostatních pojišťoven (EP2 sekce je prázdná).
Jediný zdroj je HTML "Přehled registrací" na stránce registrovani_pojistenci,
který se naparsuje a uloží jako CSV.
Co skript dělá:
1. Přihlásí se (PIN + heslo)
2. Projde stránkovaný přehled VŠECH registrací (platné i neplatné) pro IČP 09305001
3. Uloží výsledek jako CSV do složky SeznamyPojištěnců (sloupce níže)
CSV sloupce: Číslo pojištěnce; Titul; Příjmení; Jméno; Registrace od; Registrace do
"""
import csv
import os
import sys
from datetime import date
import requests
from bs4 import BeautifulSoup
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
from Knihovny.najdi_dropbox import get_dropbox_root
# ── Přihlašovací údaje ────────────────────────────────────────────────────────
PIN = "9023895287"
PIN2 = ""
HESLO = "Ax162q8+"
# ─────────────────────────────────────────────────────────────────────────────
BASE_URL = "https://eforms.zpmvcr.cz"
LOGIN_URL = f"{BASE_URL}/eforms/ekomunikace"
SEZNAM_URL = f"{BASE_URL}/eforms/smluvni_zdravotnicke_zarizeni/registrovani_pojistenci"
ICP = "09305001" # IČP MUDr. Michaela Buzalková
REGISTRACE = "3" # 1=platné, 2=neplatné, 3=všechny
TRIDIT = "1" # 1=příjmení, 2=číslo pojištěnce
CSV_HLAVICKA = ["Číslo pojištěnce", "Titul", "Příjmení", "Jméno", "Registrace od", "Registrace do"]
DEST_DIR = os.path.join(
get_dropbox_root(),
"Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "SeznamyPojištěnců",
)
def prihlaseni() -> requests.Session:
"""Přihlásí se PIN + heslem, vrátí session."""
session = requests.Session()
session.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
session.get(LOGIN_URL, timeout=15).raise_for_status()
r = session.post(LOGIN_URL, data={"pin": PIN, "pin2": PIN2, "pwd": HESLO}, timeout=15)
r.raise_for_status()
if 'name="pin"' in r.text and "Přihlásit" in r.text:
raise RuntimeError("Přihlášení selhalo — zkontroluj PIN a heslo")
print("Přihlášení úspěšné!")
return session
def parse_rows(html: str) -> list[list[str]]:
"""Naparsuje řádky přehledu. Vrátí seznam [číslo, titul, příjmení, jméno, reg_od, reg_do]."""
soup = BeautifulSoup(html, "html.parser")
rows = []
for tr in soup.select("tr.c1, tr.c2"):
vals = []
for td in tr.find_all("td"):
for sp in td.select("span.responsiveColumn"):
sp.extract()
vals.append(td.get_text(strip=True))
# platný datový řádek má vyplněné číslo pojištěnce v prvním sloupci
if len(vals) >= 6 and vals[0]:
rows.append(vals[:6])
return rows
def precti_celkem(html: str) -> int | None:
"""Z hlášky 'Přehled ... (celkem N)' získá očekávaný počet."""
import re
m = re.search(r"celkem\s+(\d+)", html)
return int(m.group(1)) if m else None
def stahni_seznam(session: requests.Session) -> list[list[str]]:
"""Projde stránkovaný přehled a vrátí všechny řádky."""
base_data = {
"icp": ICP, "arztart": "",
"mesic": str(date.today().month), "rok": str(date.today().year),
"registrace": REGISTRACE, "tridit": TRIDIT, "vyhledat": "Vyhledat",
}
vsechny: list[list[str]] = []
videno: set = set()
celkem_ocekavano = None
page = 1
while page <= 200:
data = dict(base_data)
if page > 1:
data["page"] = str(page)
r = session.post(SEZNAM_URL, data=data, timeout=30)
r.raise_for_status()
if celkem_ocekavano is None:
celkem_ocekavano = precti_celkem(r.text)
if celkem_ocekavano is not None:
print(f"Přehled hlásí celkem {celkem_ocekavano} registrací.")
rows = parse_rows(r.text)
nove = [row for row in rows if tuple(row) not in videno]
if not nove:
break
for row in nove:
videno.add(tuple(row))
vsechny.extend(nove)
print(f" Strana {page}: +{len(nove)} (celkem {len(vsechny)})")
# poslední strana — méně řádků než plná stránka
if len(rows) < 20:
break
page += 1
if celkem_ocekavano is not None and len(vsechny) != celkem_ocekavano:
print(f" POZOR: staženo {len(vsechny)}, ale přehled hlásil {celkem_ocekavano}.")
return vsechny
def uloz_csv(rows: list[list[str]]) -> str:
"""Uloží řádky jako CSV (Excel-friendly: utf-8-sig, oddělovač ;). Vrátí cestu."""
os.makedirs(DEST_DIR, exist_ok=True)
dnes = date.today().strftime("%Y-%m-%d")
filename = f"{dnes} 211 ZPMVČR vsechny registrace.csv"
path = os.path.join(DEST_DIR, filename)
with open(path, "w", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f, delimiter=";")
w.writerow(CSV_HLAVICKA)
w.writerows(rows)
return path
def hlavni() -> None:
session = prihlaseni()
print("\n=== Stahování přehledu registrací ===")
rows = stahni_seznam(session)
print(f"Staženo: {len(rows)} registrací.")
if not rows:
print("Žádné registrace — CSV se neuloží.")
return
path = uloz_csv(rows)
print(f"\nHotovo — uloženo: {path}")
if __name__ == "__main__":
hlavni()
@@ -0,0 +1,90 @@
# RBP (213) — Stahování seznamu registrovaných pojištěnců
## Co skript dělá
`StahniSeznamPojistencuRBP.py` provede v jednom spuštění:
1. **Přihlásí se** certifikátem na portál RBP (čistý Python, bez NMSigneru)
— uloží cookies do sdíleného `StahováníZpráv/213 RBP/rbp_cookies.json`
2. **Stáhne nové výpisy** z výpisové schránky `schranky-vypis-pojistencu-v-kapitaci`
— stahuje soubory, jejichž obsah začíná `H09305001` (textové `odpoved.txt` se přeskočí)
— ukládá do `…\Zúčtovací zprávy\SeznamyPojištěnců\` (Dropbox)
— zastaví se při první již stažené zprávě
— po stahování se **znovu přihlásí** (Playwright invaliduje requests session)
3. **Podá žádost** o výpis ke dnešnímu dni (typ=soubor, třídění dle příjmení)
## Platforma
RBP běží na stejné platformě jako **ZPŠ, OZP, VoZP** (portalzp.cz / json-api).
Login identický se ZPŠ/OZP, jen `BASE_URL = https://portal.rbp-zp.cz`.
## Stažení přílohy
GET `/html/prehled-zprav-ve-schrankach/zobrazit-prilohu?zprava_id={fileId}`
`fileId` z `onclick="SchrPolOpenFile(<id>)"`. Datový výpis má hlavičku `H09305001`.
## Podání žádosti
RBP je **hybrid ZPŠ/OZP**: schránka/filtr jako ZPŠ, ale `datum` je „Ke dni" (aktuální
snímek platných registrací k danému dni, default dnešní datum). Nepočítá se měsíc,
žádný stav.json — při každém běhu se podá žádost ke dni `date.today()`.
POST `https://portal.rbp-zp.cz/json-api/formular-schranky/110-vypis-pojistencu-reg-u-pzs/ulozit-formular`
Body: `{"schrXml": "...", "schrSign": "-----BEGIN PKCS7-----...", "schrFiles": []}`
### XML žádosti (řádky `\r\n`)
```xml
<SchrankaZadost NazevSchranky="VypisPojKap" NazevFiltru="ZZ_VYP_REG">
<PolozkaFiltru Nazev="icz">933189</PolozkaFiltru>
<PolozkaFiltru Nazev="datum">17.06.2026</PolozkaFiltru>
<PolozkaFiltru Nazev="razeni">jmeno</PolozkaFiltru>
<PolozkaFiltru Nazev="typ">soubor</PolozkaFiltru>
</SchrankaZadost>
```
| Položka | Hodnota | Význam |
|---------|---------|--------|
| `icz` | `933189` | **interní ID** položky IČZ (zobrazené IČZ = 09305000). |
| `datum` | `DD.MM.YYYY` | „Ke dni" — den, ke kterému chceme snímek (použijeme dnešek). |
| `razeni` | `jmeno` | `jmeno`=příjmení a jména, `rc`=rodná čísla |
| `typ` | `soubor` | `soubor`=datový soubor netříděno, `sestava`=PDF |
### Podpis XML
PKCS7/SHA-256, **bez** certifikátu (`NoCerts`) — stejně jako ZPŠ/OZP.
## Jak byly endpointy zjištěny
Odposlechem reálného podání v Chrome (MCP) — `data-xml-*` atributy + odchycený XHR na
`ulozit-formular`. Skrytý input datumu vypadal jako JWT, ale odchycený XML potvrdil
prostý formát `DD.MM.YYYY`. První ostré podání: **ref. 179775430** (17.06.2026).
## Srovnání
| | ZPŠ (209) | OZP (207) | RBP (213) |
|--|-----------|-----------|-----------|
| NazevSchranky | `VypisPojKap` | `SEZNAM_KAP` | `VypisPojKap` |
| NazevFiltru | `ZZ_VYP_REG` | `SEZNAM_KAP` | `ZZ_VYP_REG` |
| Formulář | `29-…` | `108-…` | `110-…` |
| Položka IČZ | `icz`=25520 | `nicoz`=13074913 | `icz`=933189 |
| Pole datum | ano (poslední den měsíce) | ne | ano (Ke dni, dnešek) |
| razeni / typ | jmeno / soubor | trideni=p / typ=soubor | jmeno / soubor |
## Soubory
| Soubor | Popis |
|--------|-------|
| `StahniSeznamPojistencuRBP.py` | Hlavní skript — stažení výpisů + podání žádosti |
| `log_podani.json` | Historie podání s referenčními čísly |
## Parametry
- **IČZ**: 09305000 (IČP: 09305001, MUDr. Michaela Buzalková), interní ID `933189`
- **Certifikát**: `Insurance/Certificates/MBQualifiedCert.pfx`
## Stav
Hotovo a otestováno (17.06.2026): login ✓, stažení ✓ (odpoved.txt správně přeskočeny),
podání ✓ (ref. 179775430). Výpis z prvního podání dorazí do schránky do příštího dne
— při dalším spuštění ověřit, že hlavička `H09305001` u RBP datového souboru sedí.
@@ -0,0 +1,415 @@
"""
Stahování seznamu registrovaných pojištěnců RBP (213) — čistý Python, bez NMSigneru.
RBP běží na stejné platformě jako ZPŠ/OZP/VoZP (portalzp.cz / json-api).
- schránka: /app/schranky-vypis-pojistencu-v-kapitaci
- formulář: 110-vypis-pojistencu-reg-u-pzs
- filtr XML: NazevSchranky="VypisPojKap", NazevFiltru="ZZ_VYP_REG" (jako ZPŠ)
- položky: icz (interní ID), datum (Ke dni), razeni (jmeno/rc), typ (soubor/sestava)
- datum = "Ke dni" aktuální snímek platných registrací — použijeme dnešní datum,
nepočítá se měsíc, žádný stav.json (jako OZP).
Co skript dělá v jednom spuštění:
1. Přihlásí se certifikátem (uloží cookies pro Playwright)
2. Stáhne nové výpisy z výpisové schránky (soubory s hlavičkou H09305001)
3. Znovu se přihlásí (Playwright invaliduje requests session)
4. Podá jednu žádost o aktuální výpis ke dnešnímu dni
Log podání: log_podani.json — seznam { ref_cislo, datum, podano_kdy }
"""
import json
import os
import re
import sys
import time
from datetime import date, datetime
from pathlib import Path
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import pkcs7, pkcs12
# UTF-8 výstup i na Windows konzoli
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))
from Knihovny.najdi_dropbox import get_dropbox_root
PFX_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "Certificates", "MBQualifiedCert.pfx"))
PFX_PASSWORD = b"Vlado7309208104++"
BASE_URL = "https://portal.rbp-zp.cz"
CHALLENGE_URL = f"{BASE_URL}/json-api/prihlaseni/prihlasovaci-zprava"
CERTLOGIN_URL = f"{BASE_URL}/json-api/prihlaseni/prihlaseni-certifikatem"
SUBMIT_URL = f"{BASE_URL}/json-api/formular-schranky/110-vypis-pojistencu-reg-u-pzs/ulozit-formular"
VYPIS_URL = f"{BASE_URL}/app/schranky-vypis-pojistencu-v-kapitaci"
DOWNLOAD_URL = f"{BASE_URL}/html/prehled-zprav-ve-schrankach/zobrazit-prilohu"
# Hodnoty filtru (ověřeno odchytem reálného podání na portálu)
ICZ_INTERNAL = "933189" # IČZ 09305000 — interní ID položky "icz"
RAZENI = "jmeno" # jmeno = příjmení a jména, rc = rodná čísla
TYP = "soubor" # soubor = datový soubor, sestava = PDF sestava
# Hlavička platného výpisu pojištěnců (IČP 09305001 = MUDr. Buzalková)
HLAVICKA = "H09305001"
LOG_FILE = os.path.join(os.path.dirname(__file__), "log_podani.json")
# Sdílené soubory s RBP skriptem pro stahování zpráv
STAHUJ_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "StahováníZpráv", "213 RBP"))
COOKIES_FILE = os.path.join(STAHUJ_DIR, "rbp_cookies.json")
CHROME_PROFILE = os.path.join(STAHUJ_DIR, "chrome_profile")
DOWNLOAD_DIR = os.path.join(
get_dropbox_root(),
"Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "SeznamyPojištěnců",
)
# ---------------------------------------------------------------------------
# Přihlášení
# ---------------------------------------------------------------------------
def prihlaseni() -> requests.Session:
"""Přihlásí se certifikátem, vrátí autentizovanou session. Uloží cookies pro Playwright."""
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"X-Requested-With": "XMLHttpRequest",
"Origin": BASE_URL,
"Referer": BASE_URL + "/",
})
r = session.get(f"{BASE_URL}/app/prihlaseni")
r.raise_for_status()
session.cookies.set("pzp_sign", "CERT", domain="portal.rbp-zp.cz", path="/")
r = session.post(CHALLENGE_URL, json={"login_sign": "CERT"},
headers={"Content-Type": "application/json; charset=UTF-8"})
r.raise_for_status()
zprava = r.json()["data"]["zprava"]
with open(PFX_PATH, "rb") as f:
private_key, cert, _ = pkcs12.load_key_and_certificates(f.read(), PFX_PASSWORD)
podpis = (
pkcs7.PKCS7SignatureBuilder()
.set_data(zprava.encode("utf-8"))
.add_signer(cert, private_key, hashes.SHA256())
.sign(serialization.Encoding.PEM, [pkcs7.PKCS7Options.DetachedSignature])
.decode("ascii").strip()
)
r = session.post(CERTLOGIN_URL, json={"zprava": zprava, "podpis": podpis},
headers={"Content-Type": "application/json; charset=UTF-8"})
r.raise_for_status()
data = r.json()["data"]
if not data.get("prihlasen"):
raise RuntimeError(f"Přihlášení selhalo: {r.json().get('errMsg', '')}")
print("Přihlášení úspěšné!")
cookies = [
{
"name": c.name,
"value": c.value,
"domain": c.domain if c.domain.startswith(".") else "." + c.domain,
"path": c.path or "/",
"expires": int(c.expires) if c.expires else -1,
"secure": bool(c.secure),
"httpOnly": False,
"sameSite": "Lax",
}
for c in session.cookies
]
with open(COOKIES_FILE, "w", encoding="utf-8") as f:
json.dump(cookies, f, indent=2, ensure_ascii=False)
return session
# ---------------------------------------------------------------------------
# Stahování z výpisové schránky
# ---------------------------------------------------------------------------
def safe_filename(name: str) -> str:
return re.sub(r'[\\/:*?"<>|]', "_", name).strip()
def parse_date(date_str: str) -> str:
try:
return datetime.strptime(date_str.strip()[:19], "%d.%m.%Y %H:%M:%S").strftime("%Y-%m-%d")
except Exception:
try:
return datetime.strptime(date_str.strip()[:10], "%d.%m.%Y").strftime("%Y-%m-%d")
except Exception:
return "0000-00-00"
def parse_row(cells: list) -> dict:
"""Z buněk řádku schránky vytvoří popis a cílový název souboru."""
date_raw = cells[1].strip() if len(cells) > 1 else ""
desc_raw = cells[2].strip() if len(cells) > 2 else ""
fname_raw = cells[3].strip() if len(cells) > 3 else ""
desc_lines = [l.strip() for l in desc_raw.split("\n") if l.strip()]
if len(desc_lines) >= 3:
description = desc_lines[2]
elif len(desc_lines) >= 2:
description = desc_lines[1]
else:
description = desc_lines[0] if desc_lines else ""
description = description[:80]
fname_match = re.match(r'^(.+?)\s*\(\d{2}\.\d{2}\.\d{4}\)\s*$', fname_raw)
original = fname_match.group(1).strip() if fname_match else fname_raw.split("(")[0].strip()
orig_path = Path(original)
stem = orig_path.stem or "zprava"
ext = orig_path.suffix or ""
date_iso = parse_date(date_raw)
name = f"{date_iso} {safe_filename(description)} ({safe_filename(stem)}){ext}"
if len(name) > 240:
name = f"{date_iso} ({safe_filename(stem)}){ext}"
return {"date": date_iso, "desc": description, "original": original, "filename": name}
def stahni_nove_vypisy() -> int:
"""Stáhne nové výpisy z výpisové schránky. Vrátí počet stažených souborů."""
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Chybí playwright: pip install playwright && playwright install chrome")
return 0
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
with open(COOKIES_FILE, encoding="utf-8") as f:
cookies = json.load(f)
downloaded = 0
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,
)
try:
context.add_cookies(cookies)
page = context.new_page()
page.goto(f"{VYPIS_URL}/", wait_until="domcontentloaded", timeout=30_000)
if "prihlaseni" in page.url or "login" in page.url.lower():
print("Session v prohlížeči expirovala — stahování přeskočeno")
return 0
print("Prohlížeč přihlášen OK\n")
already = set(os.listdir(DOWNLOAD_DIR))
print(f"V archivu: {len(already)} souborů.\n")
page_num = 1
seen_ids: set = set()
while True:
url = f"{VYPIS_URL}/stranka-{page_num}"
print(f" Stránka {page_num}: {url}")
try:
page.goto(url, wait_until="domcontentloaded", timeout=30_000)
except Exception as e:
print(f" Navigace selhala: {e}")
break
page.wait_for_load_state("networkidle", timeout=15_000)
data = page.evaluate("""() => {
const rows = [];
for (const tr of document.querySelectorAll('table tr')) {
const cells = Array.from(tr.querySelectorAll('td')).map(td => td.innerText.trim());
if (cells.length < 4) continue;
const dlLink = tr.querySelector('a[onclick*="SchrPolOpenFile"]');
if (!dlLink) continue;
const mFile = dlLink.getAttribute('onclick').match(/\\d+/);
rows.push({ cells, fileId: mFile ? mFile[0] : null });
}
return rows;
}""")
rows = [r for r in data if r["fileId"]]
if not rows:
print(f" Stránka {page_num} — žádné řádky, konec schránky.")
break
current_ids = {r["fileId"] for r in rows}
if current_ids & seen_ids:
print(f" Stránka {page_num} — opakující se obsah, konec schránky.")
break
seen_ids.update(current_ids)
print(f" Nalezeno {len(rows)} zpráv.")
stop = False
for row in rows:
info = parse_row(row["cells"])
target = os.path.join(DOWNLOAD_DIR, info["filename"])
if info["filename"] in already or os.path.exists(target):
print(f" [stop] Nalezena již stažená zpráva: {info['filename']}")
stop = True
break
dl_url = f"{DOWNLOAD_URL}?zprava_id={row['fileId']}"
try:
r = context.request.get(dl_url, headers={"Referer": VYPIS_URL}, timeout=30_000)
if not r.ok:
print(f" HTTP {r.status} příloha (id={row['fileId']})")
else:
body = r.body()
if not body[:len(HLAVICKA)].decode("ascii", errors="ignore").startswith(HLAVICKA):
print(f" přeskočeno (není výpis pojištěnců): {info['filename']}")
else:
with open(target, "wb") as fh:
fh.write(body)
print(f" OK: {info['filename']}")
already.add(info["filename"])
downloaded += 1
except Exception as e:
print(f" Chyba příloha (id={row['fileId']}): {e}")
time.sleep(1.0)
if stop:
break
page_num += 1
finally:
context.close()
return downloaded
# ---------------------------------------------------------------------------
# Sestavení XML a podpis žádosti
# ---------------------------------------------------------------------------
def build_xml(datum: date) -> str:
"""Sestaví XML žádosti o výpis pojištěnců ke dni `datum`."""
datum_str = datum.strftime("%d.%m.%Y")
return (
f'<SchrankaZadost NazevSchranky="VypisPojKap" NazevFiltru="ZZ_VYP_REG">\r\n'
f'<PolozkaFiltru Nazev="icz">{ICZ_INTERNAL}</PolozkaFiltru>\r\n'
f'<PolozkaFiltru Nazev="datum">{datum_str}</PolozkaFiltru>\r\n'
f'<PolozkaFiltru Nazev="razeni">{RAZENI}</PolozkaFiltru>\r\n'
f'<PolozkaFiltru Nazev="typ">{TYP}</PolozkaFiltru>\r\n'
f'</SchrankaZadost>'
)
def sign_xml(xml: str) -> str:
"""Podepíše XML certifikátem (PKCS7 detached, bez certifikátu — server cert v podpisu odmítá)."""
with open(PFX_PATH, "rb") as f:
private_key, cert, _ = pkcs12.load_key_and_certificates(f.read(), PFX_PASSWORD)
pem = (
pkcs7.PKCS7SignatureBuilder()
.set_data(xml.encode("utf-8"))
.add_signer(cert, private_key, hashes.SHA256())
.sign(serialization.Encoding.PEM, [pkcs7.PKCS7Options.DetachedSignature, pkcs7.PKCS7Options.NoCerts])
.decode("ascii")
)
return pem.replace("\r\n", "\n").replace("\n", "\r\n")
def odeslat_zadost(session: requests.Session, datum: date) -> str | None:
"""Odešle podepsanou žádost o výpis ke dni `datum`. Vrátí referenční číslo nebo None."""
xml = build_xml(datum)
podpis = sign_xml(xml)
payload = {"schrXml": xml, "schrSign": podpis, "schrFiles": []}
r = session.post(SUBMIT_URL, json=payload, headers={
"Content-Type": "application/json; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Referer": BASE_URL + "/",
})
r.raise_for_status()
try:
resp = r.json()
except Exception:
print(f" Odpověď není JSON: {r.text[:300]}")
return None
resp_str = json.dumps(resp, ensure_ascii=False)
if resp.get("errMsg") or resp.get("error"):
print(f" Chyba od serveru: {resp.get('errMsg') or resp.get('error')}")
return None
m = re.search(r'\b(1[5-9]\d{7})\b', resp_str)
ref = m.group(1) if m else None
if ref:
print(f" OK — ref. číslo: {ref}")
else:
print(f" Odpověď (bez ref. čísla): {resp_str[:300]}")
return ref or ("OK" if r.ok else None)
# ---------------------------------------------------------------------------
# Log
# ---------------------------------------------------------------------------
def uloz_log(datum: date, ref_cislo: str) -> None:
log = []
if os.path.exists(LOG_FILE):
with open(LOG_FILE, encoding="utf-8") as f:
log = json.load(f)
log.append({
"ref_cislo": ref_cislo,
"datum": datum.strftime("%d.%m.%Y"),
"podano_kdy": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
})
with open(LOG_FILE, "w", encoding="utf-8") as f:
json.dump(log, f, indent=2, ensure_ascii=False)
# ---------------------------------------------------------------------------
# Hlavní funkce
# ---------------------------------------------------------------------------
def hlavni() -> None:
# 1. Přihlášení — uloží cookies pro Playwright
prihlaseni()
# 2. Stažení nových výpisů z výpisové schránky
print("\n=== Stahování nových výpisů ===")
stazeno = stahni_nove_vypisy()
print(f"Staženo: {stazeno} souborů.\n")
# 3. Znovu přihlásit — Playwright mohl invalidovat předchozí session
print("=== Znovu přihlašuji před podáním ===")
session = prihlaseni()
# 4. Podání žádosti o výpis ke dnešnímu dni
datum = date.today()
print(f"=== Podávám žádost o výpis ke dni {datum.strftime('%d.%m.%Y')} ===")
ref = odeslat_zadost(session, datum)
if ref:
uloz_log(datum, ref)
print(f"\nHotovo — žádost podána, ref: {ref}")
else:
print("\nPodání selhalo — žádost nebyla zaevidována.")
if __name__ == "__main__":
hlavni()
@@ -0,0 +1,7 @@
[
{
"ref_cislo": "179775825",
"datum": "15.05.2026",
"podano_kdy": "2026-06-17 05:40:28"
}
]
@@ -1,7 +1,7 @@
[
{
"name": "SID",
"value": "01bb61e3cd536ffbf7c4f2b74260466e",
"value": "22319828cc5b7600290e217c8f533ca0",
"domain": ".portal.rbp-zp.cz",
"path": "/",
"expires": -1,
@@ -14,7 +14,7 @@
"value": "CERT",
"domain": ".portal.rbp-zp.cz",
"path": "/",
"expires": 1808541922,
"expires": 1813203627,
"secure": true,
"httpOnly": false,
"sameSite": "Lax"