# Lékový záznam eRecept → MySQL

Pipeline pro hromadné stažení lékových záznamů všech registrovaných pacientů
z eRecept SÚKL API a jejich uložení do relační databáze MySQL.

---

## Soubory

| Soubor | Co dělá |
|--------|---------|
| `05UlozitOdpoved.py` | Stáhne XML pro **jednoho** pacienta (ruční test/ladění) |
| `06UlozitDoMySQL.py` | DDL schématu, parsování XML, import do MySQL — používá se jako **knihovna**, ne spouštět přímo! |
| `07StahnoutVsechny.py` | **Hlavní skript** — načte pacienty z Medicusu, stáhne lékové záznamy, uloží XML i DB záznamy |
| `reimport_z_xml.py` | Reimport XML ze zálohy bez volání API — viz sekce níže |

```
recept/
├── setup.ps1                      ← vytvoří .venv, nainstaluje závislosti, Playwright chromium
├── requirements.txt               ← seznam Python závislostí
├── .venv/                         ← virtuální prostředí (Python 3.x)
│
├── LékovýZáznamWithClaude/
│   ├── 05UlozitOdpoved.py
│   ├── 06UlozitDoMySQL.py
│   ├── 07StahnoutVsechny.py
│   ├── reimport_z_xml.py
│   ├── LEKOVY_ZAZNAM_DB.md        ← tento soubor
│   ├── Logs/                      ← log každého běhu (UTF-8, YYYY-MM-DD_HH-MM-SS.log)
│   ├── Tests/                     ← starší vývojové skripty
│   └── xml_archive/               ← archiv XML odpovědí (YYYY-MM-DD/Prijmeni_Jmena_datnar.xml)
│
└── Dotazy/
    ├── prehled_pacienta.py        ← konzolový přehled pacienta
    ├── prehled_pacienta_excel.py  ← export přehledu pacienta do Excelu
    └── DOTAZY.md                  ← dokumentace dotazovacích skriptů
```

> **⚠️ NIKDY nespouštět `06UlozitDoMySQL.py` přímo** — zavolá `vytvor_schema()`,
> která provede `DROP TABLE` a smaže celou databázi.
> Pro import dat vždy použít `07StahnoutVsechny.py` nebo `reimport_z_xml.py`.

---

## Nastavení prostředí (jednorázově)

```powershell
# PowerShell — spustit jednou po naklonování projektu
cd U:\recept
.\setup.ps1
```

`setup.ps1` provede:
1. Vytvoří `.venv` s Python interpretem z `C:\Python\python.exe`
2. Nainstaluje všechny závislosti z `requirements.txt`
3. Nainstaluje Playwright Chromium (pro případné automatizace)

Po nastavení aktivace:
```powershell
.venv\Scripts\Activate.ps1
```

### requirements.txt

```
requests
requests-pkcs12
pymysql
fdb
zeep
mysql-connector-python
playwright
openpyxl
```

---

## Typické spuštění

```bash
# Hromadné stažení všech registrovaných pacientů
python 07StahnoutVsechny.py

# Pouze vybraná příjmení (testování / rodina)
python 07StahnoutVsechny.py --prijmeni Buzalka,Buzalková,Kusinová

# Dávkování po částech
python 07StahnoutVsechny.py --offset 100 --limit 50

# Reimport ze zálohy XML (bez volání API) — viz níže
python reimport_z_xml.py
```

---

## Autentizace (eRecept SÚKL, ostrý provoz)

| Parametr | Hodnota |
|----------|---------|
| Endpoint | `https://lekar-soap.erecept.sukl.cz/cuer/Lekar2` |
| mTLS certifikát | `AMBSUKL214235369G_31DEC2024.pfx` (platnost do 31. 12. 2026) |
| HTTP Basic user | UUID lékaře `e08c89c6-2b1a-4eba-8ed9-4e3e63618379` |
| SOAP operace | `NacistLekovyZaznam` |
| XML namespace | `http://www.sukl.cz/erp/201912` |
| Verze zprávy | `202501A` |

Certifikát = identifikace **ordinace**, UUID+heslo = identifikace **lékaře jako osoby**.

---

## Zdroj pacientů — Medicus (Firebird)

Pacienti se načítají přímo z `medicus.fdb` jako registrovaní pacienti ordinace:

```
DSN:     localhost:c:\medicus 3\data\medicus.fdb
User:    SYSDBA / masterkey
Charset: win1250
IČP:     09305001  (odbornost 001 — praktický lékař)
```

Podmínky registrace: `vyrazen = 'N'`, `registr.priznak IN ('V','D','A')`,
registrace platná k dnešnímu datu.

---

## Logika přírůstkového stahování

```
první stažení pacienta  →  PocetMesicu = 60 (maximum, 5 let)
opakované stažení       →  ceil(dny od posledního stažení / 30) + 1
```

- Překryv 1 měsíce zajistí, že nepřijdeme o nic na hranici období.
- `INSERT IGNORE` na `id_lp_predpis` / `id_lp_vydej` zabrání duplikátům.
- Pauza mezi voláními API: **náhodně 10–20 sekund**.

---

## Ošetření chyb API

Pacienti, kteří nejsou v eReceptu ztotožněni (nikdy nebyli v lékárně s e-receptem),
vrátí SOAP Fault `Z002`. Skript:

1. Zachytí chybu (HTTP 500 nebo SOAP Fault v těle odpovědi)
2. Uloží text chyby do `pacient.poznamka`
3. Pokračuje dalším pacientem

Při příštím úspěšném stažení se `poznamka` automaticky vymaže.

```sql
-- přehled pacientů s chybou
SELECT prijmeni, jmena, datum_narozeni, poznamka
FROM pacient
WHERE poznamka IS NOT NULL;
```

---

## XML archiv

Každá odpověď API se uloží jako soubor:
```
xml_archive/YYYY-MM-DD/Prijmeni_Jmena_YYYY-MM-DD.xml
```

Cesta je zároveň uložena v `zprava.xml_soubor`.  
Účel: možnost re-parsování při budoucích změnách schématu bez nutnosti znovu volat API.

---

## Výstup do konzole a logů

Konzole zobrazuje jen jeden řádek na pacienta:
```
[   1/1621] Abohamda Horia             OK     168p   252v   247 KB
[   5/1621] Alakbarov Farid            CHYBA  Z002 - Lekovy zaznam ne...
```

Kompletní detaily (počty nových záznamů, ID zprávy, doba čekání) jsou v:
```
Logs/YYYY-MM-DD_HH-MM-SS.log
```

---

## Databázové schéma — `medicus` (MySQL)

Všechny délky a datové typy jsou přesně dle XSD, **ne odhady**.
Lék je denormalizován přímo do řádku předpisu/výdeje.

### Relační diagram

```
pacient (1)
  └── zprava (N)          -- každé volání API = 1 zpráva
        ├── predpis (N)
        │     └── predpis_slozka (N)   -- složky IPLP z předpisu
        └── vydej (N)
              └── vydej_slozka (N)     -- složky IPLP z výdeje

vydej.id_lp_predpis → predpis.id_lp_predpis   (párování výdeje s předpisem)
predpis.kod_predepisujiciho → predepisujici.lekar_kod
vydej.kod_vydavajiciho      → vydavajici.lekarnik_kod
```

### Tabulka `pacient`

Zrcadlo registrovaných pacientů z Medicusu. Aktualizuje se při každém běhu `07`.

| Sloupec | Typ | Poznámka |
|---------|-----|----------|
| `id` | INT PK | |
| `idpac` | INT UNIQUE | IDPAC z tabulky KAR v Medicusu |
| `prijmeni` | VARCHAR(35) | |
| `jmena` | VARCHAR(24) | |
| `datum_narozeni` | DATE | |
| `aktivni` | TINYINT(1) | 0 = přeskočit při hromadném běhu |
| `poznamka` | VARCHAR(500) | poslední chyba API; NULL = OK |

### Tabulka `zprava`

Jeden řádek = jedno volání API (jeden pacient, jeden čas).

| Sloupec | Typ | Poznámka |
|---------|-----|----------|
| `id_zpravy` | CHAR(36) UNIQUE | UUID z eReceptu |
| `pacient_id` | INT FK → pacient | |
| `verze` | VARCHAR(20) | verze zprávy (202501A) |
| `odeslano` | DATETIME | čas odeslání dotazu |
| `aplikace` | VARCHAR(512) | SW SÚKL serveru |
| `id_podani` | CHAR(36) | UUID podání |
| `prijato` | DATETIME | čas přijetí odpovědi |
| `pacient_prijmeni` | VARCHAR(35) | z XML odpovědi |
| `pacient_jmena` | VARCHAR(24) | z XML odpovědi |
| `pacient_datum_narozeni` | DATE | z XML odpovědi |
| `xml_soubor` | VARCHAR(255) | relativní cesta k archivu |
| `stazeno` | DATETIME | automaticky při INSERT |

### Tabulka `predpis`

Dle `lz_nacteni_predepsany_lp_erp_type`.

| Sloupec | Typ | NOT NULL | Poznámka |
|---------|-----|----------|----------|
| `id_lp_predpis` | CHAR(36) UNIQUE | ✓ | UUID z eReceptu |
| `zprava_id` | INT FK | ✓ | |
| `kod_predepisujiciho` | VARCHAR(36) | ✓ | UUID lékaře |
| `datum_vystaveni` | DATE | ✓ | |
| `mnozstvi` | SMALLINT | ✓ | 1–9999 |
| `navod` | VARCHAR(80) | ✓ | |
| `opakovani` | INT | | |
| `modry_pruh` | TINYINT(1) | | návykové látky |
| `typ_leku` | ENUM | | HVLPReg / HVLPNereg / IPLP / INN |
| `lek_kod` | CHAR(7) | | kód SÚKL (jen HVLP) |
| `atc` | VARCHAR(7) | | ATC kód |
| `nazev` | VARCHAR(200) | | |
| `forma` | VARCHAR(27) | | |
| `sila` | VARCHAR(24) | | |
| `cesta_podani` | VARCHAR(15) | | POR, INH, … |
| `baleni` | VARCHAR(22) | | string, např. "100 ks" |
| `postup_pripravy` | VARCHAR(4000) | | receptura IPLP |

### Tabulka `predpis_slozka`

Složky IPLP předpisů (lékař typicky nevyplňuje, kvalitní data spíše u výdeje).

| Sloupec | Typ | NOT NULL |
|---------|-----|----------|
| `predpis_id` | INT FK | ✓ |
| `mnozstvi` | DECIMAL(15,6) | ✓ |
| `jednotka` | ENUM('g','ks') | ✓ |
| `nazev` | VARCHAR(200) | ✓ |
| `surovina` | CHAR(7) | |
| `hvlp_reg` | CHAR(7) | |

### Tabulka `vydej`

Dle `lz_nacteni_vydany_lp_erp_type`.

| Sloupec | Typ | NOT NULL | Poznámka |
|---------|-----|----------|----------|
| `id_lp_vydej` | CHAR(36) UNIQUE | ✓ | UUID výdeje |
| `zprava_id` | INT FK | ✓ | |
| `id_lp_predpis` | CHAR(36) FK | | NULL = výdej bez e-předpisu |
| `kod_vydavajiciho` | VARCHAR(36) | ✓ | UUID lékárníka |
| `datum_vydeje` | DATE | ✓ | |
| `mnozstvi` | DECIMAL(6,2) | ✓ | desetinné — např. 0.5 balení |
| `navod` | VARCHAR(80) | ✓ | |
| `exspirace` | DATE | | exspirace šarže |
| `sarze` | VARCHAR(50) | ✓ | |
| `seriove_cislo` | VARCHAR(20) | | léky s el. sledováním |
| `pozn` | VARCHAR(1000) | | poznámka lékárníka |
| `typ_leku` | ENUM | | HVLPReg / HVLPNereg / IPLP |
| `lek_kod` | CHAR(7) | | kód SÚKL nebo KodVZP (IPLP) |
| `atc` | VARCHAR(7) | | jen HVLP |
| `nazev` | VARCHAR(146) | | |
| `forma` | VARCHAR(27) | | |
| `sila` | VARCHAR(24) | | |
| `cesta_podani` | VARCHAR(15) | | |
| `postup_pripravy` | VARCHAR(4000) | | receptura IPLP |

### Tabulka `vydej_slozka`

Jako `predpis_slozka`, navíc `hrazeno_zp`. Data lékáren — kvalita závisí na lékárně.

| Sloupec | Typ | NOT NULL | Poznámka |
|---------|-----|----------|----------|
| `vydej_id` | INT FK | ✓ | |
| `mnozstvi` | DECIMAL(15,6) | ✓ | |
| `jednotka` | ENUM('g','ks') | ✓ | |
| `nazev` | VARCHAR(200) | ✓ | |
| `hrazeno_zp` | DECIMAL(9,2) | | částka hrazená ZP |
| `surovina` | CHAR(7) | | |
| `hvlp_reg` | CHAR(7) | | |

### Tabulka `predepisujici`

Lékaři, kteří pacientovi předepisovali (ze všech ordinací).

| Sloupec | Typ | Poznámka |
|---------|-----|----------|
| `lekar_kod` | CHAR(36) UNIQUE | UUID lékaře = predpis.kod_predepisujiciho |
| `prijmeni` | VARCHAR(35) | |
| `jmena` | VARCHAR(24) | |
| `icz` | CHAR(8) | IČZ zdravotnického zařízení |
| `icp` | CHAR(8) | IČP pracoviště — **poslední 3 číslice = kód odbornosti** (001 = prakt. lékař, 272 = alergologie…) |
| `pzs_nazev` | VARCHAR(200) | název zdravotnického zařízení |
| `ulice` | VARCHAR(150) | |
| `mesto` | VARCHAR(100) | |
| `psc` | CHAR(5) | |
| `telefon` | VARCHAR(20) | |

### Tabulka `vydavajici`

Lékárníci / lékárny, kde byl výdej.

| Sloupec | Typ | Poznámka |
|---------|-----|----------|
| `lekarnik_kod` | CHAR(36) UNIQUE | UUID lékárníka = vydej.kod_vydavajiciho |
| `prijmeni` | VARCHAR(35) | |
| `jmena` | VARCHAR(24) | |
| `pzs_nazev` | VARCHAR(200) | název lékárny |
| `ulice` | VARCHAR(150) | |
| `mesto` | VARCHAR(100) | |
| `psc` | CHAR(5) | |
| `telefon` | VARCHAR(20) | |

---

## Typy léků v Predpis i Vydej

Každý předpis / výdej obsahuje právě **jeden** z těchto elementů:

| Typ | Popis | Klíčová pole |
|-----|-------|-------------|
| `HVLPReg` | Registrovaný hromadně vyráběný LP | Kod (SÚKL), ATC, Nazev, Forma, Sila, Baleni |
| `HVLPNereg` | Neregistrovaný HVLP | stejná struktura jako HVLPReg |
| `IPLP` | Individuálně připravovaný LP (magistraliter) | Nazev, PostupPripravy, Slozka[] |
| `INN` | Předpis účinnou látkou (genericky) | Nazev, Forma, Sila, Baleni |

#### IPLP — dvojí uložení receptury

- **Předpis**: lékař zadal recepturu jako volný text v `PostupPripravy`. Složky typicky nevyplněny.
- **Výdej**: lékárna zaznamenala strukturované složky (`Slozka` s množstvím, jednotkou, názvem suroviny). Kvalita dat závisí na lékárně.

---

## Užitečné analytické dotazy

```sql
-- nejčastěji předepisované ATC skupiny za posledních 12 měsíců
SELECT atc, nazev, COUNT(*) AS pocet, MAX(datum_vystaveni) AS naposledy
FROM predpis
WHERE datum_vystaveni >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY atc, nazev
ORDER BY pocet DESC;

-- co bylo předepsáno ale nevyzvednuto (non-compliance)
SELECT pac.prijmeni, pac.jmena, p.datum_vystaveni, p.nazev, p.atc, p.navod
FROM predpis p
JOIN zprava z   ON z.id = p.zprava_id
JOIN pacient pac ON pac.id = z.pacient_id
LEFT JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis
WHERE v.id_lp_vydej IS NULL
ORDER BY p.datum_vystaveni DESC;

-- lékový záznam konkrétního pacienta (předpisy + výdeje)
SELECT p.datum_vystaveni, p.typ_leku, p.nazev, p.atc, p.navod,
       v.datum_vydeje, v.mnozstvi AS vydano
FROM pacient pac
JOIN zprava z   ON z.pacient_id = pac.id
JOIN predpis p  ON p.zprava_id  = z.id
LEFT JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis
WHERE pac.prijmeni = 'Buzalka' AND pac.jmena = 'Vladimír'
ORDER BY p.datum_vystaveni DESC;

-- IPLP magistraliter — kompletní receptury s frekvencí (napříč pacienty)
SELECT p.nazev, p.postup_pripravy, COUNT(*) AS pocet_predpisu
FROM predpis p
WHERE p.typ_leku = 'IPLP'
GROUP BY p.nazev, p.postup_pripravy
ORDER BY pocet_predpisu DESC;

-- nejčastěji používané suroviny v magistrech
SELECT nazev, jednotka, COUNT(*) AS pocet
FROM vydej_slozka
GROUP BY nazev, jednotka
ORDER BY pocet DESC;

-- generická záměna: co předepsal lékař vs. co lékárna vydala
SELECT pac.prijmeni, pac.jmena,
       p.datum_vystaveni, p.nazev AS predepsano, p.atc,
       v.nazev AS vydano, v.datum_vydeje
FROM pacient pac
JOIN zprava z   ON z.pacient_id = pac.id
JOIN predpis p  ON p.zprava_id  = z.id
JOIN vydej v    ON v.id_lp_predpis = p.id_lp_predpis
WHERE p.nazev <> v.nazev;

-- pacienti s chybou API (neztotožněni)
SELECT prijmeni, jmena, datum_narozeni, poznamka
FROM pacient
WHERE poznamka IS NOT NULL
ORDER BY prijmeni;

-- lékaři dle odbornosti — kolik předpisů pochází od které speciality
SELECT RIGHT(pr.icp, 3) AS odb_kod, COUNT(*) AS pocet_predpisu
FROM predpis p
JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho
WHERE pr.icp IS NOT NULL
GROUP BY RIGHT(pr.icp, 3)
ORDER BY pocet_predpisu DESC;

-- lékový záznam pacienta dle rodného čísla (přes Firebird → MySQL)
-- krok 1: z Medicusu zjistit příjmení a datum narozeni pro RC 7309208104
-- krok 2:
SELECT pac.prijmeni, pac.jmena, pac.datum_narozeni,
       p.datum_vystaveni,
       COALESCE(v.nazev, p.nazev) AS vydany_lek,
       v.nazev IS NULL            AS nevyzvednuto,
       p.atc, p.navod,
       pr.prijmeni AS lekar, RIGHT(pr.icp, 3) AS odb_kod
FROM pacient pac
JOIN zprava z   ON z.pacient_id = pac.id
JOIN predpis p  ON p.zprava_id  = z.id
JOIN predepisujici pr ON pr.lekar_kod = p.kod_predepisujiciho
LEFT JOIN vydej v ON v.id_lp_predpis = p.id_lp_predpis
WHERE pac.prijmeni = 'Buzalka' AND pac.datum_narozeni = '1973-09-20'
ORDER BY p.datum_vystaveni DESC;
```

---

## Reimport ze zálohy XML (`reimport_z_xml.py`)

Slouží k opětovnému naplnění MySQL databáze z lokálních XML souborů **bez volání eRecept API**.  
Použití: obnova po neúmyslném smazání databáze, migrace na nový server, re-parsování při změně schématu.

### Jak funguje

1. Načte všechny registrované pacienty z Firebirdu (ICP `09305001`, odbornost `001`)
2. Pro každý XML soubor v archivu:
   - Naparsuje XML (volá `parsuj_xml()` z `06UlozitDoMySQL.py`)
   - Dohledá pacienta v Firebirdu dle příjmení + data narození z XML
   - Pokud je registrovaný → `upsert` pacienta do MySQL (INSERT ON DUPLICATE KEY UPDATE)
   - Zavolá `uloz()` — INSERT IGNORE, takže duplicity se ignorují
3. Výpis průběhu: `[  1/1177] Buzalka_Vladimir_1973-09-20.xml    OK   12p  18v`

### Spuštění

```bash
# Výchozí adresář: xml_archive/2026-04-11
python reimport_z_xml.py

# Konkrétní podadresář
python reimport_z_xml.py xml_archive/2026-04-11

# Celý archiv rekurzivně (všechna data)
python reimport_z_xml.py xml_archive
```

### Konfigurace v souboru

```python
XML_ADRESAR = Path(__file__).parent / "xml_archive" / "2026-04-11"  # výchozí adresář
ICP = "09305001"   # IČP ordinace pro filtr registrovaných pacientů
ODB = "001"        # odbornost (001 = praktický lékař)
```

### Poznámky

- Pacienti, kteří nejsou v Firebirdu registrováni pod daným ICP/ODB, se přeskočí
  (pokud ale existují v MySQL z předchozího importu, data se aktualizují)
- Firebird slouží jako autoritativní zdroj identit — `idpac` z KAR se propíše do MySQL `pacient.idpac`
- `INSERT IGNORE` zajistí idempotentnost — opakované spuštění nepřidá duplikáty

---

## Dotazovací skripty (`Dotazy/`)

Viz samostatnou dokumentaci: [`Dotazy/DOTAZY.md`](../Dotazy/DOTAZY.md)

Stručný přehled:

| Skript | Co dělá |
|--------|---------|
| `prehled_pacienta.py` | Konzolový výpis lékového záznamu pacienta (lékaři + předpisy) |
| `prehled_pacienta_excel.py` | Totéž, ale exportuje do formátovaného souboru Excel (.xlsx) |

Pacient se identifikuje **rodným číslem** (nastavení `RODNE_CISLO` v záhlaví skriptu).  
Oba skripty zobrazují **vydaný lék** (ne předepsaný), **odbornost lékaře** a příznak `*NV` pro nevyzvednuto.

---

## Závislosti (Python)

```
requests
requests-pkcs12
pymysql
fdb
zeep
mysql-connector-python
playwright
openpyxl
```

```bash
# Instalace (nebo použít setup.ps1)
pip install requests requests-pkcs12 pymysql fdb openpyxl
```

---

## XSD zdroje

Schéma verze `202501A`, soubory v `Dokumentace/2025-04-24/WSDL_XSD/NEPRIORITNI_WEBOVE_SLUZBY/`:

| Soubor | Obsah |
|--------|-------|
| `Cuer2Schema.xsd` | `NacistLekovyZaznamOdpoved`, `lz_nacteni_predepsany_lp_erp_type`, `lz_nacteni_vydany_lp_erp_type`, `slozka_iplp_*` |
| `CuerSchema.xsd` | `hvlp_type`, `zprava_odpoved_type`, `zprava_type`, `jmeno_osoby_type`, `jednotka` |
