# 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.