# s03soubory_01_FINAL.py – dokumentace

**Finální verze importního skriptu pro vkládání PDF dokumentů do dekurzů Medicusu.**

Datum finalizace: 2026-04-04
Autor: Vladimír Buzalka + Claude (Anthropic)

---

## Co skript dělá

Zpracuje PDF soubory v určené složce (`cesta`) a pro každý soubor:
1. Ověří správnost názvu souboru (formát, RC, datum)
2. Zkontroluje, zda cílový dekurz není zamčený v Medicusu
3. Zapíše soubor do externí databáze souborů (tabulka FILES)
4. Přesune soubor do složky zpracovaných
5. Vloží odkaz (bookmark) do dekurzu pacienta jako RTF záznam

---

## Adresáře

| Proměnná | Cesta (testovací) | Popis |
|---|---|---|
| `cesta` | `u:\testimport` | Vstupní složka – sem patří soubory ke zpracování |
| `cestazpracovana` | `u:\testimportzpracovana` | Cílová složka – sem se přesouvají zpracované soubory |

> V produkci tyto cesty nahradit skutečnými složkami (Nextcloud/Dropbox).

---

## Formát názvu souboru

Každý PDF soubor musí mít název ve tvaru:

```
RC YYYY-MM-DD Příjmení, Jméno [typ dokumentu] [poznámka].pdf
```

**Příklad:**
```
7309208104 2020-10-16 Buzalka, Vladimír [LZ ortopedie] [VAS LS páteře, obstřik].pdf
```

| Část | Popis |
|---|---|
| `RC` | Rodné číslo pacienta (9 nebo 10 číslic) – musí existovat v tabulce KAR |
| `YYYY-MM-DD` | Datum dokumentu |
| `Příjmení, Jméno` | Jméno pacienta (jen pro čitelnost, nepoužívá se k vyhledání) |
| `[typ dokumentu]` | První závorka – druh nálezu (LZ ortopedie, EKG, Lab. nález…) |
| `[poznámka]` | Druhá závorka – krátký popis obsahu (může být prázdná `[]`) |

**Chybný soubor** (špatný název, RC nenalezeno v DB) je přejmenován přidáním prefixu `♥`:
```
♥chybny soubor.pdf
```
Skript ho přeskočí a nechá v složce pro ruční opravu.

---

## Databázové tabulky

| Tabulka | DB | Popis |
|---|---|---|
| `KAR` | Medicus (`medicus.fdb`) | Kartotéka pacientů – lookup RC → IDPAC |
| `DEKURS` | Medicus (`medicus.fdb`) | Dekurzy – čtení a zápis RTF záznamu |
| `FILES` | Externí DB (`MEDICUS_FILES_YYYYMM.fdb`) | Binární uložení PDF souborů |

---

## Klíčová novinka oproti s03soubory.py – ochrana před zamčeným dekurzem

### Problém
Medicus drží **exkluzivní zámek** (Firebird row lock) na záznamu tabulky DEKURS po celou dobu, kdy má lékařka pacienta otevřeného. Kdyby skript provedl `UPDATE DEKURS` do zamčeného záznamu, přepsal by lékařčiny neuložené změny.

### Řešení – detekce zámku pomocí vlákna s timeoutem

Firebird neumí NOWAIT nastavit per-statement v SQL (syntaxe `FOR UPDATE WITH LOCK NOWAIT` není platná). Nastavení NOWAIT je vlastnost transakce, nikoliv dotazu. Knihovna `fdb` navíc toto nastavení spolehlivě nepodporuje.

**Zvolené řešení:** spuštění pokusu o zámek ve vedlejším vlákně s timeoutem 2 sekundy.

```
hlavní vlákno                    vedlejší vlákno (_pokus_o_zamek)
─────────────────                ─────────────────────────────────
t.start()              ──────►   fdb.connect() [nové spojení]
t.join(timeout=2s)               SELECT ... FOR UPDATE WITH LOCK
                                   ├── záznam volný → fetchone() → rollback() → konec
                                   └── záznam zamčený → čeká (blokuje)...
─────────────────
po 2 sekundách:
  t.is_alive()?
    ANO → záznam zamčený → RuntimeError → přeskoč skupinu
    NE  → záznam volný → pokračuj se zápisem
```

### Důležitý detail – pořadí operací

**Kontrola zámku probíhá PŘED zápisem do FILES a PŘED přesunem souboru.**
Kdyby se pořadí obrátilo, mohlo by dojít k situaci:
- soubor zapsán do FILES ✓
- soubor přesunut do zpracovaných ✓
- dekurz zamčen → UPDATE selže
- soubor je pryč ze vstupní složky, ale odkaz v dekurzu chybí

Správné pořadí:
```
1. Zkontroluj zámek dekurzu (NOWAIT)
   └── zamčeno → přeskoč (soubory zůstanou v cesta)
2. Zapiš soubory do ext. DB (FILES)
3. Přesuň soubory do zpracovaných
4. Sestav RTF
5. UPDATE nebo INSERT do DEKURS
```

---

## Logika vkládání do dekurzu – 3 případy

Po úspěšné kontrole zámku skript rozhodne, co s dekurzem udělat:

```
Existuje dnešní dekurz pro pacienta?
      │
      ├── ANO → obsahuje sekci "Vložené přílohy"?
      │         ├── ANO  → Případ 1: přidá nové soubory DOVNITŘ sekce
      │         └── NE   → Případ 2: vloží celou sekci na ZAČÁTEK dekurzu (prepend)
      │
      └── NE  → Případ 3: vytvoří NOVÝ dekurz ze šablony RTF_TEMPLATE
```

### Případ 1 – `pridat_do_sekce_prilohy()`

Dnešní dekurz **existuje a má** sekci „Vložené přílohy".

- Spočítá kolik odkazů (Files:) už sekce obsahuje → nové indexy bookmarků navazují
- Nové `\pard` řádky vloží **před** uzavírací prázdný řádek sekce (`PRILOHY_CLOSING`)
- Nové bookmarky přidá na **konec** `{\info{\bookmarks ...}}`
- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?`

### Případ 2 – `merge_rtf_prepend()`

Dnešní dekurz **existuje, ale nemá** sekci příloh (lékařka do něj napsala text).

- Přečísluje existující bookmarky (posunutí o počet nových souborů)
- Novou sekci „Vložené přílohy" vloží **na začátek** těla RTF (před `\uc1\pard`)
- Nové bookmarky předřadí před existující v `{\info{\bookmarks ...}}`
- Lékařčin text zůstane zachován, jen se posune níž
- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?`

### Případ 3 – nový INSERT

Pro dnešní datum **neexistuje žádný dekurz**.

- Vyplní `RTF_TEMPLATE` (bookmarky + tělo sekce příloh)
- Provede `INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)`
- `iduzi=6` (Vladimír Buzalka), `idprac=2`, `idodd=2`

---

## RTF formát dekurzu

### Struktura bookmarku
Každý přiložený soubor je reprezentován jako:
1. **Bookmark entry** v `{\info{\bookmarks ...}}`:
   ```
   "2020-10-16 LZ ortopedie: VAS LS páteře, obstřik","Files:21923",9
   ```
   - `"popis"` – zobrazený text odkazu
   - `"Files:ID"` – odkaz na záznam v tabulce FILES (slouží Medicusu k načtení souboru)
   - `9` – číslo fontu/stylu (od 9, každý další +7)

2. **Vizuální řádek** v těle RTF:
   ```rtf
   \pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2020-10-16 LZ ortopedie: VAS LS páteře, obstřik{\*\bkmkend 0}\par
   ```
   - `\bkmkstart N` / `\bkmkend N` – index bookmarku (0, 1, 2…)
   - `\cs32\ul\cf1` – styl „Odkaz" (modrý podtržený text)

### Konstanty pro detekci sekce příloh (win1250 RTF escape)
```python
PRILOHY_HEADER  = r"Vlo\'9een\'e9 p\'f8\'edlohy:"   # "Vložené přílohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'  # uzavírací prázdný řádek
```

---

## Funkce – přehled

| Funkce | Popis |
|---|---|
| `restore_files_for_import(retezec)` | Debug utilita – vrátí soubory z Nextcloudu zpět do Dropboxu. Nepoužívá se v produkci. |
| `kontrola_rc(rc, connection)` | Ověří zda RC existuje v KAR, vrátí IDPAC nebo False. |
| `kontrola_struktury(souborname, connection)` | Ověří formát názvu souboru a existenci RC v DB. |
| `vrat_info_o_souboru(souborname, connection)` | Parsuje název souboru, dohledá IDPAC, vrátí tuple s metadaty. |
| `prejmenuj_chybny_soubor(souborname, cesta)` | Přidá prefix `♥` k chybnému souboru. |
| `_pokus_o_zamek(dekurs_id, vysledek)` | Interní – běží ve vlákně, zkouší zamknout dekurz. |
| `zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2)` | Zjistí zda dnešní dekurz existuje a není zamčený. Vyhodí RuntimeError pokud je zamčený. |
| `ma_sekci_prilohy(rtf)` | Vrátí True pokud RTF obsahuje sekci „Vložené přílohy". |
| `pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list)` | Případ 1 – přidá soubory do existující sekce příloh. |
| `merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new)` | Případ 2 – vloží sekci příloh na začátek existujícího dekurzu. |

---

## Ošetření chyb

| Situace | Chování |
|---|---|
| Soubor má chybný název | Přejmenován na `♥soubor.pdf`, přeskočen |
| RC nenalezeno v KAR | Přejmenován na `♥soubor.pdf`, přeskočen |
| Dekurz zamčený (timeout vlákna) | Skupina přeskočena, soubory zůstanou v `cesta` |
| DB konflikt při zamykání (-913 deadlock) | Skupina přeskočena, soubory zůstanou v `cesta` |
| Přesun souboru selže | 3 pokusy s 5s pauzou, poté varování |
| Jiná DB chyba | Výjimka se propaguje, skript havaruje |

---

## Vývoj a testování

| Verze | Soubor | Co přibilo |
|---|---|---|
| Prototyp | `test_import_FINAL.py` | Ruční zadání IDPAC a DATUM, ověření RTF logiky |
| v1 | `s03soubory.py` | Automatický parsing RC z názvu, dávkování po skupinách |
| **v1 FINAL** | `s03soubory_01_FINAL.py` | Ochrana před zamčeným dekurzem (threading + timeout) |

### Jak byl objeven problém se zámky
Experimentem bylo ověřeno, že Medicus drží Firebird row lock na záznamu DEKURS po celou dobu, kdy má lékařka pacienta otevřeného (`SELECT FIRST 1 ... FOR UPDATE WITH LOCK` z Pythonu čekalo dokud lékařka neuložila). NOWAIT nelze nastavit přes SQL syntaxi ani spolehlivě přes fdb TPB bajty, proto bylo zvoleno řešení přes vlákno s timeoutem.
