diff --git a/Insurance/SeznamPojistencu/zadej_davku.py b/Insurance/SeznamPojistencu/zadej_davku.py new file mode 100644 index 0000000..20a3a96 --- /dev/null +++ b/Insurance/SeznamPojistencu/zadej_davku.py @@ -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""" + + + + {id_zpravy} + + {ICZ} + + + + + + {id_zpravy} + {ICZ} + zp + + + {mesic} + {rok} + + {format_vystupu} + + + + +""" + +# ── 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) diff --git a/Insurance/StahováníZpráv/111 VZP/04_prihlaseni_a_stahuj_nove.py b/Insurance/StahováníZpráv/111 VZP/04_prihlaseni_a_stahuj_nove.py index 07ac8a9..b190f81 100644 --- a/Insurance/StahováníZpráv/111 VZP/04_prihlaseni_a_stahuj_nove.py +++ b/Insurance/StahováníZpráv/111 VZP/04_prihlaseni_a_stahuj_nove.py @@ -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,29 @@ 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í číselníků VZP ===") + run(CISELNIKY_SCRIPT) if __name__ == "__main__": diff --git a/Recepty/StahovánízVZPWithClaude/01_stahni_ciselniky.py b/Recepty/StahovánízVZPWithClaude/01_stahni_ciselniky.py new file mode 100644 index 0000000..61610f7 --- /dev/null +++ b/Recepty/StahovánízVZPWithClaude/01_stahni_ciselniky.py @@ -0,0 +1,289 @@ +""" +01 — Stáhni číselníky VZP z point.vzp.cz/Cms/Document (sekce „Číselníky"). +Soubory přejmenuje do formátu: YYYY-MM-DD NázevLinku (popis).zip +Ověří co již existuje v cílovém adresáři a stáhne pouze chybějící soubory. +Použití: python 01_stahni_ciselniky.py [--dry-run] +""" + +import json +import os +import re +import sys + +# Windows konzole - povol UTF-8 výstup +if sys.stdout.encoding != "utf-8": + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "Knihovny")) +from najdi_dropbox import get_dropbox_root + +TARGET_DIR = os.path.join( + get_dropbox_root(), + "Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "Číselníky" +) + +DRY_RUN = False # True = pouze zobraz co by se stáhlo, nic nestahuj + +CHROME_PROFILE = os.path.join(os.path.dirname(__file__), "chrome_profile") +COOKIES_FILE = os.path.join(os.path.dirname(__file__), "vzp_cookies.json") + +# Issuer CN certifikátu v Windows store (CurrentUser\My) +CERT_ISSUER_CN = "I.CA Public CA/RSA 06/2022" + + +def _set_chrome_cert_policy() -> None: + """Nastaví Chrome politiku AutoSelectCertificateForUrls — Chrome vybere certifikát automaticky.""" + import winreg + 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 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 as e: + print(f" Chyba při načítání cookies: {e}") + return 0 + + +def save_cookies(context) -> int: + 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) + return len(vzp) + except Exception as e: + print(f" Chyba při ukládání cookies: {e}") + return 0 + + +def sanitize_filename(name: str) -> str: + """Odstraní znaky nepovolené ve Windows názvech souborů.""" + return re.sub(r'[\\/:*?"<>|]', '_', name).strip() + + +# True = přidat popis v závorce do názvu souboru +SECTIONS = { + "Číselníky": True, + "Soubor platných IČP": False, +} + + +def extract_entries(page) -> list[dict]: + """ + Projde tabulky sekcí definovaných v SECTIONS a vrátí seznam slovníků: + orig_fname — původní název souboru z atributu download + new_fname — nový název: YYYY-MM-DD Název [(popis)].zip + link — Playwright Locator pro kliknutí + """ + entries = [] + for section, include_description in SECTIONS.items(): + rows = page.locator(f"h3:has-text('{section}') + table tbody tr").all() + entries.extend(_extract_rows(rows, include_description=include_description)) + return entries + + +def _extract_rows(rows: list, include_description: bool = True) -> list[dict]: + entries = [] + i = 0 + while i < len(rows): + row = rows[i] + dl_links = row.locator("a[download]").all() + + if not dl_links: + i += 1 + continue + + # Datum platnosti z