# Vykony — stahování a zpracování zdravotních výkonů
## Přehled
Kompletní pipeline pro stahování zdravotních výkonů ze **szv.mzd.gov.cz** do MongoDB,
včetně detailů každého výkonu a exportu do XLSX reportu.
```
szv.mzd.gov.cz
│
├── /Vykon/ → stahni_vykony.py → MongoDB: vykony + vykony_historie
└── /Vykon/Detail/{id}/ → stahni_detaily.py → MongoDB: detaily + detaily_historie
↓
report_vykony.py → vykony_report.xlsx
```
---
## Skripty
| Skript | Co dělá |
|--------|---------|
| `stahni_vykony.py` | Stahuje seznam všech výkonů (přehledová data), ukládá do `vykony` s historií |
| `stahni_detaily.py` | Stahuje detailní stránku každého výkonu, ukládá do `detaily` s historií |
| `report_vykony.py` | Exportuje výkony + detaily do XLSX, jeden list na odbornost |
| `debug_detail.py` | Ladící skript — stáhne HTML jednoho výkonu a vypíše co parser vidí |
### Doporučené pořadí spuštění
```bash
python stahni_vykony.py # 1. seznam výkonů (~4246 kusů, ~1 min)
python stahni_detaily.py # 2. detaily (~4246 HTTP requestů, ~10–20 min)
python report_vykony.py # 3. export do XLSX
```
---
## Nastavení skriptů
Každý skript má na začátku blok konfiguračních proměnných — **není potřeba sahat do kódu**:
### `stahni_detaily.py`
```python
KOLIK = 0 # 0 = vše; jinak max. počet ke stažení (pro testování: 10)
FORCE = False # True = přestáhni i už stažené záznamy
WORKERS = 5 # počet paralelních vláken (doporučeno 5–20)
```
### `report_vykony.py`
```python
ODBORNOSTI = ["001", "002"] # [] = všechny odbornosti
VYSTUP = Path(__file__).parent / "vykony_report.xlsx"
```
---
## MongoDB
**Host:** `192.168.1.76:27017`
**DB:** `zdravotni_vykony`
**GUI:** MongoDB Compass (zdarma, od MongoDB Inc.)
### Kolekce `vykony` — aktuální přehledový stav
Vždy obsahuje poslední verzi každého výkonu. Hodnoty jsou zkrácené (kódy bez popisů).
| Pole | Typ | Popis |
|------|-----|-------|
| `cislo_vykonu` | str | Unikátní klíč (např. `01021`) |
| `odbornost` | str | Kód odbornosti (např. `001`) |
| `nazev_vykonu` | str | Název výkonu |
| `kategorie` | str | Kód kategorie (např. `P`) |
| `typ_vykonu` | str | Kód typu (např. `A`) |
| `doba_trvani` | float\|null | Minuty |
| `omezeni_mistem` | str\|null | Kód omezení (např. `A`) |
| `omezeni_frekvenci` | str\|null | Např. `1/1 den` |
| `prime_naklady` | float\|null | Body |
| `osobni` | float\|null | Body |
| `body_rezijni` | float\|null | Body |
| `body_celkem` | float\|null | Body |
| `revize` | datetime\|null | Datum poslední revize výkonu |
| `detail_url` | str\|null | URL detailu na szv.mzd.gov.cz |
| `_aktivni` | bool | `false` pokud výkon zmizel ze scrape |
| `_platny_od` | datetime | Kdy tato verze začala platit |
| `_scraped_at` | datetime | Čas posledního úspěšného scrape |
| `_deaktivovano` | datetime | Kdy byl výkon deaktivován (jen pokud `_aktivni=false`) |
**Indexy:** `cislo_vykonu` (unique), `odbornost`, `_aktivni`, `_platny_od`
---
### Kolekce `vykony_historie` — archiv změn přehledu
Každý záznam = jedna historická verze výkonu (stav před změnou).
Obsahuje stejná pole jako `vykony`, navíc:
| Pole | Typ | Popis |
|------|-----|-------|
| `_platny_do` | datetime | Kdy tato verze přestala platit |
| `_zmenena_pole` | list[str] | Která pole se změnila (nebo `["_aktivni"]` při deaktivaci) |
**Indexy:** `cislo_vykonu`, `_platny_do`, `_zmenena_pole`
---
### Kolekce `detaily` — aktuální detailní stav
Obsahuje kompletní data z detailní stránky každého výkonu. Hodnoty jsou dlouhé (s popisy).
Skalární pole + vnořené sub-tabulky jako pole objektů.
**Skalární pole:**
| Pole | Typ | Popis |
|------|-----|-------|
| `cislo_vykonu` | str | Unikátní klíč, propojení s `vykony` |
| `nazev` | str | Plný název výkonu |
| `kategorie` | str | Dlouhý popis (např. `P - hrazen plně`) |
| `typ_formulare` | str | Např. `ambulantní` |
| `omezeni_mistem` | str | Dlouhý popis (např. `A - pouze ambulantně`) |
| `omezeni_frekvenci` | str | Např. `1/1 den` |
| `doba_trvani` | float\|null | Minuty |
| `nepocitat_rezii` | bool | |
| `popis` | str | Obecný popis (často prázdný) |
| `poznamka` | str | Poznámka |
| `podminky` | str | Podmínky pro vykázání |
| `cim_zacina` | str | Popis začátku výkonu |
| `obsah_rozsah` | str | Obsah a rozsah výkonu |
| `cim_konci` | str | Popis konce výkonu |
| `body_prime` | float\|null | Bodová hodnota — přímé náklady |
| `body_osobni` | float\|null | Bodová hodnota — osobní náklady |
| `body_rezijni` | float\|null | Bodová hodnota — režijní náklady |
| `body_celkem` | float\|null | Bodová hodnota — celkem |
| `detail_url` | str | URL zdroje |
| `_platny_od` | datetime | Kdy byla tato verze detailu uložena |
| `_scraped_at` | datetime | Čas posledního scrape |
**Sub-tabulky (pole objektů):**
| Pole | Klíče objektů |
|------|---------------|
| `autorska_odbornost` | Kód, Název, Pořadí, Sazba režie |
| `dalsi_odbornost` | Kód, Název, Sazba režie |
| `nositele` | Kategorie, Funkce, Praxe, Cas, Bodyaktualni, Poznamka |
| `materialy` | Kód, Název, Doplněk, Množství, Jednotka, Cena, DPH %, Body |
| `pripravky` | Kód, Název, Doplněk, ATC, Omezení, Množství, Jednotka, Cena, Body |
| `pristroje` | Kód, Název, D.Ž., N.Ú., D.P., DPH %, Body |
| `zum` | Kód, Název |
| `zulp` | Kód, Název |
| `bodova_hodnota` | Přímé, Osobní, Režijní, Celkem (1 řádek, flatten na skalární body_*) |
**Indexy:** `cislo_vykonu` (unique), `_scraped_at`, `_platny_od`
---
### Kolekce `detaily_historie` — archiv změn detailů
Každý záznam = stav detailu před změnou.
Obsahuje stejná pole jako `detaily`, navíc:
| Pole | Typ | Popis |
|------|-----|-------|
| `_platny_do` | datetime | Kdy tato verze přestala platit |
| `_zmenena_pole` | list[str] | Která pole se změnila |
**Indexy:** `cislo_vykonu`, `_platny_do`, `_zmenena_pole`
---
## Logika detekce změn
Stejná logika platí pro oba páry kolekcí (`vykony`/`vykony_historie` i `detaily`/`detaily_historie`):
1. Načte celou aktivní kolekci do paměti (`cislo_vykonu → dokument`)
2. Pro každý nově stažený záznam:
- **Nový** → vloží s `_platny_od = run_at`
- **Změněn** (liší se v některém z `COMPARE_FIELDS`) → stará verze jde do historie s `_platny_do` + `_zmenena_pole`, nová verze se zapíše
- **Nezměněn** → jen aktualizuje `_scraped_at`
3. Výkony co v novém scrape chybí → `_aktivni = False`, stará verze do historie (jen `vykony`)
---
## Technické poznámky
### Proč ne MVCGridHandler.axd?
Ajax endpoint (`/MVCGridHandler.axd?Name=VykonGrid&pageSize=9999`) ignoruje `pageSize`
a vrací max 50 záznamů. Navíc po vyčerpání dat cyklí od začátku.
Správný endpoint je přímo stránka `/Vykon/`.
### HTML struktura detailní stránky
- Hlavní tabulka: `
`
- Řádky: `| label | hodnota | ` (ne ` | ` jak by se čekalo)
- Sub-tabulky: `` uvnitř `| `
- Parser iteruje `find_all("tr", recursive=False)` — bez rekurze nevleze do vnořených tabulek
### Encoding
Server vrací latin znaky i při UTF-8 hlavičce → `resp.apparent_encoding` místo `resp.encoding`.
### Rate limiting
`stahni_detaily.py` dělá `time.sleep(0.1)` po každém requestu per worker.
Při 20 workerech ≈ 200 requestů/s — server to bez problémů zvládá.
---
## Typické MongoDB dotazy
```js
// Všechny aktivní výkony odbornosti 001
db.vykony.find({ odbornost: "001", _aktivni: true })
// Detail konkrétního výkonu
db.detaily.findOne({ cislo_vykonu: "01021" })
// Výkony s podmínkami (neprázdné pole)
db.detaily.find({ podminky: { $ne: "" } })
// Výkony které se někdy změnily
db.vykony_historie.distinct("cislo_vykonu")
// Historie konkrétního výkonu (detail)
db.detaily_historie.find({ cislo_vykonu: "01021" }).sort({ _platny_do: 1 })
// Co se změnilo při posledním runu
db.detaily_historie.find({ _platny_do: { $gte: ISODate("2026-06-01") } })
// Výkony s nositelem kategorie L3
db.detaily.find({ "nositele.Kategorie": "L3" })
// Výkony s materiálem (neprázdné materialy)
db.detaily.find({ "materialy.0": { $exists: true } })
// Deaktivované výkony
db.vykony.find({ _aktivni: false })
```
---
## XLSX Report
Skript `report_vykony.py` generuje `vykony_report.xlsx`:
- Jeden list na odbornost (název listu: `Odbornost 001` atd.)
- Výkony řazené podle čísla výkonu
- Více nositelů → více řádků pro jeden výkon (střídavé zbarvení po výkonech)
- Sloupec **Postup výkonu** = čím začíná + obsah a rozsah + čím končí (odděleno prázdným řádkem)
- Záhlaví zmrazeno (freeze panes)
**Sloupce reportu:**
Číslo výkonu · Název · Kategorie · Typ formuláře · Doba trvání · Omezení místem ·
Omezení frekvencí · Nepočítat režii · Body přímé · Body osobní · Body režijní · Body celkem ·
Postup výkonu · Nositel kategorie · Nositel funkce · Nositel čas · Nositel body
---
## Závislosti
```
requests
beautifulsoup4
lxml
pymongo
openpyxl
```
| |