notebook vb
This commit is contained in:
@@ -8,8 +8,8 @@ conn = fdb.connect(
|
|||||||
password="masterkey",
|
password="masterkey",
|
||||||
charset="win1250")
|
charset="win1250")
|
||||||
|
|
||||||
cesta = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
|
cesta = r"u:\testimport"
|
||||||
cestazpracovana = r"u:\NextcloudOrdinace\Dokumentace_zpracovaná"
|
cestazpracovana = r"u:\testimportzpracovana"
|
||||||
|
|
||||||
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
|
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
|
||||||
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
|
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user