20 KiB
MedicusWithClaude – poznámky pro Clauda
Stav projektu
- Zahájeno: 2026-03-13
- Cíl: průzkum Firebird DB Medicus → analýzy a reporty
- Zatím: úspěšně čteme i zapisujeme do DB, rozumíme RTF formátu
- 2026-03-17: Obnovena session – Claude si přečetl poznámky, připraven pokračovat
- 2026-03-18: Obnovena session – Claude si přečetl poznámky, připraven pokračovat. Průběžně zapisuje do tohoto souboru.
- 2026-03-20: Obnovena session – merge logika z
test_import_single.pyintegrována dos03soubory.py. Import pipeline kompletní. - 2026-04-01: Opravena chyba
BlobReader invalid BLOB handlevdekurz_report.py– viz níže.
Bezpečnost
- Pracujeme na místní kopii – poškození DB nevadí, obnova = 5 minut
- Lze bez obav experimentovat, zapisovat testovací data atd.
- Testovací záznamy nemusíme mazat
Připojení k DB
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
- fdb verze 2.0.4, Python na PATH (prostý
pythonpříkaz) - Firebird instalace:
C:\Program Files\Firebird\Firebird_2_5_CGM\bin\ - Jedná se o HQbird (IBSurgeon enhanced Firebird) – port 3070 (ne standardní 3050)
- fbtracemgr trace nefungoval spolehlivě – doporučeno použít FBScanner (stejný výrobce)
O Medicusu
- Lékařský software pro praktického lékaře
- Vyvíjen od ~roku 2000, organicky rostl přidáváním funkcí
- Firebird 2.5 – nikdy nepřejdou na nový (desetitisíce instalací, 993 tabulek)
- Žádné zabezpečení DB – vše čitelné přes SYSDBA/masterkey
- Charset: win1250
DB fakta
- 993 tabulek celkem
- Charset: win1250
Klíčové tabulky
KAR – Kartotéka (základní kámen Medicusu)
- 6319 pacientů – všichni kdo kdy prošli ordinací
- Primární klíč: IDPAC (interní ID, používá se jako FK v ostatních tabulkách)
- Identifikace pacienta v ČR/pojišťovna: RODCIS (rodné číslo, bez lomítka, např. 7309208104)
- PRIJMENI, JMENO, TITUL, TITULZA, DATNAR, POHLAVI
- POJ – pojišťovna (3 znaky, např. 111 = VZP)
- Adresy: TRV* (trvalé), PRE* (přechodné)
- VYRAZEN – příznak vyřazeného pacienta
- HLAVDGN – hlavní diagnóza
DEKURS – záznamy z návštěv
- 172 068 záznamů
- IDPAC → vazba na KAR
- DATUM (date), CAS (time) – kdy
- DEKURS – text záznamu (BLOB, RTF nebo holý text)
- DGN1 – hlavní diagnóza (5 znaků, např. 'Z000 '), VDGN1-4 – vedlejší
- IDPRAC – který pracovník
- IDODD, IDUZI – oddělení/uživatel (u Buzalky = 2, 2, 6)
- IDSKUPINA, IDTYP – volitelné (NULL OK)
Základní schéma
KAR (kartotéka) ←IDPAC→ DEKURS (záznamy z návštěv)
Testovací pacient (uživatel)
- IDPAC = 9742 – Buzalka Vladimír, RC 7309208104, nar. 20.9.1973
- IDODD=2, IDPRAC=2, IDUZI=6 (použít při INSERT do DEKURS)
RTF formát v DEKURS
Základní struktura
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
{\info{\bookmarks "popis","Typ:ID",číslo}} ← metadata Medicusu
{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f2...Courier New;}{\f5...Symbol;}}
{\colortbl ;\red0\green0\blue255;\red255\green255\blue0;\red0\green128\blue0;\red192\green192\blue192;}
{\stylesheet{styly...}}
\uc1\pard\s10\plain... ← tělo záznamu
}
Barvy (\cfX)
\cf1= modrá (odkazy)\cf2= žlutá (highlight)\cf3= zelená\cf4= šedá (pozadí buněk tabulky\clcbpat4)
Velikost písma (\fsXX = XX/2 bodů)
\fs18= 9pt (tabulky)\fs20= 10pt (normální)\fs24= 12pt atd.
Řezy a formátování
\b= tučné,\i= kurzíva,\ul= podtržené\ql= vlevo,\qr= vpravo,\qc= střed
Styly (stylesheet)
cs15/cs16= Normálnícs20= Vkládaný textcs21= Záhlaví (italic)cs22/cs23= Odkaz (underline, modrá)s8/s10= Vlevo (paragraph styl)
Záložky (bookmarks v {\info})
- Klikatelné odkazy v Medicusu
- Formát:
"popis","Typ:ID",číslo - Typy:
Files:ID,MEDLAB:ID,Lab:ID,Ock:ID,VykA:ID
České znaky
- RTF hex escape v win1250:
\'e1=á,\'9e=ž,\'fd=ý atd.
Tabulky
- Plně funkční RTF tabulky (
\trowd,\cell,\cellx...) - Používají se např. pro laboratorní výsledky (viz ID=243082, 50KB)
Holý text (starší záznamy)
- Některé starší záznamy jsou prostý text bez RTF obálky
- Nutno detekovat: začíná na
{\\rtf= RTF, jinak plain text
Příklad minimálního RTF záznamu
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
{\fonttbl{\f0\fnil\fcharset238 Arial;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
\pard\plain\f0\fs20 Text záznamu zde\par
}
INSERT do DEKURS – ověřený postup
import fdb, datetime
conn = fdb.connect(...)
cur = conn.cursor()
cur.execute('SELECT MAX(ID) FROM DEKURS')
next_id = cur.fetchone()[0] + 1
text = r'{\rtf1\ansi\ansicpg1250...\par' + '\n}' # raw string pro RTF!
cur.execute("""
INSERT INTO DEKURS (ID, IDPAC, IDODD, IDPRAC, IDUZI, DATUM, CAS, DEKURS, DGN1)
VALUES (?, ?, 2, 2, 6, ?, ?, ?, ?)
""", (next_id, idpac, datum, cas, text, dgn1))
conn.commit()
- RTF string musí být raw string (r'...') kvůli zpětným lomítkům
- DGN1 = 5 znaků s mezerou:
'Z000 ' - DATUM = datetime.date(...), CAS = datetime.time(...)
Soubory v projektu
explore_db.py– výpis všech tabulek a jejich sloupcůinspect_table.py– detailní info o konkrétní tabulce (arg: název tabulky)sample_dekurs.py– ukázky nejstarších a nejnovějších záznamůsample_dekurs2.py– celý text jednoho záznamusample_rtf.py– nejdelší záznamy pacienta (dle délky RTF)analyze_rtf.py– analýza RTF tagů v záznamuinsert_test.py– testovací INSERT do DEKURSCLAUDE_NOTES.md– tento soubor
Export FILES do externích DB (zjištěno 2026-03-17 přes FBScanner)
Mechanismus přesunu
Medicus vytváří měsíční externí Firebird databáze ve formátu MEDICUS_FILES_YYYYMM.fdb.
Postup exportu:
- SELECT dat z FILES po 10 záznamech:
select first 10 ID, BODY, DATUM from FILES where ID > X ORDER BY ID ASC - Binární data (skeny) se zapíší do externího
.fdbsouboru - V hlavní DB se BODY přepíše referenčním číslem (ID v externím souboru):
update FILES set BODY = 379 where ID = 10009 - Do EXTERNI_DB se zapíše lokace:
insert into EXTERNI_DB (DBNAME, SERVER, PATH, HESLO) values ('DB202501', 'localhost/3053', 'u:\MEDICUS_FILES_202501.fdb', '<encrypted>')
Výsledek
- Hlavní DB: FILES.BODY obsahuje jen malé číslo (referenci) místo MB blobu
- Skenované soubory jsou fyzicky v
u:\MEDICUS_FILES_YYYYMM.fdb - Medicus načítá soubory přes EXTERNI_DB tabulku (SERVER + PATH + HESLO)
- Hesla v EXTERNI_DB jsou šifrovaná (base64 AES)
FBScanner
- Nainstalován, funguje jako proxy: Medicus → [3050] FBScanner → [3053] Firebird
- Community Edition 2.5 – stačí pro monitoring
- Export proveden na záloze (ne produkční DB)
Přímý zápis do externí DB – dokončeno (2026-03-17)
Nový soubor: funkce_ext.py
Náhrada za funkce.zapis_file(). Ukládá PDF přímo do měsíční externí FDB
místo do hlavní medicus.fdb.
Funkce: zapis_file_ext(vstupconnection, idpac, cesta, souborname, prvnizavorka, soubordate, souborfiledate, poznamka, ext_base_path=r'u:\\')
- Parametry shodné s
funkce.zapis_file()→ jen prohodit import - Vrací
fileidstejně jako původní funkce
Co dělá:
- Z
soubordateodvodíYYYYMM→DBNAME = 'DB202603' - Načte PDF jako bytes
- Vyhledá EXTERNI_DB pro daný měsíc:
- Nalezeno → připojí se k existující FDB (masterkey)
- Nenalezeno → vytvoří novou
MEDICUS_FILES_YYYYMM.fdb, zaregistruje do EXTERNI_DB
- Vygeneruje
UID = uuid.uuid4().hex(32 znaků) - Zapíše do DATA tabulky:
UID, DATA (blob), DATASIZE, CHUNK=0 - Sestaví 48bajtovou BODY referenci
- Získá
fileidz Gen_Files - Vloží do FILES s BODY = reference (ne blob)
Úprava s03soubory.py
- Přidán import
funkce_ext - Volání
funkce.zapis_file(...)nahrazenofunkce_ext.zapis_file_ext(...)
BODY reference – binární formát (48 B)
magic 4 B = b'\xee\xbb\xaa\x0b'
uid 32 B = UUID4 hex (ASCII, 32 znaků)
dblen 4 B = len(DBNAME) jako little-endian uint32
dbname N B = DBNAME ASCII (typicky 'DB202603', 8 B)
Celkem typicky: 4 + 32 + 4 + 8 = 48 bajtů
Struktura DATA tabulky (externe DB)
UID VARCHAR(32) NOT NULL
DATA BLOB SUB_TYPE 0
DATASIZE INTEGER
CHUNK INTEGER
Generator: GEN_UID (Medicus ho asi nepoužívá přímo, UID je UUID string)
EXTERNI_DB tabulka (hlavní DB)
DBNAME např. 'DB202603'
SERVER např. 'localhost/3053' (FBScanner proxy)
PATH např. 'u:\MEDICUS_FILES_202603.fdb'
HESLO šifrované heslo (Medicus AES) – zkopírujeme z existující položky
Při vytváření nové DB: HESLO zkopírujeme z libovolné existující EXTERNI_DB položky (všechny používají masterkey, jen jinak zašifrované – ale stejný klíč).
Poznámka k HESLO šifrování
Hesla v EXTERNI_DB jsou šifrovaná Medicusem. Při vytváření nové DB proto zkopírujeme HESLO z existující položky (stejné heslo = stejný šifrovaný text). Medicus pak dokáže novou DB otevřít standardně. Pokud by to nefungovalo, lze heslo zadat manuálně přes správce Medicusu.
Stav 2026-03-18 – nastudováno po obnově session
Co bylo dokončeno (včera večer)
funkce_ext.zapis_file_ext()je otestována a funkčnís03soubory.pyvolázapis_file_extmísto původnífunkce.zapis_file- Celý import pipeline funguje end-to-end
Celý import pipeline (s03soubory.py)
- Scanuje složku
u:\NextcloudOrdinace\Dokumentace_ke_zpracování - Ověří formát názvu souboru:
RC YYYY-MM-DD Jmeno, Jmeno. [prvnizavorka] [druhazavorka].pdf - Ověří rodné číslo v KAR tabulce
- Seskupí soubory podle RC (jeden pacient = jeden dekurs)
- Pro každý PDF zavolá
funkce_ext.zapis_file_ext()→ zapíše do externí DB - Přesune soubor do
u:\NextcloudOrdinace\Dokumentace_zpracovaná - Sestaví RTF se záložkami (klikatelné odkazy na soubory v Medicusu)
- Zapíše dekurs do DEKURS tabulky
Klíčové soubory
funkce_ext.py– zápis PDF do měsíční externí FDB (náhrada za funkce.zapis_file)funkce.py– původní funkce (zapis_file, zapis_dekurs, get_files_id, get_idpac, convert_to1250)s03soubory.py– hlavní import script (spouštět na Windows)s04_presun_externi_db.py– utility: přesun ext DB souborů do u:\externi\ a update PATH v EXTERNI_DBtest_ext_db4.py– ověření BODY bajty z FILES tabulky (hex dump)
Formát názvu souboru pro import
RC YYYY-MM-DD Prijmeni, Jmeno. [prvnizavorka] [druhazavorka].pdf
např: 7309208104 2026-03-17 Buzalka, Vladimír. [EKG] [normální nález].pdf
Oprava RTF klikacích odkazů (2026-03-18)
Problém
s03soubory.py generoval RTF s \cs22 pro styl odkazu, ale Medicus vyžaduje \cs32.
Druhý+ bookmark neměl link styling vůbec. poradi se nikdy neinkrementoval.
Zjištěno analýzou Medicusem vytvořeného DEKURS (ID=263480, AhojClaude.pdf)
Správný RTF formát klikacího odkazu:
{\*\cs32\f0\ul\fs20\cf1 Odkaz;} ← stylesheet: cs32, NE cs22
\uc1\pard\s10\plain\cs20\f0\i\fs20 Přílohy: {\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 TEXT{\*\bkmkend 0}\par
- "Přílohy:" je inline s prvním odkazem na stejném
\pardřádku - Každý další odkaz:
\pard\s10{\*\bkmkstart N}\plain\cs32\f0\ul\fs20\cf1 TEXT{\*\bkmkend N}\par
Opraveno v s03soubory.py + test_import_3files.py
\cs22→\cs32v stylesheet i těle RTF- Header
Vložené přílohy:je na samostatném\pardřádku (RTF:Vlo\'9een\'e9 p\'f8\'edlohy:) - Všechny bookmarky mají stejný formát:
\pard\s10{\*\bkmkstart N}\plain\cs32\f0\ul\fs20\cf1 TEXT{\*\bkmkend N}\par - Přidáno
poradi += 1(bug: poradi se nikdy neinkrementoval) - Odstraněna proměnná
prvnibookmark– již nepotřebná
RTF kódování českých znaků (win1250)
- ž =
\'9e, é =\'e9, ř =\'f8, í =\'ed, á =\'e1 - "Vložené přílohy:" →
Vlo\'9een\'e9 p\'f8\'edlohy:
Merge do existujícího dekurzu (2026-03-18)
Rozhodovací logika (3 případy)
- Poslední dekurs pacienta je z dnešního dne A má
Vložené přílohy:→ přidat soubor DO existující sekce (pridat_do_sekce_prilohy) - Poslední dekurs je z dnešního dne, ale sekci nemá
→ prepend nové sekce na začátek (
merge_rtf_prepend) - Poslední dekurs je z jiného dne / neexistuje → nový dekurs pro dnešek
- Funkce:
najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni)
pridat_do_sekce_prilohy (přidání do existující sekce)
- Spočítat
"Files:"v{\info{\bookmarks}}= N → nový bkmkstart = N - Posunout všechny bkmkstart/bkmkend ≥ N o +1 (uvolnit index N)
- Vložit nový
\pardpřed\pard\s10\plain\cs15\f0\fs20 \par(konec sekce) - Vložit bookmark na pozici N do
{\info{\bookmarks}}
merge_rtf_prepend (prepend nové sekce)
- Posunout existující
\bkmkstart N/\bkmkend No počet nových souborů - Přidat naše bookmarky NA ZAČÁTEK
{\info{\bookmarks ...}} - Vložit naše
\pardtělo PŘED první\uc1\pardexistujícího těla
Detekce sekce
- Přítomnost:
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"– hledáme v RTF - Konec sekce:
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
Skripty
test_import_merge.py– 3 soubory → merge do dnešního dekurzu (bez sekce přílohy)test_import_single.py– 1 soubor → merge DO existující sekce přílohytest_import_3files.py– 3 soubory → vždy nový dekurs (bez merge logiky, starší)
Stav testování (2026-03-18)
test_import_3files.py✅ ověřeno – klikací odkazy fungují, "Vložené přílohy:" správnětest_import_merge.py✅ připraveno – merge 3 souborů do dnešního dekurzu (bez sekce)test_import_single.py✅ připraveno – přidání 1 souboru DO existující sekce přílohy
Stav s03soubory.py (2026-03-20) – KOMPLETNÍ ✅
- Merge logika plně integrována z
test_import_single.py - Přidány konstanty
PRILOHY_HEADER,PRILOHY_CLOSING - Přidány funkce:
najdi_posledni_dekurs_dnes(),ma_sekci_prilohy(),pridat_do_sekce_prilohy(),merge_rtf_prepend() - RTF šablona přesunuta do konstanty
RTF_TEMPLATE - Odstraněn dead code:
prvnibookmark, staré šablony, nepoužívanéconvert_to1250 pridat_do_sekce_prilohy()podporuje více souborů najednou (oproti test_import_single.py, kde byl jen 1)idpacse bere z prvního záznamu skupiny (čistěji než zrowpo skončení loopu)
Rozhodovací logika s03soubory.py – 3 případy
- Dnešní dekurs má sekci
Vložené přílohy→ soubory přidány dovnitř sekce (pridat_do_sekce_prilohy) - Dnešní dekurs nemá sekci příloh → nová sekce vložena na začátek (
merge_rtf_prepend) - Žádný dnešní dekurs → nový záznam (INSERT)
Stav testovacích skriptů
test_import_3files.py✅ ověřeno – klikací odkazy fungujítest_import_merge.py✅ otestováno – merge 3 souborů (jen 2 případy, bez detekce sekce)test_import_single.py✅ otestováno – všechny 3 případy, základ pro s03soubory.py
Oprava BlobReader v dekurz_report.py (2026-04-01)
Chyba
Exception ignored while calling deallocator <function BlobReader.__del__>:
fdb.fbcore.DatabaseError: BlobReader.close/isc_close_blob: invalid BLOB handle (-901)
Skript fungoval správně (exit code 0, Excel uložen), ale garbage collector házel warningy.
Příčina
fdb vrací BLOB sloupce jako BlobReader objekty. Po conn.close() se při GC pokoušely
zavřít handle, který již neexistoval.
Oprava
Explicitně zavřít BlobReader hned po .read(), ještě za živa spojení:
# PŘED (špatně):
rtf = dekurs_blob.read() if hasattr(dekurs_blob, 'read') else (dekurs_blob or '')
# PO (správně):
if hasattr(dekurs_blob, 'read'):
rtf = dekurs_blob.read()
dekurs_blob.close()
else:
rtf = dekurs_blob or ''
Obecné pravidlo
Kdykoli čteme BLOB z fdb, vždy .close() po .read() – jinak GC po conn.close() hlásí chybu.
Další postup (nápady)
- Otestovat
s03soubory.pyna Windows se skutečnými soubory (všechny 3 případy) - Napsat
rtf_to_text()pro extrakci čistého textu z dekurzů - Prozkoumat tabulky: LECH/LECD (léky?), POU (poukazy?), AMBULEKY (výkony?)
- První report – domluvit s uživatelem co chce vidět
KAR.POZNAMKA – automatické tagy (2026-04-09)
Pole POZNAMKA v tabulce KAR je BLOB SUB_TYPE 1 (text blob, win1250) –
Medicus ho renderuje jako RTF stejně jako DEKURS. Lze psát i prostý text.
Konvence automatických tagů
Automaticky zapisované informace se ukládají na začátek POZNAMKA ve formátu:
[[klic:hodnota1, hodnota2]] #zapsáno DD-MM-YYYY HH:MM#
- Oddělovače
[[a]]se v lékařských poznámkách přirozeně nevyskytují - Tag lze programově najít a nahradit regexem
- Ruční text lékaře/sestry pod tagem zůstává nedotčen
Implementovaný tag: prev_prohlidka
[[prev_prohlidka:15-05-2024 01022, 15-04-2026]] #zapsáno 09-04-2026 14:30#
15-05-2024= datum poslední PP01022= kód výkonu (01022 = dospělí, 01021 = starší varianta)15-04-2026= nejdřívější možný termín příští PP (23 měsíců od poslední)- Skript:
MedicusWithClaudeTest/prev_prohlidka.py
Tabulka VZPARC – výkonové dávky pojišťovnám (zjištěno 2026-04-09)
Zachyceno přes Firebird trace log při exportu dávek z Medicusu.
Struktura
- 1954 záznamů, data od 2007 (s DAVKA blobem od 2013)
- Klíčové sloupce:
ID,ROK,DRUH,DATUMOD,DATUMDO,DAVKA(blob),ICP,ODB,IDICZ - Vazba na ICZ přes
IDICZ
Typy dávek (DRUH)
| DRUH | Popis |
|---|---|
98 |
Výkonová dávka – obsahuje výkony vykázané pojišťovně (DP98 formát) |
05 |
Doplňková dávka (DP05) |
80 |
Registrační dávka – přihlášení/odhlášení pacientů (DP80) |
36 |
Jiný typ |
DAVKA blob – formát (CP852)
Stejný formát jako KDAVKA v PORTAL, dekódování identické:
text = raw.encode('cp1250', errors='replace').decode('cp852', errors='replace')
Struktura DP98 dávky:
DP98... – hlavička (IČP, rok, měsíc, disk, počet případů, částka)
A ... – ambulantní případ: RC pacienta na pozici 34, délka 10
V ... – výkon: V + DDMM + YYYY + KOD(5) + ...
Z ... – ZULP
L ... – lék
Parsování RC z A řádku
aktualni_rc = line[34:44].strip() # pevná pozice, délka 10
Parsování výkonu z V řádku
dd = int(line[1:3])
mm = int(line[3:5])
yyyy = int(line[5:9])
kod = line[9:14].strip() # 5znakový kód výkonu
Poznámka: PORTAL vs VZPARC
- PORTAL – starší tabulka, KDAVKA blob, data jen do 2015
- VZPARC – správná tabulka pro výkonové dávky, data od 2013 do dnes
- Pro hledání výkonů per pacient vždy používat VZPARC
Skript prev_prohlidka.py (MedicusWithClaudeTest/)
Účel: najde datum poslední preventivní prohlídky (PP) pro každého registrovaného pacienta a zapíše/aktualizuje tag v KAR.POZNAMKA.
Co dělá
- Načte registrované pacienty (přesný dotaz přes REGISTR + ICP, IČP=09305001)
- Projde všechny VZPARC DRUH=98 dávky, hledá výkony
01022nebo01021 - Pro každého pacienta zachová jen nejnovější datum PP
- Zapíše/přepíše tag
[[prev_prohlidka:...]]na začátek KAR.POZNAMKA
Výsledek posledního spuštění (2026-04-09)
- Registrovaných pacientů: 1621
- Dávek VZPARC DRUH=98: 778
- Pacientů s nalezenou PP: 1289
- Zapsáno do KAR: 1120
- Nenalezeno (odregistrovaní nebo bez PP): 169
Závislosti
pip install python-dateutil
(fdb již musí být nainstalováno)