Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
import os, shutil, fdb, time, threading
|
||||
import re, datetime, funkce, funkce_ext
|
||||
|
||||
# Connect to the Firebird database
|
||||
conn = fdb.connect(
|
||||
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||
user='SYSDBA',
|
||||
password="masterkey",
|
||||
charset="win1250")
|
||||
|
||||
cesta = r"u:\testimport"
|
||||
cestazpracovana = r"u:\testimportzpracovana"
|
||||
|
||||
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
|
||||
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
|
||||
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
|
||||
|
||||
# ─── Helper funkce ────────────────────────────────────────────────────────────
|
||||
|
||||
def restore_files_for_import(retezec):
|
||||
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
|
||||
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
|
||||
if not os.path.exists(drop):
|
||||
print(f"The directory '{drop}' does not exist.")
|
||||
return
|
||||
for item in os.listdir(drop):
|
||||
item_path = os.path.join(drop, item)
|
||||
if os.path.isfile(item_path) or os.path.islink(item_path):
|
||||
os.unlink(item_path)
|
||||
print(f"Deleted file: {item_path}")
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
print(f"Deleted directory: {item_path}")
|
||||
for item in os.listdir(next):
|
||||
item_path = os.path.join(next, item)
|
||||
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
|
||||
shutil.copy(item_path, os.path.join(drop, item))
|
||||
print(f"Copied file: {item_path}")
|
||||
|
||||
|
||||
def kontrola_rc(rc, connection):
|
||||
cur = connection.cursor()
|
||||
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return row[1]
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def kontrola_struktury(souborname, connection):
|
||||
if souborname.endswith('.pdf'):
|
||||
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
|
||||
match = pattern.search(souborname)
|
||||
vpohode = True
|
||||
if match and len(match.groups()) == 5:
|
||||
datum = match.group(2)
|
||||
try:
|
||||
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
|
||||
except:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
cur = connection.cursor()
|
||||
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
|
||||
row = cur.fetchone()[0]
|
||||
if row != 1:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
else:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
else:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
return vpohode
|
||||
|
||||
|
||||
def vrat_info_o_souboru(souborname, connection):
|
||||
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
|
||||
match = pattern.search(souborname)
|
||||
rc = match.group(1)
|
||||
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
|
||||
jmeno = match.group(3)
|
||||
prvnizavorka = match.group(4)
|
||||
druhazavorka = match.group(5)
|
||||
cur = connection.cursor()
|
||||
cur.execute("select idpac from kar where rodcis=?", (rc,))
|
||||
idpac = cur.fetchone()[0]
|
||||
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
|
||||
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
|
||||
|
||||
|
||||
def prejmenuj_chybny_soubor(souborname, cesta):
|
||||
if souborname[0] != "♥":
|
||||
soubornovy = "♥" + souborname
|
||||
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
|
||||
|
||||
|
||||
def _pokus_o_zamek(dekurs_id, vysledek):
|
||||
"""Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení.
|
||||
Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}.
|
||||
Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený.
|
||||
"""
|
||||
conn_t = None
|
||||
try:
|
||||
conn_t = fdb.connect(
|
||||
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||
user='SYSDBA', password='masterkey', charset='win1250'
|
||||
)
|
||||
cur_t = conn_t.cursor()
|
||||
cur_t.execute(
|
||||
"SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK",
|
||||
(dekurs_id,)
|
||||
)
|
||||
cur_t.fetchone()
|
||||
conn_t.rollback() # Uvolni zámek – sloužil jen k ověření
|
||||
vysledek['ok'] = True
|
||||
except Exception as e:
|
||||
vysledek['chyba'] = str(e)
|
||||
finally:
|
||||
if conn_t:
|
||||
try:
|
||||
conn_t.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2):
|
||||
"""Zjistí zda existuje dnešní dekurz a ověří že není zamčený.
|
||||
|
||||
Vrátí:
|
||||
(id, rtf) – dnešní dekurz existuje a není zamčený
|
||||
None – žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba)
|
||||
|
||||
Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený).
|
||||
|
||||
Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme
|
||||
pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund
|
||||
neskončí, záznam je zamčený a přeskočíme celou skupinu.
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
|
||||
# Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT)
|
||||
cur.execute("""
|
||||
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
|
||||
WHERE IDPAC = ?
|
||||
ORDER BY ID DESC
|
||||
""", (idpac,))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
print(f" Žádný dekurz pro pacienta IDPAC={idpac}")
|
||||
return None
|
||||
|
||||
dekurs_id, dekurs_datum, dekurs_rtf = row
|
||||
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
|
||||
|
||||
if dekurs_datum != datum_vlozeni:
|
||||
print(f" → jiný den ({dekurs_datum} ≠ {datum_vlozeni}), vytvoříme nový (INSERT)")
|
||||
return None
|
||||
|
||||
# Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený
|
||||
print(f" → dnešní den ({datum_vlozeni}) ✓ – ověřuji zámek (timeout {timeout_sec}s)...")
|
||||
vysledek = {}
|
||||
t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True)
|
||||
t.start()
|
||||
t.join(timeout=timeout_sec)
|
||||
|
||||
if t.is_alive():
|
||||
# Vlákno stále čeká na zámek = záznam drží Medicus
|
||||
raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)")
|
||||
|
||||
if 'chyba' in vysledek:
|
||||
raise fdb.DatabaseError(vysledek['chyba'])
|
||||
|
||||
print(f" → záznam volný, pokračuji se zápisem")
|
||||
return (dekurs_id, dekurs_rtf)
|
||||
|
||||
|
||||
def ma_sekci_prilohy(rtf):
|
||||
return PRILOHY_HEADER in rtf
|
||||
|
||||
|
||||
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
|
||||
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
|
||||
|
||||
Postup:
|
||||
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
|
||||
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
|
||||
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
|
||||
"""
|
||||
# 1. Počet existujících Files: odkazů
|
||||
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
|
||||
if bkm_match:
|
||||
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
|
||||
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
|
||||
else:
|
||||
n_files = 0
|
||||
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
|
||||
|
||||
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
|
||||
prilohy_pos = rtf.find(PRILOHY_HEADER)
|
||||
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
|
||||
if closing_pos == -1:
|
||||
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
|
||||
|
||||
new_pards = ''
|
||||
for i, fname in enumerate(filenameforbookmark_list):
|
||||
idx = n_files + i
|
||||
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
|
||||
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
|
||||
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
|
||||
|
||||
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
|
||||
|
||||
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
|
||||
def append_bookmarks(m):
|
||||
entries = [e for e in m.group(1).split(';') if e.strip()]
|
||||
entries.extend(bookmark_list)
|
||||
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
|
||||
|
||||
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
|
||||
return rtf
|
||||
|
||||
|
||||
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
|
||||
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
|
||||
rtf = existing_rtf
|
||||
rtf = re.sub(r'\\bkmkstart (\d+)',
|
||||
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
|
||||
rtf = re.sub(r'\\bkmkend (\d+)',
|
||||
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
|
||||
|
||||
new_bkm_str = ';'.join(new_bkm_list)
|
||||
|
||||
def merge_bkm(m):
|
||||
existing = m.group(1).strip()
|
||||
combined = new_bkm_str + (';' + existing if existing else '')
|
||||
return '{\\info{\\bookmarks ' + combined + '}}'
|
||||
|
||||
if re.search(r'\{\\info\{\\bookmarks', rtf):
|
||||
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
|
||||
else:
|
||||
rtf = re.sub(r'(\\deflang\d+)',
|
||||
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
|
||||
|
||||
match = re.search(r'\\uc1\\pard', rtf)
|
||||
if match:
|
||||
pos = match.start()
|
||||
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
|
||||
return rtf
|
||||
|
||||
|
||||
# Šablona RTF pro nový dekurs
|
||||
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
|
||||
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
|
||||
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
|
||||
BOOKMARKSTEXT
|
||||
\pard\s10\plain\cs15\f0\fs20 \par
|
||||
}"""
|
||||
|
||||
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
|
||||
|
||||
info = []
|
||||
for soubor in os.listdir(cesta):
|
||||
if os.path.isfile(os.path.join(cesta, soubor)):
|
||||
print(soubor)
|
||||
if kontrola_struktury(soubor, conn):
|
||||
info.append(vrat_info_o_souboru(soubor, conn))
|
||||
else:
|
||||
prejmenuj_chybny_soubor(soubor, cesta)
|
||||
|
||||
info = sorted(info, key=lambda x: (x[0], x[1]))
|
||||
print(info)
|
||||
|
||||
skupiny = {}
|
||||
for row in info:
|
||||
skupiny[row[0]] = []
|
||||
for row in info:
|
||||
skupiny[row[0]].append(row)
|
||||
|
||||
for key in skupiny.keys():
|
||||
print(f"\n{'='*60}")
|
||||
print(f"RC: {key}, souborů: {len(skupiny[key])}")
|
||||
|
||||
idpac = skupiny[key][0][1]
|
||||
datumzapisu = datetime.datetime.now().date()
|
||||
caszapisu = datetime.datetime.now().time()
|
||||
|
||||
# ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ──────
|
||||
print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...")
|
||||
try:
|
||||
existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu)
|
||||
except RuntimeError as e:
|
||||
# Vlákno nepřišlo do timeoutu = záznam drží Medicus
|
||||
print(f"\n!!! DEKURZ ZAMČEN – soubory skupiny RC={key} přeskočeny.")
|
||||
print(" Spusťte skript znovu až bude záznam volný.")
|
||||
continue
|
||||
except fdb.DatabaseError as e:
|
||||
chyba = str(e).lower()
|
||||
if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba:
|
||||
print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) – soubory skupiny RC={key} přeskočeny.")
|
||||
print(" Spusťte skript znovu až bude záznam volný.")
|
||||
continue
|
||||
raise # jiná DB chyba – propaguj dál
|
||||
|
||||
cislo = 9
|
||||
poradi = 0
|
||||
bookmark_list = []
|
||||
filenameforbookmark_list = []
|
||||
bookmarks_body = ''
|
||||
|
||||
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
|
||||
for row in skupiny[key]:
|
||||
fileid = funkce_ext.zapis_file_ext(
|
||||
vstupconnection=conn, idpac=row[1],
|
||||
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
|
||||
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
|
||||
print(f" → FILES.ID = {fileid} ({row[6]})")
|
||||
|
||||
# Přesun souboru do zpracovaných
|
||||
for attempt in range(3):
|
||||
try:
|
||||
dest = os.path.join(cestazpracovana, row[6])
|
||||
if not os.path.exists(dest):
|
||||
shutil.move(os.path.join(cesta, row[6]), dest)
|
||||
else:
|
||||
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
||||
shutil.move(os.path.join(cesta, row[6]),
|
||||
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
|
||||
print(" Přesun OK!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" Attempt {attempt + 1} failed: {e}")
|
||||
if attempt < 2:
|
||||
print(" Retrying in 5 seconds...")
|
||||
time.sleep(5)
|
||||
else:
|
||||
print(" Max retries reached. Command failed.")
|
||||
|
||||
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
|
||||
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
|
||||
filenameforbookmark_list.append(filenameforbookmark)
|
||||
cislo += 7
|
||||
|
||||
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
|
||||
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
|
||||
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
|
||||
poradi += 1
|
||||
|
||||
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
|
||||
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
|
||||
+ bookmarks_body + '\n'
|
||||
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
|
||||
|
||||
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
|
||||
cur = conn.cursor()
|
||||
|
||||
if existujici:
|
||||
dekurs_id, existing_rtf = existujici
|
||||
|
||||
if ma_sekci_prilohy(existing_rtf):
|
||||
# Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř
|
||||
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
|
||||
print(">>> Přidávám soubory DO existující sekce...")
|
||||
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
|
||||
else:
|
||||
# Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend
|
||||
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
|
||||
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
|
||||
|
||||
print("\n=== Výsledný RTF ===")
|
||||
print(merged_rtf)
|
||||
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
|
||||
conn.commit()
|
||||
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} – hotovo!")
|
||||
|
||||
else:
|
||||
# Případ 3: žádný dnešní dekurz → vytvoříme nový
|
||||
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
|
||||
bookmark_str = ';'.join(bookmark_list)
|
||||
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
|
||||
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
|
||||
|
||||
print("\n=== Výsledný RTF ===")
|
||||
print(rtf)
|
||||
|
||||
dekursid = funkce.get_dekurs_id(conn)
|
||||
cur.execute(
|
||||
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
|
||||
)
|
||||
conn.commit()
|
||||
print(f"\n>>> Nový DEKURS ID={dekursid}")
|
||||
|
||||
print("\n=== HOTOVO ===")
|
||||
conn.close()
|
||||
@@ -0,0 +1,397 @@
|
||||
import os, shutil, fdb, time, threading
|
||||
import re, datetime, funkce, funkce_ext
|
||||
|
||||
# Connect to the Firebird database
|
||||
conn = fdb.connect(
|
||||
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||
user='SYSDBA',
|
||||
password="masterkey",
|
||||
charset="win1250")
|
||||
|
||||
cesta = r"u:\testimport"
|
||||
cestazpracovana = r"u:\testimportzpracovana"
|
||||
|
||||
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
|
||||
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
|
||||
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
|
||||
|
||||
# ─── Helper funkce ────────────────────────────────────────────────────────────
|
||||
|
||||
def restore_files_for_import(retezec):
|
||||
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
|
||||
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
|
||||
if not os.path.exists(drop):
|
||||
print(f"The directory '{drop}' does not exist.")
|
||||
return
|
||||
for item in os.listdir(drop):
|
||||
item_path = os.path.join(drop, item)
|
||||
if os.path.isfile(item_path) or os.path.islink(item_path):
|
||||
os.unlink(item_path)
|
||||
print(f"Deleted file: {item_path}")
|
||||
elif os.path.isdir(item_path):
|
||||
shutil.rmtree(item_path)
|
||||
print(f"Deleted directory: {item_path}")
|
||||
for item in os.listdir(next):
|
||||
item_path = os.path.join(next, item)
|
||||
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
|
||||
shutil.copy(item_path, os.path.join(drop, item))
|
||||
print(f"Copied file: {item_path}")
|
||||
|
||||
|
||||
def kontrola_rc(rc, connection):
|
||||
cur = connection.cursor()
|
||||
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return row[1]
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def kontrola_struktury(souborname, connection):
|
||||
if souborname.endswith('.pdf'):
|
||||
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
|
||||
match = pattern.search(souborname)
|
||||
vpohode = True
|
||||
if match and len(match.groups()) == 5:
|
||||
datum = match.group(2)
|
||||
try:
|
||||
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
|
||||
except:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
cur = connection.cursor()
|
||||
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
|
||||
row = cur.fetchone()[0]
|
||||
if row != 1:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
else:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
else:
|
||||
vpohode = False
|
||||
return vpohode
|
||||
return vpohode
|
||||
|
||||
|
||||
def vrat_info_o_souboru(souborname, connection):
|
||||
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
|
||||
match = pattern.search(souborname)
|
||||
rc = match.group(1)
|
||||
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
|
||||
jmeno = match.group(3)
|
||||
prvnizavorka = match.group(4)
|
||||
druhazavorka = match.group(5)
|
||||
cur = connection.cursor()
|
||||
cur.execute("select idpac from kar where rodcis=?", (rc,))
|
||||
idpac = cur.fetchone()[0]
|
||||
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
|
||||
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
|
||||
|
||||
|
||||
def prejmenuj_chybny_soubor(souborname, cesta):
|
||||
if souborname[0] != "♥":
|
||||
soubornovy = "♥" + souborname
|
||||
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
|
||||
|
||||
|
||||
def _pokus_o_zamek(dekurs_id, vysledek):
|
||||
"""Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení.
|
||||
Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}.
|
||||
Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený.
|
||||
"""
|
||||
conn_t = None
|
||||
try:
|
||||
conn_t = fdb.connect(
|
||||
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||
user='SYSDBA', password='masterkey', charset='win1250'
|
||||
)
|
||||
cur_t = conn_t.cursor()
|
||||
cur_t.execute(
|
||||
"SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK",
|
||||
(dekurs_id,)
|
||||
)
|
||||
cur_t.fetchone()
|
||||
conn_t.rollback() # Uvolni zámek – sloužil jen k ověření
|
||||
vysledek['ok'] = True
|
||||
except Exception as e:
|
||||
vysledek['chyba'] = str(e)
|
||||
finally:
|
||||
if conn_t:
|
||||
try:
|
||||
conn_t.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2):
|
||||
"""Zjistí zda existuje dnešní dekurz a ověří že není zamčený.
|
||||
|
||||
Vrátí:
|
||||
(id, rtf) – dnešní dekurz existuje a není zamčený
|
||||
None – žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba)
|
||||
|
||||
Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený).
|
||||
|
||||
Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme
|
||||
pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund
|
||||
neskončí, záznam je zamčený a přeskočíme celou skupinu.
|
||||
"""
|
||||
cur = conn.cursor()
|
||||
|
||||
# Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT)
|
||||
cur.execute("""
|
||||
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
|
||||
WHERE IDPAC = ?
|
||||
ORDER BY ID DESC
|
||||
""", (idpac,))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
print(f" Žádný dekurz pro pacienta IDPAC={idpac}")
|
||||
return None
|
||||
|
||||
dekurs_id, dekurs_datum, dekurs_rtf = row
|
||||
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
|
||||
|
||||
if dekurs_datum != datum_vlozeni:
|
||||
print(f" → jiný den ({dekurs_datum} ≠ {datum_vlozeni}), vytvoříme nový (INSERT)")
|
||||
return None
|
||||
|
||||
# Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený
|
||||
print(f" → dnešní den ({datum_vlozeni}) ✓ – ověřuji zámek (timeout {timeout_sec}s)...")
|
||||
vysledek = {}
|
||||
t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True)
|
||||
t.start()
|
||||
t.join(timeout=timeout_sec)
|
||||
|
||||
if t.is_alive():
|
||||
# Vlákno stále čeká na zámek = záznam drží Medicus
|
||||
raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)")
|
||||
|
||||
if 'chyba' in vysledek:
|
||||
raise fdb.DatabaseError(vysledek['chyba'])
|
||||
|
||||
print(f" → záznam volný, pokračuji se zápisem")
|
||||
return (dekurs_id, dekurs_rtf)
|
||||
|
||||
|
||||
def ma_sekci_prilohy(rtf):
|
||||
return PRILOHY_HEADER in rtf
|
||||
|
||||
|
||||
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
|
||||
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
|
||||
|
||||
Postup:
|
||||
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
|
||||
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
|
||||
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
|
||||
"""
|
||||
# 1. Počet existujících Files: odkazů
|
||||
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
|
||||
if bkm_match:
|
||||
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
|
||||
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
|
||||
else:
|
||||
n_files = 0
|
||||
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
|
||||
|
||||
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
|
||||
prilohy_pos = rtf.find(PRILOHY_HEADER)
|
||||
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
|
||||
if closing_pos == -1:
|
||||
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
|
||||
|
||||
new_pards = ''
|
||||
for i, fname in enumerate(filenameforbookmark_list):
|
||||
idx = n_files + i
|
||||
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
|
||||
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
|
||||
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
|
||||
|
||||
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
|
||||
|
||||
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
|
||||
def append_bookmarks(m):
|
||||
entries = [e for e in m.group(1).split(';') if e.strip()]
|
||||
entries.extend(bookmark_list)
|
||||
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
|
||||
|
||||
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
|
||||
return rtf
|
||||
|
||||
|
||||
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
|
||||
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
|
||||
rtf = existing_rtf
|
||||
rtf = re.sub(r'\\bkmkstart (\d+)',
|
||||
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
|
||||
rtf = re.sub(r'\\bkmkend (\d+)',
|
||||
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
|
||||
|
||||
new_bkm_str = ';'.join(new_bkm_list)
|
||||
|
||||
def merge_bkm(m):
|
||||
existing = m.group(1).strip()
|
||||
combined = new_bkm_str + (';' + existing if existing else '')
|
||||
return '{\\info{\\bookmarks ' + combined + '}}'
|
||||
|
||||
if re.search(r'\{\\info\{\\bookmarks', rtf):
|
||||
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
|
||||
else:
|
||||
rtf = re.sub(r'(\\deflang\d+)',
|
||||
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
|
||||
|
||||
match = re.search(r'\\uc1\\pard', rtf)
|
||||
if match:
|
||||
pos = match.start()
|
||||
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
|
||||
return rtf
|
||||
|
||||
|
||||
# Šablona RTF pro nový dekurs
|
||||
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
|
||||
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
|
||||
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
|
||||
BOOKMARKSTEXT
|
||||
\pard\s10\plain\cs15\f0\fs20 \par
|
||||
}"""
|
||||
|
||||
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
|
||||
|
||||
info = []
|
||||
for soubor in os.listdir(cesta):
|
||||
if os.path.isfile(os.path.join(cesta, soubor)):
|
||||
print(soubor)
|
||||
if kontrola_struktury(soubor, conn):
|
||||
info.append(vrat_info_o_souboru(soubor, conn))
|
||||
else:
|
||||
prejmenuj_chybny_soubor(soubor, cesta)
|
||||
|
||||
info = sorted(info, key=lambda x: (x[0], x[1]))
|
||||
print(info)
|
||||
|
||||
skupiny = {}
|
||||
for row in info:
|
||||
skupiny[row[0]] = []
|
||||
for row in info:
|
||||
skupiny[row[0]].append(row)
|
||||
|
||||
for key in skupiny.keys():
|
||||
print(f"\n{'='*60}")
|
||||
print(f"RC: {key}, souborů: {len(skupiny[key])}")
|
||||
|
||||
idpac = skupiny[key][0][1]
|
||||
datumzapisu = datetime.datetime.now().date()
|
||||
caszapisu = datetime.datetime.now().time()
|
||||
|
||||
# ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ──────
|
||||
print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...")
|
||||
try:
|
||||
existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu)
|
||||
except RuntimeError as e:
|
||||
# Vlákno nepřišlo do timeoutu = záznam drží Medicus
|
||||
print(f"\n!!! DEKURZ ZAMČEN – soubory skupiny RC={key} přeskočeny.")
|
||||
print(" Spusťte skript znovu až bude záznam volný.")
|
||||
continue
|
||||
except fdb.DatabaseError as e:
|
||||
chyba = str(e).lower()
|
||||
if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba:
|
||||
print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) – soubory skupiny RC={key} přeskočeny.")
|
||||
print(" Spusťte skript znovu až bude záznam volný.")
|
||||
continue
|
||||
raise # jiná DB chyba – propaguj dál
|
||||
|
||||
cislo = 9
|
||||
poradi = 0
|
||||
bookmark_list = []
|
||||
filenameforbookmark_list = []
|
||||
bookmarks_body = ''
|
||||
|
||||
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
|
||||
for row in skupiny[key]:
|
||||
fileid = funkce_ext.zapis_file_ext(
|
||||
vstupconnection=conn, idpac=row[1],
|
||||
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
|
||||
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
|
||||
print(f" → FILES.ID = {fileid} ({row[6]})")
|
||||
|
||||
# Přesun souboru do zpracovaných
|
||||
for attempt in range(3):
|
||||
try:
|
||||
dest = os.path.join(cestazpracovana, row[6])
|
||||
if not os.path.exists(dest):
|
||||
shutil.move(os.path.join(cesta, row[6]), dest)
|
||||
else:
|
||||
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
|
||||
shutil.move(os.path.join(cesta, row[6]),
|
||||
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
|
||||
print(" Přesun OK!")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" Attempt {attempt + 1} failed: {e}")
|
||||
if attempt < 2:
|
||||
print(" Retrying in 5 seconds...")
|
||||
time.sleep(5)
|
||||
else:
|
||||
print(" Max retries reached. Command failed.")
|
||||
|
||||
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
|
||||
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
|
||||
filenameforbookmark_list.append(filenameforbookmark)
|
||||
cislo += 7
|
||||
|
||||
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
|
||||
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
|
||||
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
|
||||
poradi += 1
|
||||
|
||||
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
|
||||
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
|
||||
+ bookmarks_body + '\n'
|
||||
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
|
||||
|
||||
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
|
||||
cur = conn.cursor()
|
||||
|
||||
if existujici:
|
||||
dekurs_id, existing_rtf = existujici
|
||||
|
||||
if ma_sekci_prilohy(existing_rtf):
|
||||
# Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř
|
||||
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
|
||||
print(">>> Přidávám soubory DO existující sekce...")
|
||||
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
|
||||
else:
|
||||
# Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend
|
||||
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
|
||||
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
|
||||
|
||||
print("\n=== Výsledný RTF ===")
|
||||
print(merged_rtf)
|
||||
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
|
||||
conn.commit()
|
||||
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} – hotovo!")
|
||||
|
||||
else:
|
||||
# Případ 3: žádný dnešní dekurz → vytvoříme nový
|
||||
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
|
||||
bookmark_str = ';'.join(bookmark_list)
|
||||
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
|
||||
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
|
||||
|
||||
print("\n=== Výsledný RTF ===")
|
||||
print(rtf)
|
||||
|
||||
dekursid = funkce.get_dekurs_id(conn)
|
||||
cur.execute(
|
||||
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
|
||||
)
|
||||
conn.commit()
|
||||
print(f"\n>>> Nový DEKURS ID={dekursid}")
|
||||
|
||||
print("\n=== HOTOVO ===")
|
||||
conn.close()
|
||||
@@ -0,0 +1,226 @@
|
||||
# s03soubory_01_FINAL.py – dokumentace
|
||||
|
||||
**Finální verze importního skriptu pro vkládání PDF dokumentů do dekurzů Medicusu.**
|
||||
|
||||
Datum finalizace: 2026-04-04
|
||||
Autor: Vladimír Buzalka + Claude (Anthropic)
|
||||
|
||||
---
|
||||
|
||||
## Co skript dělá
|
||||
|
||||
Zpracuje PDF soubory v určené složce (`cesta`) a pro každý soubor:
|
||||
1. Ověří správnost názvu souboru (formát, RC, datum)
|
||||
2. Zkontroluje, zda cílový dekurz není zamčený v Medicusu
|
||||
3. Zapíše soubor do externí databáze souborů (tabulka FILES)
|
||||
4. Přesune soubor do složky zpracovaných
|
||||
5. Vloží odkaz (bookmark) do dekurzu pacienta jako RTF záznam
|
||||
|
||||
---
|
||||
|
||||
## Adresáře
|
||||
|
||||
| Proměnná | Cesta (testovací) | Popis |
|
||||
|---|---|---|
|
||||
| `cesta` | `u:\testimport` | Vstupní složka – sem patří soubory ke zpracování |
|
||||
| `cestazpracovana` | `u:\testimportzpracovana` | Cílová složka – sem se přesouvají zpracované soubory |
|
||||
|
||||
> V produkci tyto cesty nahradit skutečnými složkami (Nextcloud/Dropbox).
|
||||
|
||||
---
|
||||
|
||||
## Formát názvu souboru
|
||||
|
||||
Každý PDF soubor musí mít název ve tvaru:
|
||||
|
||||
```
|
||||
RC YYYY-MM-DD Příjmení, Jméno [typ dokumentu] [poznámka].pdf
|
||||
```
|
||||
|
||||
**Příklad:**
|
||||
```
|
||||
7309208104 2020-10-16 Buzalka, Vladimír [LZ ortopedie] [VAS LS páteře, obstřik].pdf
|
||||
```
|
||||
|
||||
| Část | Popis |
|
||||
|---|---|
|
||||
| `RC` | Rodné číslo pacienta (9 nebo 10 číslic) – musí existovat v tabulce KAR |
|
||||
| `YYYY-MM-DD` | Datum dokumentu |
|
||||
| `Příjmení, Jméno` | Jméno pacienta (jen pro čitelnost, nepoužívá se k vyhledání) |
|
||||
| `[typ dokumentu]` | První závorka – druh nálezu (LZ ortopedie, EKG, Lab. nález…) |
|
||||
| `[poznámka]` | Druhá závorka – krátký popis obsahu (může být prázdná `[]`) |
|
||||
|
||||
**Chybný soubor** (špatný název, RC nenalezeno v DB) je přejmenován přidáním prefixu `♥`:
|
||||
```
|
||||
♥chybny soubor.pdf
|
||||
```
|
||||
Skript ho přeskočí a nechá v složce pro ruční opravu.
|
||||
|
||||
---
|
||||
|
||||
## Databázové tabulky
|
||||
|
||||
| Tabulka | DB | Popis |
|
||||
|---|---|---|
|
||||
| `KAR` | Medicus (`medicus.fdb`) | Kartotéka pacientů – lookup RC → IDPAC |
|
||||
| `DEKURS` | Medicus (`medicus.fdb`) | Dekurzy – čtení a zápis RTF záznamu |
|
||||
| `FILES` | Externí DB (`MEDICUS_FILES_YYYYMM.fdb`) | Binární uložení PDF souborů |
|
||||
|
||||
---
|
||||
|
||||
## Klíčová novinka oproti s03soubory.py – ochrana před zamčeným dekurzem
|
||||
|
||||
### Problém
|
||||
Medicus drží **exkluzivní zámek** (Firebird row lock) na záznamu tabulky DEKURS po celou dobu, kdy má lékařka pacienta otevřeného. Kdyby skript provedl `UPDATE DEKURS` do zamčeného záznamu, přepsal by lékařčiny neuložené změny.
|
||||
|
||||
### Řešení – detekce zámku pomocí vlákna s timeoutem
|
||||
|
||||
Firebird neumí NOWAIT nastavit per-statement v SQL (syntaxe `FOR UPDATE WITH LOCK NOWAIT` není platná). Nastavení NOWAIT je vlastnost transakce, nikoliv dotazu. Knihovna `fdb` navíc toto nastavení spolehlivě nepodporuje.
|
||||
|
||||
**Zvolené řešení:** spuštění pokusu o zámek ve vedlejším vlákně s timeoutem 2 sekundy.
|
||||
|
||||
```
|
||||
hlavní vlákno vedlejší vlákno (_pokus_o_zamek)
|
||||
───────────────── ─────────────────────────────────
|
||||
t.start() ──────► fdb.connect() [nové spojení]
|
||||
t.join(timeout=2s) SELECT ... FOR UPDATE WITH LOCK
|
||||
├── záznam volný → fetchone() → rollback() → konec
|
||||
└── záznam zamčený → čeká (blokuje)...
|
||||
─────────────────
|
||||
po 2 sekundách:
|
||||
t.is_alive()?
|
||||
ANO → záznam zamčený → RuntimeError → přeskoč skupinu
|
||||
NE → záznam volný → pokračuj se zápisem
|
||||
```
|
||||
|
||||
### Důležitý detail – pořadí operací
|
||||
|
||||
**Kontrola zámku probíhá PŘED zápisem do FILES a PŘED přesunem souboru.**
|
||||
Kdyby se pořadí obrátilo, mohlo by dojít k situaci:
|
||||
- soubor zapsán do FILES ✓
|
||||
- soubor přesunut do zpracovaných ✓
|
||||
- dekurz zamčen → UPDATE selže
|
||||
- soubor je pryč ze vstupní složky, ale odkaz v dekurzu chybí
|
||||
|
||||
Správné pořadí:
|
||||
```
|
||||
1. Zkontroluj zámek dekurzu (NOWAIT)
|
||||
└── zamčeno → přeskoč (soubory zůstanou v cesta)
|
||||
2. Zapiš soubory do ext. DB (FILES)
|
||||
3. Přesuň soubory do zpracovaných
|
||||
4. Sestav RTF
|
||||
5. UPDATE nebo INSERT do DEKURS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logika vkládání do dekurzu – 3 případy
|
||||
|
||||
Po úspěšné kontrole zámku skript rozhodne, co s dekurzem udělat:
|
||||
|
||||
```
|
||||
Existuje dnešní dekurz pro pacienta?
|
||||
│
|
||||
├── ANO → obsahuje sekci "Vložené přílohy"?
|
||||
│ ├── ANO → Případ 1: přidá nové soubory DOVNITŘ sekce
|
||||
│ └── NE → Případ 2: vloží celou sekci na ZAČÁTEK dekurzu (prepend)
|
||||
│
|
||||
└── NE → Případ 3: vytvoří NOVÝ dekurz ze šablony RTF_TEMPLATE
|
||||
```
|
||||
|
||||
### Případ 1 – `pridat_do_sekce_prilohy()`
|
||||
|
||||
Dnešní dekurz **existuje a má** sekci „Vložené přílohy".
|
||||
|
||||
- Spočítá kolik odkazů (Files:) už sekce obsahuje → nové indexy bookmarků navazují
|
||||
- Nové `\pard` řádky vloží **před** uzavírací prázdný řádek sekce (`PRILOHY_CLOSING`)
|
||||
- Nové bookmarky přidá na **konec** `{\info{\bookmarks ...}}`
|
||||
- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?`
|
||||
|
||||
### Případ 2 – `merge_rtf_prepend()`
|
||||
|
||||
Dnešní dekurz **existuje, ale nemá** sekci příloh (lékařka do něj napsala text).
|
||||
|
||||
- Přečísluje existující bookmarky (posunutí o počet nových souborů)
|
||||
- Novou sekci „Vložené přílohy" vloží **na začátek** těla RTF (před `\uc1\pard`)
|
||||
- Nové bookmarky předřadí před existující v `{\info{\bookmarks ...}}`
|
||||
- Lékařčin text zůstane zachován, jen se posune níž
|
||||
- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?`
|
||||
|
||||
### Případ 3 – nový INSERT
|
||||
|
||||
Pro dnešní datum **neexistuje žádný dekurz**.
|
||||
|
||||
- Vyplní `RTF_TEMPLATE` (bookmarky + tělo sekce příloh)
|
||||
- Provede `INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)`
|
||||
- `iduzi=6` (Vladimír Buzalka), `idprac=2`, `idodd=2`
|
||||
|
||||
---
|
||||
|
||||
## RTF formát dekurzu
|
||||
|
||||
### Struktura bookmarku
|
||||
Každý přiložený soubor je reprezentován jako:
|
||||
1. **Bookmark entry** v `{\info{\bookmarks ...}}`:
|
||||
```
|
||||
"2020-10-16 LZ ortopedie: VAS LS páteře, obstřik","Files:21923",9
|
||||
```
|
||||
- `"popis"` – zobrazený text odkazu
|
||||
- `"Files:ID"` – odkaz na záznam v tabulce FILES (slouží Medicusu k načtení souboru)
|
||||
- `9` – číslo fontu/stylu (od 9, každý další +7)
|
||||
|
||||
2. **Vizuální řádek** v těle RTF:
|
||||
```rtf
|
||||
\pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2020-10-16 LZ ortopedie: VAS LS páteře, obstřik{\*\bkmkend 0}\par
|
||||
```
|
||||
- `\bkmkstart N` / `\bkmkend N` – index bookmarku (0, 1, 2…)
|
||||
- `\cs32\ul\cf1` – styl „Odkaz" (modrý podtržený text)
|
||||
|
||||
### Konstanty pro detekci sekce příloh (win1250 RTF escape)
|
||||
```python
|
||||
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:" # "Vložené přílohy:"
|
||||
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par' # uzavírací prázdný řádek
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Funkce – přehled
|
||||
|
||||
| Funkce | Popis |
|
||||
|---|---|
|
||||
| `restore_files_for_import(retezec)` | Debug utilita – vrátí soubory z Nextcloudu zpět do Dropboxu. Nepoužívá se v produkci. |
|
||||
| `kontrola_rc(rc, connection)` | Ověří zda RC existuje v KAR, vrátí IDPAC nebo False. |
|
||||
| `kontrola_struktury(souborname, connection)` | Ověří formát názvu souboru a existenci RC v DB. |
|
||||
| `vrat_info_o_souboru(souborname, connection)` | Parsuje název souboru, dohledá IDPAC, vrátí tuple s metadaty. |
|
||||
| `prejmenuj_chybny_soubor(souborname, cesta)` | Přidá prefix `♥` k chybnému souboru. |
|
||||
| `_pokus_o_zamek(dekurs_id, vysledek)` | Interní – běží ve vlákně, zkouší zamknout dekurz. |
|
||||
| `zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2)` | Zjistí zda dnešní dekurz existuje a není zamčený. Vyhodí RuntimeError pokud je zamčený. |
|
||||
| `ma_sekci_prilohy(rtf)` | Vrátí True pokud RTF obsahuje sekci „Vložené přílohy". |
|
||||
| `pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list)` | Případ 1 – přidá soubory do existující sekce příloh. |
|
||||
| `merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new)` | Případ 2 – vloží sekci příloh na začátek existujícího dekurzu. |
|
||||
|
||||
---
|
||||
|
||||
## Ošetření chyb
|
||||
|
||||
| Situace | Chování |
|
||||
|---|---|
|
||||
| Soubor má chybný název | Přejmenován na `♥soubor.pdf`, přeskočen |
|
||||
| RC nenalezeno v KAR | Přejmenován na `♥soubor.pdf`, přeskočen |
|
||||
| Dekurz zamčený (timeout vlákna) | Skupina přeskočena, soubory zůstanou v `cesta` |
|
||||
| DB konflikt při zamykání (-913 deadlock) | Skupina přeskočena, soubory zůstanou v `cesta` |
|
||||
| Přesun souboru selže | 3 pokusy s 5s pauzou, poté varování |
|
||||
| Jiná DB chyba | Výjimka se propaguje, skript havaruje |
|
||||
|
||||
---
|
||||
|
||||
## Vývoj a testování
|
||||
|
||||
| Verze | Soubor | Co přibilo |
|
||||
|---|---|---|
|
||||
| Prototyp | `test_import_FINAL.py` | Ruční zadání IDPAC a DATUM, ověření RTF logiky |
|
||||
| v1 | `s03soubory.py` | Automatický parsing RC z názvu, dávkování po skupinách |
|
||||
| **v1 FINAL** | `s03soubory_01_FINAL.py` | Ochrana před zamčeným dekurzem (threading + timeout) |
|
||||
|
||||
### Jak byl objeven problém se zámky
|
||||
Experimentem bylo ověřeno, že Medicus drží Firebird row lock na záznamu DEKURS po celou dobu, kdy má lékařka pacienta otevřeného (`SELECT FIRST 1 ... FOR UPDATE WITH LOCK` z Pythonu čekalo dokud lékařka neuložila). NOWAIT nelze nastavit přes SQL syntaxi ani spolehlivě přes fdb TPB bajty, proto bylo zvoleno řešení přes vlákno s timeoutem.
|
||||
@@ -0,0 +1,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 A–R (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ů")
|
||||
@@ -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'))
|
||||
@@ -86,17 +112,17 @@ 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'),
|
||||
'MedLab': ('4A235A', 'F5EEF8'),
|
||||
'Lab': ('145A32', 'D5F5E3'),
|
||||
'Očkování': ('7E5109', 'FDEBD0'),
|
||||
'Neschop.': ('922B21', 'FADBD8'),
|
||||
'Léčiva': ('1A5276', 'D6EAF8'),
|
||||
'SpecVys': ('0B5345', 'D1F2EB'),
|
||||
'Platby': ('4D5656', 'EAECEE'),
|
||||
'Ostatní': ('2C3E50', 'EBF5FB'),
|
||||
'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):
|
||||
@@ -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,53 +162,63 @@ 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),
|
||||
('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),
|
||||
('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),
|
||||
('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),
|
||||
]
|
||||
|
||||
# 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 A–R
|
||||
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]]
|
||||
@@ -207,21 +246,20 @@ def get_det_hodnoty(typ, rid, datum, jmeno_cel):
|
||||
return []
|
||||
|
||||
ZAROVNANI = {
|
||||
'Recepty': ['left','left','left','center'],
|
||||
'Výkony': ['left','left','center','center'],
|
||||
'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'],
|
||||
'Recepty': ['left','left','left','center'],
|
||||
'Výkony': ['left','left','center','center','right'],
|
||||
'Soubory': ['left','left','left','left'],
|
||||
'Žá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'],
|
||||
'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) 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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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".
|
||||
Reference in New Issue
Block a user