notebookvb

This commit is contained in:
Vladimir Buzalka
2026-04-29 06:51:47 +02:00
parent a1b9c93506
commit a9c143ba24
141 changed files with 30711 additions and 0 deletions
+500
View File
@@ -0,0 +1,500 @@
# 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.py` integrována do `s03soubory.py`. Import pipeline kompletní.
- **2026-04-01**: Opravena chyba `BlobReader invalid BLOB handle` v `dekurz_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
```python
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ý `python` pří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ý text
- `cs21` = 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
```python
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áznamu
- `sample_rtf.py` nejdelší záznamy pacienta (dle délky RTF)
- `analyze_rtf.py` analýza RTF tagů v záznamu
- `insert_test.py` testovací INSERT do DEKURS
- `CLAUDE_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:**
1. SELECT dat z FILES po 10 záznamech: `select first 10 ID, BODY, DATUM from FILES where ID > X ORDER BY ID ASC`
2. Binární data (skeny) se zapíší do externího `.fdb` souboru
3. 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`
4. 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í `fileid` stejně jako původní funkce
**Co dělá:**
1. Z `soubordate` odvodí `YYYYMM``DBNAME = 'DB202603'`
2. Načte PDF jako bytes
3. 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
4. Vygeneruje `UID = uuid.uuid4().hex` (32 znaků)
5. Zapíše do DATA tabulky: `UID, DATA (blob), DATASIZE, CHUNK=0`
6. Sestaví 48bajtovou BODY referenci
7. Získá `fileid` z Gen_Files
8. Vloží do FILES s BODY = reference (ne blob)
### Úprava `s03soubory.py`
- Přidán import `funkce_ext`
- Volání `funkce.zapis_file(...)` nahrazeno `funkce_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.py` volá `zapis_file_ext` místo původní `funkce.zapis_file`
- Celý import pipeline funguje end-to-end
### Celý import pipeline (s03soubory.py)
1. Scanuje složku `u:\NextcloudOrdinace\Dokumentace_ke_zpracování`
2. Ověří formát názvu souboru: `RC YYYY-MM-DD Jmeno, Jmeno. [prvnizavorka] [druhazavorka].pdf`
3. Ověří rodné číslo v KAR tabulce
4. Seskupí soubory podle RC (jeden pacient = jeden dekurs)
5. Pro každý PDF zavolá `funkce_ext.zapis_file_ext()` → zapíše do externí DB
6. Přesune soubor do `u:\NextcloudOrdinace\Dokumentace_zpracovaná`
7. Sestaví RTF se záložkami (klikatelné odkazy na soubory v Medicusu)
8. 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_DB
- `test_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
1. `\cs22``\cs32` v stylesheet i těle RTF
2. Header `Vložené přílohy:` je na **samostatném** `\pard` řádku (RTF: `Vlo\'9een\'e9 p\'f8\'edlohy:`)
3. Všechny bookmarky mají stejný formát: `\pard\s10{\*\bkmkstart N}\plain\cs32\f0\ul\fs20\cf1 TEXT{\*\bkmkend N}\par`
4. Přidáno `poradi += 1` (bug: poradi se nikdy neinkrementoval)
5. 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)
1. 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`)
2. Poslední dekurs je z dnešního dne, ale sekci nemá
**prepend nové sekce na začátek** (`merge_rtf_prepend`)
3. 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)
1. Spočítat `"Files:"` v `{\info{\bookmarks}}` = N → nový bkmkstart = N
2. Posunout všechny bkmkstart/bkmkend ≥ N o +1 (uvolnit index N)
3. Vložit nový `\pard` před `\pard\s10\plain\cs15\f0\fs20 \par` (konec sekce)
4. Vložit bookmark na pozici N do `{\info{\bookmarks}}`
### merge_rtf_prepend (prepend nové sekce)
1. Posunout existující `\bkmkstart N` / `\bkmkend N` o počet nových souborů
2. Přidat naše bookmarky NA ZAČÁTEK `{\info{\bookmarks ...}}`
3. Vložit naše `\pard` tělo PŘED první `\uc1\pard` existují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řílohy
- `test_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)
- `idpac` se bere z prvního záznamu skupiny (čistěji než z `row` po skončení loopu)
### Rozhodovací logika s03soubory.py 3 případy
1. Dnešní dekurs **má** sekci `Vložené přílohy` → soubory přidány **dovnitř** sekce (`pridat_do_sekce_prilohy`)
2. Dnešní dekurs **nemá** sekci příloh → nová sekce vložena **na začátek** (`merge_rtf_prepend`)
3. Žá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í:
```python
# 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.py` na 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í PP
- `01022` = 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é:
```python
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
```python
aktualni_rc = line[34:44].strip() # pevná pozice, délka 10
```
### Parsování výkonu z V řádku
```python
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á
1. Načte registrované pacienty (přesný dotaz přes REGISTR + ICP, IČP=09305001)
2. Projde všechny VZPARC DRUH=98 dávky, hledá výkony `01022` nebo `01021`
3. Pro každého pacienta zachová jen **nejnovější** datum PP
4. 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)
+37
View File
@@ -0,0 +1,37 @@
@echo off
:: Firebird SQL trace pro Medicus
:: Zachytí všechny SQL příkazy při exportu souborů do externích DB
::
:: INSTRUKCE:
:: 1. Spusť tento soubor jako Administrator (pravé tlačítko → Spustit jako správce)
:: 2. Nechej okno otevřené
:: 3. V Medicusu proveď export souborů do externích DB
:: 4. Vrať se sem a stiskni Ctrl+C pro zastavení
:: 5. Log najdeš v medicus_trace_output.txt
echo.
echo === MEDICUS SQL TRACE ===
echo Hledam Firebird fbtracemgr...
echo.
set FB_BIN=C:\Program Files\Firebird\Firebird_2_5_CGM\bin
if not exist "%FB_BIN%\fbtracemgr.exe" (
echo CHYBA: fbtracemgr.exe nenalezen v %FB_BIN%
echo Zkontroluj cestu a uprav tento soubor.
pause
exit /b 1
)
echo Nalezen: %FB_BIN%\fbtracemgr.exe
echo.
echo POZOR: Nyni PROVEĎ export v Medicusu, pak se vrat a stiskni Ctrl+C
echo Výstup se zapisuje do: medicus_trace_output.txt
echo.
powershell -Command "& '%FB_BIN%\fbtracemgr.exe' -se localhost:service_mgr -user SYSDBA -password masterkey -start -config '%~dp0medicus_trace.conf' | Tee-Object -FilePath '%~dp0medicus_trace_output.txt'"
echo.
echo === TRACE ZASTAVEN ===
echo Výsledky jsou v medicus_trace_output.txt
pause
+34
View File
@@ -0,0 +1,34 @@
import fdb, re
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute('SELECT DEKURS FROM DEKURS WHERE ID=243082')
text = cur.fetchone()[0]
text = text.read() if hasattr(text, 'read') else text
text = text.decode('windows-1250', errors='replace') if isinstance(text, bytes) else text
# colortbl
ct = re.search(r'\\colortbl[^}]+\}', text)
if ct: print('COLORTBL:', ct.group(0))
# stylesheet
ss = re.search(r'\\stylesheet\{.+?\}(?=\n)', text, re.DOTALL)
if ss: print('\nSTYLESHEET:', ss.group(0)[:600])
# Najdi různé styly použité v textu
print('\n--- Použité RTF tagy (unikátní) ---')
tags = re.findall(r'\\[a-z]+\d*', text)
from collections import Counter
for tag, count in Counter(tags).most_common(40):
print(f" {tag:<20} {count}x")
# Ukázka tabulky
tbl_start = text.find(r'\trowd')
if tbl_start > 0:
print('\n--- Začátek tabulky ---')
print(text[tbl_start:tbl_start+300])
conn.close()
@@ -0,0 +1,42 @@
"""check_last_dekurs.py zobrazí posledních 5 záznamů v DEKURS + FILES pro pacienta 9742"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250'
)
cur = conn.cursor()
print("=== Posledních 5 záznamů DEKURS pro Buzalka (idpac=9742) ===")
cur.execute("""
SELECT id, datum, cas, OCTET_LENGTH(dekurs) as delka
FROM dekurs WHERE idpac = 9742
ORDER BY id DESC ROWS 5
""")
for row in cur.fetchall():
print(f" ID={row[0]} datum={row[1]} cas={row[2]} délka={row[3]} B")
print()
print("=== Obsah posledního DEKURS záznamu ===")
cur.execute("""
SELECT dekurs FROM dekurs WHERE idpac = 9742
ORDER BY id DESC ROWS 1
""")
row = cur.fetchone()
if row:
text = row[0]
if isinstance(text, bytes):
text = text.decode('cp1250', errors='replace')
print(text)
print()
print("=== Posledních 5 záznamů FILES pro Buzalka (idpac=9742) ===")
cur.execute("""
SELECT id, filename, datum, OCTET_LENGTH(body) as bodylen
FROM files WHERE idpac = 9742
ORDER BY id DESC ROWS 5
""")
for row in cur.fetchall():
print(f" ID={row[0]} filename={row[1]} datum={row[2]} body={row[3]} B")
conn.close()
+70
View File
@@ -0,0 +1,70 @@
"""
Pomocný skript pro Clauda spusť na Windows: python db_query.py
Výsledky zapíše do db_query_result.txt
"""
import fdb
import json
import sys
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
results = {}
# 1. Tabulky s klíčovými slovy (soubory, dokumenty)
cur.execute("""
SELECT RDB$RELATION_NAME
FROM RDB$RELATIONS
WHERE RDB$SYSTEM_FLAG = 0 AND RDB$VIEW_BLR IS NULL
ORDER BY RDB$RELATION_NAME
""")
all_tables = [row[0].strip() for row in cur.fetchall()]
keywords = ['FILE', 'SOUB', 'DOC', 'SCAN', 'OBRAZ', 'IMG', 'ATTACH',
'EXTERN', 'PRILOHA', 'ZPRAV', 'ARCH', 'PRIL', 'FOTO']
matches = [t for t in all_tables if any(k in t.upper() for k in keywords)]
results['tables_matching'] = matches
results['total_tables'] = len(all_tables)
# 2. Podrobnosti o nalezených tabulkách (sloupce + počet řádků)
table_details = {}
for t in matches:
try:
cur.execute(f"SELECT COUNT(*) FROM {t}")
count = cur.fetchone()[0]
except:
count = "error"
cur.execute("""
SELECT r.RDB$FIELD_NAME, f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH
FROM RDB$RELATION_FIELDS r
JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = r.RDB$FIELD_SOURCE
WHERE r.RDB$RELATION_NAME = ?
ORDER BY r.RDB$FIELD_POSITION
""", (t,))
cols = [(row[0].strip(), row[1], row[2]) for row in cur.fetchall()]
table_details[t] = {"count": count, "columns": cols}
results['table_details'] = table_details
# 3. Uložit výsledky
output = []
output.append(f"=== VÝSLEDKY DB PRŮZKUMU ===\n")
output.append(f"Celkem tabulek v DB: {results['total_tables']}\n")
output.append(f"\nTabulky obsahující klíčová slova (soubory/dokumenty):\n")
for t in results['tables_matching']:
d = results['table_details'].get(t, {})
output.append(f"\n ** {t} ** (řádků: {d.get('count', '?')})")
for col in d.get('columns', []):
output.append(f" - {col[0]} (type={col[1]}, len={col[2]})")
with open('db_query_result.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(output))
print("Hotovo! Výsledky jsou v db_query_result.txt")
conn.close()
+75
View File
@@ -0,0 +1,75 @@
"""
Podrobný průzkum FILES a EXTERNI_DB spusť na Windows: python db_query2.py
"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
out = []
# 1. Obsah EXTERNI_DB
out.append("=== EXTERNI_DB (konfigurace externích databází) ===")
cur.execute("SELECT DBNAME, PATH, SERVER, HESLO FROM EXTERNI_DB")
for row in cur.fetchall():
out.append(f" DBNAME={row[0]} PATH={row[1]} SERVER={row[2]} HESLO={row[3]}")
# 2. FILES přehled po rocích
out.append("\n=== FILES počty záznamů po rocích ===")
cur.execute("""
SELECT EXTRACT(YEAR FROM DATUM) as ROK, COUNT(*) as POCET,
SUM(OCTET_LENGTH(BODY)) as CELKEM_BYTU
FROM FILES
GROUP BY EXTRACT(YEAR FROM DATUM)
ORDER BY ROK
""")
total_bytes = 0
for row in cur.fetchall():
rok, pocet, bytu = row
mb = (bytu or 0) / 1024 / 1024
total_bytes += (bytu or 0)
out.append(f" Rok {int(rok) if rok else '?'}: {pocet} souborů, {mb:.1f} MB")
out.append(f" CELKEM: {total_bytes/1024/1024:.1f} MB v BLOB datech FILES")
# 3. FILES ukázka typů souborů
out.append("\n=== FILES typy souborů (dle přípony FILENAME) ===")
cur.execute("""
SELECT LOWER(SUBSTRING(FILENAME FROM CHAR_LENGTH(FILENAME) - 3)) as EXT,
COUNT(*) as POCET
FROM FILES
WHERE FILENAME IS NOT NULL
GROUP BY 1
ORDER BY 2 DESC
""")
for row in cur.fetchall():
out.append(f" {row[0]} : {row[1]} souborů")
# 4. FILES ukázka prvních 5 záznamů (bez BODY)
out.append("\n=== FILES ukázka 5 záznamů (bez binárních dat) ===")
cur.execute("""
SELECT ID, IDPAC, FILENAME, DATUM, OCTET_LENGTH(BODY) as VELIKOST,
POZNAMKA, IDDOCTYP
FROM FILES
ORDER BY DATUM DESC
ROWS 5
""")
for row in cur.fetchall():
out.append(f" ID={row[0]} IDPAC={row[1]} soubor={row[2]} datum={row[3]}"
f" velikost={row[4]}B pozn={row[5]} typ={row[6]}")
# 5. DOCTYP typy dokumentů
out.append("\n=== DOCTYP číselník typů dokumentů ===")
cur.execute("SELECT ID, NAZEV, SYSID FROM DOCTYP ORDER BY ID")
for row in cur.fetchall():
out.append(f" ID={row[0]} nazev={row[1]} sysid={row[2]}")
result = '\n'.join(out)
print(result)
with open('db_query2_result.txt', 'w', encoding='utf-8') as f:
f.write(result)
print("\nUloženo do db_query2_result.txt")
conn.close()
@@ -0,0 +1,532 @@
=== VÝSLEDKY DB PRŮZKUMU ===
Celkem tabulek v DB: 993
Tabulky obsahující klíčová slova (soubory/dokumenty):
** CHAT_ZPRAVY ** (řádků: 0)
- ID (type=8, len=4)
- IDADRESAR (type=8, len=4)
- ODCHOZI (type=14, len=1)
- ODESLANO (type=35, len=8)
- TEXT (type=261, len=8)
- OTHER_ID (type=8, len=4)
- PRECTENO (type=35, len=8)
- SENT (type=35, len=8)
** CLICKDOC ** (řádků: 2)
- KEY (type=37, len=100)
- VALUE (type=37, len=100)
** CLICKDOC_CINNOSTI ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDCINNOSTI (type=8, len=4)
** CLICKDOC_KAR ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDPAC (type=8, len=4)
** CLICKDOC_OBJ ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDOBJ (type=8, len=4)
- REMOTECREATE (type=14, len=1)
- REMOTECHANGE (type=14, len=1)
- APPROVAL (type=7, len=2)
- SEND_REMINDER_SMS (type=14, len=1)
** CLICKDOC_UZIVATEL ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDUZI (type=8, len=4)
** DEKIMG ** (řádků: 8)
- ID (type=8, len=4)
- HASH (type=37, len=32)
- REFCOUNT (type=8, len=4)
- DATA (type=261, len=8)
** DEKIMGREF ** (řádků: 7)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- IDDEKURS (type=8, len=4)
- IDDEKIMG (type=8, len=4)
** DOCLIST ** (řádků: 461429)
- ID (type=8, len=4)
- DATUM (type=12, len=4)
- TABULKA (type=37, len=20)
- IDREC (type=8, len=4)
- TYP (type=7, len=2)
- POPIS (type=37, len=40)
- IDPAC (type=8, len=4)
- IDODDEL (type=8, len=4)
** DOCTYP ** (řádků: 27)
- ID (type=8, len=4)
- NAZEV (type=37, len=32)
- SYSID (type=8, len=4)
** DOMPECEARCHIV ** (řádků: 0)
- ID (type=8, len=4)
- CREATED (type=35, len=8)
- DATA (type=261, len=8)
** ED_BOOKOFSUBMISSIONATTACH ** (řádků: 0)
- ID (type=8, len=4)
- BOOK_OF_SUBMISSION_ID (type=8, len=4)
- ORDER (type=8, len=4)
- HAS_ALREADY_BEEN_SENT (type=14, len=1)
- NAME (type=37, len=255)
- CONTENT (type=261, len=8)
** EET_ZPRAVARECORD ** (řádků: 0)
- ID (type=8, len=4)
- UUID_ZPRAVY (type=14, len=16)
- ID_TRZBA (type=8, len=4)
- GUID_TRZBA (type=14, len=16)
- DAT_ODESL (type=35, len=8)
- CERTTHUMBPRINT (type=37, len=64)
- REQUEST (type=261, len=8)
- RESPONSE (type=261, len=8)
- RESP_TYPE (type=8, len=4)
- RESP_UUID_ZPRAVY (type=37, len=50)
- RESP_DAT_PRIJ (type=35, len=8)
- RESP_DAT_ODMIT (type=35, len=8)
- RESP_BKP (type=37, len=44)
- RESP_FIK (type=37, len=39)
- RESP_KOD (type=8, len=4)
- RESP_CHYBA (type=37, len=100)
- PRVNI_ZASLANI (type=7, len=2)
- RESP_TEST (type=7, len=2)
- RESP_EXCEPTIONMESSAGE (type=261, len=8)
** ES_KONZULTACE_FILE ** (řádků: 0)
- ID (type=8, len=4)
- ID_KONZULTACE (type=8, len=4)
- CAS (type=35, len=8)
- NAME (type=37, len=256)
- CONTENT (type=261, len=8)
- MIME (type=37, len=100)
- PATIENT_DOC (type=14, len=1)
- PORADI (type=8, len=4)
** EXTERNI_DB ** (řádků: 1)
- DBNAME (type=37, len=12)
- PATH (type=37, len=120)
- SERVER (type=37, len=32)
- HESLO (type=37, len=64)
** FILES ** (řádků: 10305)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DOCID (type=8, len=4)
- TYP (type=8, len=4)
- FILENAME (type=37, len=254)
- BODY (type=261, len=8)
- DATUM (type=12, len=4)
- IDDOCTYP (type=8, len=4)
- IDPRAC (type=8, len=4)
- IDUZI (type=8, len=4)
- POZNAMKA (type=37, len=100)
- DATSOUBORU (type=35, len=8)
- ID_EDOKUMENT (type=8, len=4)
- EXT_ID (type=37, len=36)
** FNUSA_ATTACHMENT ** (řádků: 0)
- ID (type=8, len=4)
- DATA (type=261, len=8)
- ATTACH_INT_TYP (type=37, len=1)
- ATTACH_INT_ID (type=8, len=4)
- LAST_UPDATED (type=35, len=8)
** GRARCH ** (řádků: 0)
- ID (type=8, len=4)
- NAME (type=37, len=16)
- PATH (type=37, len=150)
** GRDOC ** (řádků: 0)
- ID (type=8, len=4)
- IDNEM (type=37, len=60)
- IDPAC (type=8, len=4)
- SKUPINA (type=8, len=4)
- DATUM (type=12, len=4)
- PREVIEW (type=261, len=8)
- PICT (type=261, len=8)
- LOCATION (type=37, len=16)
** HISTDOC ** (řádků: 32250)
- ID (type=8, len=4)
- TYP (type=37, len=7)
- DATUM (type=12, len=4)
- DATA (type=261, len=8)
- CENA (type=16, len=8)
- REPPOZN (type=37, len=60)
- REPDATUM (type=12, len=4)
- IDPACI (type=8, len=4)
- IDZAR (type=8, len=4)
- IDODD (type=8, len=4)
- IDPRAC (type=8, len=4)
- IDUZIV (type=8, len=4)
- IDLEK (type=8, len=4)
- IDZARPR (type=8, len=4)
- IDODDPR (type=8, len=4)
- IDPRACPR (type=8, len=4)
- IDLEKPR (type=8, len=4)
- STAV (type=14, len=1)
- REPSTAV (type=14, len=1)
- IDSABLONPRAC (type=37, len=55)
- REPTEXT (type=261, len=8)
- TYPCENY (type=14, len=1)
- CENAPACIENT (type=16, len=8)
- IDDOKLAD (type=8, len=4)
- POZNAMKA (type=37, len=100)
- PRINTED (type=14, len=1)
- PRIJALJINE (type=37, len=150)
- BMKDATA (type=261, len=8)
- PORCISLO (type=8, len=4)
- DRUHPOJ (type=8, len=4)
- IDLEKZPR (type=8, len=4)
- IDHLAV (type=8, len=4)
- CREATED (type=35, len=8)
- IDSOUHLASPACSABL (type=8, len=4)
- REPIDUZIVINS (type=8, len=4)
- REPIDUZIVUPD (type=8, len=4)
- CGMNUMBER_ODESILATEL (type=37, len=12)
- CGMNUMBER_PRIJEMCE (type=37, len=12)
- CLICKBOX_ATT_ID (type=8, len=4)
- CLICKBOX_MAILBOX (type=37, len=255)
- ID_EDOKUMENT (type=8, len=4)
** HISTDOCLAB ** (řádků: 27185)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDMETOD (type=8, len=4)
- POZN (type=261, len=8)
- IDPLADET (type=8, len=4)
** HISTDOCLABPARAMS ** (řádků: 0)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDHISTDOCLAB (type=8, len=4)
- IDPARAM (type=8, len=4)
- TYP (type=14, len=1)
- TEXT (type=14, len=8)
** HISTDOCSABL ** (řádků: 7)
- ID (type=8, len=4)
- TYP (type=37, len=7)
- DATA (type=261, len=8)
- IDZAR (type=8, len=4)
- IDODD (type=8, len=4)
- IDPRAC (type=8, len=4)
- IDUZIV (type=8, len=4)
- IDLEK (type=8, len=4)
- IDZARPR (type=8, len=4)
- IDODDPR (type=8, len=4)
- IDPRACPR (type=8, len=4)
- IDLEKPR (type=8, len=4)
- STAV (type=14, len=1)
- IDSABLONPRAC (type=37, len=55)
- NAZEV (type=37, len=48)
- REPSTAV (type=14, len=1)
- CENA (type=37, len=20)
- DOPLATEK (type=37, len=20)
- USEPRAC (type=8, len=4)
- USEUZIV (type=8, len=4)
** HISTDOCSABLLAB ** (řádků: 19)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDMETOD (type=8, len=4)
** HISTDOCVYK ** (řádků: 1868)
- IDHDVYK (type=8, len=4)
- ID (type=8, len=4)
- KOD (type=14, len=7)
- POCET (type=16, len=8)
- CENA (type=16, len=8)
- CENADOPL (type=16, len=8)
- POJ (type=14, len=3)
- ICZ (type=14, len=8)
- ICP (type=14, len=8)
- DATUM (type=12, len=4)
- REV (type=14, len=1)
- TYP (type=7, len=2)
- NOT_CENA_PRUMER (type=14, len=1)
- VARSYM (type=37, len=20)
- SKUPINA (type=14, len=2)
** HISTDOC_DEF ** (řádků: 0)
- ID (type=8, len=4)
- KOD (type=37, len=5)
- NAZEV (type=37, len=40)
- IDUZI (type=8, len=4)
- DATA (type=261, len=8)
- SKUPINA (type=8, len=4)
** HISTDOC_EPOUKAZ ** (řádků: 849)
- IDHISTDOC (type=8, len=4)
- ID_DOKLADU (type=14, len=9)
- IDUZI (type=8, len=4)
- IDPRAC (type=8, len=4)
- ODESLANO (type=35, len=8)
- CHYBA (type=14, len=1)
- SCHVALENI (type=14, len=1)
- MODIFIED (type=14, len=1)
- ODESLANO_LAST (type=35, len=8)
- ID_ZP (type=37, len=36)
- ID_DOKLADU_VYDEJ (type=37, len=9)
- ID_ZP_VYDEJ (type=37, len=36)
- VYDANO (type=35, len=8)
** HISTDOC_EPOUKAZ_ZMENY ** (řádků: 3)
- ID_LEKAR (type=37, len=36)
- LAST_DATE (type=12, len=4)
** HISTDOC_EPRILOHY ** (řádků: 17)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- NAZEV (type=37, len=255)
- POPIS (type=37, len=255)
- TYP (type=37, len=7)
- SOUBOR (type=261, len=8)
- ID_PRILOHY (type=37, len=36)
** HISTDOC_EZADANKA ** (řádků: 2195)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- TYP (type=37, len=10)
- DATUMVYS (type=12, len=4)
- CASVYS (type=13, len=4)
- CISLO (type=37, len=30)
- POZNAMKA (type=261, len=8)
- HOTOVO (type=14, len=1)
- IDZAD (type=8, len=4)
- VYSTEXT (type=261, len=8)
- DATUMEXP (type=12, len=4)
- CASEXP (type=13, len=4)
- SABLONY (type=261, len=8)
- PRINTED (type=35, len=8)
- SENDED (type=35, len=8)
- TEMPDAT (type=35, len=8)
- SEZNAMMETOD (type=261, len=8)
- SEZNAMSABLON (type=261, len=8)
- DIAG1 (type=37, len=10)
- DIAG2 (type=37, len=10)
- DIAG3 (type=37, len=10)
- STATIM (type=14, len=1)
- ODEBRANO (type=35, len=8)
- BEZODBERU (type=14, len=1)
- EXPORTOVAT (type=35, len=8)
- NAH1 (type=37, len=10)
- NAH2 (type=37, len=10)
- ODEBRAL (type=37, len=30)
- POCETZKUM (type=8, len=4)
- OS_NAZEV (type=37, len=100)
- OS_ID (type=37, len=20)
- PARAMETRY (type=261, len=8)
- POZNAMKY (type=261, len=8)
- STRINGPARAMETRY (type=261, len=8)
- VZOREK1 (type=37, len=25)
- VZOREK2 (type=37, len=25)
- VZOREK3 (type=37, len=25)
- VZOREK4 (type=37, len=25)
- PLATCE (type=37, len=30)
- OSVOBOZENDPH (type=14, len=1)
- PLATCEPOZNAMKA (type=37, len=100)
- KATALOGALIAS (type=37, len=20)
- KATALOGNAZEV (type=37, len=100)
- PRIMARNIVZORKY (type=37, len=256)
** HISTDOC_SCHVALENI ** (řádků: 0)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDVZPARC (type=8, len=4)
- PRILOHA (type=261, len=8)
- ODESLANO (type=14, len=1)
** HOSP_ZPRAVY ** (řádků: 0)
- ID (type=8, len=4)
- DATUM (type=12, len=4)
- CAS (type=13, len=4)
- ZPRAVA (type=261, len=8)
- IDUZI (type=8, len=4)
- IDPAC (type=8, len=4)
- ID_EDOKUMENT (type=8, len=4)
- IDHOSP (type=8, len=4)
** IZIPDOC ** (řádků: 0)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DATUM (type=12, len=4)
- STATUS (type=8, len=4)
- IDENT (type=8, len=4)
- ZAPIS (type=261, len=8)
- DEKID (type=8, len=4)
- DGN (type=14, len=5)
- CAS (type=13, len=4)
- IDZAR (type=8, len=4)
- IDODD (type=8, len=4)
- IDPRAC (type=8, len=4)
- AUTOR (type=8, len=4)
- LOCKED (type=14, len=1)
- TYP (type=14, len=1)
- VDGN1 (type=14, len=5)
- VDGN2 (type=14, len=5)
- VDGN3 (type=14, len=5)
- VDGN4 (type=14, len=5)
** IZIPFILES ** (řádků: 0)
- ID (type=8, len=4)
- DOCID (type=8, len=4)
- TYP (type=8, len=4)
- FILENAME (type=37, len=50)
- BODY (type=261, len=8)
- DATUM (type=12, len=4)
** LEKZPRAVY ** (řádků: 13748)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DEKURSID (type=8, len=4)
- DATUM (type=12, len=4)
- TEXT (type=261, len=8)
- IDUZI (type=8, len=4)
- IDNOSVYK (type=8, len=4)
- TYP (type=14, len=1)
- ODESILATEL (type=37, len=254)
- DEKURS_COPYED (type=35, len=8)
- EISPUBLIC (type=14, len=1)
- PRIJEMCE (type=37, len=254)
- IDPRAC (type=8, len=4)
- SIGNATURE (type=261, len=8)
- SIGNATURE_INFO (type=261, len=8)
- IDCERTIFICATE (type=8, len=4)
- TST (type=261, len=8)
- NAZEV (type=37, len=80)
- ID_EDOKUMENT (type=8, len=4)
- PRIJATO (type=14, len=1)
- JMENOLEK (type=37, len=50)
- ICP (type=14, len=8)
- ES_STAV (type=37, len=1)
- PR_TYP (type=14, len=1)
- PR_ID (type=8, len=4)
- STORNO (type=14, len=1)
- UID (type=37, len=16)
- PUVODNI (type=8, len=4)
- CAS (type=13, len=4)
- DATAKT (type=35, len=8)
- ODALIAS (type=37, len=8)
- PDF (type=261, len=8)
- PREPIS (type=8, len=4)
- OZNACENI_O (type=37, len=50)
- STAV (type=14, len=1)
- CLICKBOX_ATT_ID (type=8, len=4)
- CGMNUMBER_ODESILATEL (type=37, len=12)
- CLICKBOX_MAILBOX (type=37, len=255)
- CGMNUMBER_PRIJEMCE (type=37, len=12)
- CGMNUMBER (type=37, len=12)
** LEKZPRAVY_PR ** (řádků: 134)
- ID (type=8, len=4)
- LEKZPRID (type=8, len=4)
- PRID (type=8, len=4)
- IDUZI (type=8, len=4)
- TYP (type=14, len=1)
- EXT (type=14, len=4)
** LEKZPRAVY_SEND ** (řádků: 0)
- LEKZPRID (type=8, len=4)
- KDY (type=35, len=8)
- KOMU (type=37, len=80)
** MEDINETIN_ATTACHMENT ** (řádků: 120)
- ID (type=8, len=4)
- ID_MEDINETIN (type=8, len=4)
- FILE_NAME (type=37, len=254)
- FILE_EXT (type=37, len=5)
- FILE_TYPE (type=8, len=4)
- DATA (type=261, len=8)
- ID_FILES (type=8, len=4)
- ZPRACOVANO (type=14, len=1)
** MEDINETOUT_ATTACHMENT ** (řádků: 2236)
- ID (type=8, len=4)
- ID_MEDINETOUT (type=8, len=4)
- FILE_ID (type=37, len=36)
- FILE_NAME (type=37, len=254)
- FILE_EXT (type=37, len=5)
- FILE_TYPE (type=8, len=4)
- DATA (type=261, len=8)
- ATTACH_INT_TYP (type=37, len=1)
- ATTACH_INT_ID (type=8, len=4)
** OSE_ZPRAVY ** (řádků: 0)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DATUM (type=35, len=8)
- UZI (type=37, len=3)
- DATA (type=261, len=8)
- SIGNATURE (type=261, len=8)
- SIGNATURE_INFO (type=261, len=8)
- IDCERTIFICATE (type=8, len=4)
- STORNO (type=35, len=8)
- STORNOBY (type=14, len=3)
** SOC_FINPRIJEM_HISTDOC ** (řádků: 0)
- ID_SFH (type=8, len=4)
- ID_SFP (type=8, len=4)
- ID_HDC (type=8, len=4)
** ZPRAVY ** (řádků: 26)
- ID (type=8, len=4)
- IDZPRAVY (type=8, len=4)
- IDOD (type=8, len=4)
- IDKOMUZAR (type=8, len=4)
- IDKOMUODD (type=8, len=4)
- IDKOMUPRAC (type=8, len=4)
- IDKOMUUZIV (type=8, len=4)
- PREDMET (type=37, len=100)
- TEXT (type=261, len=8)
- DATUM (type=12, len=4)
- CAS (type=13, len=4)
- URGENTNI (type=8, len=4)
- PRECTENA (type=8, len=4)
- DATUMPRECT (type=12, len=4)
- CASPRECT (type=13, len=4)
- SMAZANA (type=8, len=4)
- OZNAMOPRECT (type=8, len=4)
- TYP (type=8, len=4)
- KOMU (type=261, len=8)
- PLATNOST (type=35, len=8)
- LZE_ODPOVED (type=14, len=1)
- ID_EXT (type=8, len=4)
** ZPRAVY_ANKETY ** (řádků: 0)
- ID (type=8, len=4)
- IDUZI (type=8, len=4)
- IDPRIJEMCE (type=8, len=4)
- ID_EXT (type=8, len=4)
- DATA (type=261, len=8)
- DATUM (type=12, len=4)
- ODESLANO (type=14, len=1)
** ZPRAVY_EXT ** (řádků: 18)
- ID (type=8, len=4)
- ID_EXT (type=8, len=4)
- ID_ODESILATEL (type=8, len=4)
- TYP (type=37, len=1)
- PRIORITA (type=37, len=1)
- DATUM (type=35, len=8)
- PREDMET (type=37, len=254)
- DATA (type=261, len=8)
- STAV (type=37, len=1)
- AKCE (type=37, len=20)
- PRIJEMCE (type=37, len=3)
- ODESLAL (type=37, len=20)
- ODB (type=37, len=3)
** ZPRAVY_LOCK ** (řádků: 1)
- IDUZI (type=8, len=4)
- BEGINTIME (type=35, len=8)
- ENDTIME (type=35, len=8)
+45
View File
@@ -0,0 +1,45 @@
"""
Průzkum Medicus databáze výpis tabulek a jejich sloupců.
"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
# Všechny uživatelské tabulky
cur.execute("""
SELECT rdb$relation_name
FROM rdb$relations
WHERE rdb$view_blr IS NULL
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
ORDER BY rdb$relation_name
""")
tables = [row[0].strip() for row in cur.fetchall()]
print(f"Počet tabulek: {len(tables)}\n")
print("=== SEZNAM TABULEK ===")
for t in tables:
print(f" {t}")
print("\n=== SLOUPCE KAŽDÉ TABULKY ===")
for table in tables:
cur.execute("""
SELECT rf.rdb$field_name, f.rdb$field_type, f.rdb$field_length
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE rf.rdb$relation_name = ?
ORDER BY rf.rdb$field_position
""", (table,))
cols = cur.fetchall()
col_names = [c[0].strip() for c in cols]
print(f"\n{table}")
print(" " + ", ".join(col_names))
cur.close()
conn.close()
+114
View File
@@ -0,0 +1,114 @@
import os,fdb,datetime
# conn = fdb.connect(
# dsn=r'localhost:u:\medicus 3\data\medicus.fdb', # Database path
# user='SYSDBA', # Username
# password="masterkey", # Password,
# charset="win1250")
# cur = conn.cursor()
def zapis_file(vstupconnection,idpac,cesta,souborname,prvnizavorka,soubordate,souborfiledate,poznamka):
import funkce
cur=vstupconnection.cursor()
fileid = funkce.get_files_id(vstupconnection)
with open(os.path.join(cesta,souborname), 'rb') as f:
daticka = f.read()
query = "insert into files (id,iduzi,iddoctyp,typ,idpac,filename,body,datum,datsouboru,poznamka) values(?,?,?,?,?,?,?,?,?,?)"
cur.execute(query,(fileid,6,1,1,idpac,prvnizavorka+".pdf",daticka,soubordate,souborfiledate,poznamka[:99]))
vstupconnection.commit()
return fileid
def zapis_dekurs(vstupconnection, idpac, idodd, iduzi, idprac, idfile, filename, text, datumzpravy,
datumsouboru):
import funkce
dekursid = funkce.get_dekurs_id(vstupconnection)
cur = vstupconnection.cursor()
print("Funkce zapis_dekurs hlasí OK")
print("idpac", idpac)
print("idodd", idodd)
print("iduzi", iduzi)
print("idfile", idfile)
print("filename", filename)
print("text", text)
print("datumzpravy", datumzpravy)
print("datumsouboru", datumsouboru)
print("dekursid", dekursid)
# rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks "BOOKMARKNAME","Files:FILEID",9}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
# {\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
# {\stylesheet{\s0\fi0\li0\ql\ri0\sb0\sa0 Norm\'e1ln\'ed;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
# \uc1\pard\s0\plain\cs20\f0\i\fs20 P\'f8\'edlohy: {\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 BOOKMARKNAME{\*\bkmkend 0}\par
# \pard\s0\plain\cs15\f0\fs20 \par
# }
# """
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks "BOOKMARKNAME","Files:FILEID",9}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs22\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 P\'f8\'edlohy:\par
\pard\s10{\*\bkmkstart 0}\plain\cs22\f0\ul\fs20\cf1 BOOKMARKNAME{\*\bkmkend 0}\par
\pard\s10\plain\cs15\f0\fs20 \par
}
"""
# id idpac filename body docid typ datum iddoctyp poznamka idpac=2 iduzi=2 datsouboru id_edokument ext_id
encodedbookmark = funkce.convert_to1250(filename)
print("Encodedbookmark", encodedbookmark)
rtf = rtf.replace("BOOKMARKNAME", encodedbookmark)
rtf = rtf.replace("FILEID", str(idfile))
rtf = rtf.replace("TEXTENTER", text)
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
print("Datumzapisu", datumzapisu)
print("Caszapisu", caszapisu)
print("RTF", rtf)
cur.execute("insert into dekurs (id,idpac,idodd,iduzi,idprac,datum,cas,dekurs) values(?,?,?,?,?,?,?,?)",
(dekursid, idpac, idodd, iduzi, idprac, datumzapisu, caszapisu, rtf))
vstupconnection.commit()
def convert_to1250(retezec):
retezec=retezec.encode("cp1250")
retezec=str(retezec)[2:]
retezec = retezec[:-1]
retezec=retezec.replace(r"\x",r"\'")
return retezec
# x=convert_to1250("Příloha")
# print(x,len(x))
def get_dekurs_id(connection):
try:
query = "SELECT GEN_ID(Gen_Dekurs, 1) FROM RDB$DATABASE"
cur = connection.cursor()
cur.execute(query)
newid=cur.fetchone()[0]
print("Funkce GET_DEKURS_ID přiřadila nové ID:",newid)
return(newid)
except:
print("Funkce GET_DEKURS_ID nepřiřadila nové ID")
return(None)
def get_files_id(connection):
try:
query = "SELECT GEN_ID(Gen_Files, 1) FROM RDB$DATABASE"
cur=connection.cursor()
cur.execute(query)
newid=cur.fetchone()[0]
print(newid)
return(newid)
except:
return(None)
def get_idpac(rodnecislo,connection):
try:
query = "SELECT idpac,prijmeni FROM kar where rodcis=?"
cur = connection.cursor()
cur.execute(query,(rodnecislo,))
tmp_nacteno=cur.fetchone()
tmp_id = tmp_nacteno[0]
tmp_jmeno=tmp_nacteno[1]
print(f"Pacient s rodným číslem {rodnecislo} má ID {tmp_id} a jméno {tmp_jmeno}")
return (tmp_id)
except:
return(None)
+184
View File
@@ -0,0 +1,184 @@
"""funkce_ext.py zápis PDF souborů přímo do měsíční externí Firebird DB
místo do hlavní medicus.fdb spouštět na Windows
Náhrada za funkce.zapis_file() v s03soubory.py.
Binární data souboru se uloží do MEDICUS_FILES_YYYYMM.fdb (DATA tabulka),
do hlavní FILES.BODY se vloží 48bajtová reference.
Formát FILES.BODY reference (48 B):
magic 4 B = b'\\xee\\xbb\\xaa\\x0b'
uid 32 B = UUID4 hex (ASCII, 32 znaků)
dblen 4 B = délka DBNAME jako little-endian uint32
dbname N B = DBNAME ASCII (např. 'DB202603', 8 B)
"""
import os
import struct
import uuid
import fdb
# Magic bajty identifikující referenci na externí DB
MAGIC = b'\xee\xbb\xaa\x0b'
# ─── Pomocné funkce ──────────────────────────────────────────────────────────
def make_body_ref(uid: str, dbname: str) -> bytes:
"""Sestaví 48bajtovou binární referenci pro FILES.BODY."""
uid_bytes = uid.encode('ascii') # 32 B
dbname_bytes = dbname.encode('ascii') # typicky 8 B ('DB202603')
return MAGIC + uid_bytes + struct.pack('<I', len(dbname_bytes)) + dbname_bytes
def _get_ext_server(vstupconnection):
"""Zjistí SERVER z existující EXTERNI_DB (např. 'localhost/3053')."""
cur = vstupconnection.cursor()
cur.execute(
"SELECT SERVER FROM EXTERNI_DB WHERE DBNAME LIKE 'DB%' ORDER BY DBNAME DESC ROWS 1"
)
row = cur.fetchone()
return row[0] if row else 'localhost/3053'
def _get_ext_heslo(vstupconnection):
"""Přečte šifrované heslo z existující EXTERNI_DB.
Všechny externí DB mají stejné heslo (masterkey, jen zašifrované Medicusem),
takže stačí vzít heslo z libovolné existující položky."""
cur = vstupconnection.cursor()
cur.execute(
"SELECT HESLO FROM EXTERNI_DB WHERE DBNAME LIKE 'DB%' ORDER BY DBNAME DESC ROWS 1"
)
row = cur.fetchone()
return row[0] if row else 'masterkey'
def _vytvor_externi_db(ext_path):
"""Vytvoří novou prázdnou externí Firebird DB s tabulkou DATA a generátorem GEN_UID.
Schéma odpovídá tomu, co vytváří Medicus sám při prvním exportu daného měsíce."""
print(f" Vytvářím novou ext DB: {ext_path}")
conn = fdb.create_database(
dsn=f'localhost:{ext_path}',
user='SYSDBA',
password='masterkey',
charset='WIN1250',
page_size=16384
)
cur = conn.cursor()
cur.execute("""
CREATE TABLE DATA (
UID VARCHAR(32) NOT NULL,
DATA BLOB SUB_TYPE 0,
DATASIZE INTEGER,
CHUNK INTEGER
)
""")
cur.execute("CREATE GENERATOR GEN_UID")
conn.commit()
print(f" Nová ext DB vytvořena: {ext_path}")
return conn
# ─── Hlavní funkce ───────────────────────────────────────────────────────────
def zapis_file_ext(vstupconnection, idpac, cesta, souborname, prvnizavorka,
soubordate, souborfiledate, poznamka,
ext_base_path=r'u:\externi'):
"""Zapíše PDF soubor do měsíční externí DB a vrátí fileid.
Parametry jsou shodné s funkce.zapis_file() stačí prohodit import.
Postup:
1. Určí měsíc z soubordate → DBNAME (např. 'DB202603')
2. Načte binární data PDF
3. Najde nebo vytvoří měsíční MEDICUS_FILES_YYYYMM.fdb
4. Zapíše data do DATA tabulky pod novým UID (UUID4 hex)
5. Sestaví 48bajtovou BODY referenci
6. Získá nové FILES ID z Gen_Files
7. Vloží záznam do hlavní FILES tabulky (BODY = reference, ne BLOB)
8. Vrátí fileid (shodné s funkce.zapis_file())
"""
import funkce # get_files_id ze stávajícího funkce.py
# 1. Měsíc a DBNAME
yyyymm = soubordate.strftime('%Y%m')
dbname = f'DB{yyyymm}'
print(f" DBNAME: {dbname}")
# 2. Binární data PDF
soubor_cesta = os.path.join(cesta, souborname)
with open(soubor_cesta, 'rb') as f:
data = f.read()
print(f" Načten soubor: {souborname} ({len(data):,} B)")
# 3. Připojení k externí DB
cur = vstupconnection.cursor()
cur.execute(
"SELECT SERVER, PATH FROM EXTERNI_DB WHERE DBNAME = ?", (dbname,)
)
row = cur.fetchone()
if row:
# Existující měsíční DB připojíme se
ext_server_raw, ext_path = row[0], row[1]
# SERVER je "localhost/3053" (FBScanner proxy) bereme jen hostname
ext_server = ext_server_raw.split('/')[0]
print(f" Existující ext DB: {dbname}{ext_path}")
conn_ext = fdb.connect(
dsn=f'{ext_server}:{ext_path}',
user='SYSDBA',
password='masterkey',
charset='WIN1250'
)
else:
# Nová měsíční DB vytvoříme a zaregistrujeme
ext_filename = f'MEDICUS_FILES_{yyyymm}.fdb'
ext_path = os.path.join(ext_base_path, ext_filename)
conn_ext = _vytvor_externi_db(ext_path)
# Zaregistrovat do EXTERNI_DB (heslo zkopírujeme z existující položky)
ext_server = _get_ext_server(vstupconnection)
heslo = _get_ext_heslo(vstupconnection)
cur.execute(
"INSERT INTO EXTERNI_DB (DBNAME, SERVER, PATH, HESLO) VALUES (?, ?, ?, ?)",
(dbname, ext_server, ext_path, heslo)
)
vstupconnection.commit()
print(f" Registrována v EXTERNI_DB: {dbname}, server={ext_server}")
# 4. Zápis do DATA tabulky
uid = uuid.uuid4().hex # 32 hex znaků, vždy unikátní
cur_ext = conn_ext.cursor()
cur_ext.execute(
"INSERT INTO DATA (UID, DATA, DATASIZE, CHUNK) VALUES (?, ?, ?, ?)",
(uid, data, len(data), 0)
)
conn_ext.commit()
conn_ext.close()
print(f" Zapsáno do ext DB: UID={uid}")
# 5. BODY reference (48 B)
body_ref = make_body_ref(uid, dbname)
print(f" BODY ref: {body_ref.hex()}")
# 6. Nové FILES ID
fileid = funkce.get_files_id(vstupconnection)
if fileid is None:
raise RuntimeError("Nepodařilo se získat nové FILES ID (Gen_Files selhal)!")
# 7. INSERT do hlavní FILES tabulky
cur.execute(
"INSERT INTO FILES "
"(id, iduzi, iddoctyp, typ, idpac, filename, body, datum, datsouboru, poznamka) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(fileid, 6, 1, 1, idpac,
prvnizavorka + '.pdf',
body_ref,
soubordate, souborfiledate,
poznamka[:99])
)
vstupconnection.commit()
print(f" FILES záznam vytvořen: ID={fileid}, idpac={idpac}, "
f"soubor={prvnizavorka}.pdf")
return fileid
+23
View File
@@ -0,0 +1,23 @@
import fdb
import datetime
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute('SELECT MAX(ID) FROM DEKURS')
next_id = cur.fetchone()[0] + 1
print('Next ID:', next_id)
text = r'{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\fonttbl{\f0\fnil\fcharset238 Arial;}}{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}' + '\n' + r'\pard\plain\f0\fs20 Test Claude AI - automaticky vlo\u382en\'fd z\'e1znam 13.03.2026\par' + '\n}'
cur.execute("""
INSERT INTO DEKURS (ID, IDPAC, IDODD, IDPRAC, IDUZI, DATUM, CAS, DEKURS, DGN1)
VALUES (?, 9742, 2, 2, 6, ?, ?, ?, 'Z000 ')
""", (next_id, datetime.date(2026, 3, 13), datetime.time(12, 0, 0), text))
conn.commit()
print('Vloženo OK, ID:', next_id)
conn.close()
@@ -0,0 +1,28 @@
import fdb
import sys
table = sys.argv[1] if len(sys.argv) > 1 else 'DEKURS'
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute("""
SELECT rf.rdb$field_name, f.rdb$field_type, f.rdb$field_length
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE rf.rdb$relation_name = ?
ORDER BY rf.rdb$field_position
""", (table,))
cols = cur.fetchall()
print(f"=== {table} sloupce ===")
for c in cols:
print(f" {c[0].strip():<30} typ: {c[1]} délka: {c[2]}")
cur.execute(f'SELECT COUNT(*) FROM {table}')
print(f"\nPočet záznamů: {cur.fetchone()[0]}")
cur.close()
conn.close()
@@ -0,0 +1,16 @@
# Firebird trace konfigurace zachytí SQL příkazy Medicusu
# Soubor: medicus_trace.conf
<database c:\\medicus 3\\data\\medicus.fdb>
enabled true
log_connections false
log_transactions false
log_statement_prepare false
log_statement_start false
log_statement_finish true
log_procedure_finish false
print_plan false
print_perf false
time_threshold 0
max_sql_length 32768
</database>
Binary file not shown.
@@ -0,0 +1,22 @@
"""read_last_dekurs.py jednorázový skript: vypíše obsah posledního DEKURS záznamu pro Buzalka"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, CAS, CHAR_LENGTH(DEKURS), DEKURS
FROM DEKURS
WHERE IDPAC = 9742
ORDER BY ID DESC
""")
row = cur.fetchone()
idek, datum, cas, delka, obsah = row
print(f"ID={idek} datum={datum} cas={cas} délka={delka} B")
print("=" * 60)
print(obsah)
conn.close()
+323
View File
@@ -0,0 +1,323 @@
import os, shutil, fdb, time
import re, datetime, funkce, funkce_ext
# Connect to the Firebird database
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password="masterkey",
charset="win1250")
cesta = r"u:\testimport"
cestazpracovana = r"u:\testimportzpracovana"
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─── Helper funkce ────────────────────────────────────────────────────────────
def restore_files_for_import(retezec):
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
if not os.path.exists(drop):
print(f"The directory '{drop}' does not exist.")
return
for item in os.listdir(drop):
item_path = os.path.join(drop, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
print(f"Deleted file: {item_path}")
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
print(f"Deleted directory: {item_path}")
for item in os.listdir(next):
item_path = os.path.join(next, item)
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
shutil.copy(item_path, os.path.join(drop, item))
print(f"Copied file: {item_path}")
def kontrola_rc(rc, connection):
cur = connection.cursor()
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
row = cur.fetchone()
if row:
return row[1]
else:
return False
def kontrola_struktury(souborname, connection):
if souborname.endswith('.pdf'):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
vpohode = True
if match and len(match.groups()) == 5:
datum = match.group(2)
try:
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
except:
vpohode = False
return vpohode
cur = connection.cursor()
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
row = cur.fetchone()[0]
if row != 1:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
return vpohode
def vrat_info_o_souboru(souborname, connection):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
rc = match.group(1)
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
jmeno = match.group(3)
prvnizavorka = match.group(4)
druhazavorka = match.group(5)
cur = connection.cursor()
cur.execute("select idpac from kar where rodcis=?", (rc,))
idpac = cur.fetchone()[0]
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
def prejmenuj_chybny_soubor(souborname, cesta):
if souborname[0] != "":
soubornovy = "" + souborname
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
def najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni):
"""Vrátí (id, rtf) posledního dekurzu pacienta pokud je z dnešního dne, jinak None."""
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum == datum_vlozeni:
print(f" → dnešní den ({datum_vlozeni}) ✓")
return (dekurs_id, dekurs_rtf)
else:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový")
return None
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
n_files = 0
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pards = ''
for i, fname in enumerate(filenameforbookmark_list):
idx = n_files + i
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
def append_bookmarks(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.extend(bookmark_list)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# Šablona RTF pro nový dekurs
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
info = []
for soubor in os.listdir(cesta):
if os.path.isfile(os.path.join(cesta, soubor)):
print(soubor)
if kontrola_struktury(soubor, conn):
info.append(vrat_info_o_souboru(soubor, conn))
else:
prejmenuj_chybny_soubor(soubor, cesta)
info = sorted(info, key=lambda x: (x[0], x[1]))
print(info)
skupiny = {}
for row in info:
skupiny[row[0]] = []
for row in info:
skupiny[row[0]].append(row)
for key in skupiny.keys():
print(f"\n{'='*60}")
print(f"RC: {key}, souborů: {len(skupiny[key])}")
cislo = 9
poradi = 0
bookmark_list = []
filenameforbookmark_list = []
bookmarks_body = ''
idpac = skupiny[key][0][1]
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
for row in skupiny[key]:
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=row[1],
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
print(f" → FILES.ID = {fileid} ({row[6]})")
# Přesun souboru do zpracovaných
for attempt in range(3):
try:
dest = os.path.join(cestazpracovana, row[6])
if not os.path.exists(dest):
shutil.move(os.path.join(cesta, row[6]), dest)
else:
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
shutil.move(os.path.join(cesta, row[6]),
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
print(" Přesun OK!")
break
except Exception as e:
print(f" Attempt {attempt + 1} failed: {e}")
if attempt < 2:
print(" Retrying in 5 seconds...")
time.sleep(5)
else:
print(" Max retries reached. Command failed.")
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
filenameforbookmark_list.append(filenameforbookmark)
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
cur = conn.cursor()
print(f"\n>>> Hledám poslední dekurs pro IDPAC={idpac}...")
existujici = najdi_posledni_dekurs_dnes(conn, idpac, datumzapisu)
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# Případ 1: dnešní dekurs má sekci příloh → přidáme soubory dovnitř
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubory DO existující sekce...")
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
else:
# Případ 2: dnešní dekurs existuje, ale sekci příloh nemá → prepend
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# Případ 3: žádný dnešní dekurs → vytvoříme nový
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
print("\n=== HOTOVO ===")
conn.close()
+397
View File
@@ -0,0 +1,397 @@
import os, shutil, fdb, time, threading
import re, datetime, funkce, funkce_ext
# Connect to the Firebird database
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password="masterkey",
charset="win1250")
cesta = r"u:\testimport"
cestazpracovana = r"u:\testimportzpracovana"
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─── Helper funkce ────────────────────────────────────────────────────────────
def restore_files_for_import(retezec):
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
if not os.path.exists(drop):
print(f"The directory '{drop}' does not exist.")
return
for item in os.listdir(drop):
item_path = os.path.join(drop, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
print(f"Deleted file: {item_path}")
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
print(f"Deleted directory: {item_path}")
for item in os.listdir(next):
item_path = os.path.join(next, item)
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
shutil.copy(item_path, os.path.join(drop, item))
print(f"Copied file: {item_path}")
def kontrola_rc(rc, connection):
cur = connection.cursor()
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
row = cur.fetchone()
if row:
return row[1]
else:
return False
def kontrola_struktury(souborname, connection):
if souborname.endswith('.pdf'):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
vpohode = True
if match and len(match.groups()) == 5:
datum = match.group(2)
try:
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
except:
vpohode = False
return vpohode
cur = connection.cursor()
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
row = cur.fetchone()[0]
if row != 1:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
return vpohode
def vrat_info_o_souboru(souborname, connection):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
rc = match.group(1)
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
jmeno = match.group(3)
prvnizavorka = match.group(4)
druhazavorka = match.group(5)
cur = connection.cursor()
cur.execute("select idpac from kar where rodcis=?", (rc,))
idpac = cur.fetchone()[0]
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
def prejmenuj_chybny_soubor(souborname, cesta):
if souborname[0] != "":
soubornovy = "" + souborname
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
def _pokus_o_zamek(dekurs_id, vysledek):
"""Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení.
Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}.
Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený.
"""
conn_t = None
try:
conn_t = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur_t = conn_t.cursor()
cur_t.execute(
"SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK",
(dekurs_id,)
)
cur_t.fetchone()
conn_t.rollback() # Uvolni zámek sloužil jen k ověření
vysledek['ok'] = True
except Exception as e:
vysledek['chyba'] = str(e)
finally:
if conn_t:
try:
conn_t.close()
except Exception:
pass
def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2):
"""Zjistí zda existuje dnešní dekurz a ověří že není zamčený.
Vrátí:
(id, rtf) dnešní dekurz existuje a není zamčený
None žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba)
Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený).
Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme
pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund
neskončí, záznam je zamčený a přeskočíme celou skupinu.
"""
cur = conn.cursor()
# Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT)
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
print(f" Žádný dekurz pro pacienta IDPAC={idpac}")
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum != datum_vlozeni:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový (INSERT)")
return None
# Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený
print(f" → dnešní den ({datum_vlozeni}) ✓ ověřuji zámek (timeout {timeout_sec}s)...")
vysledek = {}
t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True)
t.start()
t.join(timeout=timeout_sec)
if t.is_alive():
# Vlákno stále čeká na zámek = záznam drží Medicus
raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)")
if 'chyba' in vysledek:
raise fdb.DatabaseError(vysledek['chyba'])
print(f" → záznam volný, pokračuji se zápisem")
return (dekurs_id, dekurs_rtf)
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
n_files = 0
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pards = ''
for i, fname in enumerate(filenameforbookmark_list):
idx = n_files + i
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
def append_bookmarks(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.extend(bookmark_list)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# Šablona RTF pro nový dekurs
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
info = []
for soubor in os.listdir(cesta):
if os.path.isfile(os.path.join(cesta, soubor)):
print(soubor)
if kontrola_struktury(soubor, conn):
info.append(vrat_info_o_souboru(soubor, conn))
else:
prejmenuj_chybny_soubor(soubor, cesta)
info = sorted(info, key=lambda x: (x[0], x[1]))
print(info)
skupiny = {}
for row in info:
skupiny[row[0]] = []
for row in info:
skupiny[row[0]].append(row)
for key in skupiny.keys():
print(f"\n{'='*60}")
print(f"RC: {key}, souborů: {len(skupiny[key])}")
idpac = skupiny[key][0][1]
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
# ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ──────
print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...")
try:
existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu)
except RuntimeError as e:
# Vlákno nepřišlo do timeoutu = záznam drží Medicus
print(f"\n!!! DEKURZ ZAMČEN soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
except fdb.DatabaseError as e:
chyba = str(e).lower()
if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba:
print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
raise # jiná DB chyba propaguj dál
cislo = 9
poradi = 0
bookmark_list = []
filenameforbookmark_list = []
bookmarks_body = ''
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
for row in skupiny[key]:
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=row[1],
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
print(f" → FILES.ID = {fileid} ({row[6]})")
# Přesun souboru do zpracovaných
for attempt in range(3):
try:
dest = os.path.join(cestazpracovana, row[6])
if not os.path.exists(dest):
shutil.move(os.path.join(cesta, row[6]), dest)
else:
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
shutil.move(os.path.join(cesta, row[6]),
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
print(" Přesun OK!")
break
except Exception as e:
print(f" Attempt {attempt + 1} failed: {e}")
if attempt < 2:
print(" Retrying in 5 seconds...")
time.sleep(5)
else:
print(" Max retries reached. Command failed.")
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
filenameforbookmark_list.append(filenameforbookmark)
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
cur = conn.cursor()
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubory DO existující sekce...")
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
else:
# Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# Případ 3: žádný dnešní dekurz → vytvoříme nový
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
print("\n=== HOTOVO ===")
conn.close()
@@ -0,0 +1,397 @@
import os, shutil, fdb, time, threading
import re, datetime, funkce, funkce_ext
# Connect to the Firebird database
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password="masterkey",
charset="win1250")
cesta = r"u:\testimport"
cestazpracovana = r"u:\testimportzpracovana"
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─── Helper funkce ────────────────────────────────────────────────────────────
def restore_files_for_import(retezec):
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
if not os.path.exists(drop):
print(f"The directory '{drop}' does not exist.")
return
for item in os.listdir(drop):
item_path = os.path.join(drop, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
print(f"Deleted file: {item_path}")
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
print(f"Deleted directory: {item_path}")
for item in os.listdir(next):
item_path = os.path.join(next, item)
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
shutil.copy(item_path, os.path.join(drop, item))
print(f"Copied file: {item_path}")
def kontrola_rc(rc, connection):
cur = connection.cursor()
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
row = cur.fetchone()
if row:
return row[1]
else:
return False
def kontrola_struktury(souborname, connection):
if souborname.endswith('.pdf'):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
vpohode = True
if match and len(match.groups()) == 5:
datum = match.group(2)
try:
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
except:
vpohode = False
return vpohode
cur = connection.cursor()
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
row = cur.fetchone()[0]
if row != 1:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
return vpohode
def vrat_info_o_souboru(souborname, connection):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
rc = match.group(1)
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
jmeno = match.group(3)
prvnizavorka = match.group(4)
druhazavorka = match.group(5)
cur = connection.cursor()
cur.execute("select idpac from kar where rodcis=?", (rc,))
idpac = cur.fetchone()[0]
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
def prejmenuj_chybny_soubor(souborname, cesta):
if souborname[0] != "":
soubornovy = "" + souborname
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
def _pokus_o_zamek(dekurs_id, vysledek):
"""Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení.
Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}.
Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený.
"""
conn_t = None
try:
conn_t = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur_t = conn_t.cursor()
cur_t.execute(
"SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK",
(dekurs_id,)
)
cur_t.fetchone()
conn_t.rollback() # Uvolni zámek sloužil jen k ověření
vysledek['ok'] = True
except Exception as e:
vysledek['chyba'] = str(e)
finally:
if conn_t:
try:
conn_t.close()
except Exception:
pass
def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2):
"""Zjistí zda existuje dnešní dekurz a ověří že není zamčený.
Vrátí:
(id, rtf) dnešní dekurz existuje a není zamčený
None žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba)
Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený).
Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme
pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund
neskončí, záznam je zamčený a přeskočíme celou skupinu.
"""
cur = conn.cursor()
# Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT)
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
print(f" Žádný dekurz pro pacienta IDPAC={idpac}")
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum != datum_vlozeni:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový (INSERT)")
return None
# Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený
print(f" → dnešní den ({datum_vlozeni}) ✓ ověřuji zámek (timeout {timeout_sec}s)...")
vysledek = {}
t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True)
t.start()
t.join(timeout=timeout_sec)
if t.is_alive():
# Vlákno stále čeká na zámek = záznam drží Medicus
raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)")
if 'chyba' in vysledek:
raise fdb.DatabaseError(vysledek['chyba'])
print(f" → záznam volný, pokračuji se zápisem")
return (dekurs_id, dekurs_rtf)
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
n_files = 0
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pards = ''
for i, fname in enumerate(filenameforbookmark_list):
idx = n_files + i
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
def append_bookmarks(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.extend(bookmark_list)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# Šablona RTF pro nový dekurs
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
info = []
for soubor in os.listdir(cesta):
if os.path.isfile(os.path.join(cesta, soubor)):
print(soubor)
if kontrola_struktury(soubor, conn):
info.append(vrat_info_o_souboru(soubor, conn))
else:
prejmenuj_chybny_soubor(soubor, cesta)
info = sorted(info, key=lambda x: (x[0], x[1]))
print(info)
skupiny = {}
for row in info:
skupiny[row[0]] = []
for row in info:
skupiny[row[0]].append(row)
for key in skupiny.keys():
print(f"\n{'='*60}")
print(f"RC: {key}, souborů: {len(skupiny[key])}")
idpac = skupiny[key][0][1]
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
# ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ──────
print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...")
try:
existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu)
except RuntimeError as e:
# Vlákno nepřišlo do timeoutu = záznam drží Medicus
print(f"\n!!! DEKURZ ZAMČEN soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
except fdb.DatabaseError as e:
chyba = str(e).lower()
if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba:
print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
raise # jiná DB chyba propaguj dál
cislo = 9
poradi = 0
bookmark_list = []
filenameforbookmark_list = []
bookmarks_body = ''
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
for row in skupiny[key]:
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=row[1],
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
print(f" → FILES.ID = {fileid} ({row[6]})")
# Přesun souboru do zpracovaných
for attempt in range(3):
try:
dest = os.path.join(cestazpracovana, row[6])
if not os.path.exists(dest):
shutil.move(os.path.join(cesta, row[6]), dest)
else:
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
shutil.move(os.path.join(cesta, row[6]),
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
print(" Přesun OK!")
break
except Exception as e:
print(f" Attempt {attempt + 1} failed: {e}")
if attempt < 2:
print(" Retrying in 5 seconds...")
time.sleep(5)
else:
print(" Max retries reached. Command failed.")
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
filenameforbookmark_list.append(filenameforbookmark)
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
cur = conn.cursor()
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubory DO existující sekce...")
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
else:
# Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# Případ 3: žádný dnešní dekurz → vytvoříme nový
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
print("\n=== HOTOVO ===")
conn.close()
@@ -0,0 +1,226 @@
# 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.
@@ -0,0 +1,108 @@
"""s04_presun_externi_db.py Přesun externích DB souborů z u:\ do u:\externi\
Spustit na Windows s ZAVŘENÝM Medicusem!
Co dělá:
1. Připojí se k hlavní medicus.fdb
2. Načte všechny záznamy z EXTERNI_DB
3. Pro každý záznam zkopíruje FDB soubor z původní lokace do u:\externi\
4. Aktualizuje EXTERNI_DB.PATH na novou lokaci
5. Vytiskne přehled výsledků
Po spuštění zkus v Medicusu otevřít přílohu pacienta mělo by to fungovat.
Pokud ne, zálohuj si databázi a spusť znovu (skript je idempotentní).
"""
import os
import shutil
import fdb
# ── Konfigurace ──────────────────────────────────────────────────────────────
MAIN_DB = r'c:\medicus 3\data\medicus.fdb'
CÍL_SLOŽKA = r'u:\externi'
# ─────────────────────────────────────────────────────────────────────────────
def main():
os.makedirs(CÍL_SLOŽKA, exist_ok=True)
print(f"Cílová složka: {CÍL_SLOŽKA}")
print()
print("Připojuji se k hlavní DB...")
conn = fdb.connect(
dsn=f'localhost:{MAIN_DB}',
user='SYSDBA',
password='masterkey',
charset='WIN1250'
)
cur = conn.cursor()
# Načti všechny záznamy z EXTERNI_DB
cur.execute("SELECT DBNAME, SERVER, PATH, HESLO FROM EXTERNI_DB ORDER BY DBNAME")
zaznamy = cur.fetchall()
print(f"Nalezeno {len(zaznamy)} záznamů v EXTERNI_DB:")
print()
ok = 0
preskoceno = 0
chyba = 0
for dbname, server, path_puvodni, heslo in zaznamy:
print(f" [{dbname}] {path_puvodni}")
# Název souboru (jen basename, bez cesty)
basename = os.path.basename(path_puvodni)
# Normalize: Medicus někdy ukládá s malým .fdb, někdy velkým .FDB
path_cil = os.path.join(CÍL_SLOŽKA, basename)
# Zkontroluj, jestli zdrojový soubor existuje
# (zkus obě varianty přípony)
path_src = None
for kandidat in [path_puvodni,
path_puvodni.replace('.fdb', '.FDB'),
path_puvodni.replace('.FDB', '.fdb')]:
if os.path.isfile(kandidat):
path_src = kandidat
break
if path_src is None:
print(f" ⚠ ZDROJOVÝ SOUBOR NENALEZEN: {path_puvodni} přeskakuji")
chyba += 1
continue
# Pokud je soubor už v cíli, přeskoč kopírování
if os.path.isfile(path_cil):
velikost = os.path.getsize(path_cil)
print(f" → Soubor již existuje v cíli ({velikost:,} B), přeskakuji kopírování")
else:
velikost = os.path.getsize(path_src)
print(f" → Kopíruji ({velikost:,} B)...", end=' ', flush=True)
shutil.copy2(path_src, path_cil)
print("OK")
# Aktualizuj PATH v EXTERNI_DB pokud se liší
if path_puvodni != path_cil:
cur.execute(
"UPDATE EXTERNI_DB SET PATH = ? WHERE DBNAME = ?",
(path_cil, dbname)
)
print(f" → EXTERNI_DB.PATH aktualizováno: {path_cil}")
else:
print(f" → PATH již ukazuje na cíl, není třeba měnit")
preskoceno += 1
ok += 1
conn.commit()
conn.close()
print()
print("=" * 60)
print(f"Hotovo! Zpracováno: {ok}, přeskočeno: {preskoceno}, chyb: {chyba}")
print()
if chyba > 0:
print("⚠ Některé soubory nebyly nalezeny viz výpis výše.")
print(" Pokud jsou to staré DB které již neexistují, nevadí to.")
print("Nyní spusť Medicus a zkus otevřít přílohu pacienta.")
if __name__ == '__main__':
main()
@@ -0,0 +1,37 @@
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
print("=== NEJSTARŠÍ záznamy ===")
cur.execute("""
SELECT FIRST 5 ID, DATUM, DEKURS
FROM DEKURS
WHERE DEKURS IS NOT NULL
ORDER BY DATUM ASC
""")
for row in cur.fetchall():
text = row[2].read() if hasattr(row[2], 'read') else row[2]
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
print(f"\n--- ID={row[0]} DATUM={row[1]} ---")
print(repr(text[:300]))
print("\n\n=== NEJNOVĚJŠÍ záznamy ===")
cur.execute("""
SELECT FIRST 5 ID, DATUM, DEKURS
FROM DEKURS
WHERE DEKURS IS NOT NULL
ORDER BY DATUM DESC
""")
for row in cur.fetchall():
text = row[2].read() if hasattr(row[2], 'read') else row[2]
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
print(f"\n--- ID={row[0]} DATUM={row[1]} ---")
print(repr(text[:300]))
conn.close()
@@ -0,0 +1,25 @@
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS
FROM DEKURS
WHERE DEKURS IS NOT NULL
ORDER BY DATUM DESC
""")
row = cur.fetchone()
text = row[2].read() if hasattr(row[2], 'read') else row[2]
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
print(f"ID={row[0]} DATUM={row[1]}")
print(f"Délka: {len(text)} znaků")
print("\n--- CELÝ TEXT ---")
print(text)
conn.close()
+35
View File
@@ -0,0 +1,35 @@
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
# Načti všechny záznamy pacienta a zjisti délku v Pythonu
cur.execute("""
SELECT ID, DATUM, DEKURS
FROM DEKURS
WHERE IDPAC = 9742 AND DEKURS IS NOT NULL
ORDER BY DATUM DESC
""")
rows = cur.fetchall()
def read_blob(val):
text = val.read() if hasattr(val, 'read') else val
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
return text
# Seřaď podle délky
data = [(r[0], r[1], read_blob(r[2])) for r in rows]
data.sort(key=lambda x: len(x[2]), reverse=True)
print("Top 10 nejdelších záznamů:")
for d in data[:10]:
print(f" ID={d[0]} datum={d[1]} délka={len(d[2])}")
print("\n--- Nejdelší záznam (celý RTF) ---")
print(data[0][2])
conn.close()
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
"""Test připojení k externí DB a prozkoumání její struktury - spusť na Windows"""
import fdb, os, glob
# Najde první existující externí FDB soubor
soubory = glob.glob(r'u:\MEDICUS_FILES_*.fdb')
if not soubory:
soubory = glob.glob(r'u:\externi\MEDICUS_FILES_*.fdb')
if not soubory:
print("Žádné externí FDB soubory nenalezeny!")
exit()
soubor = sorted(soubory)[-1] # vezme nejnovější
print(f"Zkouším: {soubor}")
# Test 1: masterkey
try:
conn = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='masterkey', charset='win1250')
print("✓ Připojení s masterkey FUNGUJE!")
cur = conn.cursor()
# Jaké tabulky jsou v externí DB?
cur.execute("SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$SYSTEM_FLAG=0 AND RDB$VIEW_BLR IS NULL")
tabulky = [r[0].strip() for r in cur.fetchall()]
print(f"Tabulky v externí DB: {tabulky}")
# Kolik záznamů v FILES?
if 'FILES' in tabulky:
cur.execute("SELECT COUNT(*), MIN(ID), MAX(ID) FROM FILES")
row = cur.fetchone()
print(f"FILES: {row[0]} záznamů, ID od {row[1]} do {row[2]}")
cur.execute("SELECT ID, IDPAC, FILENAME, DATUM, OCTET_LENGTH(BODY) FROM FILES ORDER BY ID ROWS 3")
for r in cur.fetchall():
print(f" ID={r[0]} IDPAC={r[1]} FILE={r[2]} DATUM={r[3]} BODY={r[4]}B")
# Zjistit generátor
cur.execute("SELECT RDB$GENERATOR_NAME, RDB$GENERATOR_ID FROM RDB$GENERATORS WHERE RDB$SYSTEM_FLAG=0")
for r in cur.fetchall():
print(f"Generátor: {r[0].strip()} = {r[1]}")
conn.close()
except Exception as e:
print(f"✗ masterkey NEFUNGUJE: {e}")
# Test s jiným heslem - zkus prázdné
try:
conn = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='', charset='win1250')
print("✓ Připojení s prázdným heslem FUNGUJE!")
conn.close()
except Exception as e2:
print(f"✗ Prázdné heslo také nefunguje: {e2}")
+46
View File
@@ -0,0 +1,46 @@
"""Prozkoumá strukturu tabulky DATA v externí DB - spusť na Windows"""
import fdb, glob
soubory = glob.glob(r'u:\MEDICUS_FILES_*.fdb') + glob.glob(r'u:\MEDICUS_FILES_*.FDB')
if not soubory:
soubory = glob.glob(r'u:\externi\MEDICUS_FILES_*.fdb') + glob.glob(r'u:\externi\MEDICUS_FILES_*.FDB')
soubor = sorted(soubory)[-1]
print(f"Soubor: {soubor}\n")
conn = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='masterkey', charset='win1250')
cur = conn.cursor()
# Sloupce tabulky DATA
print("=== Sloupce tabulky DATA ===")
cur.execute("""
SELECT r.RDB$FIELD_NAME, f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH, f.RDB$SEGMENT_LENGTH
FROM RDB$RELATION_FIELDS r
JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = r.RDB$FIELD_SOURCE
WHERE r.RDB$RELATION_NAME = 'DATA'
ORDER BY r.RDB$FIELD_POSITION
""")
for row in cur.fetchall():
typ = {7:'SMALLINT',8:'INTEGER',12:'DATE',13:'TIME',14:'CHAR',16:'INT64',35:'TIMESTAMP',37:'VARCHAR',261:'BLOB'}.get(row[1], str(row[1]))
print(f" {row[0].strip():30} {typ} len={row[2]}")
# Generátory
print("\n=== Generátory ===")
cur.execute("SELECT RDB$GENERATOR_NAME, RDB$GENERATOR_ID FROM RDB$GENERATORS WHERE RDB$SYSTEM_FLAG=0")
for row in cur.fetchall():
print(f" {row[0].strip()} = {row[1]}")
# Počet záznamů a ukázka
print("\n=== Počet záznamů ===")
cur.execute("SELECT COUNT(*) FROM DATA")
print(f" {cur.fetchone()[0]} záznamů")
print("\n=== Ukázka 3 záznamů (bez BLOB) ===")
cur.execute("""
SELECT ID, OCTET_LENGTH(BODY) as VELIKOST
FROM DATA ORDER BY ID ROWS 3
""")
for row in cur.fetchall():
print(f" ID={row[0]} BODY={row[1]}B")
conn.close()
+47
View File
@@ -0,0 +1,47 @@
"""Zjistí propojení mezi hlavní DB a externí DB - spusť na Windows"""
import fdb, glob
# Hlavní DB
conn_main = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250')
cur = conn_main.cursor()
# Najít exportované záznamy v FILES (BODY není velký BLOB)
print("=== FILES záznamy po exportu (malý BODY = reference na externí DB) ===")
cur.execute("""
SELECT ID, IDPAC, FILENAME, DATUM, EXT_ID, DOCID,
OCTET_LENGTH(BODY) as BODY_LEN,
CAST(BODY AS VARCHAR(100)) as BODY_TEXT
FROM FILES
WHERE OCTET_LENGTH(BODY) < 100
ORDER BY ID DESC ROWS 5
""")
for r in cur.fetchall():
print(f" ID={r[0]} IDPAC={r[1]} FILE={r[2]}")
print(f" DATUM={r[3]} EXT_ID={r[4]} DOCID={r[5]}")
print(f" BODY_LEN={r[6]} BODY_TEXT='{r[7]}'")
# Ukázka záznamu s plným BODY (ještě ne exportovaný)
print("\n=== FILES záznamy PŘED exportem (velký BODY = binary data) ===")
cur.execute("""
SELECT ID, FILENAME, DATUM, EXT_ID, OCTET_LENGTH(BODY) as BODY_LEN
FROM FILES
WHERE OCTET_LENGTH(BODY) > 1000
ORDER BY ID DESC ROWS 3
""")
for r in cur.fetchall():
print(f" ID={r[0]} FILE={r[1]} DATUM={r[2]} EXT_ID={r[3]} BODY={r[4]}B")
conn_main.close()
# Porovnat s externí DB
print("\n=== DATA záznamy v externí DB ===")
soubory = glob.glob(r'u:\MEDICUS_FILES_*.fdb') + glob.glob(r'u:\MEDICUS_FILES_*.FDB')
soubor = sorted(soubory)[0] # vezmeme nejstarší
print(f"Soubor: {soubor}")
conn_ext = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='masterkey', charset='win1250')
cur_ext = conn_ext.cursor()
cur_ext.execute("SELECT UID, DATASIZE, CHUNK FROM DATA ORDER BY CHUNK ROWS 5")
for r in cur_ext.fetchall():
print(f" UID='{r[0]}' DATASIZE={r[1]} CHUNK={r[2]}")
conn_ext.close()
+20
View File
@@ -0,0 +1,20 @@
"""Čte přesné bajty BODY z exportovaného záznamu FILES - spusť na Windows"""
import fdb
conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250')
cur = conn.cursor()
# Vezmi jeden exportovaný záznam
cur.execute("SELECT ID, BODY FROM FILES WHERE ID = 10487")
row = cur.fetchone()
fileid = row[0]
body_bytes = row[1] # fdb vrací BLOB jako bytes přímo
print(f"FILE ID: {fileid}")
print(f"BODY délka: {len(body_bytes)} bajtů")
print(f"BODY hex: {body_bytes.hex()}")
print(f"BODY repr: {repr(body_bytes)}")
print(f"BODY jako string (latin1): {body_bytes.decode('latin1')}")
conn.close()
@@ -0,0 +1,108 @@
"""test_import_3files.py jednorázový test: 3 soubory jednoho pacienta → 1 dekurs s klikacími odkazy
Testuje opravenou RTF logiku (cs32, Přílohy: inline, poradi++)
Spustit na Windows.
"""
import datetime
import os
import fdb
import funkce
import funkce_ext
CESTA = r'u:\\'
IDPAC = 9742 # Buzalka Vladimír, RC 7309208104
SOUBORY = [
{
'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy1].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude copy1',
'datum': datetime.date(2026, 3, 18),
},
{
'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy2].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude copy2',
'datum': datetime.date(2026, 3, 18),
},
{
'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude',
'datum': datetime.date(2026, 3, 18),
},
]
# ── Připojení ─────────────────────────────────────────────────────────────────
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250'
)
# ── Krok 1: vložit každý soubor do ext DB ─────────────────────────────────────
bookmark = ''
bookmarks = ''
cislo = 9
poradi = 0
for s in SOUBORY:
cesta_souboru = os.path.join(CESTA, s['souborname'])
datumsouboru = datetime.datetime.fromtimestamp(os.path.getmtime(cesta_souboru))
print(f"\n>>> Zpracovávám: {s['souborname']}")
fileid = funkce_ext.zapis_file_ext(
vstupconnection = conn,
idpac = IDPAC,
cesta = CESTA,
souborname = s['souborname'],
prvnizavorka = s['prvnizavorka'],
soubordate = s['datum'],
souborfiledate = datumsouboru,
poznamka = s['druhazavorka'],
)
print(f" → FILES.ID = {fileid}")
# {\info{\bookmarks ...}} sekce
filenameforbookmark = s['datum'].strftime('%Y-%m-%d') + ' ' + s['prvnizavorka'] + ': ' + s['druhazavorka']
bookmark += '"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo) + ';'
cislo += 7
# tělo RTF klikatelné záložky (cs32, všechny stejný formát)
bookmarks += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark +
r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
bookmark = bookmark[:-1] # odstranit poslední ;
# ── Krok 2: sestavit RTF ──────────────────────────────────────────────────────
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
rtf = rtf.replace('BOOKMARKNAMES', bookmark)
rtf = rtf.replace('BOOKMARKSTEXT', bookmarks)
print('\n=== Výsledný RTF ===')
print(rtf)
# ── Krok 3: zapsat dekurs ─────────────────────────────────────────────────────
dekursid = funkce.get_dekurs_id(conn)
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
cur = conn.cursor()
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?,?,?,?,?,?,?,?)",
(dekursid, 6, 2, 2, IDPAC, datumzapisu, caszapisu, rtf)
)
conn.commit()
conn.close()
print(f'\n=== HOTOVO ===')
print(f'DEKURS.ID = {dekursid}')
print('Otevři Medicus → karta Buzalka Vladimír → najdi dnešní záznam → klikej na odkazy!')
@@ -0,0 +1,238 @@
"""test_import_single.py vloží 1 soubor do dekurzu s rozšířenou logikou:
1. Poslední dekurs pacienta je z dnešního dne A má sekci 'Vložené přílohy'
→ soubor se přidá DO té sekce (ne nová sekce)
2. Poslední dekurs je z dnešního dne, ale sekci 'Vložené přílohy' nemá
→ prepend nové sekce na začátek
3. Poslední dekurs je z jiného dne / neexistuje
→ nový dekurs pro dnešek
Spustit na Windows.
"""
import datetime, os, re, fdb
import funkce, funkce_ext
CESTA = r'u:\\'
IDPAC = 9742
DATUM = datetime.date(2026, 3, 18)
SOUBORY = [
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude - zařazení].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude - zařazení',
'datum': DATUM},
]
# Vzor pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─────────────────────────────────────────────────────────────────────────────
def najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni):
"""Vrátí (id, rtf) posledního dekurzu pacienta pokud je z dnešního dne."""
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum == datum_vlozeni:
print(f" → dnešní den ({datum_vlozeni}) ✓")
return (dekurs_id, dekurs_rtf)
else:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový")
return None
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, new_bkm_entry, filenameforbookmark):
"""Přidá soubor do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nový index = N
2. Posune bkmkstart/bkmkend >= N o +1 (uvolní místo pro nový)
3. Vloží nový \pard před uzavírací prázdný řádek sekce
4. Vloží bookmark na pozici N do {\info{\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů v bookmarks listu
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
bkm_entries = []
n_files = 0
new_idx = n_files
print(f" Počet existujících Files odkazů: {n_files} → nový bkmkstart={new_idx}")
# 2. Posunout bkmkstart/bkmkend >= n_files o +1
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + (
str(int(m.group(1)) + 1) if int(m.group(1)) >= n_files
else m.group(1)),
rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + (
str(int(m.group(1)) + 1) if int(m.group(1)) >= n_files
else m.group(1)),
rtf)
# 3. Najít uzavírající prázdný řádek sekce (první výskyt po hlavičce)
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pard = (r'\pard\s10{\*\bkmkstart ' + str(new_idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(new_idx) + r'}\par')
rtf = rtf[:closing_pos] + new_pard + '\n' + rtf[closing_pos:]
# 4. Vložit bookmark na pozici n_files do {\info{\bookmarks}}
def insert_bookmark(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.insert(n_files, new_bkm_entry)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', insert_bookmark, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží nový obsah na ZAČÁTEK stávajícího dekurzu (žádná existující sekce)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# ─────────────────────────────────────────────────────────────────────────────
conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250')
# ── Krok 1: vložit soubory do ext DB ─────────────────────────────────────────
bookmark_list = []
bookmarks_body = ''
cislo = 9
poradi = 0
for s in SOUBORY:
cesta_souboru = os.path.join(CESTA, s['souborname'])
datumsouboru = datetime.datetime.fromtimestamp(os.path.getmtime(cesta_souboru))
print(f"\n>>> Zpracovávám: {s['souborname']}")
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=IDPAC,
cesta=CESTA, souborname=s['souborname'],
prvnizavorka=s['prvnizavorka'],
soubordate=s['datum'], souborfiledate=datumsouboru,
poznamka=s['druhazavorka'],
)
print(f" → FILES.ID = {fileid}")
filenameforbookmark = (s['datum'].strftime('%Y-%m-%d') + ' '
+ s['prvnizavorka'] + ': ' + s['druhazavorka'])
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 2: rozhodovací logika ────────────────────────────────────────────────
print(f"\n>>> Hledám poslední dekurs pro IDPAC={IDPAC}...")
existujici = najdi_posledni_dekurs_dnes(conn, IDPAC, DATUM)
cur = conn.cursor()
now = datetime.datetime.now()
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# ── Případ 1: dnešní dekurs s existující sekcí → přidáme do ní ──────
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubor DO existující sekce...")
filenameforbookmark = (SOUBORY[0]['datum'].strftime('%Y-%m-%d') + ' '
+ SOUBORY[0]['prvnizavorka'] + ': ' + SOUBORY[0]['druhazavorka'])
merged_rtf = pridat_do_sekce_prilohy(
existing_rtf,
bookmark_list[0],
filenameforbookmark
)
else:
# ── Případ 2: dnešní dekurs bez sekce → prepend ───────────────────────
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → prepend nové sekce")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(SOUBORY))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# ── Případ 3: žádný dnešní dekurs → nový ─────────────────────────────────
print(f"\n>>> Žádný dekurs pro {DATUM} → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
rtf = rtf.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?,?,?,?,?,?,?,?)",
(dekursid, 6, 2, 2, IDPAC, now.date(), now.time(), rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
conn.close()
print("\n=== HOTOVO ===")
print("Otevři Medicus → karta Buzalka → zkontroluj dekurs!")
@@ -0,0 +1,156 @@
# test_import_FINAL.py detailní dokumentace
## Co skript dělá
Importuje PDF soubory (lékařské zprávy) do Medicus DB. Konkrétně:
1. Uloží fyzický soubor do **externí Firebird DB** (tabulka FILES)
2. Vloží nebo aktualizuje **dekurs pacienta** (tabulka DEKURS) s klikacím RTF odkazem na soubor
---
## Vstupní data (konfigurace nahoře)
```python
CESTA = r'u:\\' # adresář se zdrojovými PDF soubory
IDPAC = 9742 # ID pacienta v DB
DATUM = datetime.date(2026, 3, 18) # datum zprávy (ne dnešek!)
SOUBORY = [
{
'souborname': 'název souboru.pdf',
'prvnizavorka': 'typ zprávy', # např. "vyšetření"
'druhazavorka': 'poznámka', # volný text
'datum': DATUM,
},
...
]
```
Pozor: `DATUM` je datum zprávy (ne dnešek). Podle tohoto data se hledá existující dekurs.
---
## Rozhodovací logika 3 scénáře
```
Poslední dekurs pacienta
├─ z JINÉHO dne / neexistuje
│ └─→ SCÉNÁŘ 3: vytvoří nový dekurs
└─ z DNEŠNÍHO dne (= DATUM)
├─ MÁ sekci "Vložené přílohy"
│ └─→ SCÉNÁŘ 1: přidá odkaz DO existující sekce
└─ NEMÁ sekci "Vložené přílohy"
└─→ SCÉNÁŘ 2: prepend nové sekce na začátek
```
Klíčová funkce pro detekci: `ma_sekci_prilohy(rtf)` hledá RTF string `Vlo\'9een\'e9 p\'f8\'edlohy:` (= „Vložené přílohy:" zakódováno win1250).
---
## Krok 1 uložení souboru do ext DB
Volá `funkce_ext.zapis_file_ext(...)` pro každý soubor. Vrátí `fileid` (ID záznamu v tabulce FILES).
Z každého souboru se postaví:
- **bookmark entry** pro `{\info{\bookmarks ...}}` blok RTF:
`"2026-03-18 vyšetření: poznámka","Files:1234",9`
- **RTF pard** (klikací odkaz) pro tělo dekurzu:
`\pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2026-03-18 vyšetření: poznámka{\*\bkmkend 0}\par`
Číslo `cislo` začíná na 9 a roste po 7 (interní Medicus konvence). Index `poradi` (bkmkstart) začíná na 0 a roste po 1.
---
## Krok 2 práce s dekurzem
### Scénář 1: přidání DO existující sekce (`pridat_do_sekce_prilohy`)
Situace: dnešní dekurs již má blok „Vložené přílohy" s nějakými odkazy.
Postup:
1. Spočítá počet existujících `Files:` odkazů v `{\info{\bookmarks}}` → to je index nového (`new_idx`)
2. Posune všechny `\bkmkstart N` / `\bkmkend N` kde `N >= new_idx` o +1 (uvolní místo)
3. Vloží nový `\pard` řádek **před** uzavírací `\pard\s10\plain\cs15\f0\fs20 \par` sekce
4. Vloží nový bookmark na pozici `new_idx` v `{\info{\bookmarks}}`
Výsledek: soubor se přidá na konec existujícího seznamu příloh, indexy zůstanou konzistentní.
### Scénář 2: prepend nové sekce (`merge_rtf_prepend`)
Situace: dnešní dekurs existuje, ale ještě nemá blok příloh.
Postup:
1. Posune všechny existující `\bkmkstart N` / `\bkmkend N` o +n_new (počet nových souborů)
2. Přidá nové bookmarky **na začátek** `{\info{\bookmarks}}` bloku
- Pokud `{\info{\bookmarks}}` neexistuje, vloží ho za `\deflang1029`
3. Vloží nové tělo (záhlaví „Vložené přílohy:" + řádky s odkazy) **před** první `\uc1\pard` těla stávajícího dekurzu
Výsledek: sekce příloh je viditelně nahoře, stávající text dekurzu zůstane pod ní.
### Scénář 3: nový dekurs
Situace: žádný dnešní dekurs neexistuje.
Sestaví RTF šablonu s:
- `{\info{\bookmarks ...}}` všechny bookmarky
- záhlaví „Vložené přílohy:" + klikací řádky
- uzavírací prázdný řádek
Vloží jako nový řádek do tabulky DEKURS s `iduzi=6, idprac=2, idodd=2` (Vladimír Buzalka, ordinace).
---
## RTF formát dekurzu
```rtf
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
{\info{\bookmarks "2026-03-18 vyšetření: poznámka","Files:1234",9}}
{\fonttbl{\f0\fnil\fcharset238 Arial;} ...}
{\colortbl ;\red0\green0\blue255; ...}
{\stylesheet ... {\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 Vložené přílohy:\par
\pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2026-03-18 vyšetření: poznámka{\*\bkmkend 0}\par
\pard\s10\plain\cs15\f0\fs20 \par
}
```
- **cs20** = kurzíva (záhlaví sekce)
- **cs32** = podtržený modrý text (klikací odkaz)
- **cs15** = normální text
- `\cf1` = modrá barva (první v colortbl)
---
## Závislosti
| Import | Odkud | Co dělá |
|--------|-------|---------|
| `funkce_ext.zapis_file_ext` | `funkce_ext.py` | Uloží soubor do ext DB (tabulka FILES), vrátí fileid |
| `funkce.get_dekurs_id` | `funkce.py` | Vrátí nové ID pro INSERT do tabulky DEKURS |
| `fdb` | pip | Připojení k Firebird DB |
---
## Tabulky v DB
| Tabulka | DB | Popis |
|---------|----|-------|
| `DEKURS` | hlavní (`medicus.fdb`) | Záznamy dekurzu, pole `DEKURS` obsahuje RTF text |
| `FILES` | ext DB (`MEDICUS_FILES_*.fdb`) | Binární obsah souborů |
---
## Jak spustit
Skript se spouští jednorázově na Windows stroji s přístupem k Firebird DB. Před spuštěním:
1. Upravit `SOUBORY` seznam PDF souborů ke zpracování
2. Zkontrolovat `IDPAC`, `DATUM`, `CESTA`
3. Ověřit, že PDF soubory fyzicky existují na `CESTA`
Po spuštění ověřit v Medicus: karta pacienta → záložka Dekurzy → kliknout na odkaz.
@@ -0,0 +1,47 @@
"""test_import_april2026.py jednorázový testovací import do DB202604
Spustit na Windows (Medicus může být spuštěný, jen ne na kartě tohoto pacienta).
"""
import datetime
import os
import fdb
import funkce_ext
SOUBOR_CESTA = r'u:\\'
SOUBOR_NAZEV = '7309208104 Buzalka, Vladimír 2026-04-01 [testovaci zprava] [všechno OK].pdf'
IDPAC = 9742 # Buzalka Vladimír (RC 7309208104)
PRVNI_ZAVORKA = 'testovaci zprava'
POZNAMKA = 'všechno OK'
DATUM_ZPRAVY = datetime.date(2026, 4, 1)
DATUM_SOUBORU = datetime.datetime.fromtimestamp(
os.path.getmtime(os.path.join(SOUBOR_CESTA, SOUBOR_NAZEV)))
print(f"Soubor: {os.path.join(SOUBOR_CESTA, SOUBOR_NAZEV)}")
print(f"Datum zprávy: {DATUM_ZPRAVY}")
print(f"Datum souboru:{DATUM_SOUBORU}")
print()
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='WIN1250'
)
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn,
idpac=IDPAC,
cesta=SOUBOR_CESTA,
souborname=SOUBOR_NAZEV,
prvnizavorka=PRVNI_ZAVORKA,
soubordate=DATUM_ZPRAVY,
souborfiledate=DATUM_SOUBORU,
poznamka=POZNAMKA,
)
conn.close()
print()
print(f"Hotovo! FILES.ID = {fileid}")
print("Otevři Medicus, přejdi na kartu Buzalka Vladimír a zkontroluj záložku Soubory.")
@@ -0,0 +1,182 @@
"""test_import_merge.py vloží přílohy do dekurzu daného dne.
Pokud dekurs pro daný den existuje → vloží přílohy NAHORU před stávající text.
Pokud neexistuje → vytvoří nový dekurs.
Spustit na Windows.
"""
import datetime, os, re, fdb
import funkce, funkce_ext
CESTA = r'u:\\'
IDPAC = 9742 # Buzalka Vladimír
DATUM = datetime.date(2026, 3, 18)
SOUBORY = [
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy1].pdf',
'prvnizavorka': 'vyšetření', 'druhazavorka': 'ahoj Claude copy1', 'datum': DATUM},
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy2].pdf',
'prvnizavorka': 'vyšetření', 'druhazavorka': 'ahoj Claude copy2', 'datum': DATUM},
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude].pdf',
'prvnizavorka': 'vyšetření', 'druhazavorka': 'ahoj Claude', 'datum': DATUM},
]
# ─────────────────────────────────────────────────────────────────────────────
def najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni):
"""Najde POSLEDNÍ dekurs pacienta (jakýkoli datum) a vrátí (id, rtf)
pouze pokud je ze stejného dne jako datum_vlozeni. Jinak vrátí None.
Logika: zajímá nás jen poslední zápis pokud je z dnešního dne,
vložíme přílohy do něj. Pokud je starší, zakládáme nový dnešní záznam.
"""
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum == datum_vlozeni:
print(f" → shoduje se s dneškem ({datum_vlozeni}), budeme mergovat")
return (dekurs_id, dekurs_rtf)
else:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový")
return None
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží nový obsah (přílohy) na ZAČÁTEK stávajícího dekurzu.
existing_rtf stávající RTF string z DB
new_bkm_list list stringů typu '"popis","Files:123",9'
new_body_pards RTF string: header \par + \pard řádky s odkazy
n_new počet nových bkmkstart indexů (= počet souborů)
"""
rtf = existing_rtf
# 1. Posunout stávající bkmkstart/bkmkend indexy o n_new,
# aby nedošlo ke kolizi s našimi novými (0, 1, 2 …)
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
# 2. Přidat naše bookmarky na ZAČÁTEK {\info{\bookmarks ...}}
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
# žádný {\info} blok vložíme za \deflang...
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
# 3. Vložit naše tělo před první \uc1\pard těla stávajícího dekurzu
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = rtf.index(r'\uc1\pard', match.start())
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
else:
# fallback připojit před poslední }
rtf = rtf.rstrip()
if rtf.endswith('}'):
rtf = rtf[:-1] + new_body_pards + '\n}'
return rtf
# ─────────────────────────────────────────────────────────────────────────────
conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250')
# ── Krok 1: vložit soubory do ext DB ─────────────────────────────────────────
bookmark_list = []
bookmarks_body = ''
cislo = 9
poradi = 0
for s in SOUBORY:
cesta_souboru = os.path.join(CESTA, s['souborname'])
datumsouboru = datetime.datetime.fromtimestamp(os.path.getmtime(cesta_souboru))
print(f"\n>>> Zpracovávám: {s['souborname']}")
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=IDPAC,
cesta=CESTA, souborname=s['souborname'],
prvnizavorka=s['prvnizavorka'],
soubordate=s['datum'], souborfiledate=datumsouboru,
poznamka=s['druhazavorka'],
)
print(f" → FILES.ID = {fileid}")
filenameforbookmark = (s['datum'].strftime('%Y-%m-%d') + ' '
+ s['prvnizavorka'] + ': ' + s['druhazavorka'])
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# Tělo přílohy (záhlaví + odkaz řádky + prázdný řádek)
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 2: existující dekurs pro tento den? ──────────────────────────────────
print(f"\n>>> Hledám poslední dekurs pro IDPAC={IDPAC}...")
existujici = najdi_posledni_dekurs_dnes(conn, IDPAC, DATUM)
cur = conn.cursor()
now = datetime.datetime.now()
if existujici:
dekurs_id, existing_rtf = existujici
print(f"\n>>> Nalezen existující dekurs pro {DATUM}: ID={dekurs_id}")
print(">>> Vkládám přílohy na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(SOUBORY))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
print(f"\n>>> Žádný dekurs pro {DATUM} nenalezen vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
rtf = rtf.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?,?,?,?,?,?,?,?)",
(dekursid, 6, 2, 2, IDPAC, now.date(), now.time(), rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
conn.close()
print("\n=== HOTOVO ===")
print("Otevři Medicus → karta Buzalka → zkontroluj dekurs!")