Compare commits
59 Commits
a5066de467
...
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 | |||
| 7a7c35f778 | |||
| 66addefcf8 | |||
| 4b6e091709 | |||
| bb2973aa6d | |||
| f8b7741f12 | |||
| df36516193 | |||
| 8481a1b6f1 | |||
| fa201467ad | |||
| 9eea870ab6 | |||
| c14a4c21b2 | |||
| 32aabcbe6d | |||
| 1f66388064 | |||
| d0c16e6497 | |||
| bd6272c8d5 | |||
| c12f6ca394 | |||
| e6c740744b |
@@ -10,6 +10,7 @@ __pycache__/
|
||||
|
||||
# Claude Code
|
||||
.claude/worktrees/
|
||||
.claude/settings.local.json
|
||||
|
||||
# Certifikáty (soukromé klíče - nikdy do gitu!)
|
||||
**/*.pfx
|
||||
|
||||
@@ -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,13 @@
|
||||
"""
|
||||
01 - Přihlášení na VZP Point (Inbox)
|
||||
Otevře Chrome, přihlásí se certifikátem a naviguje na schránku zpráv.
|
||||
Okno zůstane otevřené — skript čeká na stisk Enter.
|
||||
01 - Přihlášení na VZP Point (plně automatizované)
|
||||
|
||||
Jak to funguje:
|
||||
1. Nastaví Chrome politiku AutoSelectCertificateForUrls — Chrome vybere
|
||||
certifikát automaticky bez dialogu (certifikát musí být v Windows store)
|
||||
2. Otevře Chrome, klikne na 'Certifikát', počká na přesměrování a uloží cookies
|
||||
|
||||
Certifikát ve Windows store: MUDr. Michaela Buzalková (I.CA EU Qualified CA2, platný do 16.1.2027)
|
||||
|
||||
Použití: python 01_prihlaseni.py
|
||||
"""
|
||||
|
||||
@@ -9,16 +15,33 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import winreg
|
||||
|
||||
INBOX_URL = "https://point.vzp.cz/Inbox/Message"
|
||||
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"))
|
||||
|
||||
# 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 pro vzp.cz."""
|
||||
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:
|
||||
"""Načte dříve uložené cookies (včetně session-only) zpět do kontextu."""
|
||||
if not os.path.exists(COOKIES_FILE):
|
||||
return 0
|
||||
try:
|
||||
@@ -32,7 +55,6 @@ def load_cookies(context) -> int:
|
||||
|
||||
|
||||
def save_cookies(context) -> int:
|
||||
"""Uloží VZP cookies (i session-only) do JSON souboru."""
|
||||
try:
|
||||
all_cookies = context.cookies()
|
||||
vzp = [c for c in all_cookies if "vzp.cz" in c.get("domain", "")]
|
||||
@@ -44,17 +66,6 @@ def save_cookies(context) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _delete_chrome_cert_policy() -> None:
|
||||
"""Smaže AutoSelectCertificateForUrls politiku — Chrome pak zobrazí dialog přirozeně."""
|
||||
key_path = r"SOFTWARE\Policies\Google\Chrome\AutoSelectCertificateForUrls"
|
||||
try:
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, access=winreg.KEY_SET_VALUE)
|
||||
winreg.DeleteValue(key, "1")
|
||||
winreg.CloseKey(key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
@@ -62,7 +73,7 @@ def main() -> None:
|
||||
print("Chybí playwright: pip install playwright && playwright install chrome")
|
||||
sys.exit(1)
|
||||
|
||||
_delete_chrome_cert_policy()
|
||||
_set_chrome_cert_policy()
|
||||
|
||||
with sync_playwright() as p:
|
||||
context = p.chromium.launch_persistent_context(
|
||||
@@ -74,15 +85,10 @@ def main() -> None:
|
||||
args=["--force-renderer-accessibility"],
|
||||
)
|
||||
try:
|
||||
# Načti dříve uložené cookies (vč. session-only) z JSON
|
||||
loaded = load_cookies(context)
|
||||
startup = context.cookies()
|
||||
vzp_start = [c for c in startup if "vzp.cz" in c.get("domain", "")]
|
||||
print(f"Profil: {CHROME_PROFILE}")
|
||||
print(f"Cookies z JSON: {loaded}, VZP v kontextu: {len(vzp_start)}")
|
||||
print(f"Cookies z JSON: {loaded}")
|
||||
|
||||
page = context.new_page()
|
||||
|
||||
print("Naviguji na VZP Point...")
|
||||
try:
|
||||
page.goto(INBOX_URL, wait_until="domcontentloaded", timeout=30_000)
|
||||
@@ -95,34 +101,24 @@ def main() -> None:
|
||||
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(30)
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(" Pokud se zobrazil dialog výběru certifikátu,")
|
||||
print(" vyberte certifikát MUDr. Buzalkové a klikněte OK.")
|
||||
print(" Čekám 60 sekund...")
|
||||
print("=" * 60)
|
||||
|
||||
page = context.new_page()
|
||||
# Čekáme na přesměrování — buď auto-výběr přes politiku, nebo ruční klik
|
||||
try:
|
||||
page.goto(INBOX_URL, wait_until="domcontentloaded", timeout=30_000)
|
||||
except Exception as e:
|
||||
print(f"Navigace po auth: {e}")
|
||||
page.wait_for_url("https://point.vzp.cz/**", timeout=60_000)
|
||||
except Exception:
|
||||
print(f" Timeout čekání na přesměrování. URL: {page.url}")
|
||||
|
||||
if not page.url.startswith("https://point.vzp.cz"):
|
||||
print(f"Přihlášení selhalo. URL: {page.url}")
|
||||
return
|
||||
|
||||
print(f"OK — přihlášení úspěšné. URL: {page.url}")
|
||||
|
||||
# Diagnostika: cookies po auth
|
||||
after = context.cookies()
|
||||
vzp_after = [c for c in after if "vzp.cz" in c.get("domain", "")]
|
||||
print(f"Cookies po auth: VZP: {len(vzp_after)}")
|
||||
for c in vzp_after:
|
||||
exp = c.get("expires", -1)
|
||||
persistent = "PERSISTENT" if exp > 0 else "SESSION-ONLY"
|
||||
# Zkrácený název pro přehlednost
|
||||
name = c["name"][:60]
|
||||
print(f" - {name} ({c['domain']}) [{persistent}]")
|
||||
|
||||
print("Okno zůstane otevřené. Stiskněte Enter pro zavření...")
|
||||
input()
|
||||
print(f"Přihlášení úspěšné. URL: {page.url}")
|
||||
|
||||
finally:
|
||||
saved = save_cookies(context)
|
||||
|
||||
@@ -14,10 +14,13 @@ import winreg
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||
|
||||
INBOX_URL = "https://point.vzp.cz/Inbox/Message"
|
||||
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(os.path.dirname(__file__), "Staženo")
|
||||
DOWNLOAD_DIR = os.path.join(get_dropbox_root(), "Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "111 VZP")
|
||||
|
||||
|
||||
def load_cookies(context) -> int:
|
||||
|
||||
@@ -15,10 +15,13 @@ import winreg
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||
|
||||
INBOX_URL = "https://point.vzp.cz/Inbox/Message"
|
||||
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(os.path.dirname(__file__), "Staženo")
|
||||
DOWNLOAD_DIR = os.path.join(get_dropbox_root(), "Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "111 VZP")
|
||||
|
||||
|
||||
def load_cookies(context) -> int:
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Přihlásí se na VZP Point, stáhne nové zprávy a aktualizuje číselníky.
|
||||
|
||||
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Í:
|
||||
python 04_prihlaseni_a_stahuj_nove.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
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, script],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"Skript {os.path.basename(script)} skončil s chybou (kód {result.returncode})")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=== Přihlášení ===")
|
||||
run(os.path.join(DIR, "01_prihlaseni.py"))
|
||||
|
||||
print("\n=== Stahování nových zpráv ===")
|
||||
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__":
|
||||
main()
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
IČZ: 9305000 - MUDr. Michaela Buzalková
|
||||
Období: 3/2026
|
||||
|
||||
IČP: 9305001 - Ordinace praktického lékaře pro dospělé
|
||||
|
||||
Věková skupina Počet reg. placených poj. Koeficient Počet jednicových pojištěnců
|
||||
15 - 19 let 7 1,06 7,42
|
||||
20 - 24 let 35 0,90 31,50
|
||||
25 - 29 let 32 0,95 30,40
|
||||
30 - 34 let 37 1,00 37,00
|
||||
35 - 39 let 56 1,05 58,80
|
||||
40 - 44 let 54 1,05 56,70
|
||||
45 - 49 let 103 1,10 113,30
|
||||
50 - 54 let 105 1,43 150,15
|
||||
55 - 59 let 60 1,54 92,40
|
||||
60 - 64 let 58 1,59 92,22
|
||||
65 - 69 let 46 1,80 82,80
|
||||
70 - 74 let 52 2,12 110,24
|
||||
75 - 79 let 118 2,54 299,72
|
||||
80 - 84 let 71 3,07 217,97
|
||||
nad 85 let 65 3,60 234,00
|
||||
|
||||
Celkem registrovných placených pojištěnců: 899
|
||||
Celkem jednicových pojištěnců: 1 614,62
|
||||
Navýšení kapitačního paušálu: 1,00 Kč
|
||||
Navýšení kapitační platby: 1 614,62 Kč
|
||||
Degresní koeficient ve výši 0,984831 nebyl uplatněn.
|
||||
|
||||
Navýšení kapitační platby za období: 3/2026 1 614,62 Kč
|
||||
Navýšení kapitační platby za IČZ: 9305000 1 614,62 Kč
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
IÈZ: 9305000 - MUDr. Michaela Buzalková
|
||||
Období: 3/2026
|
||||
|
||||
IÈP: 9305001 - Ordinace praktického lékaøe pro dospìlé
|
||||
|
||||
Vìková skupina Poèet reg. placených poj. Koeficient Poèet jednicových poji¹tìncù
|
||||
15 - 19 let 7 1,06 7,42
|
||||
20 - 24 let 35 0,90 31,50
|
||||
25 - 29 let 32 0,95 30,40
|
||||
30 - 34 let 37 1,00 37,00
|
||||
35 - 39 let 56 1,05 58,80
|
||||
40 - 44 let 54 1,05 56,70
|
||||
45 - 49 let 103 1,10 113,30
|
||||
50 - 54 let 105 1,43 150,15
|
||||
55 - 59 let 60 1,54 92,40
|
||||
60 - 64 let 58 1,59 92,22
|
||||
65 - 69 let 46 1,80 82,80
|
||||
70 - 74 let 52 2,12 110,24
|
||||
75 - 79 let 118 2,54 299,72
|
||||
80 - 84 let 71 3,07 217,97
|
||||
nad 85 let 65 3,60 234,00
|
||||
|
||||
Celkem registrovných placených poji¹tìncù: 899
|
||||
Celkem jednicových poji¹tìncù: 1 614,62
|
||||
Sazba kapitaèního pau¹álu: 76,00 Kè
|
||||
Celková kapitaèní platba: 122 711,12 Kè
|
||||
Degresní koeficient ve vý¹i 0,984831 nebyl uplatnìn.
|
||||
|
||||
Celková kapitaèní platba za období: 3/2026 122 711,12 Kè
|
||||
Celková kapitaèní platba za IÈZ: 9305000 122 711,12 Kè
|
||||
@@ -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,94 +1,87 @@
|
||||
"""
|
||||
01 - Přihlášení na portál VoZP — extrakce cookies (jako admin).
|
||||
Přihlášení na portál VoZP pomocí certifikátu (bez NMSigneru).
|
||||
|
||||
POSTUP:
|
||||
1. Spusť: python 01_prihlaseni.py
|
||||
2. Vyskočí UAC — schval (skript potřebuje admin pro čtení Chrome cookies
|
||||
kvůli App-Bound Encryption v Chrome v130+).
|
||||
3. Otevře se nové okno s běžícím skriptem (admin).
|
||||
4. Skript otevře Chrome (jako user) na VoZP login.
|
||||
5. Přihlas se certifikátem (NMSigner vyskočí — klikni ANO).
|
||||
6. Vrať se k Chrome → ZAVŘI ho (všechna okna).
|
||||
7. Stiskni Enter v admin okně skriptu.
|
||||
8. Cookies se uloží do vozp_cookies.json.
|
||||
JAK TO FUNGUJE:
|
||||
Portál používá challenge-response autentizaci:
|
||||
1. Skript načte přihlašovací stránku → dostane session cookie (SID)
|
||||
2. Požádá server o zprávu k podpisu (challenge s časovým razítkem)
|
||||
3. Podepíše ji certifikátem jako PKCS7/CMS detached signature (RSA + SHA-256)
|
||||
4. Odešle podpis na server → server ověří a vrátí přihlášení
|
||||
5. Výsledné cookies uloží do vozp_cookies.json pro další skripty
|
||||
|
||||
POŽADAVKY:
|
||||
pip install rookiepy
|
||||
POUŽITÍ:
|
||||
pip install requests cryptography
|
||||
python 01_prihlaseni.py
|
||||
|
||||
CERTIFIKÁT:
|
||||
Exportuj z Windows: certmgr.msc → Osobní → pravý klik → Exportovat
|
||||
Formát: PKCS #12 (.PFX), bez CA řetězu
|
||||
Uložit do: ../../Certificates/MBQualifiedCert.pfx
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
LOGIN_URL = "https://portal.vozp.cz/app/prihlaseni"
|
||||
INBOX_URL = "https://portal.vozp.cz/app/prehled-zprav-ve-schrankach"
|
||||
import requests
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.serialization import pkcs7, pkcs12
|
||||
|
||||
PFX_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../Certificates/MBQualifiedCert.pfx"))
|
||||
PFX_PASSWORD = b"Vlado7309208104++"
|
||||
COOKIES_FILE = os.path.abspath(os.path.join(os.path.dirname(__file__), "vozp_cookies.json"))
|
||||
|
||||
|
||||
def is_admin() -> bool:
|
||||
try:
|
||||
return ctypes.windll.shell32.IsUserAnAdmin() != 0
|
||||
except Exception:
|
||||
return False
|
||||
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"
|
||||
|
||||
|
||||
def relaunch_as_admin() -> None:
|
||||
print("Skript potřebuje admin práva (Chrome v130+ App-Bound Encryption).")
|
||||
print("Otevírám UAC...")
|
||||
params = " ".join(f'"{a}"' for a in sys.argv)
|
||||
ret = ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "runas", sys.executable, params, None, 1
|
||||
def main() -> None:
|
||||
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 + "/",
|
||||
})
|
||||
|
||||
# 1. Načti login stránku → SID cookie
|
||||
r = session.get(f"{BASE_URL}/app/prihlaseni")
|
||||
r.raise_for_status()
|
||||
session.cookies.set("pzp_sign", "CERT", domain="portal.vozp.cz", path="/")
|
||||
|
||||
# 2. Získej challenge
|
||||
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"]
|
||||
|
||||
# 3. Podepis (PKCS7 detached, RSA + SHA-256, bez CA řetězu)
|
||||
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()
|
||||
)
|
||||
if ret <= 32:
|
||||
print(f"UAC selhalo (kód {ret}). Spusť ručně jako admin.")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
# 4. Přihlas se
|
||||
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"]
|
||||
|
||||
def open_chrome_as_user(url: str) -> None:
|
||||
"""Spustí default browser jako standardní user, i když Python běží jako admin."""
|
||||
# explorer.exe běží jako user a otevře URL v defaultním prohlížeči
|
||||
try:
|
||||
subprocess.Popen(["explorer.exe", url])
|
||||
if not data.get("prihlasen"):
|
||||
print(f"Přihlášení selhalo: {r.json()['errMsg']}")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
# Fallback
|
||||
import webbrowser
|
||||
webbrowser.open(url)
|
||||
|
||||
print(f"Přihlášení úspěšné — {data.get('url', '')}")
|
||||
|
||||
def read_cookies_rookiepy() -> list[dict]:
|
||||
import rookiepy
|
||||
raw = rookiepy.chrome(["vozp.cz", ".vozp.cz", "portal.vozp.cz", "portalzp.cz"])
|
||||
out = []
|
||||
for c in raw:
|
||||
dom = c["domain"]
|
||||
if not any(d in dom for d in ["vozp.cz", "portalzp.cz"]):
|
||||
continue
|
||||
out.append({
|
||||
"name": c["name"],
|
||||
"value": c["value"],
|
||||
"domain": dom if dom.startswith(".") else "." + dom,
|
||||
"path": c.get("path") or "/",
|
||||
"expires": int(c["expires"]) if c.get("expires") else -1,
|
||||
"secure": bool(c.get("secure", False)),
|
||||
"httpOnly": bool(c.get("http_only", False)),
|
||||
"sameSite": "Lax",
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def read_cookies_browser_cookie3() -> list[dict]:
|
||||
import browser_cookie3
|
||||
cj = browser_cookie3.chrome(domain_name="vozp.cz")
|
||||
out = []
|
||||
for c in cj:
|
||||
if not any(d in c.domain for d in ["vozp.cz", "portalzp.cz"]):
|
||||
continue
|
||||
out.append({
|
||||
# 5. Ulož cookies
|
||||
cookies = [
|
||||
{
|
||||
"name": c.name,
|
||||
"value": c.value,
|
||||
"domain": c.domain if c.domain.startswith(".") else "." + c.domain,
|
||||
@@ -97,70 +90,12 @@ def read_cookies_browser_cookie3() -> list[dict]:
|
||||
"secure": bool(c.secure),
|
||||
"httpOnly": False,
|
||||
"sameSite": "Lax",
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not is_admin():
|
||||
relaunch_as_admin()
|
||||
return # nedosažitelné
|
||||
|
||||
print("=" * 60)
|
||||
print("BĚŽÍM JAKO ADMIN — PŘIHLÁŠENÍ DO VoZP")
|
||||
print("=" * 60)
|
||||
print(f"1. Otevírám Chrome (jako user) na: {LOGIN_URL}")
|
||||
print("2. Přihlas se certifikátem (NMSigner → klikni ANO)")
|
||||
print("3. Po přihlášení ZAVŘI Chrome úplně (všechna okna)")
|
||||
print("4. Vrať se sem a stiskni Enter")
|
||||
print("=" * 60)
|
||||
|
||||
open_chrome_as_user(LOGIN_URL)
|
||||
|
||||
input("\nAž jsi přihlášen/a a Chrome ZAVŘENÝ, stiskni Enter...")
|
||||
|
||||
cookies: list[dict] = []
|
||||
last_err = ""
|
||||
|
||||
print("\nČtu cookies (jako admin)...")
|
||||
try:
|
||||
cookies = read_cookies_rookiepy()
|
||||
print(f" rookiepy OK: {len(cookies)} cookies")
|
||||
except ImportError:
|
||||
last_err = "rookiepy není nainstalovaný"
|
||||
print(f" {last_err} — zkouším browser_cookie3...")
|
||||
except Exception as e:
|
||||
last_err = f"rookiepy: {e}"
|
||||
print(f" rookiepy selhalo: {e} — zkouším browser_cookie3...")
|
||||
|
||||
if not cookies:
|
||||
try:
|
||||
cookies = read_cookies_browser_cookie3()
|
||||
print(f" browser_cookie3 OK: {len(cookies)} cookies")
|
||||
except ImportError:
|
||||
print(" browser_cookie3 není nainstalovaný.")
|
||||
except Exception as e:
|
||||
print(f" browser_cookie3: {e}")
|
||||
|
||||
if not cookies:
|
||||
print("\nCookies se nepodařilo přečíst ani jako admin.")
|
||||
print("Zkontroluj, že:")
|
||||
print(" - Chrome je úplně zavřený (Task Manager → žádný chrome.exe)")
|
||||
print(" - Jsi se na VoZP přihlásil/a (DB má nové záznamy)")
|
||||
print(" - Máš nainstalované: pip install rookiepy browser-cookie3")
|
||||
input("\nEnter pro zavření...")
|
||||
sys.exit(1)
|
||||
|
||||
}
|
||||
for c in session.cookies
|
||||
]
|
||||
with open(COOKIES_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(cookies, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\nUloženo {len(cookies)} cookies do {COOKIES_FILE}")
|
||||
for c in cookies:
|
||||
exp = c["expires"]
|
||||
tag = "PERSISTENT" if exp > 0 else "SESSION"
|
||||
print(f" - {c['name'][:50]} ({c['domain']}) [{tag}]")
|
||||
|
||||
input("\nHotovo. Enter pro zavření...")
|
||||
print(f"Uloženo {len(cookies)} cookies → {COOKIES_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -14,7 +14,8 @@ import winreg
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import requests as req
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||
|
||||
LOGIN_URL = "https://portal.vozp.cz/app/prihlaseni"
|
||||
BASE_URL = "https://portal.vozp.cz"
|
||||
@@ -23,7 +24,7 @@ DOWNLOAD_URL = f"{BASE_URL}/html/prehled-zprav-ve-schrankach/zobrazit-prilohu"
|
||||
|
||||
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__), "vozp_cookies.json"))
|
||||
DOWNLOAD_DIR = os.path.join(os.path.dirname(__file__), "Staženo")
|
||||
DOWNLOAD_DIR = os.path.join(get_dropbox_root(), "Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "201 VoZP")
|
||||
|
||||
# Všechny schránky — ID-segment : zobrazovaný název
|
||||
SCHRANKY = {
|
||||
@@ -150,41 +151,28 @@ def collect_rows(page) -> list[dict]:
|
||||
return [r for r in data if r["fileId"]]
|
||||
|
||||
|
||||
def make_requests_session(context) -> req.Session:
|
||||
"""Vytvoří requests.Session se cookies z Playwright kontextu."""
|
||||
session = req.Session()
|
||||
for c in context.cookies():
|
||||
session.cookies.set(
|
||||
c["name"], c["value"],
|
||||
domain=c.get("domain", "").lstrip(".")
|
||||
)
|
||||
session.headers.update({
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": INBOX_URL,
|
||||
})
|
||||
return session
|
||||
|
||||
|
||||
def download_file(session: req.Session, file_id: str, target: str) -> bool:
|
||||
"""Stáhne soubor přes přímý HTTP požadavek. Vrací True při úspěchu."""
|
||||
def download_file(context, file_id: str, target: str) -> bool:
|
||||
"""Stáhne soubor přes Playwright request context (sdílí cookies se session)."""
|
||||
url = f"{DOWNLOAD_URL}?zprava_id={file_id}"
|
||||
try:
|
||||
r = session.get(url, timeout=30, stream=True)
|
||||
r.raise_for_status()
|
||||
r = context.request.get(url, headers={"Referer": INBOX_URL})
|
||||
if not r.ok:
|
||||
print(f" HTTP {r.status} (id={file_id})")
|
||||
return False
|
||||
with open(target, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
f.write(r.body())
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" Chyba stahování (id={file_id}): {e}")
|
||||
return False
|
||||
|
||||
|
||||
def process_schránka(page, session: req.Session, segment: str, name: str, already: set) -> tuple[int, int]:
|
||||
def process_schránka(page, context, segment: str, name: str, already: set) -> tuple[int, int]:
|
||||
"""Projde všechny stránky schránky a stáhne soubory. Vrací (staženo, přeskočeno)."""
|
||||
downloaded = 0
|
||||
skipped = 0
|
||||
page_num = 1
|
||||
seen_ids: set = set()
|
||||
|
||||
while True:
|
||||
url = f"{INBOX_URL}/{segment}/stranka-{page_num}"
|
||||
@@ -202,6 +190,13 @@ def process_schránka(page, session: req.Session, segment: str, name: str, alrea
|
||||
print(f" Stránka {page_num} — žádné řádky, končím schránku.")
|
||||
break
|
||||
|
||||
# Portál vrátí poslední stránku znovu místo prázdné — detekuj opakování
|
||||
current_ids = {r["fileId"] for r in rows}
|
||||
if current_ids & seen_ids:
|
||||
print(f" Stránka {page_num} — opakující se obsah, končím schránku.")
|
||||
break
|
||||
seen_ids.update(current_ids)
|
||||
|
||||
print(f" Nalezeno {len(rows)} zpráv.")
|
||||
|
||||
for row in rows:
|
||||
@@ -213,19 +208,11 @@ def process_schránka(page, session: req.Session, segment: str, name: str, alrea
|
||||
continue
|
||||
|
||||
print(f" Stahuji: {info['filename']}")
|
||||
if download_file(session, row["fileId"], target):
|
||||
if download_file(context, row["fileId"], target):
|
||||
already.add(info["filename"])
|
||||
downloaded += 1
|
||||
time.sleep(0.3)
|
||||
|
||||
# Zkontroluj, jestli existuje další stránka
|
||||
has_next = page.evaluate("""() => {
|
||||
return !!Array.from(document.querySelectorAll('a')).find(
|
||||
a => a.innerText.trim() === 'Další stránka' && !a.closest('[aria-disabled]')
|
||||
);
|
||||
}""")
|
||||
if not has_next:
|
||||
break
|
||||
page_num += 1
|
||||
|
||||
return downloaded, skipped
|
||||
@@ -268,6 +255,7 @@ def main() -> None:
|
||||
ignore_https_errors=True,
|
||||
args=["--force-renderer-accessibility"],
|
||||
)
|
||||
logged_in = False
|
||||
try:
|
||||
loaded = load_cookies(context)
|
||||
print(f"Cookies načtené z JSON: {loaded}")
|
||||
@@ -277,7 +265,7 @@ def main() -> None:
|
||||
if not ensure_logged_in(page, context):
|
||||
return
|
||||
|
||||
session = make_requests_session(context)
|
||||
logged_in = True
|
||||
already = set(os.listdir(DOWNLOAD_DIR))
|
||||
print(f"V archivu: {len(already)} souborů.\n")
|
||||
|
||||
@@ -286,7 +274,7 @@ def main() -> None:
|
||||
|
||||
for segment, name in SCHRANKY.items():
|
||||
print(f"\n=== Schránka: {name} ===")
|
||||
dl, sk = process_schránka(page, session, segment, name, already)
|
||||
dl, sk = process_schránka(page, context, segment, name, already)
|
||||
print(f" Schránka {name}: staženo {dl}, přeskočeno {sk}")
|
||||
total_dl += dl
|
||||
total_skip += sk
|
||||
@@ -295,8 +283,9 @@ def main() -> None:
|
||||
print(f"Hotovo. Celkem staženo: {total_dl}, přeskočeno: {total_skip}")
|
||||
|
||||
finally:
|
||||
saved = save_cookies(context)
|
||||
print(f"Uloženo {saved} VoZP cookies.")
|
||||
if logged_in:
|
||||
saved = save_cookies(context)
|
||||
print(f"Uloženo {saved} VoZP cookies.")
|
||||
context.close()
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ from pathlib import Path
|
||||
|
||||
import requests as req
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
|
||||
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||
|
||||
LOGIN_URL = "https://portal.vozp.cz/app/prihlaseni"
|
||||
BASE_URL = "https://portal.vozp.cz"
|
||||
INBOX_URL = f"{BASE_URL}/app/prehled-zprav-ve-schrankach"
|
||||
@@ -24,7 +27,7 @@ DOWNLOAD_URL = f"{BASE_URL}/html/prehled-zprav-ve-schrankach/zobrazit-prilohu"
|
||||
|
||||
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__), "vozp_cookies.json"))
|
||||
DOWNLOAD_DIR = os.path.join(os.path.dirname(__file__), "Staženo")
|
||||
DOWNLOAD_DIR = os.path.join(get_dropbox_root(), "Ordinace", "Dokumentace_ke_zpracování", "Zúčtovací zprávy", "201 VoZP")
|
||||
|
||||
SCHRANKY = {
|
||||
"171-schranka-poskytovatele-zdravotnich-sluzeb": "Schránka PZS",
|
||||
@@ -264,6 +267,7 @@ def main() -> None:
|
||||
ignore_https_errors=True,
|
||||
args=["--force-renderer-accessibility"],
|
||||
)
|
||||
logged_in = False
|
||||
try:
|
||||
loaded = load_cookies(context)
|
||||
print(f"Cookies načtené z JSON: {loaded}")
|
||||
@@ -273,6 +277,7 @@ def main() -> None:
|
||||
if not ensure_logged_in(page, context):
|
||||
return
|
||||
|
||||
logged_in = True
|
||||
session = make_requests_session(context)
|
||||
already = set(os.listdir(DOWNLOAD_DIR))
|
||||
print(f"V archivu: {len(already)} souborů.\n")
|
||||
@@ -289,8 +294,9 @@ def main() -> None:
|
||||
print(f"Hotovo. Celkem nových souborů: {total_dl}")
|
||||
|
||||
finally:
|
||||
saved = save_cookies(context)
|
||||
print(f"Uloženo {saved} VoZP cookies.")
|
||||
if logged_in:
|
||||
saved = save_cookies(context)
|
||||
print(f"Uloženo {saved} VoZP cookies.")
|
||||
context.close()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Přihlásí se na portál VoZP a stáhne nové zprávy.
|
||||
|
||||
Kombinuje 01_prihlaseni.py + 03_stahuj_nove.py do jednoho spuštění.
|
||||
|
||||
POUŽITÍ:
|
||||
python 04_prihlaseni_a_stahuj_nove.py
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def run(script: str) -> None:
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(DIR, script)],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"Skript {script} skončil s chybou (kód {result.returncode})")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=== Přihlášení ===")
|
||||
run("01_prihlaseni.py")
|
||||
|
||||
print("\n=== Stahování nových zpráv ===")
|
||||
run("03_stahuj_nove.py")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,146 @@
|
||||
# VoZP — Automatické stahování zpráv
|
||||
|
||||
## Co to dělá
|
||||
|
||||
Dva skripty které se přihlásí na portál VoZP a stáhnou všechny zprávy ze schránek:
|
||||
|
||||
1. `01_prihlaseni.py` — přihlásí se certifikátem, uloží cookies do `vozp_cookies.json`
|
||||
2. `02_stahuj_vse.py` — použije cookies, projde všechny schránky, stáhne soubory do `Staženo/`
|
||||
|
||||
---
|
||||
|
||||
## Jak funguje přihlášení (challenge-response)
|
||||
|
||||
Portál **nepoužívá heslo** — autentizuje certifikátem přes 4 kroky:
|
||||
|
||||
### Krok 1 — Získej session
|
||||
```
|
||||
GET https://portal.vozp.cz/app/prihlaseni
|
||||
→ server nastaví cookie: SID=...
|
||||
```
|
||||
Ručně nastav cookie: `pzp_sign=CERT` (říká serveru, že chceš certifikát)
|
||||
|
||||
### Krok 2 — Získej challenge
|
||||
```
|
||||
POST /json-api/prihlaseni/prihlasovaci-zprava
|
||||
Body: {"login_sign": "CERT"}
|
||||
|
||||
Odpověď:
|
||||
{
|
||||
"data": {
|
||||
"zprava": "Prohlášení:\r\nTímto se přihlašuji k Portálu VOZP ČR\r\n\r\nOkamžik vygenerování ... mikrosekundu: 20.04.2026 13:22:34.058119"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Krok 3 — Podepis certifikátem
|
||||
Zprávu z kroku 2 podepíš jako **PKCS7 / CMS SignedData**:
|
||||
- Algoritmus: **RSA + SHA-256**
|
||||
- Typ: **DetachedSignature** (obsah není vložen do podpisu)
|
||||
- **BEZ CA řetězu** — pouze end-entity certifikát, žádné CA certifikáty
|
||||
|
||||
```python
|
||||
podpis = (
|
||||
pkcs7.PKCS7SignatureBuilder()
|
||||
.set_data(zprava.encode("utf-8"))
|
||||
.add_signer(cert, private_key, hashes.SHA256())
|
||||
.sign(Encoding.PEM, [PKCS7Options.DetachedSignature])
|
||||
)
|
||||
```
|
||||
|
||||
> ⚠️ Přidání CA řetězu způsobí chybu "neznámý klientský certifikát" — i když thumbprint sedí!
|
||||
|
||||
### Krok 4 — Přihlášení
|
||||
```
|
||||
POST /json-api/prihlaseni/prihlaseni-certifikatem
|
||||
Body: {
|
||||
"zprava": "<původní challenge text>",
|
||||
"podpis": "-----BEGIN PKCS7-----\n...\n-----END PKCS7-----"
|
||||
}
|
||||
|
||||
Úspěch: {"data": {"prihlasen": true, "url": "/app/uvodni-stranka"}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Certifikát
|
||||
|
||||
| Položka | Hodnota |
|
||||
|---|---|
|
||||
| Soubor | `U:\ordinaceprojekt\Insurance\Certificates\MBQualifiedCert.pfx` |
|
||||
| Vlastník | MUDr. Michaela Buzalková |
|
||||
| Vydavatel | I.CA EU Qualified CA2/RSA 06/2022 |
|
||||
| Platnost | do 2027-01-16 |
|
||||
| Thumbprint | `056ED80A3CDDE31DD36EECE0181B4E78D61122A7` |
|
||||
| Serial (hex) | `0247068517B0049E2E` |
|
||||
|
||||
Jak exportovat správný certifikát z Windows:
|
||||
```powershell
|
||||
# Najdi certifikát podle thumbprintu
|
||||
$cert = Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.Thumbprint -eq "056ED80A3CDDE31DD36EECE0181B4E78D61122A7"}
|
||||
$pwd = Read-Host "Heslo" -AsSecureString
|
||||
Export-PfxCertificate -Cert $cert -FilePath "MBQualifiedCert.pfx" -Password $pwd
|
||||
```
|
||||
|
||||
> Při exportu: PKCS #12, **nezaškrtávej** "Include all certificates in the certification path"
|
||||
|
||||
---
|
||||
|
||||
## Stahování souborů
|
||||
|
||||
`02_stahuj_vse.py` používá **Playwright** (viditelný Chrome) pro navigaci + `context.request.get()` pro stahování.
|
||||
|
||||
Endpoint stahování:
|
||||
```
|
||||
GET /html/prehled-zprav-ve-schrankach/zobrazit-prilohu?zprava_id={id}
|
||||
```
|
||||
|
||||
### Schránky které se prochází
|
||||
|
||||
| Segment URL | Název |
|
||||
|---|---|
|
||||
| `171-schranka-poskytovatele-zdravotnich-sluzeb` | Schránka PZS |
|
||||
| `183-schranka-klientu-portalu` | Schránka klientů portálu |
|
||||
| `185-schranka-pzs` | Schránka PZS2 |
|
||||
| `187-schranka-klienta` | Schránka klienta |
|
||||
| `198-vypis-registrovanych-pacientu` | Výpis registrovaných pacientů |
|
||||
| `200-zuctovaci-zpravy` | Zúčtovací zprávy |
|
||||
| `205-vypis-osobnich-uctu-pojistencu` | Výpis osobních účtů pojištěnců |
|
||||
|
||||
### Paginace — důležité
|
||||
|
||||
Portál při přetečení stránek (např. stranka-16 když existuje jen 15) **vrátí poslední stránku znovu** místo prázdné. Detekce: sleduj `fileId` napříč stránkami, zastav při opakování.
|
||||
|
||||
```python
|
||||
current_ids = {r["fileId"] for r in rows}
|
||||
if current_ids & seen_ids:
|
||||
break # opakující se obsah = konec
|
||||
seen_ids.update(current_ids)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pojmenování stažených souborů
|
||||
|
||||
```
|
||||
YYYY-MM-DD Popis zprávy (původní_název_souboru).přípona
|
||||
```
|
||||
Příklad: `2026-04-15 Odpověď na podání č. 176867777 (Protokol-OK).html`
|
||||
|
||||
---
|
||||
|
||||
## Závislosti
|
||||
|
||||
```
|
||||
pip install requests cryptography playwright
|
||||
playwright install chrome
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spuštění
|
||||
|
||||
```bash
|
||||
python 01_prihlaseni.py # přihlásí, uloží cookies
|
||||
python 02_stahuj_vse.py # stáhne vše nové
|
||||
```
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
+98
@@ -0,0 +1,98 @@
|
||||
H0930500100097260211
|
||||
I 120ćÖMA LUD·K 011125018309032021201
|
||||
I 220ODLASOVµ KRISTíNA 025313312212052021201
|
||||
I 315LADISLAVOVµ ERIKA 065809508630102025201
|
||||
I 415KüÖ¦OVµ TEREZA 085701508214092025201
|
||||
I 585LE¦µKOVµ JANA 365429074 01112016201
|
||||
I 685FAM·ROVµ MARIE 395815085 01112016201
|
||||
I 785KO曵KOVµ MARIE 396015429 01112016201
|
||||
I 885HEUSCHNEIDER ARNOćT 400406144 23062020201
|
||||
I 985FAM·RA JIüÖ 400424003 01112016201
|
||||
I 1085KAMENÖK JAROSLAV 400510088 22022023201
|
||||
I 1185HOLE¬KOVµ JARMILA 405101482 07022023201
|
||||
I 1280JAKUćOVµ •UDMILA 415817101 01112016201
|
||||
I 1380HAMERNÖK JOSEF 420622031 01112016201
|
||||
I 1480SKALOVµ ALENA 435522111 01112016201
|
||||
I 1580RADOVµ MARIE 436120039 01112016201
|
||||
I 1680KOTíNEK STANISLAV 440201053 08032019201
|
||||
I 1780HAMERNÖKOVµ EMILIE 445306004 01112016201
|
||||
I 1880KUSµKOVµ JAROSLAVA 445318078 01112016201
|
||||
I 1980DVORSKµ V·RA 455429053 01112016201
|
||||
I 2080PECHROVµ MILUćKA 456111032 01112016201
|
||||
I 2180SMETANOVµ MARIE 456114025 01112016201
|
||||
I 2275ć›ASTNí LIBOR 461001479 25022022201
|
||||
I 2375ćESTµKOVµ MIROSLAVA 465810001 01112016201
|
||||
I 2475MUZIKµüOVµ JAROSLAVA 475123056 01112016201
|
||||
I 2575ULRICHOVµ VLASTA 476208033 01112016201
|
||||
I 2675PROCHµZKOVµ JAROSLAVA 476210075 01112016201
|
||||
I 2775PERNICOVµ MIROSLAVA 486023438 01112016201
|
||||
I 2875BIL¬IK ćT·PµN 491030140 01112016201
|
||||
I 2975DROZD FRANTIćEK 500504119 01072017201
|
||||
I 3075KOTíNKOVµ VLASTA 505408284 15012019201
|
||||
I 3175MARHOULOVµ JITKA 506022198 01112016201
|
||||
I 3270ćOTOLA RADOMÖR 510214143 12042021201
|
||||
I 3370BIL¬IKOVµ JANA 516110008 01112016201
|
||||
I 3470NIMEüICKµ IRENA 536117083 01012026201
|
||||
I 3570PROD·LAL JIüÖ 550418077001112016201
|
||||
I 3665DOLE¦AL VµCLAV 560731079702102018201
|
||||
I 3765MEDKOVµ HEDVIKA 585204184401012016201
|
||||
I 3865ZVµROVµ MARIE 586205138201112016201
|
||||
I 3965ćABRćULA KAREL 600321077421102021201
|
||||
I 4065ćULC JIüÖ 600804024701112016201
|
||||
I 4160VAN·¬EK KAREL 630920061221082025201
|
||||
I 4260KOUDA MIROSLAV 630927233201112016201
|
||||
I 4360LEKSOVµ IVA 645902155801012018201
|
||||
I 4460ćABRćULOVµ ALENA 646123109521102021201
|
||||
I 4560POU¬KOVµ DANA 655311038801112016201
|
||||
I 4660KURKOVµ DANIELA 656201029012112024201
|
||||
I 4755TU¬KOVµ MARIE 665716055420092018201
|
||||
I 4855POSPÖćIL JIüÖ 670810111401112016201
|
||||
I 4955PINTÖü MILAN 680730037901112016201
|
||||
I 5055AUBRECHTOVµ JITKA 685110042101112016201
|
||||
I 5155MILATOVµ MARTINA 685322207901112016201
|
||||
I 5255ćÖMOVµ HELENA 686024155320052019201
|
||||
I 5355KITTLER RADEK 690415049501112016201
|
||||
I 5455KUBELKA DAVID 690511008001112016201
|
||||
I 5555RATKIEWICZOVµ ROMANA 705518041801112016201
|
||||
I 5655TŢMOVµ RENµTA 705908762901112016201
|
||||
I 5750¬ELÖKOVSKµ HANA 715428039701112016201
|
||||
I 5850KUBELKOVµ BLANKA 716221004401112016201
|
||||
I 5950SLABí MICHAL 720117008601112016201
|
||||
I 6050GIERTLI PETER 721207752101112016201
|
||||
I 6150KüÖ¦ JIüÖ 730628019001112016201
|
||||
I 6250VOJµ¬EK ALEć 730930044901112016201
|
||||
I 6350STRNAD JIüÖ 740417599901112016201
|
||||
I 6450UNGER MICHAL 740612045801112016201
|
||||
I 6550NE¬AS VµCLAV 740821184401112016201
|
||||
I 6650ćÖPKOVSKí MARTIN 740822042401112016201
|
||||
I 6750ćµMAL ROMAN 741107045801112016201
|
||||
I 6850HEJKAL VµCLAV 750331043001112016201
|
||||
I 6950MAJTµŐ PETR 750604586601112016201
|
||||
I 7045MAJTµŐOVµ RADKA 765711592801112016201
|
||||
I 7145DRAKSEL DANIEL 770627006601112016201
|
||||
I 7245YATES HANA 775503537601112016201
|
||||
I 7345JANYćKOVµ LUCIE 775806551301012020201
|
||||
I 7445ODLASOVµ DENISA 776231028101112016201
|
||||
I 7545HOMOLKOVA HANNA 795108998201112016201
|
||||
I 7645NIMEüICKí PETR 801212446101112016201
|
||||
I 7745LADISLAVOVµ ERIKA 805111445630102025201
|
||||
I 7840KüÖ¦ MICHAL 830107055801112016201
|
||||
I 7940FIćEROVµ PETRA 835602011201112016201
|
||||
I 8040EKHARD PETRA 836202290001112016201
|
||||
I 8140MI¬ULKA JAN 841217512301112016201
|
||||
I 8240ćKOCH PETR 850220385331072019201
|
||||
I 8340BARTEJSOVµ PLA¬KOVµ PETRA 865102016904102017201
|
||||
I 8435VOćICKµ DAGMAR 866021042701112016201
|
||||
I 8535POU¬EK KAREL 870110039701112016201
|
||||
I 8635JEüµBKOVµ JANA 885809049401072023201
|
||||
I 8735MOU¬KA ZDEN·K 891028042301112016201
|
||||
I 8835RATKIEWICZOVµ ROMANA 905415112801112016201
|
||||
I 8935PAVLŢ JANA 905709176905042018201
|
||||
I 9035¬ELÖKOVSKµ KRISTíNA 905905073701112016201
|
||||
I 9135ćABRćULOVµ ANNA 906110008124032022201
|
||||
I 9230BRYNDA¬OVµ NIKOLA 916211043001112016201
|
||||
I 9330TU¬EK JIüÖ 930918040815092022201
|
||||
I 9430PROSÖKOVµ JACQUELINE 935512017001072017201
|
||||
I 9530ZEMµNEK JAN 950106248327082024201
|
||||
I 9625MAJTµŐ RADIM 960425624431032022201
|
||||
I 9725SµGNEROVµ KRISTíNA 975927357915042025201
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user