227 lines
9.4 KiB
Markdown
227 lines
9.4 KiB
Markdown
# 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.
|