diff --git a/Insurance/StahováníSeznamuPojištěnců/111 VZP/NOTES.md b/Insurance/StahováníSeznamuPojištěnců/111 VZP/NOTES.md new file mode 100644 index 0000000..5a7676e --- /dev/null +++ b/Insurance/StahováníSeznamuPojištěnců/111 VZP/NOTES.md @@ -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": }` + +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é. diff --git a/Insurance/StahováníSeznamuPojištěnců/111 VZP/StahniSeznamPojistencuVZP.py b/Insurance/StahováníSeznamuPojištěnců/111 VZP/StahniSeznamPojistencuVZP.py new file mode 100644 index 0000000..b13b173 --- /dev/null +++ b/Insurance/StahováníSeznamuPojištěnců/111 VZP/StahniSeznamPojistencuVZP.py @@ -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