Merge remote-tracking branch 'origin/master'

This commit is contained in:
michaela.buzalkova
2026-04-11 08:55:19 +02:00
17 changed files with 2801 additions and 63 deletions
+185
View File
@@ -0,0 +1,185 @@
import subprocess
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB"
EXT_DIR = Path(r"U:\externi")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# HELPERS
# ============================================================
def gbak_and_zip(label: str, db_conn: str, fbk: Path, zipf: Path, log: Path) -> dict:
"""
Run gbak backup and ZIP the result.
Returns a result dict.
"""
result = {
"label": label,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
# 1) GBAK
print(f"GBAK: {label} ... ", end="", flush=True)
t0 = time.time()
cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)")
# 2) ZIP
t1 = time.time()
processed = 0
fbk_size = result["fbk_size"]
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True)
print()
result["t_zip"] = time.time() - t1
result["zip_size"] = zipf.stat().st_size
# 3) Delete FBK + LOG
fbk.unlink()
log.unlink()
result["ok"] = True
return result
def format_result(r: dict) -> str:
ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0
return (
f" {r['label']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB → "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"({ratio:.0f}% komprese, "
f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
backed_up = []
errors = []
# ----------------------------------------------------------
# 1) Hlavní DB MEDICUS.FDB
# ----------------------------------------------------------
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip"
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
try:
r = gbak_and_zip("MEDICUS", MAIN_DB, fbk, zipf, log)
backed_up.append(r)
except Exception:
errors.append({"label": "MEDICUS", "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# 2) Externí DB MEDICUS_FILES_*.fdb
# ----------------------------------------------------------
fdb_all = sorted(
set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")),
key=lambda p: p.name.lower(),
)
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
zipf = BACKUP_DIR / f"{name}_{ts}.zip"
log = BACKUP_DIR / f"{name}_{ts}.log"
db_conn = f"localhost/{FB_PORT}:{fdb}"
try:
r = gbak_and_zip(name, db_conn, fbk, zipf, log)
backed_up.append(r)
except Exception:
errors.append({"label": name, "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# Report
# ----------------------------------------------------------
total = 1 + len(fdb_all)
report = [
f"Backup Medicus {now.strftime('%d.%m.%Y %H:%M')}",
f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}",
f"Výstupní adresář: {BACKUP_DIR}",
"",
]
if backed_up:
report.append("--- Zálohováno ---")
total_zip = sum(r["zip_size"] for r in backed_up)
for r in backed_up:
report.append(format_result(r))
report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB")
report.append("")
if errors:
report.append("--- CHYBY ---")
for e in errors:
report.append(f" {e['label']}:\n{e['error']}")
report.append("")
has_errors = bool(errors)
subject = (
f"{'X' if has_errors else 'OK'} MEDICUS backup "
f"{len(backed_up)}/{total}"
+ (f" {len(errors)} chyb" if has_errors else "")
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+235
View File
@@ -0,0 +1,235 @@
import subprocess
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB"
EXT_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# HELPERS
# ============================================================
def run_gbak(label: str, db_conn: str, fbk: Path, log: Path) -> dict:
"""Run gbak, return result dict (without zip info)."""
result = {
"label": label,
"ok": False,
"fbk": fbk,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
print(f"GBAK: {label} ... ", end="", flush=True)
t0 = time.time()
cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)")
result["ok"] = True
return result
def zip_single(label: str, fbk: Path, zipf: Path) -> tuple[int, float]:
"""ZIP one FBK into its own ZIP. Returns (zip_size, t_zip)."""
t1 = time.time()
processed = 0
fbk_size = fbk.stat().st_size
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True)
print()
return zipf.stat().st_size, time.time() - t1
def zip_multiple(fbk_results: list[dict], zipf: Path) -> tuple[int, float]:
"""ZIP multiple FBK files into one ZIP. Returns (zip_size, t_zip)."""
t1 = time.time()
total_fbk_size = sum(r["fbk_size"] for r in fbk_results)
total_processed = 0
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
for r in fbk_results:
fbk = r["fbk"]
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
total_processed += len(buf)
pct = total_processed * 100 / total_fbk_size
print(f"\r ZIP {fbk.name}: {pct:6.2f}%", end="", flush=True)
print()
return zipf.stat().st_size, time.time() - t1
def format_result(r: dict) -> str:
ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0
return (
f" {r['label']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB → "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"({ratio:.0f}% komprese, "
f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
backed_up = []
errors = []
# ----------------------------------------------------------
# 1) Hlavní DB MEDICUS.FDB → vlastní ZIP
# ----------------------------------------------------------
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip"
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
try:
r = run_gbak("MEDICUS", MAIN_DB, fbk, log)
log.unlink()
zip_size, t_zip = zip_single("MEDICUS", fbk, zipf)
fbk.unlink()
r["zip_size"] = zip_size
r["t_zip"] = t_zip
backed_up.append(r)
except Exception:
errors.append({"label": "MEDICUS", "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# 2) Externí DB MEDICUS_FILES_*.fdb → všechny do jednoho ZIP
# ----------------------------------------------------------
fdb_all = sorted(
set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")),
key=lambda p: p.name.lower(),
)
ext_results = []
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
log = BACKUP_DIR / f"{name}_{ts}.log"
db_conn = f"localhost/{FB_PORT}:{fdb}"
try:
r = run_gbak(name, db_conn, fbk, log)
log.unlink()
ext_results.append(r)
except Exception:
errors.append({"label": name, "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ZIP všechny externí FBK do jednoho souboru
if ext_results:
ext_zip = BACKUP_DIR / f"MEDICUS_FILES_{ts}.zip"
print(f"\nZIP externích DB → {ext_zip.name}")
try:
zip_size, t_zip = zip_multiple(ext_results, ext_zip)
for r in ext_results:
r["zip_size"] = zip_size # sdílená velikost výsledného ZIPu
r["t_zip"] = t_zip
r["fbk"].unlink()
backed_up.append(r)
except Exception:
errors.append({"label": "MEDICUS_FILES (zip)", "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
for r in ext_results:
if r["fbk"].exists():
r["fbk"].unlink()
# ----------------------------------------------------------
# Report
# ----------------------------------------------------------
total = 1 + len(fdb_all)
report = [
f"Backup Medicus {now.strftime('%d.%m.%Y %H:%M')}",
f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}",
f"Výstupní adresář: {BACKUP_DIR}",
"",
]
if backed_up:
report.append("--- Zálohováno ---")
# Hlavní DB
main_results = [r for r in backed_up if r["label"] == "MEDICUS"]
ext_backed = [r for r in backed_up if r["label"] != "MEDICUS"]
for r in main_results:
report.append(format_result(r))
if ext_backed:
total_ext_fbk = sum(r["fbk_size"] for r in ext_backed)
ext_zip_size = ext_backed[0]["zip_size"] if ext_backed else 0
ratio = 100 * (1 - ext_zip_size / total_ext_fbk) if total_ext_fbk else 0
report.append(f" Externí DB ({len(ext_backed)} souborů):")
for r in ext_backed:
report.append(f" {r['label']}: FBK {r['fbk_size']/1024/1024:.1f} MB (gbak {r['t_gbak']:.0f}s)")
report.append(
f" → společný ZIP: {ext_zip_size/1024/1024:.1f} MB "
f"({ratio:.0f}% komprese, zip {ext_backed[0]['t_zip']:.0f}s)"
)
total_zip = sum(r["zip_size"] for r in main_results) + (ext_backed[0]["zip_size"] if ext_backed else 0)
report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB")
report.append("")
if errors:
report.append("--- CHYBY ---")
for e in errors:
report.append(f" {e['label']}:\n{e['error']}")
report.append("")
has_errors = bool(errors)
subject = (
f"{'X' if has_errors else 'OK'} MEDICUS backup "
f"{len(backed_up)}/{total}"
+ (f" {len(errors)} chyb" if has_errors else "")
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+2 -2
View File
@@ -18,8 +18,8 @@ FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
SRC_DIR = Path(r"U:\externi")
BACKUP_DIR = Path(r"U:\externabackup")
SRC_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
+172
View File
@@ -0,0 +1,172 @@
import subprocess
import json
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
SRC_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
# Find all FDB files (case-insensitive)
fdb_files = sorted(SRC_DIR.glob("MEDICUS_FILES_*.fdb"))
fdb_upper = sorted(SRC_DIR.glob("MEDICUS_FILES_*.FDB"))
fdb_all = sorted(
set(fdb_files + fdb_upper),
key=lambda p: p.name.lower(),
)
backed_up = []
errors = []
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
zipf = BACKUP_DIR / f"{name}_{ts}.zip"
log = BACKUP_DIR / f"{name}_{ts}.log"
result = {
"file": fdb.name,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
try:
# 1) GBAK
print(f"GBAK: {fdb.name} ... ", end="", flush=True)
t0 = time.time()
db_conn = f"localhost/{FB_PORT}:{fdb}"
cmd = [
GBAK, "-b",
"-user", FB_USER,
"-pas", FB_PASS,
db_conn, str(fbk),
"-v",
]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd, stdout=f, stderr=subprocess.STDOUT, check=True,
)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s)")
# 2) ZIP
t1 = time.time()
processed = 0
fbk_size = result["fbk_size"]
with zipfile.ZipFile(
zipf, "w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9,
) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(
f"\r ZIP {name}: {pct:6.2f}%",
end="", flush=True,
)
print()
result["t_zip"] = time.time() - t1
result["zip_size"] = zipf.stat().st_size
# 3) DELETE FBK + LOG
fbk.unlink()
log.unlink()
result["ok"] = True
backed_up.append(result)
except Exception:
result["error"] = traceback.format_exc()
errors.append(result)
for f in (fbk, log):
if f.exists():
f.unlink()
# Build report
report = []
report.append(f"Backup externi DB - {now.strftime('%d.%m.%Y %H:%M')}")
report.append(f"Celkem souboru: {len(fdb_all)}")
report.append(f"Zalohovano: {len(backed_up)}")
report.append(f"Chyby: {len(errors)}")
report.append("")
if backed_up:
report.append("--- Backed up ---")
total_zip = 0
for r in backed_up:
total_zip += r["zip_size"]
report.append(
f" {r['file']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB -> "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"(gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
report.append(f" Total ZIP: {total_zip / 1024 / 1024:.1f} MB")
report.append("")
if errors:
report.append("--- ERRORS ---")
for r in errors:
report.append(f" {r['file']}: {r['error']}")
report.append("")
# Send email
has_errors = len(errors) > 0
subject = (
f"{'X' if has_errors else 'OK'} "
f"MEDICUS externi DB - "
f"backup {len(backed_up)}/{len(fdb_all)}"
f"{f', {len(errors)} errors' if has_errors else ''}"
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+189
View File
@@ -0,0 +1,189 @@
import subprocess
import json
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
SRC_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
# Find all FDB files (case-insensitive)
fdb_files = sorted(SRC_DIR.glob("MEDICUS_FILES_*.fdb"))
fdb_upper = sorted(SRC_DIR.glob("MEDICUS_FILES_*.FDB"))
fdb_all = sorted(
set(fdb_files + fdb_upper),
key=lambda p: p.name.lower(),
)
backed_up = []
errors = []
fbk_paths = [] # FBK files to be zipped together
# --------------------------------------------------------
# 1) GBAK all databases
# --------------------------------------------------------
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
log = BACKUP_DIR / f"{name}_{ts}.log"
result = {
"file": fdb.name,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
try:
print(f"GBAK: {fdb.name} ... ", end="", flush=True)
t0 = time.time()
db_conn = f"localhost/{FB_PORT}:{fdb}"
cmd = [
GBAK, "-b",
"-user", FB_USER,
"-pas", FB_PASS,
db_conn, str(fbk),
"-v",
]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd, stdout=f, stderr=subprocess.STDOUT, check=True,
)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s)")
# Delete log, keep FBK for zipping
log.unlink()
result["ok"] = True
fbk_paths.append((fbk, result))
backed_up.append(result)
except Exception:
result["error"] = traceback.format_exc()
errors.append(result)
for f in (fbk, log):
if f.exists():
f.unlink()
# --------------------------------------------------------
# 2) ZIP all FBK files into one archive
# --------------------------------------------------------
total_zip_size = 0
if fbk_paths:
zip_path = BACKUP_DIR / f"MEDICUS_FILES_{ts}.zip"
print(f"\nZIP: {zip_path.name}")
t_zip_start = time.time()
# Calculate total size for progress
total_fbk_size = sum(fbk.stat().st_size for fbk, _ in fbk_paths)
total_processed = 0
with zipfile.ZipFile(
zip_path, "w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9,
) as zf:
for fbk, result in fbk_paths:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
total_processed += len(buf)
pct = total_processed * 100 / total_fbk_size
print(
f"\r {fbk.name}: {pct:6.2f}%",
end="", flush=True,
)
print()
t_zip_total = time.time() - t_zip_start
total_zip_size = zip_path.stat().st_size
print(f"ZIP OK ({t_zip_total:.0f}s, {total_zip_size/1024/1024:.1f} MB)")
# Fill zip_size into each result and delete FBK files
for fbk, result in fbk_paths:
result["zip_size"] = total_zip_size
fbk.unlink()
# --------------------------------------------------------
# Build report
# --------------------------------------------------------
report = []
report.append(f"Backup externi DB - {now.strftime('%d.%m.%Y %H:%M')}")
report.append(f"Celkem souboru: {len(fdb_all)}")
report.append(f"Zalohovano: {len(backed_up)}")
report.append(f"Chyby: {len(errors)}")
report.append("")
if backed_up:
report.append("--- Backed up ---")
total_fbk_mb = sum(r["fbk_size"] for r in backed_up) / 1024 / 1024
for r in backed_up:
report.append(
f" {r['file']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB "
f"(gbak {r['t_gbak']:.0f}s)"
)
report.append(f" Total FBK: {total_fbk_mb:.1f} MB -> ZIP: {total_zip_size/1024/1024:.1f} MB")
report.append("")
if errors:
report.append("--- ERRORS ---")
for r in errors:
report.append(f" {r['file']}: {r['error']}")
report.append("")
# Send email
has_errors = len(errors) > 0
subject = (
f"{'X' if has_errors else 'OK'} "
f"MEDICUS externi DB - "
f"backup {len(backed_up)}/{len(fdb_all)}"
f"{f', {len(errors)} errors' if has_errors else ''}"
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+103
View File
@@ -361,3 +361,106 @@ Správný RTF formát klikacího odkazu:
- 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)
+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.
@@ -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,352 @@
import sys, io, re, os, glob
from datetime import date, datetime, timedelta
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
import fdb
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
VYSTUPNI_ADRESAR = r'u:\Dropbox\Ordinace\Reporty'
NAZEV_REPORTU = 'Dekurzy_TEST'
DATUM_OD = (date.today() - timedelta(days=100)).strftime('%Y-%m-%d')
DATUM_DO = date.today().strftime('%Y-%m-%d')
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute(f"""
SELECT d.DATUM, d.CAS, u.ZKRATKA, k.PRIJMENI, k.JMENO, k.RODCIS, k.POJ, d.DEKURS
FROM DEKURS d
JOIN KAR k ON k.IDPAC = d.IDPAC
LEFT JOIN UZIVATEL u ON u.IDUZI = d.IDUZI
WHERE d.DATUM >= '{DATUM_OD}' AND d.DATUM <= '{DATUM_DO}'
ORDER BY d.DATUM DESC, d.CAS DESC, k.PRIJMENI, k.JMENO
""")
raw_rows = cur.fetchall()
TOP_TYPY = ['VykA', 'Rec', 'Files', 'MEDLAB', 'Lab', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
def rtf_na_text(rtf):
"""Jednoduchý převod RTF na čistý text."""
# Odstraň info blok (bookmarky apod.)
text = re.sub(r'\{\\info.*?\}', '', rtf, flags=re.DOTALL)
# Odstraň ostatní skupiny v {} rekurzivně (fonty, barvy apod.)
for _ in range(6):
text = re.sub(r'\{[^{}]*\}', '', text)
# Nový řádek za \par \line
text = re.sub(r'\\par\b\s*', '\n', text)
text = re.sub(r'\\line\b\s*', '\n', text)
# Dekóduj RTF hex escape sekvence (\'xx) jako cp1250 → správná čeština
def decode_hex(m):
try:
return bytes.fromhex(m.group(1)).decode('cp1250')
except Exception:
return ''
text = re.sub(r"\\'([0-9a-fA-F]{2})", decode_hex, text)
# Odstraň ostatní RTF příkazy
text = re.sub(r'\\[a-zA-Z]+\-?[0-9]*\s?', '', text)
text = re.sub(r'[{}\\]', '', text)
# Vyčisti prázdné řádky a whitespace
lines = [l.strip() for l in text.splitlines()]
lines = [l for l in lines if l]
return '\n'.join(lines)
# Parse dekurzů
rows = []
for datum, cas, zkratka, prijmeni, jmeno, rodcis, poj, dekurs_blob in raw_rows:
rtf = dekurs_blob.read() if hasattr(dekurs_blob, 'read') else (dekurs_blob or '')
pocty = {}
ids_by_typ = {t: [] for t in TOP_TYPY}
ids_ostatni = []
for nazev, typ, rid in re.findall(r'"([^"]+)","([A-Za-z]+):(\d+)"', rtf):
pocty[typ] = pocty.get(typ, 0) + 1
if typ in ids_by_typ:
ids_by_typ[typ].append(int(rid))
else:
ids_ostatni.append((typ, int(rid), nazev))
top = [pocty.get(t, 0) for t in TOP_TYPY]
ostatni = sum(v for k, v in pocty.items() if k not in TOP_TYPY)
iniciala = jmeno[0] + '.' if jmeno and jmeno.strip() else ''
jmeno_cel = f"{prijmeni.strip()}, {iniciala}" if prijmeni else iniciala
text_dekurzu = rtf_na_text(rtf)
rows.append((datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni, text_dekurzu))
# ── Načtení detailů z DB ────────────────────────────────────────────────────
def fetch_details(cur, table, pk, id_col, fields, ids):
if not ids:
return {}
result = {}
batch_size = 1000
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
ph = ','.join('?' * len(batch))
cur.execute(f"SELECT {pk}, {','.join(fields)} FROM {table} WHERE {id_col} IN ({ph})", batch)
for row in cur.fetchall():
result[row[0]] = row[1:]
return result
def get_ids(rows, typ):
return list({rid for *_, ids_by_typ, _, _text in rows for rid in ids_by_typ[typ]})
rec_det = fetch_details(cur, 'RECEPT', 'ID', 'ID', ['LEK','DSIG'], get_ids(rows,'Rec'))
vyka_det = fetch_details(cur, 'DOKLADD', 'ID', 'ID', ['KOD','DDGN','BODY'], get_ids(rows,'VykA'))
files_det = fetch_details(cur, 'FILES', 'ID', 'ID', ['FILENAME','DATUM'], get_ids(rows,'Files'))
medlab_det = fetch_details(cur, 'HISTDOC', 'ID', 'ID', ['DATUM','TYP'], get_ids(rows,'MEDLAB'))
lab_det = fetch_details(cur, 'LABVH', 'IDVH', 'IDVH', ['DATUM','CISLO'], get_ids(rows,'Lab'))
ock_det = fetch_details(cur, 'OCKZAZ', 'ID', 'ID', ['DATUM','LATKA'], get_ids(rows,'Ock'))
nes_det = fetch_details(cur, 'NES', 'ID', 'ID', ['ZACNES','KONNES'], get_ids(rows,'Nes'))
lec_det = fetch_details(cur, 'LECD', 'ID', 'ID', ['KOD','DATOSE'], get_ids(rows,'Lec'))
spec_det = fetch_details(cur, 'SPECVYS', 'IDSPECVYS','IDSPECVYS',['TYP','DATUM'], get_ids(rows,'SpecVys'))
pla_det = fetch_details(cur, 'PLA', 'IDPLA', 'IDPLA', ['DATUM','CENA','DOKLAD'], get_ids(rows,'PlaPac'))
conn.close()
print(f"Načteno {len(rows)} dekurzů (období: {DATUM_OD} {DATUM_DO})")
# ── Styly ──────────────────────────────────────────────────────────────────
tenka_cara = Side(style='thin', color='AAAAAA')
ohraniceni = Border(left=tenka_cara, right=tenka_cara, top=tenka_cara, bottom=tenka_cara)
hl_font = Font(bold=True, color="FFFFFF")
hl_fill = PatternFill("solid", fgColor="2E75B6")
r_fill = [PatternFill("solid", fgColor="FFFFFF"), PatternFill("solid", fgColor="DCE6F1")]
BARVY_LISTU = {
'Recepty': ('1F6B33', 'E2EFDA'),
'Výkony': ('2E4057', 'D6E4F0'),
'Soubory': ('7B3F00', 'FAE5D3'),
'Žádanky': ('4A235A', 'F5EEF8'),
'Lab výsl.': ('145A32', 'D5F5E3'),
'Očkování': ('7E5109', 'FDEBD0'),
'Neschop.': ('922B21', 'FADBD8'),
'Léčiva': ('1A5276', 'D6EAF8'),
'SpecVys': ('0B5345', 'D1F2EB'),
'Platby': ('4D5656', 'EAECEE'),
'Ostatní': ('2C3E50', 'EBF5FB'),
}
def zapis_hlavicku(ws, sloupce, sirky, barva_hex):
hl_fill_l = PatternFill("solid", fgColor=barva_hex)
for col, (nazev, sirka) in enumerate(zip(sloupce, sirky), start=1):
cell = ws.cell(row=1, column=col, value=nazev)
cell.font = hl_font
cell.fill = hl_fill_l
cell.alignment = Alignment(horizontal='center')
cell.border = ohraniceni
ws.column_dimensions[cell.column_letter].width = sirka
def zapis_radek(ws, row_i, hodnoty, zarovnani, barva_hex):
fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \
else PatternFill("solid", fgColor=barva_hex)
for col_i, (val, align) in enumerate(zip(hodnoty, zarovnani), start=1):
cell = ws.cell(row=row_i, column=col_i, value=val)
cell.fill = fill
cell.border = ohraniceni
cell.alignment = Alignment(horizontal=align)
if col_i == 1 and isinstance(val, __import__('datetime').date):
cell.number_format = 'DD.MM.YYYY'
def hyperlink_cell(ws, row_i, col_i, cil_list, cil_radek, text, barva_hex):
fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \
else PatternFill("solid", fgColor=barva_hex)
cell = ws.cell(row=row_i, column=col_i)
cell.value = str(text)
# Název listu s mezerou musí být v apostrofech
sheet_ref = f"'{cil_list}'" if ' ' in cil_list else cil_list
cell.hyperlink = f'#{sheet_ref}!A{cil_radek}'
cell.font = Font(color="0000FF", underline='single')
cell.fill = fill
cell.border = ohraniceni
cell.alignment = Alignment(horizontal='center')
# ── Workbook ───────────────────────────────────────────────────────────────
wb = openpyxl.Workbook()
LISTY = [
('Recepty', 'Rec', rec_det, ['Datum','Jméno','Recept','Dávkování'], [12,25,25,12], None),
('Výkony', 'VykA', vyka_det, ['Datum','Jméno','Kód výkonu','Diagnóza','Body'], [12,25,14,10,8], None),
('Soubory', 'Files', files_det, ['Datum','Jméno','Soubor','Datum souboru'], [12,25,35,14], None),
('Žádanky', 'MEDLAB', medlab_det, ['Datum','Jméno','Typ'], [12,25,15], None),
('Lab výsl.', 'Lab', lab_det, ['Datum','Jméno','Číslo'], [12,25,20], None),
('Očkování', 'Ock', ock_det, ['Datum','Jméno','Datum očkování','Vakcína'], [12,25,14,30], None),
('Neschop.', 'Nes', nes_det, ['Datum','Jméno','Od','Do'], [12,25,12,12], None),
('Léčiva', 'Lec', lec_det, ['Datum','Jméno','Kód','Datum výkonu'], [12,25,12,14], None),
('SpecVys', 'SpecVys', spec_det, ['Datum','Jméno','Typ vyšetření','Datum vyšetření'], [12,25,25,14], None),
('Platby', 'PlaPac', pla_det, ['Datum','Jméno','Datum platby','Částka','Doklad'], [12,25,14,12,15], None),
('Ostatní', None, None, ['Datum','Jméno','Typ','ID','Název'], [12,25,12,10,30], None),
]
ws_d = wb.active
ws_d.title = "Dekurz"
# List "Text dekurzu" hned za Dekurzem
ws_text = wb.create_sheet("Text dekurzu")
ws_listy = {}
for nazev, *_ in LISTY:
ws_listy[nazev] = wb.create_sheet(nazev)
for nazev, typ, det, sloupce, sirky, _ in LISTY:
barva_hl, _ = BARVY_LISTU[nazev]
zapis_hlavicku(ws_listy[nazev], sloupce, sirky, barva_hl)
ws_listy[nazev].freeze_panes = 'A2'
# Záhlaví listu "Text dekurzu"
zapis_hlavicku(ws_text,
['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Text dekurzu'],
[12, 8, 8, 25, 14, 100],
'2E75B6')
ws_text.freeze_panes = 'A2'
row_ptr_text = 2
# Zobrazované názvy sloupců pro TOP_TYPY (stejné pořadí)
NAZVY_TYPY = ['VykA', 'Rec', 'Files', 'Žádanky', 'Lab výsl.', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
# Záhlaví Dekurz sloupce AR (bez textu)
nazvy_d = ['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Pojišťovna'] + ['HodVyk'] + NAZVY_TYPY + ['Ostatní']
sirky_d = [12, 8, 8, 25, 14, 12 ] + [10] + [38, 8, 8, 8, 8, 8, 8, 8, 8, 8] + [8]
zapis_hlavicku(ws_d, nazvy_d, sirky_d, '2E75B6')
ws_d.freeze_panes = 'A2'
ws_d.auto_filter.ref = f"A1:R{len(rows)+1}"
row_ptr = {nazev: 2 for nazev, *_ in LISTY}
# ── Plnění dat ─────────────────────────────────────────────────────────────
def get_det_hodnoty(typ, rid, datum, jmeno_cel):
if typ == 'Rec':
d = rec_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1] or '']
elif typ == 'VykA':
d = vyka_det.get(rid, ('', '', 0))
return [datum, jmeno_cel, d[0] or '', (d[1] or '').strip(), d[2] or 0]
elif typ == 'Files':
d = files_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1]]
elif typ == 'MEDLAB':
d = medlab_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[1] or '']
elif typ == 'Lab':
d = lab_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[1] or '']
elif typ == 'Ock':
d = ock_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0], d[1] or '']
elif typ == 'Nes':
d = nes_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0], d[1]]
elif typ == 'Lec':
d = lec_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1]]
elif typ == 'SpecVys':
d = spec_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1]]
elif typ == 'PlaPac':
d = pla_det.get(rid, ('', '', ''))
return [datum, jmeno_cel, d[0], d[1], d[2] or '']
return []
ZAROVNANI = {
'Recepty': ['left','left','left','center'],
'Výkony': ['left','left','center','center','right'],
'Soubory': ['left','left','left','left'],
'MedLab': ['left','left','center'],
'Lab': ['left','left','center'],
'Očkování': ['left','left','left','left'],
'Neschop.': ['left','left','left','left'],
'Léčiva': ['left','left','center','left'],
'SpecVys': ['left','left','left','left'],
'Platby': ['left','left','left','right','center'],
'Ostatní': ['left','left','center','center','left'],
}
for row_i, (datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni, text_dekurzu) in enumerate(rows, start=2):
fill_d = r_fill[row_i % 2]
for col_i in range(1, len(nazvy_d) + 1):
ws_d.cell(row=row_i, column=col_i).fill = fill_d
ws_d.cell(row=row_i, column=col_i).border = ohraniceni
ws_d.cell(row=row_i, column=1, value=datum).number_format = 'DD.MM.YYYY'
ws_d.cell(row=row_i, column=2, value=str(cas)[:5] if cas else '').alignment = Alignment(horizontal='center')
ws_d.cell(row=row_i, column=3, value=zkratka or '').alignment = Alignment(horizontal='center')
ws_d.cell(row=row_i, column=4, value=jmeno_cel)
# Sloupec 5 Rodné číslo jako hyperlink na list "Text dekurzu"
hyperlink_cell(ws_d, row_i, 5, 'Text dekurzu', row_ptr_text, rodcis or '', 'DCE6F1')
ws_d.cell(row=row_i, column=6, value=poj or '').alignment = Alignment(horizontal='center')
# Sloupec G HodVyk (součet bodů všech výkonů dekurzu)
hodvyk = sum((vyka_det.get(rid, ('', '', 0))[2] or 0) for rid in ids_by_typ['VykA'])
cell_hv = ws_d.cell(row=row_i, column=7, value=hodvyk if hodvyk else '')
cell_hv.alignment = Alignment(horizontal='center')
for col_off, (typ, pocet) in enumerate(zip(TOP_TYPY, top)):
col_i = 8 + col_off
if pocet == 0:
continue
nazev_listu = next((n for n, t, *_ in LISTY if t == typ), None)
if nazev_listu and ids_by_typ[typ]:
_, barva_ll = BARVY_LISTU[nazev_listu]
# Pro výkony (VykA) přidej kódy do závorky
if typ == 'VykA':
kody = [str(vyka_det.get(rid, ('',))[0] or '').strip() for rid in ids_by_typ[typ]]
kody_str = ', '.join(k for k in kody if k)
display_text = f"{pocet} ({kody_str})" if kody_str else pocet
else:
display_text = pocet
hyperlink_cell(ws_d, row_i, col_i, nazev_listu, row_ptr[nazev_listu], display_text, barva_ll[1:] if len(barva_ll) > 6 else 'DCE6F1')
ws_det = ws_listy[nazev_listu]
barva_hl, barva_r = BARVY_LISTU[nazev_listu]
for rid in ids_by_typ[typ]:
hodnoty = get_det_hodnoty(typ, rid, datum, jmeno_cel)
zarovnani_l = ZAROVNANI.get(nazev_listu, ['left']*10)
zapis_radek(ws_det, row_ptr[nazev_listu], hodnoty, zarovnani_l, barva_r)
row_ptr[nazev_listu] += 1
else:
ws_d.cell(row=row_i, column=col_i, value=pocet).alignment = Alignment(horizontal='center')
if ostatni:
ws_det = ws_listy['Ostatní']
barva_hl, barva_r = BARVY_LISTU['Ostatní']
hyperlink_cell(ws_d, row_i, 18, 'Ostatní', row_ptr['Ostatní'], ostatni, barva_r)
for typ, rid, nazev in ids_ostatni:
zapis_radek(ws_det, row_ptr['Ostatní'],
[datum, jmeno_cel, typ, rid, nazev],
ZAROVNANI['Ostatní'], barva_r)
row_ptr['Ostatní'] += 1
# Zápis do listu "Text dekurzu"
fill_t = r_fill[row_ptr_text % 2]
for col_i in range(1, 7):
ws_text.cell(row=row_ptr_text, column=col_i).fill = fill_t
ws_text.cell(row=row_ptr_text, column=col_i).border = ohraniceni
c1 = ws_text.cell(row=row_ptr_text, column=1, value=datum)
c1.number_format = 'DD.MM.YYYY'
c1.alignment = Alignment(horizontal='left', vertical='center')
ws_text.cell(row=row_ptr_text, column=2, value=str(cas)[:5] if cas else '').alignment = Alignment(horizontal='center', vertical='center')
ws_text.cell(row=row_ptr_text, column=3, value=zkratka or '').alignment = Alignment(horizontal='center', vertical='center')
ws_text.cell(row=row_ptr_text, column=4, value=jmeno_cel).alignment = Alignment(horizontal='left', vertical='center')
ws_text.cell(row=row_ptr_text, column=5, value=rodcis or '').alignment = Alignment(horizontal='left', vertical='center')
cell_txt = ws_text.cell(row=row_ptr_text, column=6, value=text_dekurzu)
cell_txt.alignment = Alignment(horizontal='left', vertical='top', wrap_text=True)
row_ptr_text += 1
# Autofiltr na detailních listech
for nazev, *_ in LISTY:
ws = ws_listy[nazev]
max_col = ws.max_column
max_row = ws.max_row
if max_row > 1:
ws.auto_filter.ref = f"A1:{ws.cell(row=1, column=max_col).column_letter}{max_row}"
# Smazat starý TEST report
for stary in glob.glob(os.path.join(VYSTUPNI_ADRESAR, f'* {NAZEV_REPORTU}.xlsx')):
os.remove(stary)
print(f"Smazán: {stary}")
# Uložit nový
os.makedirs(VYSTUPNI_ADRESAR, exist_ok=True)
casova_znacka = datetime.now().strftime('%Y-%m-%d %H-%M-%S')
vystup = os.path.join(VYSTUPNI_ADRESAR, f'{casova_znacka} {NAZEV_REPORTU}.xlsx')
wb.save(vystup)
print(f"Uloženo: {vystup}")
for nazev, *_ in LISTY:
print(f" {nazev}: {row_ptr[nazev]-2} řádků")
+97 -34
View File
@@ -26,7 +26,32 @@ cur.execute(f"""
""")
raw_rows = cur.fetchall()
TOP_TYPY = ['Rec', 'VykA', 'Files', 'MEDLAB', 'Lab', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
TOP_TYPY = ['VykA', 'Rec', 'Files', 'MEDLAB', 'Lab', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
def rtf_na_text(rtf):
"""Jednoduchý převod RTF na čistý text."""
# Odstraň info blok (bookmarky apod.)
text = re.sub(r'\{\\info.*?\}', '', rtf, flags=re.DOTALL)
# Odstraň ostatní skupiny v {} rekurzivně (fonty, barvy apod.)
for _ in range(6):
text = re.sub(r'\{[^{}]*\}', '', text)
# Nový řádek za \par \line
text = re.sub(r'\\par\b\s*', '\n', text)
text = re.sub(r'\\line\b\s*', '\n', text)
# Dekóduj RTF hex escape sekvence (\'xx) jako cp1250 → správná čeština
def decode_hex(m):
try:
return bytes.fromhex(m.group(1)).decode('cp1250')
except Exception:
return ''
text = re.sub(r"\\'([0-9a-fA-F]{2})", decode_hex, text)
# Odstraň ostatní RTF příkazy
text = re.sub(r'\\[a-zA-Z]+\-?[0-9]*\s?', '', text)
text = re.sub(r'[{}\\]', '', text)
# Vyčisti prázdné řádky a whitespace
lines = [l.strip() for l in text.splitlines()]
lines = [l for l in lines if l]
return '\n'.join(lines)
# Parse dekurzů
rows = []
@@ -45,7 +70,8 @@ for datum, cas, zkratka, prijmeni, jmeno, rodcis, poj, dekurs_blob in raw_rows:
ostatni = sum(v for k, v in pocty.items() if k not in TOP_TYPY)
iniciala = jmeno[0] + '.' if jmeno and jmeno.strip() else ''
jmeno_cel = f"{prijmeni.strip()}, {iniciala}" if prijmeni else iniciala
rows.append((datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni))
text_dekurzu = rtf_na_text(rtf)
rows.append((datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni, text_dekurzu))
# ── Načtení detailů z DB ────────────────────────────────────────────────────
def fetch_details(cur, table, pk, id_col, fields, ids):
@@ -62,10 +88,10 @@ def fetch_details(cur, table, pk, id_col, fields, ids):
return result
def get_ids(rows, typ):
return list({rid for _, _, _, _, _, _, _, _, ids_by_typ, _ in rows for rid in ids_by_typ[typ]})
return list({rid for *_, ids_by_typ, _, _text in rows for rid in ids_by_typ[typ]})
rec_det = fetch_details(cur, 'RECEPT', 'ID', 'ID', ['LEK','DSIG'], get_ids(rows,'Rec'))
vyka_det = fetch_details(cur, 'DOKLADD', 'ID', 'ID', ['KOD','DDGN'], get_ids(rows,'VykA'))
vyka_det = fetch_details(cur, 'DOKLADD', 'ID', 'ID', ['KOD','DDGN','BODY'], get_ids(rows,'VykA'))
files_det = fetch_details(cur, 'FILES', 'ID', 'ID', ['FILENAME','DATUM'], get_ids(rows,'Files'))
medlab_det = fetch_details(cur, 'HISTDOC', 'ID', 'ID', ['DATUM','TYP'], get_ids(rows,'MEDLAB'))
lab_det = fetch_details(cur, 'LABVH', 'IDVH', 'IDVH', ['DATUM','CISLO'], get_ids(rows,'Lab'))
@@ -89,8 +115,8 @@ BARVY_LISTU = {
'Recepty': ('1F6B33', 'E2EFDA'),
'Výkony': ('2E4057', 'D6E4F0'),
'Soubory': ('7B3F00', 'FAE5D3'),
'MedLab': ('4A235A', 'F5EEF8'),
'Lab': ('145A32', 'D5F5E3'),
'Žádanky': ('4A235A', 'F5EEF8'),
'Lab výsl.': ('145A32', 'D5F5E3'),
'Očkování': ('7E5109', 'FDEBD0'),
'Neschop.': ('922B21', 'FADBD8'),
'Léčiva': ('1A5276', 'D6EAF8'),
@@ -124,7 +150,10 @@ def hyperlink_cell(ws, row_i, col_i, cil_list, cil_radek, text, barva_hex):
fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \
else PatternFill("solid", fgColor=barva_hex)
cell = ws.cell(row=row_i, column=col_i)
cell.value = f'=HYPERLINK("#{cil_list}!A{cil_radek}","{text}")'
cell.value = str(text)
# Název listu s mezerou musí být v apostrofech
sheet_ref = f"'{cil_list}'" if ' ' in cil_list else cil_list
cell.hyperlink = f'#{sheet_ref}!A{cil_radek}'
cell.font = Font(color="0000FF", underline='single')
cell.fill = fill
cell.border = ohraniceni
@@ -133,13 +162,12 @@ def hyperlink_cell(ws, row_i, col_i, cil_list, cil_radek, text, barva_hex):
# ── Workbook ───────────────────────────────────────────────────────────────
wb = openpyxl.Workbook()
# Pořadí listů a jejich konfigurace: (název, typ_bookmarku, detail_dict, sloupce, šířky, pk_label)
LISTY = [
('Recepty', 'Rec', rec_det, ['Datum','Jméno','Recept','Dávkování'], [12,25,25,12], None),
('Výkony', 'VykA', vyka_det, ['Datum','Jméno','Kód výkonu','Diagnóza'], [12,25,14,10], None),
('Výkony', 'VykA', vyka_det, ['Datum','Jméno','Kód výkonu','Diagnóza','Body'], [12,25,14,10,8], None),
('Soubory', 'Files', files_det, ['Datum','Jméno','Soubor','Datum souboru'], [12,25,35,14], None),
('MedLab', 'MEDLAB', medlab_det, ['Datum','Jméno','Typ'], [12,25,15], None),
('Lab', 'Lab', lab_det, ['Datum','Jméno','Číslo'], [12,25,20], None),
('Žádanky', 'MEDLAB', medlab_det, ['Datum','Jméno','Typ'], [12,25,15], None),
('Lab výsl.', 'Lab', lab_det, ['Datum','Jméno','Číslo'], [12,25,20], None),
('Očkování', 'Ock', ock_det, ['Datum','Jméno','Datum očkování','Vakcína'], [12,25,14,30], None),
('Neschop.', 'Nes', nes_det, ['Datum','Jméno','Od','Do'], [12,25,12,12], None),
('Léčiva', 'Lec', lec_det, ['Datum','Jméno','Kód','Datum výkonu'], [12,25,12,14], None),
@@ -148,38 +176,49 @@ LISTY = [
('Ostatní', None, None, ['Datum','Jméno','Typ','ID','Název'], [12,25,12,10,30], None),
]
# Vytvoříme listy
ws_d = wb.active
ws_d.title = "Dekurz"
# List "Text dekurzu" hned za Dekurzem
ws_text = wb.create_sheet("Text dekurzu")
ws_listy = {}
for nazev, *_ in LISTY:
ws_listy[nazev] = wb.create_sheet(nazev)
# Záhlaví listů
for nazev, typ, det, sloupce, sirky, _ in LISTY:
barva_hl, _ = BARVY_LISTU[nazev]
zapis_hlavicku(ws_listy[nazev], sloupce, sirky, barva_hl)
ws_listy[nazev].freeze_panes = 'A2'
# Záhlaví Dekurz
nazvy_d = ['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Pojišťovna'] + TOP_TYPY + ['Ostatní']
sirky_d = [12, 8, 8, 25, 14, 12 ] + [8]*10 + [8]
# Záhlaví listu "Text dekurzu"
zapis_hlavicku(ws_text,
['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Text dekurzu'],
[12, 8, 8, 25, 14, 100],
'2E75B6')
ws_text.freeze_panes = 'A2'
row_ptr_text = 2
# Zobrazované názvy sloupců pro TOP_TYPY (stejné pořadí)
NAZVY_TYPY = ['VykA', 'Rec', 'Files', 'Žádanky', 'Lab výsl.', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
# Záhlaví Dekurz sloupce AR
nazvy_d = ['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Pojišťovna'] + ['HodVyk'] + NAZVY_TYPY + ['Ostatní']
sirky_d = [12, 8, 8, 25, 14, 12 ] + [10] + [38, 8, 8, 8, 8, 8, 8, 8, 8, 8] + [8]
zapis_hlavicku(ws_d, nazvy_d, sirky_d, '2E75B6')
ws_d.freeze_panes = 'A2'
ws_d.auto_filter.ref = f"A1:Q{len(rows)+1}"
ws_d.auto_filter.ref = f"A1:R{len(rows)+1}"
# Aktuální řádek pro každý list
row_ptr = {nazev: 2 for nazev, *_ in LISTY}
# ── Plnění dat ─────────────────────────────────────────────────────────────
def get_det_hodnoty(typ, rid, datum, jmeno_cel):
"""Vrátí seznam hodnot pro řádek detailního listu."""
if typ == 'Rec':
d = rec_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1] or '']
elif typ == 'VykA':
d = vyka_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', (d[1] or '').strip()]
d = vyka_det.get(rid, ('', '', 0))
return [datum, jmeno_cel, d[0] or '', (d[1] or '').strip(), d[2] or 0]
elif typ == 'Files':
d = files_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1]]
@@ -208,10 +247,10 @@ def get_det_hodnoty(typ, rid, datum, jmeno_cel):
ZAROVNANI = {
'Recepty': ['left','left','left','center'],
'Výkony': ['left','left','center','center'],
'Výkony': ['left','left','center','center','right'],
'Soubory': ['left','left','left','left'],
'MedLab': ['left','left','center'],
'Lab': ['left','left','center'],
'Žádanky': ['left','left','center'],
'Lab výsl.': ['left','left','center'],
'Očkování': ['left','left','left','left'],
'Neschop.': ['left','left','left','left'],
'Léčiva': ['left','left','center','left'],
@@ -220,8 +259,7 @@ ZAROVNANI = {
'Ostatní': ['left','left','center','center','left'],
}
for row_i, (datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni) in enumerate(rows, start=2):
# Ohraničení řádku Dekurz
for row_i, (datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni, text_dekurzu) in enumerate(rows, start=2):
fill_d = r_fill[row_i % 2]
for col_i in range(1, len(nazvy_d) + 1):
ws_d.cell(row=row_i, column=col_i).fill = fill_d
@@ -231,20 +269,30 @@ for row_i, (datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_ty
ws_d.cell(row=row_i, column=2, value=str(cas)[:5] if cas else '').alignment = Alignment(horizontal='center')
ws_d.cell(row=row_i, column=3, value=zkratka or '').alignment = Alignment(horizontal='center')
ws_d.cell(row=row_i, column=4, value=jmeno_cel)
ws_d.cell(row=row_i, column=5, value=rodcis or '')
# Sloupec 5 Rodné číslo jako hyperlink na list "Text dekurzu"
hyperlink_cell(ws_d, row_i, 5, 'Text dekurzu', row_ptr_text, rodcis or '', 'DCE6F1')
ws_d.cell(row=row_i, column=6, value=poj or '').alignment = Alignment(horizontal='center')
# Sloupce bookmarků
# Sloupec G HodVyk (součet bodů všech výkonů dekurzu)
hodvyk = sum((vyka_det.get(rid, ('', '', 0))[2] or 0) for rid in ids_by_typ['VykA'])
cell_hv = ws_d.cell(row=row_i, column=7, value=hodvyk if hodvyk else '')
cell_hv.alignment = Alignment(horizontal='center')
for col_off, (typ, pocet) in enumerate(zip(TOP_TYPY, top)):
col_i = 7 + col_off
col_i = 8 + col_off
if pocet == 0:
continue
# Najdi název listu pro tento typ
nazev_listu = next((n for n, t, *_ in LISTY if t == typ), None)
if nazev_listu and ids_by_typ[typ]:
_, barva_ll = BARVY_LISTU[nazev_listu]
hyperlink_cell(ws_d, row_i, col_i, nazev_listu, row_ptr[nazev_listu], pocet, barva_ll[1:] if len(barva_ll) > 6 else 'DCE6F1')
# Zapiš řádky na detailní list
# Pro výkony (VykA) přidej kódy do závorky
if typ == 'VykA':
kody = [str(vyka_det.get(rid, ('',))[0] or '').strip() for rid in ids_by_typ[typ]]
kody_str = ', '.join(k for k in kody if k)
display_text = f"{pocet} ({kody_str})" if kody_str else pocet
else:
display_text = pocet
hyperlink_cell(ws_d, row_i, col_i, nazev_listu, row_ptr[nazev_listu], display_text, barva_ll[1:] if len(barva_ll) > 6 else 'DCE6F1')
ws_det = ws_listy[nazev_listu]
barva_hl, barva_r = BARVY_LISTU[nazev_listu]
for rid in ids_by_typ[typ]:
@@ -255,17 +303,32 @@ for row_i, (datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_ty
else:
ws_d.cell(row=row_i, column=col_i, value=pocet).alignment = Alignment(horizontal='center')
# Ostatní
if ostatni:
ws_det = ws_listy['Ostatní']
barva_hl, barva_r = BARVY_LISTU['Ostatní']
hyperlink_cell(ws_d, row_i, 17, 'Ostatní', row_ptr['Ostatní'], ostatni, barva_r)
hyperlink_cell(ws_d, row_i, 18, 'Ostatní', row_ptr['Ostatní'], ostatni, barva_r)
for typ, rid, nazev in ids_ostatni:
zapis_radek(ws_det, row_ptr['Ostatní'],
[datum, jmeno_cel, typ, rid, nazev],
ZAROVNANI['Ostatní'], barva_r)
row_ptr['Ostatní'] += 1
# Zápis do listu "Text dekurzu"
fill_t = r_fill[row_ptr_text % 2]
for col_i in range(1, 7):
ws_text.cell(row=row_ptr_text, column=col_i).fill = fill_t
ws_text.cell(row=row_ptr_text, column=col_i).border = ohraniceni
c1 = ws_text.cell(row=row_ptr_text, column=1, value=datum)
c1.number_format = 'DD.MM.YYYY'
c1.alignment = Alignment(horizontal='left', vertical='center')
ws_text.cell(row=row_ptr_text, column=2, value=str(cas)[:5] if cas else '').alignment = Alignment(horizontal='center', vertical='center')
ws_text.cell(row=row_ptr_text, column=3, value=zkratka or '').alignment = Alignment(horizontal='center', vertical='center')
ws_text.cell(row=row_ptr_text, column=4, value=jmeno_cel).alignment = Alignment(horizontal='left', vertical='center')
ws_text.cell(row=row_ptr_text, column=5, value=rodcis or '').alignment = Alignment(horizontal='left', vertical='center')
cell_txt = ws_text.cell(row=row_ptr_text, column=6, value=text_dekurzu)
cell_txt.alignment = Alignment(horizontal='left', vertical='top', wrap_text=True)
row_ptr_text += 1
# Autofiltr na detailních listech
for nazev, *_ in LISTY:
ws = ws_listy[nazev]
+99
View File
@@ -215,3 +215,102 @@ UNION SELECT
UNION SELECT
first 1 cast('Pojistovna' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(P.IDICP as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from ICP P join ICZ Z on (Z.IDICZ = P.IDICZ) where Z.POJ = '207' and P.ODB = '001'
```
---
## Ošetřující lékař pacienta
### Kde je uložen
Ošetřující lékař není přímo v tabulce `KAR`. Je uložen v tabulce **`KARUZIV`** a čte se
přes stored procedure **`KARUZIV_SEL`**.
### Tabulka KARUZIV
Vazba pacient → lékař. Jeden pacient může mít více záznamů (více lékařů/pracovišť).
| Sloupec | Popis |
|------------|-------|
| `IDPAC` | FK na KAR pacient |
| `IDLEKAR` | FK na LEKARI externí lékař (specialista, cizí ordinace) |
| `IDUZI` | FK na UZIVATEL interní uživatel Medicusu (vlastní lékař) |
| `IDPRAC` | FK na PRACOVISTE pracoviště |
| `IDODD` | FK na ODDEL oddělení |
| `AUTOMAT` | `'F'` = ručně přiřazen, `'T'` = automaticky |
Pokud je vyplněn `IDLEKAR` → jde se do tabulky `LEKARI` (cizí lékaři).
Pokud je vyplněn `IDUZI` → jde se do tabulky `UZIVATEL` (lékaři v Medicusu).
### Tabulka REGISTR
Druhý zdroj registrace pacienta u lékaře/pojišťovny.
| Sloupec | Popis |
|------------------|-------|
| `IDPAC` | FK na KAR |
| `IDICP` | FK na ICP identifikace pracoviště/pojišťovny |
| `IDUZI` | FK na UZIVATEL lékař (nepovinný, dohledává se přes ICP) |
| `DATUM` | Datum začátku registrace |
| `DATUM_ZRUSENI` | Datum zrušení (NULL = stále platná) |
| `PRIZNAK` | `'V'`/`'D'`/`'A'` = aktivní; `'Z'`/`'N'` = zrušená/neaktivní |
### Stored procedure KARUZIV_SEL(IIDPAC, INCL_AUTOMAT)
Parametry:
- `IIDPAC` IDPAC pacienta
- `INCL_AUTOMAT` `'T'` = vrátit i automaticky přiřazené, `'F'` = jen ruční
Vrací sloupce: `ID, IDPAC, IDODD, ODD, IDUZI, IDPRAC, IDLEKAR, AUTOMAT, TITUL, PRIJMENI, JMENO, TITUL2, ODBORN`
**Logika (3 průchody):**
1. `KARUZIV` kde `AUTOMAT = 'F'` ručně přiřazení lékaři
2. `KARUZIV` kde `AUTOMAT = 'T'` automaticky přiřazení (jen pokud `INCL_AUTOMAT = 'T'`)
3. `REGISTR` aktivní registrace (datum platný, `PRIZNAK``'Z'`/`'N'`, nezrušená)
- přes `IDICP``ICP``PRACOVISTE``PRACUZIV``UZIVATEL`
### Použití v panelu pacienta (UNION dotaz)
```sql
-- Ošetřující lékař praktický (odbornost 001 nebo 002)
SELECT ... FROM KARUZIV_SEL(:IDPAC, 'T') WHERE ODBORN in ('001', '002')
-- → UNION část ID = 'OseLekPrak'
-- Všichni lékaři přiřazení ke kartě
SELECT ... FROM KARUZIV_SEL(:IDPAC, 'T')
-- → UNION část ID = 'OseLek'
```
### Zapojené tabulky (přehled)
```
KAR
└── KARUZIV ──► LEKARI (externí lékaři, specialisté)
└► UZIVATEL (interní lékaři v Medicusu)
└► PRACOVISTE (pracoviště / odbornost)
└► ODDEL (oddělení)
└── REGISTR ──► ICP (identifikace pracoviště)
└► PRACOVISTE ──► PRACUZIV ──► UZIVATEL
```
### Barevné rozlišení v GUI Medicusu
- **Černá** = záznam pochází z `KARUZIV` (explicitně přiřazený ošetřující lékař, `IDUZI` vyplněno)
- **Červená** = záznam pochází z `REGISTR` (registrující lékař SP vrací `ID = 0 - REGISTR.ID`)
- **Červená (ext.)** = záznam z `KARUZIV` kde je vyplněno `IDLEKAR` (externí lékař z tabulky `LEKARI`)
### Duplikát ošetřujícího lékaře known issue
`KARUZIV_SEL` prochází **vždy oba zdroje** (KARUZIV i REGISTR) bez ohledu na to, zda už byl lékař nalezen. Pokud má pacient záznam v KARUZIV (černá) i v REGISTR (červená) se stejným lékařem, zobrazí se **dvakrát**.
Příčina: SP neobsahuje podmínku „přeskoč REGISTR, pokud KARUZIV již vrátil výsledky".
**Stav ordinace Buzalková (duben 2026):**
- Všech 1620 registrovaných pacientů má v `KARUZIV` záznam IDUZI=4 (Michaela, černá)
- 1537 pacientů má v `REGISTR` IDUZI=4 (Michaela, červená) → duplikát
- Chování je konzistentní, ale GUI zobrazuje oba řádky čeká se na vyjádření supportu Medicusu
**Možná řešení (zatím neaplikováno):**
- A) Smazat KARUZIV záznamy → zůstane jen červená z REGISTR (jeden řádek)
- B) Nastavit REGISTR.IDUZI zpět na NULL → REGISTR path hledá přes PRACOVISTE (najde Michalu jako první NOSVYK='A') → duplikát stále, ale přes jiný lookup
- C) Řešení přes support Medicusu
+155
View File
@@ -0,0 +1,155 @@
"""
Najde datum poslední preventivní prohlídky (výkon 01022 nebo 01021)
pro každého registrovaného pacienta z VZPARC DRUH=98 (výkonové dávky pojišťovnám).
Výsledek zapíše do KAR.POZNAMKA ve formátu:
[[prev_prohlidka:YYYY-MM-DD 01022, YYYY-MM-DD]]
kde první datum je datum PP a druhé je nejdřívější možný termín příští (23 měsíců).
"""
import fdb
import re
import sys
from datetime import date
from dateutil.relativedelta import relativedelta
sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
KODY_PP = {'01022', '01021'}
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
# --- 1. Načti registrované pacienty (RC → IDPAC, aktuální POZNAMKA) ---
dnes = date.today().strftime('%Y-%m-%d')
cur.execute(f"""
SELECT KAR.IDPAC, KAR.RODCIS, KAR.PRIJMENI, KAR.JMENO, KAR.POZNAMKA
FROM KAR
WHERE KAR.VYRAZEN = 'N'
AND EXISTS (
SELECT r.ID FROM REGISTR r
JOIN ICP i ON r.IDICP = i.IDICP
WHERE r.IDPAC = KAR.IDPAC
AND r.DATUM <= '{dnes}'
AND (r.DATUM_ZRUSENI IS NULL OR r.DATUM_ZRUSENI >= '{dnes}')
AND r.PRIZNAK IN ('V','D','A')
AND i.ICP = '09305001'
AND i.ODB = '001'
)
""")
pacienti = {} # RODCIS -> {'idpac', 'prijmeni', 'jmeno', 'poznamka'}
for row in cur.fetchall():
idpac, rodcis, prijmeni, jmeno, poznamka = row
if rodcis:
pacienti[rodcis.strip()] = {
'idpac': idpac,
'prijmeni': prijmeni,
'jmeno': jmeno,
'poznamka': poznamka or '',
}
print(f"Registrovaných pacientů: {len(pacienti)}")
# --- 2. Projdi všechny VZPARC DRUH=98 dávky a hledej výkony 01022/01021 ---
# Výsledek: RC -> (nejnovejsi_datum, kod)
vysledky = {} # RC -> (nejnovejsi_datum, kod)
cur.execute("""
SELECT DAVKA FROM VZPARC
WHERE DRUH = '98' AND DAVKA IS NOT NULL
""")
davky = cur.fetchall()
print(f"VZPARC DRUH=98 blobů celkem: {len(davky)}")
for (kdavka_raw,) in davky:
if not kdavka_raw:
continue
# Dekódování CP852
try:
text = kdavka_raw.encode('cp1250', errors='replace').decode('cp852', errors='replace')
except Exception:
continue
if not text.startswith('DP98'):
continue
aktualni_rc = None
for line in text.splitlines():
if not line:
continue
typ = line[0]
if typ == 'A':
# RC je na pevné pozici 34, délka 10
if len(line) >= 44:
aktualni_rc = line[34:44].strip()
else:
aktualni_rc = None
elif typ == 'V' and aktualni_rc:
# V + DDMMYYYY + KOD(5)
if len(line) < 14:
continue
try:
dd = int(line[1:3])
mm = int(line[3:5])
yyyy = int(line[5:9])
kod = line[9:14].strip()
except (ValueError, IndexError):
continue
if kod not in KODY_PP:
continue
try:
datum = date(yyyy, mm, dd)
except ValueError:
continue
# Ulož jen nejnovější datum
if aktualni_rc not in vysledky or datum > vysledky[aktualni_rc][0]:
vysledky[aktualni_rc] = (datum, kod)
print(f"Pacientů s nalezenou PP (01022/01021): {len(vysledky)}")
# --- 3. Zapiš do KAR.POZNAMKA ---
TAG_RE = re.compile(r'\[\[prev_prohlidka:[^\]]*\]\]\s*#zaps[^#]*#')
TAG_FORMAT = '[[prev_prohlidka:{pp_datum} {kod}, {pristi_datum}]] #zapsáno {zapsano}#'
zapsano = 0
preskoceno = 0
nenalezeno = 0
from datetime import datetime
ted = datetime.now().strftime('%d-%m-%Y %H:%M')
for rc, (datum_pp, kod) in sorted(vysledky.items()):
if rc not in pacienti:
nenalezeno += 1
continue
pac = pacienti[rc]
pristi = datum_pp + relativedelta(months=23)
tag = TAG_FORMAT.format(
pp_datum=datum_pp.strftime('%d-%m-%Y'),
kod=kod,
pristi_datum=pristi.strftime('%d-%m-%Y'),
zapsano=ted,
)
stara = pac['poznamka']
# Odstraň starý tag a vlož nový na začátek
nova = TAG_RE.sub('', stara).lstrip('\n')
nova = tag + ('\n' + nova if nova else '')
cur.execute(
"UPDATE KAR SET POZNAMKA=? WHERE IDPAC=?",
(nova, pac['idpac'])
)
zapsano += 1
conn.commit()
print(f"Zapsáno: {zapsano}, nenalezeno v KAR: {nenalezeno}, přeskočeno: {preskoceno}")
conn.close()
+9
View File
@@ -0,0 +1,9 @@
# Medicus TO DO LIST
## Otevřené úkoly
1. **Změna Jourová → Buzalková v ošetřujících lékařích**
Zjistit od supportu Medicusu jak správně přiřadit ošetřujícího lékaře tak,
aby se v GUI zobrazoval pouze jeden řádek (černý). Aktuálně se zobrazuje
duplikát černá (z KARUZIV) + červená (z REGISTR). Viz poznámky v
`MedicusWithClaudeSelects/SELECTS.md` sekce "Duplikát ošetřujícího lékaře".