notebook vb

This commit is contained in:
2026-04-04 08:57:00 +02:00
parent 8782ec1bde
commit 8ba7bae707
4 changed files with 1022 additions and 2 deletions
+2 -2
View File
@@ -8,8 +8,8 @@ conn = fdb.connect(
password="masterkey",
charset="win1250")
cesta = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
cestazpracovana = r"u:\NextcloudOrdinace\Dokumentace_zpracovaná"
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:"
+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()
+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,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.