notebookvb

This commit is contained in:
Vladimir Buzalka
2026-04-29 06:51:47 +02:00
parent a1b9c93506
commit a9c143ba24
141 changed files with 30711 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# Claude Code
.claude/
# Python
.venv/
__pycache__/
*.pyc
# PyCharm
.idea/
*.iml
# OS
.DS_Store
Thumbs.db
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import fitz # PyMuPDF
import os
from pathlib import Path
# ===============================
# 📁 FOLDER SETTINGS
# ===============================
input_folder = Path(r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\A4 expand")
output_suffix = "_A4scaled.pdf"
# A4 portrait size in points
A4_WIDTH, A4_HEIGHT = 595, 842
# ===============================
# ⚙️ FUNCTION
# ===============================
def normalize_to_A4_portrait(pdf_path: Path):
output_path = pdf_path.with_name(pdf_path.stem + output_suffix)
# skip if already exists
if output_path.exists():
try:
output_path.unlink()
print(f"🧹 Deleted old: {output_path.name}")
except PermissionError:
print(f"⚠️ Skipped {output_path.name} (open elsewhere)")
return
src = fitz.open(pdf_path)
dst = fitz.open()
changed = False
for i, page in enumerate(src, 1):
rect = page.rect
w, h = rect.width, rect.height
# check if already close to A4 size (within 2%)
if abs(w - A4_WIDTH) < 12 and abs(h - A4_HEIGHT) < 17:
# copy directly without change
dst.insert_pdf(src, from_page=i - 1, to_page=i - 1)
print(f"{pdf_path.name} page {i}: already A4, copied as-is.")
continue
changed = True
scale = min(A4_WIDTH / w, A4_HEIGHT / h)
new_w, new_h = w * scale, h * scale
# horizontal center, vertical TOP
x_off = (A4_WIDTH - new_w) / 2
y_off = 0 # 👈 place at top edge
new_page = dst.new_page(width=A4_WIDTH, height=A4_HEIGHT)
new_page.show_pdf_page(
fitz.Rect(x_off, y_off, x_off + new_w, y_off + new_h),
src,
page.number
)
print(f"📄 {pdf_path.name} page {i}: {w:.0f}×{h:.0f}{new_w:.0f}×{new_h:.0f} (top-aligned)")
dst.save(output_path)
src.close()
dst.close()
if changed:
print(f"✅ Saved normalized A4 PDF: {output_path.name}\n")
else:
print(f"➡️ No change needed: {pdf_path.name}\n")
# ===============================
# 🚀 MAIN LOOP
# ===============================
for file in input_folder.glob("*.pdf"):
normalize_to_A4_portrait(file)
print("\n🎉 All PDFs processed — scaled to A4 portrait, top-aligned.")
+11
View File
@@ -0,0 +1,11 @@
import fitz # PyMuPDF
pdf = r"u:\Dropbox\Ordinace\Dokumentace_ke_zpracování\A4 expand\0602054486 2025-07-02 [LZ ORL] [zalehnutí levého ucha, cerumen, vyčištění, OK].pdf"
doc = fitz.open(pdf)
for i, page in enumerate(doc, 1):
print(f"Page {i}")
print(" MediaBox:", page.mediabox)
print(" CropBox :", page.cropbox)
print(" Rect :", page.rect)
print()
+86
View File
@@ -0,0 +1,86 @@
import subprocess
from pathlib import Path
from datetime import datetime
import traceback
import sys
from EmailMessagingGraph import send_mail
# =========================
# CONFIG TEST PC
# =========================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
DB = r"localhost/3050:Z:\Medicus 3\data\MEDICUS.FDB"
BACKUP_DIR = Path(r"D:\medicusbackup")
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
MAIL_TO = "vladimir.buzalka@buzalka.cz"
# =========================
# MAIN
# =========================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
cmd = [
GBAK,
"-b",
"-user", FB_USER,
"-pas", FB_PASS,
DB,
str(fbk),
"-v",
]
try:
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd,
stdout=f,
stderr=subprocess.STDOUT,
check=True,
)
send_mail(
MAIL_TO,
"✅ MEDICUS záloha OK (test PC)",
f"""Záloha MEDICUS proběhla úspěšně.
Čas: {now}
Soubor: {fbk}
Log: {log}
""",
)
except Exception:
err = traceback.format_exc()
send_mail(
MAIL_TO,
"❌ MEDICUS CHYBA ZÁLOHY (test PC)",
f"""Při záloze MEDICUS došlo k chybě.
Čas: {now}
Chyba:
{err}
Log:
{log}
""",
)
raise
if __name__ == "__main__":
main()
@@ -0,0 +1,23 @@
from pathlib import Path
import zipfile
# =========================
# CONFIG
# =========================
SRC = Path(r"D:\medicusbackup\MEDICUS_2026-01-24_17-07-44.fbk")
DST = SRC.with_suffix(".zip")
# =========================
# ZIP MAX COMPRESSION
# =========================
with zipfile.ZipFile(
DST,
mode="w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9
) as z:
z.write(SRC, arcname=SRC.name)
print(f"ZIP created: {DST}")
@@ -0,0 +1,39 @@
from pathlib import Path
import zipfile
SRC = Path(r"D:\medicusbackup\MEDICUS_2026-01-24_17-07-44.fbk")
DST = SRC.with_suffix(".zip")
total = SRC.stat().st_size
processed = 0
chunk = 8 * 1024 * 1024 # 8 MB
with zipfile.ZipFile(
DST,
"w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9,
) as zf:
zi = zipfile.ZipInfo(SRC.name)
zi.compress_type = zipfile.ZIP_DEFLATED
# ⬇⬇⬇ DŮLEŽITÉ ⬇⬇⬇
with zf.open(zi, "w", force_zip64=True) as z:
with open(SRC, "rb") as f:
while True:
buf = f.read(chunk)
if not buf:
break
z.write(buf)
processed += len(buf)
pct = processed * 100 / total
print(
f"\rZIP: {pct:6.2f}% ({processed//1024//1024} MB)",
end="",
flush=True,
)
print("\nHotovo.")
+141
View File
@@ -0,0 +1,141 @@
import subprocess
import time
from pathlib import Path
import zipfile
import zstandard as zstd
# ============================================================
# CONFIG
# ============================================================
SRC = Path(r"D:\medicusbackup\MEDICUS_2026-01-24_17-07-44.fbk")
BASE = SRC.with_suffix("")
CHUNK = 8 * 1024 * 1024 # 8 MB
SEVENZIP_EXE = r"c:\Program Files (x86)\7-Zip\7z.exe"
ORIG_SIZE = SRC.stat().st_size
# ============================================================
# ZIP (ZIP64 + progress)
# ============================================================
def zip_test():
dst = BASE.with_suffix(".zip")
total = ORIG_SIZE
processed = 0
start = time.time()
with zipfile.ZipFile(
dst,
"w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9,
) as zf:
zi = zipfile.ZipInfo(SRC.name)
zi.compress_type = zipfile.ZIP_DEFLATED
# ⬇⬇⬇ KLÍČOVÁ OPRAVA ⬇⬇⬇
with zf.open(zi, "w", force_zip64=True) as z:
with open(SRC, "rb") as f:
while buf := f.read(CHUNK):
z.write(buf)
processed += len(buf)
print(
f"\rZIP {processed*100/total:6.2f}%",
end="",
flush=True,
)
print()
return time.time() - start, dst.stat().st_size
# ============================================================
# 7Z (CLI + progress)
# ============================================================
def sevenzip_test():
dst = BASE.with_suffix(".7z")
start = time.time()
proc = subprocess.Popen(
[SEVENZIP_EXE, "a", "-t7z", "-mx=9", str(dst), str(SRC)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
for line in proc.stdout:
if "%" in line:
print(f"\r7Z {line.strip():>6}", end="", flush=True)
proc.wait()
print()
return time.time() - start, dst.stat().st_size
# ============================================================
# ZSTD (stream + progress)
# ============================================================
def zstd_test():
dst = BASE.with_suffix(".zst")
total = ORIG_SIZE
processed = 0
cctx = zstd.ZstdCompressor(level=19)
start = time.time()
with open(SRC, "rb") as src, open(dst, "wb") as out:
with cctx.stream_writer(out) as compressor:
while buf := src.read(CHUNK):
compressor.write(buf)
processed += len(buf)
print(
f"\rZSTD {processed*100/total:6.2f}%",
end="",
flush=True,
)
print()
return time.time() - start, dst.stat().st_size
# ============================================================
# MAIN
# ============================================================
results = {}
print("\n--- ZIP ---")
t, s = zip_test()
results["ZIP"] = (t, s)
# print("\n--- 7Z ---")
# t, s = sevenzip_test()
# results["7Z"] = (t, s)
#
# print("\n--- ZSTD ---")
# t, s = zstd_test()
# results["ZSTD"] = (t, s)
# ============================================================
# SUMMARY
# ============================================================
print("\n=== SUMMARY ===")
for name, (t, size) in results.items():
ratio = 100 * (1 - size / ORIG_SIZE)
print(
f"{name:5s}: "
f"{t:7.1f} s "
f"{size/1024/1024:8.1f} MB "
f"(-{ratio:5.1f} %)"
)
+161
View File
@@ -0,0 +1,161 @@
import subprocess
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"
DB = r"localhost/3050:Z:\Medicus 3\data\MEDICUS.FDB"
BACKUP_DIR = Path(r"D:\medicusbackup")
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
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")
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
zipf = fbk.with_suffix(".zip")
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
report = []
report.append(f"Čas spuštění: {now}")
report.append(f"Databáze: {DB}")
report.append("")
try:
# ====================================================
# 1) GBAK
# ====================================================
t0 = time.time()
cmd = [
GBAK,
"-b",
"-user", FB_USER,
"-pas", FB_PASS,
DB,
str(fbk),
"-v",
]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd,
stdout=f,
stderr=subprocess.STDOUT,
check=True,
)
t_gbak = time.time() - t0
fbk_size = fbk.stat().st_size
report.append("GBAK záloha: OK")
report.append(f"FBK soubor: {fbk}")
report.append(f"Velikost FBK: {fbk_size/1024/1024:.1f} MB")
report.append(f"Čas GBAK: {t_gbak:.1f} s")
report.append("")
# ====================================================
# 2) ZIP
# ====================================================
t1 = time.time()
processed = 0
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"\rZIP: {pct:6.2f}% "
f"({processed//1024//1024} MB)",
end="",
flush=True,
)
print("\nZIP hotov.")
t_zip = time.time() - t1
zip_size = zipf.stat().st_size
report.append("ZIP komprese: OK")
report.append(f"ZIP soubor: {zipf}")
report.append(f"Velikost ZIP: {zip_size/1024/1024:.1f} MB")
report.append(
f"Kompresní poměr: "
f"{100 * (1 - zip_size / fbk_size):.1f} %"
)
report.append(f"Čas ZIP: {t_zip:.1f} s")
report.append("")
# ====================================================
# 3) DELETE FBK
# ====================================================
fbk.unlink()
report.append("FBK soubor byl po úspěšné kompresi smazán.")
report.append("")
report.append(f"LOG: {log}")
# ====================================================
# MAIL OK
# ====================================================
send_mail(
MAIL_TO,
"✅ MEDICUS záloha + ZIP OK",
"\n".join(report),
)
except Exception:
err = traceback.format_exc()
send_mail(
MAIL_TO,
"❌ MEDICUS chyba při záloze / ZIP",
f"""Při záloze MEDICUS došlo k chybě.
Čas: {now}
Chyba:
{err}
FBK (pokud existuje): {fbk}
LOG: {log}
""",
)
raise
if __name__ == "__main__":
main()
@@ -0,0 +1,161 @@
import subprocess
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"
DB = r"localhost/3050:Z:\Medicus 3\data\MEDICUS.FDB"
BACKUP_DIR = Path(r"D:\medicusbackup")
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
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")
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
zipf = fbk.with_suffix(".zip")
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
report = []
report.append(f"Čas spuštění: {now}")
report.append(f"Databáze: {DB}")
report.append("")
try:
# ====================================================
# 1) GBAK
# ====================================================
t0 = time.time()
cmd = [
GBAK,
"-b",
"-user", FB_USER,
"-pas", FB_PASS,
DB,
str(fbk),
"-v",
]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd,
stdout=f,
stderr=subprocess.STDOUT,
check=True,
)
t_gbak = time.time() - t0
fbk_size = fbk.stat().st_size
report.append("GBAK záloha: OK")
report.append(f"FBK soubor: {fbk}")
report.append(f"Velikost FBK: {fbk_size/1024/1024:.1f} MB")
report.append(f"Čas GBAK: {t_gbak:.1f} s")
report.append("")
# ====================================================
# 2) ZIP
# ====================================================
t1 = time.time()
processed = 0
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"\rZIP: {pct:6.2f}% "
f"({processed//1024//1024} MB)",
end="",
flush=True,
)
print("\nZIP hotov.")
t_zip = time.time() - t1
zip_size = zipf.stat().st_size
report.append("ZIP komprese: OK")
report.append(f"ZIP soubor: {zipf}")
report.append(f"Velikost ZIP: {zip_size/1024/1024:.1f} MB")
report.append(
f"Kompresní poměr: "
f"{100 * (1 - zip_size / fbk_size):.1f} %"
)
report.append(f"Čas ZIP: {t_zip:.1f} s")
report.append("")
# ====================================================
# 3) DELETE FBK
# ====================================================
fbk.unlink()
report.append("FBK soubor byl po úspěšné kompresi smazán.")
report.append("")
report.append(f"LOG: {log}")
# ====================================================
# MAIL OK
# ====================================================
send_mail(
MAIL_TO,
"✅ MEDICUS záloha + ZIP OK",
"\n".join(report),
)
except Exception:
err = traceback.format_exc()
send_mail(
MAIL_TO,
"❌ MEDICUS chyba při záloze / ZIP",
f"""Při záloze MEDICUS došlo k chybě.
Čas: {now}
Chyba:
{err}
FBK (pokud existuje): {fbk}
LOG: {log}
""",
)
raise
if __name__ == "__main__":
main()
+185
View File
@@ -0,0 +1,185 @@
import subprocess
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB"
EXT_DIR = Path(r"U:\externi")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# HELPERS
# ============================================================
def gbak_and_zip(label: str, db_conn: str, fbk: Path, zipf: Path, log: Path) -> dict:
"""
Run gbak backup and ZIP the result.
Returns a result dict.
"""
result = {
"label": label,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
# 1) GBAK
print(f"GBAK: {label} ... ", end="", flush=True)
t0 = time.time()
cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)")
# 2) ZIP
t1 = time.time()
processed = 0
fbk_size = result["fbk_size"]
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True)
print()
result["t_zip"] = time.time() - t1
result["zip_size"] = zipf.stat().st_size
# 3) Delete FBK + LOG
fbk.unlink()
log.unlink()
result["ok"] = True
return result
def format_result(r: dict) -> str:
ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0
return (
f" {r['label']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB → "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"({ratio:.0f}% komprese, "
f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
backed_up = []
errors = []
# ----------------------------------------------------------
# 1) Hlavní DB MEDICUS.FDB
# ----------------------------------------------------------
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip"
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
try:
r = gbak_and_zip("MEDICUS", MAIN_DB, fbk, zipf, log)
backed_up.append(r)
except Exception:
errors.append({"label": "MEDICUS", "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# 2) Externí DB MEDICUS_FILES_*.fdb
# ----------------------------------------------------------
fdb_all = sorted(
set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")),
key=lambda p: p.name.lower(),
)
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
zipf = BACKUP_DIR / f"{name}_{ts}.zip"
log = BACKUP_DIR / f"{name}_{ts}.log"
db_conn = f"localhost/{FB_PORT}:{fdb}"
try:
r = gbak_and_zip(name, db_conn, fbk, zipf, log)
backed_up.append(r)
except Exception:
errors.append({"label": name, "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# Report
# ----------------------------------------------------------
total = 1 + len(fdb_all)
report = [
f"Backup Medicus {now.strftime('%d.%m.%Y %H:%M')}",
f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}",
f"Výstupní adresář: {BACKUP_DIR}",
"",
]
if backed_up:
report.append("--- Zálohováno ---")
total_zip = sum(r["zip_size"] for r in backed_up)
for r in backed_up:
report.append(format_result(r))
report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB")
report.append("")
if errors:
report.append("--- CHYBY ---")
for e in errors:
report.append(f" {e['label']}:\n{e['error']}")
report.append("")
has_errors = bool(errors)
subject = (
f"{'X' if has_errors else 'OK'} MEDICUS backup "
f"{len(backed_up)}/{total}"
+ (f" {len(errors)} chyb" if has_errors else "")
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+185
View File
@@ -0,0 +1,185 @@
import subprocess
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB"
EXT_DIR = Path(r"U:\externi")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# HELPERS
# ============================================================
def gbak_and_zip(label: str, db_conn: str, fbk: Path, zipf: Path, log: Path) -> dict:
"""
Run gbak backup and ZIP the result.
Returns a result dict.
"""
result = {
"label": label,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
# 1) GBAK
print(f"GBAK: {label} ... ", end="", flush=True)
t0 = time.time()
cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)")
# 2) ZIP
t1 = time.time()
processed = 0
fbk_size = result["fbk_size"]
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True)
print()
result["t_zip"] = time.time() - t1
result["zip_size"] = zipf.stat().st_size
# 3) Delete FBK + LOG
fbk.unlink()
log.unlink()
result["ok"] = True
return result
def format_result(r: dict) -> str:
ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0
return (
f" {r['label']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB → "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"({ratio:.0f}% komprese, "
f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
backed_up = []
errors = []
# ----------------------------------------------------------
# 1) Hlavní DB MEDICUS.FDB
# ----------------------------------------------------------
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip"
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
try:
r = gbak_and_zip("MEDICUS", MAIN_DB, fbk, zipf, log)
backed_up.append(r)
except Exception:
errors.append({"label": "MEDICUS", "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# 2) Externí DB MEDICUS_FILES_*.fdb
# ----------------------------------------------------------
fdb_all = sorted(
set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")),
key=lambda p: p.name.lower(),
)
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
zipf = BACKUP_DIR / f"{name}_{ts}.zip"
log = BACKUP_DIR / f"{name}_{ts}.log"
db_conn = f"localhost/{FB_PORT}:{fdb}"
try:
r = gbak_and_zip(name, db_conn, fbk, zipf, log)
backed_up.append(r)
except Exception:
errors.append({"label": name, "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# Report
# ----------------------------------------------------------
total = 1 + len(fdb_all)
report = [
f"Backup Medicus {now.strftime('%d.%m.%Y %H:%M')}",
f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}",
f"Výstupní adresář: {BACKUP_DIR}",
"",
]
if backed_up:
report.append("--- Zálohováno ---")
total_zip = sum(r["zip_size"] for r in backed_up)
for r in backed_up:
report.append(format_result(r))
report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB")
report.append("")
if errors:
report.append("--- CHYBY ---")
for e in errors:
report.append(f" {e['label']}:\n{e['error']}")
report.append("")
has_errors = bool(errors)
subject = (
f"{'X' if has_errors else 'OK'} MEDICUS backup "
f"{len(backed_up)}/{total}"
+ (f" {len(errors)} chyb" if has_errors else "")
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+235
View File
@@ -0,0 +1,235 @@
import subprocess
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB"
EXT_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# HELPERS
# ============================================================
def run_gbak(label: str, db_conn: str, fbk: Path, log: Path) -> dict:
"""Run gbak, return result dict (without zip info)."""
result = {
"label": label,
"ok": False,
"fbk": fbk,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
print(f"GBAK: {label} ... ", end="", flush=True)
t0 = time.time()
cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)")
result["ok"] = True
return result
def zip_single(label: str, fbk: Path, zipf: Path) -> tuple[int, float]:
"""ZIP one FBK into its own ZIP. Returns (zip_size, t_zip)."""
t1 = time.time()
processed = 0
fbk_size = fbk.stat().st_size
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True)
print()
return zipf.stat().st_size, time.time() - t1
def zip_multiple(fbk_results: list[dict], zipf: Path) -> tuple[int, float]:
"""ZIP multiple FBK files into one ZIP. Returns (zip_size, t_zip)."""
t1 = time.time()
total_fbk_size = sum(r["fbk_size"] for r in fbk_results)
total_processed = 0
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
for r in fbk_results:
fbk = r["fbk"]
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
total_processed += len(buf)
pct = total_processed * 100 / total_fbk_size
print(f"\r ZIP {fbk.name}: {pct:6.2f}%", end="", flush=True)
print()
return zipf.stat().st_size, time.time() - t1
def format_result(r: dict) -> str:
ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0
return (
f" {r['label']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB → "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"({ratio:.0f}% komprese, "
f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
backed_up = []
errors = []
# ----------------------------------------------------------
# 1) Hlavní DB MEDICUS.FDB → vlastní ZIP
# ----------------------------------------------------------
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip"
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
try:
r = run_gbak("MEDICUS", MAIN_DB, fbk, log)
log.unlink()
zip_size, t_zip = zip_single("MEDICUS", fbk, zipf)
fbk.unlink()
r["zip_size"] = zip_size
r["t_zip"] = t_zip
backed_up.append(r)
except Exception:
errors.append({"label": "MEDICUS", "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ----------------------------------------------------------
# 2) Externí DB MEDICUS_FILES_*.fdb → všechny do jednoho ZIP
# ----------------------------------------------------------
fdb_all = sorted(
set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")),
key=lambda p: p.name.lower(),
)
ext_results = []
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
log = BACKUP_DIR / f"{name}_{ts}.log"
db_conn = f"localhost/{FB_PORT}:{fdb}"
try:
r = run_gbak(name, db_conn, fbk, log)
log.unlink()
ext_results.append(r)
except Exception:
errors.append({"label": name, "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
for f in (fbk, log):
if f.exists():
f.unlink()
# ZIP všechny externí FBK do jednoho souboru
if ext_results:
ext_zip = BACKUP_DIR / f"MEDICUS_FILES_{ts}.zip"
print(f"\nZIP externích DB → {ext_zip.name}")
try:
zip_size, t_zip = zip_multiple(ext_results, ext_zip)
for r in ext_results:
r["zip_size"] = zip_size # sdílená velikost výsledného ZIPu
r["t_zip"] = t_zip
r["fbk"].unlink()
backed_up.append(r)
except Exception:
errors.append({"label": "MEDICUS_FILES (zip)", "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
for r in ext_results:
if r["fbk"].exists():
r["fbk"].unlink()
# ----------------------------------------------------------
# Report
# ----------------------------------------------------------
total = 1 + len(fdb_all)
report = [
f"Backup Medicus {now.strftime('%d.%m.%Y %H:%M')}",
f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}",
f"Výstupní adresář: {BACKUP_DIR}",
"",
]
if backed_up:
report.append("--- Zálohováno ---")
# Hlavní DB
main_results = [r for r in backed_up if r["label"] == "MEDICUS"]
ext_backed = [r for r in backed_up if r["label"] != "MEDICUS"]
for r in main_results:
report.append(format_result(r))
if ext_backed:
total_ext_fbk = sum(r["fbk_size"] for r in ext_backed)
ext_zip_size = ext_backed[0]["zip_size"] if ext_backed else 0
ratio = 100 * (1 - ext_zip_size / total_ext_fbk) if total_ext_fbk else 0
report.append(f" Externí DB ({len(ext_backed)} souborů):")
for r in ext_backed:
report.append(f" {r['label']}: FBK {r['fbk_size']/1024/1024:.1f} MB (gbak {r['t_gbak']:.0f}s)")
report.append(
f" → společný ZIP: {ext_zip_size/1024/1024:.1f} MB "
f"({ratio:.0f}% komprese, zip {ext_backed[0]['t_zip']:.0f}s)"
)
total_zip = sum(r["zip_size"] for r in main_results) + (ext_backed[0]["zip_size"] if ext_backed else 0)
report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB")
report.append("")
if errors:
report.append("--- CHYBY ---")
for e in errors:
report.append(f" {e['label']}:\n{e['error']}")
report.append("")
has_errors = bool(errors)
subject = (
f"{'X' if has_errors else 'OK'} MEDICUS backup "
f"{len(backed_up)}/{total}"
+ (f" {len(errors)} chyb" if has_errors else "")
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+172
View File
@@ -0,0 +1,172 @@
import subprocess
import json
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
SRC_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
# Find all FDB files (case-insensitive)
fdb_files = sorted(SRC_DIR.glob("MEDICUS_FILES_*.fdb"))
fdb_upper = sorted(SRC_DIR.glob("MEDICUS_FILES_*.FDB"))
fdb_all = sorted(
set(fdb_files + fdb_upper),
key=lambda p: p.name.lower(),
)
backed_up = []
errors = []
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
zipf = BACKUP_DIR / f"{name}_{ts}.zip"
log = BACKUP_DIR / f"{name}_{ts}.log"
result = {
"file": fdb.name,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
try:
# 1) GBAK
print(f"GBAK: {fdb.name} ... ", end="", flush=True)
t0 = time.time()
db_conn = f"localhost/{FB_PORT}:{fdb}"
cmd = [
GBAK, "-b",
"-user", FB_USER,
"-pas", FB_PASS,
db_conn, str(fbk),
"-v",
]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd, stdout=f, stderr=subprocess.STDOUT, check=True,
)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s)")
# 2) ZIP
t1 = time.time()
processed = 0
fbk_size = result["fbk_size"]
with zipfile.ZipFile(
zipf, "w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9,
) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(
f"\r ZIP {name}: {pct:6.2f}%",
end="", flush=True,
)
print()
result["t_zip"] = time.time() - t1
result["zip_size"] = zipf.stat().st_size
# 3) DELETE FBK + LOG
fbk.unlink()
log.unlink()
result["ok"] = True
backed_up.append(result)
except Exception:
result["error"] = traceback.format_exc()
errors.append(result)
for f in (fbk, log):
if f.exists():
f.unlink()
# Build report
report = []
report.append(f"Backup externi DB - {now.strftime('%d.%m.%Y %H:%M')}")
report.append(f"Celkem souboru: {len(fdb_all)}")
report.append(f"Zalohovano: {len(backed_up)}")
report.append(f"Chyby: {len(errors)}")
report.append("")
if backed_up:
report.append("--- Backed up ---")
total_zip = 0
for r in backed_up:
total_zip += r["zip_size"]
report.append(
f" {r['file']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB -> "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"(gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
report.append(f" Total ZIP: {total_zip / 1024 / 1024:.1f} MB")
report.append("")
if errors:
report.append("--- ERRORS ---")
for r in errors:
report.append(f" {r['file']}: {r['error']}")
report.append("")
# Send email
has_errors = len(errors) > 0
subject = (
f"{'X' if has_errors else 'OK'} "
f"MEDICUS externi DB - "
f"backup {len(backed_up)}/{len(fdb_all)}"
f"{f', {len(errors)} errors' if has_errors else ''}"
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+172
View File
@@ -0,0 +1,172 @@
import subprocess
import json
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
SRC_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
# Find all FDB files (case-insensitive)
fdb_files = sorted(SRC_DIR.glob("MEDICUS_FILES_*.fdb"))
fdb_upper = sorted(SRC_DIR.glob("MEDICUS_FILES_*.FDB"))
fdb_all = sorted(
set(fdb_files + fdb_upper),
key=lambda p: p.name.lower(),
)
backed_up = []
errors = []
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
zipf = BACKUP_DIR / f"{name}_{ts}.zip"
log = BACKUP_DIR / f"{name}_{ts}.log"
result = {
"file": fdb.name,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
try:
# 1) GBAK
print(f"GBAK: {fdb.name} ... ", end="", flush=True)
t0 = time.time()
db_conn = f"localhost/{FB_PORT}:{fdb}"
cmd = [
GBAK, "-b",
"-user", FB_USER,
"-pas", FB_PASS,
db_conn, str(fbk),
"-v",
]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd, stdout=f, stderr=subprocess.STDOUT, check=True,
)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s)")
# 2) ZIP
t1 = time.time()
processed = 0
fbk_size = result["fbk_size"]
with zipfile.ZipFile(
zipf, "w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9,
) as zf:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
processed += len(buf)
pct = processed * 100 / fbk_size
print(
f"\r ZIP {name}: {pct:6.2f}%",
end="", flush=True,
)
print()
result["t_zip"] = time.time() - t1
result["zip_size"] = zipf.stat().st_size
# 3) DELETE FBK + LOG
fbk.unlink()
log.unlink()
result["ok"] = True
backed_up.append(result)
except Exception:
result["error"] = traceback.format_exc()
errors.append(result)
for f in (fbk, log):
if f.exists():
f.unlink()
# Build report
report = []
report.append(f"Backup externi DB - {now.strftime('%d.%m.%Y %H:%M')}")
report.append(f"Celkem souboru: {len(fdb_all)}")
report.append(f"Zalohovano: {len(backed_up)}")
report.append(f"Chyby: {len(errors)}")
report.append("")
if backed_up:
report.append("--- Backed up ---")
total_zip = 0
for r in backed_up:
total_zip += r["zip_size"]
report.append(
f" {r['file']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB -> "
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
f"(gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
)
report.append(f" Total ZIP: {total_zip / 1024 / 1024:.1f} MB")
report.append("")
if errors:
report.append("--- ERRORS ---")
for r in errors:
report.append(f" {r['file']}: {r['error']}")
report.append("")
# Send email
has_errors = len(errors) > 0
subject = (
f"{'X' if has_errors else 'OK'} "
f"MEDICUS externi DB - "
f"backup {len(backed_up)}/{len(fdb_all)}"
f"{f', {len(errors)} errors' if has_errors else ''}"
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+189
View File
@@ -0,0 +1,189 @@
import subprocess
import json
import os
from pathlib import Path
from datetime import datetime
import zipfile
import time
import traceback
from EmailMessagingGraph import send_mail
# ============================================================
# CONFIG
# ============================================================
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
FB_USER = "SYSDBA"
FB_PASS = "masterkey"
FB_PORT = "3050"
SRC_DIR = Path(r"c:\medicusext")
BACKUP_DIR = Path(r"U:\medicusbackup")
MAIL_TO = "vladimir.buzalka@buzalka.cz"
CHUNK = 8 * 1024 * 1024 # 8 MB
# ============================================================
# MAIN
# ============================================================
def main():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
now = datetime.now()
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
# Find all FDB files (case-insensitive)
fdb_files = sorted(SRC_DIR.glob("MEDICUS_FILES_*.fdb"))
fdb_upper = sorted(SRC_DIR.glob("MEDICUS_FILES_*.FDB"))
fdb_all = sorted(
set(fdb_files + fdb_upper),
key=lambda p: p.name.lower(),
)
backed_up = []
errors = []
fbk_paths = [] # FBK files to be zipped together
# --------------------------------------------------------
# 1) GBAK all databases
# --------------------------------------------------------
for fdb in fdb_all:
name = fdb.stem
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
log = BACKUP_DIR / f"{name}_{ts}.log"
result = {
"file": fdb.name,
"ok": False,
"fbk_size": 0,
"zip_size": 0,
"t_gbak": 0,
"t_zip": 0,
"error": None,
}
try:
print(f"GBAK: {fdb.name} ... ", end="", flush=True)
t0 = time.time()
db_conn = f"localhost/{FB_PORT}:{fdb}"
cmd = [
GBAK, "-b",
"-user", FB_USER,
"-pas", FB_PASS,
db_conn, str(fbk),
"-v",
]
with open(log, "w", encoding="utf-8") as f:
subprocess.run(
cmd, stdout=f, stderr=subprocess.STDOUT, check=True,
)
result["t_gbak"] = time.time() - t0
result["fbk_size"] = fbk.stat().st_size
print(f"OK ({result['t_gbak']:.0f}s)")
# Delete log, keep FBK for zipping
log.unlink()
result["ok"] = True
fbk_paths.append((fbk, result))
backed_up.append(result)
except Exception:
result["error"] = traceback.format_exc()
errors.append(result)
for f in (fbk, log):
if f.exists():
f.unlink()
# --------------------------------------------------------
# 2) ZIP all FBK files into one archive
# --------------------------------------------------------
total_zip_size = 0
if fbk_paths:
zip_path = BACKUP_DIR / f"MEDICUS_FILES_{ts}.zip"
print(f"\nZIP: {zip_path.name}")
t_zip_start = time.time()
# Calculate total size for progress
total_fbk_size = sum(fbk.stat().st_size for fbk, _ in fbk_paths)
total_processed = 0
with zipfile.ZipFile(
zip_path, "w",
compression=zipfile.ZIP_DEFLATED,
compresslevel=9,
) as zf:
for fbk, result in fbk_paths:
zi = zipfile.ZipInfo(fbk.name)
zi.compress_type = zipfile.ZIP_DEFLATED
with zf.open(zi, "w", force_zip64=True) as z:
with open(fbk, "rb") as src:
while buf := src.read(CHUNK):
z.write(buf)
total_processed += len(buf)
pct = total_processed * 100 / total_fbk_size
print(
f"\r {fbk.name}: {pct:6.2f}%",
end="", flush=True,
)
print()
t_zip_total = time.time() - t_zip_start
total_zip_size = zip_path.stat().st_size
print(f"ZIP OK ({t_zip_total:.0f}s, {total_zip_size/1024/1024:.1f} MB)")
# Fill zip_size into each result and delete FBK files
for fbk, result in fbk_paths:
result["zip_size"] = total_zip_size
fbk.unlink()
# --------------------------------------------------------
# Build report
# --------------------------------------------------------
report = []
report.append(f"Backup externi DB - {now.strftime('%d.%m.%Y %H:%M')}")
report.append(f"Celkem souboru: {len(fdb_all)}")
report.append(f"Zalohovano: {len(backed_up)}")
report.append(f"Chyby: {len(errors)}")
report.append("")
if backed_up:
report.append("--- Backed up ---")
total_fbk_mb = sum(r["fbk_size"] for r in backed_up) / 1024 / 1024
for r in backed_up:
report.append(
f" {r['file']}: "
f"FBK {r['fbk_size']/1024/1024:.1f} MB "
f"(gbak {r['t_gbak']:.0f}s)"
)
report.append(f" Total FBK: {total_fbk_mb:.1f} MB -> ZIP: {total_zip_size/1024/1024:.1f} MB")
report.append("")
if errors:
report.append("--- ERRORS ---")
for r in errors:
report.append(f" {r['file']}: {r['error']}")
report.append("")
# Send email
has_errors = len(errors) > 0
subject = (
f"{'X' if has_errors else 'OK'} "
f"MEDICUS externi DB - "
f"backup {len(backed_up)}/{len(fdb_all)}"
f"{f', {len(errors)} errors' if has_errors else ''}"
)
send_mail(MAIL_TO, subject, "\n".join(report))
print("\n" + "\n".join(report))
if errors:
raise RuntimeError(f"{len(errors)} backup(s) failed")
if __name__ == "__main__":
main()
+46
View File
@@ -0,0 +1,46 @@
---
Upravená verze skriptu, prosím upravte si cesty, toto vše vložit do nějakého cmd souboru:
editují se pouze následující sekce:
rem /zalozeni slozek do kterych se ma zalohovat kdyz neexistuji/
rem /nastaveni serveru a portu a cest k db/
rem /zaloha/ - v případě že je soubor gbak umístěn jinde, je jiná cesta k Firebirdu
rem/odkud kam kopirovat/
@echo off
rem /nacteni aktualniho datumu a casu/
For /f "tokens=1-3 delims=/." %%a in ("%DATE%") do (set mydate=%%c-%%b-%%a)
For /f "tokens=1-2 delims=/:" %%a in ("%TIME%") do (set mytime=%%a-%%b)
@echo on
rem /zalozeni slozek do kterych se ma zalohovat kdyz neexistuji/
if not exist "C:\zalohy\MEDICUS" mkdir "C:\zalohy\MEDICUS"
if not exist "C:\backup\MEDICUS" mkdir "C:\backup\MEDICUS"
rem /nastaveni serveru a portu a cest k db/
set ufdb="localhost/3050:c:\Medicus\Medicus Komfort\data\sestavy.fdb"
rem /nastaveni cesty pro umisteni zalohy/
set ufbk="C:\zalohy\MEDICUS\MEDICUS3_%mydate%%mytime%.FBK" -y "C:\zalohy\MEDICUS\log%mydate%_%mytime%.txt"
rem /zaloha/
"C:\Program Files (x86)\Firebird\Firebird_2_5\bin\gbak" -b -user SYSDBA -pas masterke %ufdb% %ufbk% -v
rem/odkud kam kopirovat/
set odkud="C:\zalohy\MEDICUS*.*"
set kam="C:\backup\MEDICUS"
SET drv1=%odkud:~1,1%
SET drv2=%kam:~1,1%
if %drv1%==%drv2% goto :move
if not %drv1%==%drv2% goto :copy
:move
rem /presunuti do jineho umisteni/
move /Y %odkud% %kam%
goto :exit
:copy
rem /kopiruj a smaz/
copy /Y %odkud% %kam%
del /Q %odkud%
goto :exit
:exit
timeout /T 5
exit
Zajímavé odkazy rovněž zde:
https://docplayer.cz/665150-Databaze-firebird-zalohovani-obnoveni-roman-fic-1-29-14-databaze.html
http://www.epos.cz/obis4wiki/firebird:zalohovani_databazi_-_gbak
Pohodový den přeje
Mgr. Josef Kauc
KONZULTANT MEDICUS
+91
View File
@@ -0,0 +1,91 @@
"""
EmailMessagingGraph.py
----------------------
Private Microsoft Graph mail sender
Application permissions, shared mailbox
"""
import msal
import requests
from functools import lru_cache
from typing import Union, List
# =========================
# PRIVATE CONFIG (ONLY YOU)
# =========================
TENANT_ID = "7d269944-37a4-43a1-8140-c7517dc426e9"
CLIENT_ID = "4b222bfd-78c9-4239-a53f-43006b3ed07f"
CLIENT_SECRET = "Txg8Q~MjhocuopxsJyJBhPmDfMxZ2r5WpTFj1dfk"
SENDER = "reports@buzalka.cz"
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
SCOPE = ["https://graph.microsoft.com/.default"]
@lru_cache(maxsize=1)
def _get_token() -> str:
app = msal.ConfidentialClientApplication(
CLIENT_ID,
authority=AUTHORITY,
client_credential=CLIENT_SECRET,
)
token = app.acquire_token_for_client(scopes=SCOPE)
if "access_token" not in token:
raise RuntimeError(f"Graph auth failed: {token}")
return token["access_token"]
def send_mail(
to: Union[str, List[str]],
subject: str,
body: str,
*,
html: bool = False,
):
"""
Send email via Microsoft Graph.
:param to: email or list of emails
:param subject: subject
:param body: email body
:param html: True = HTML, False = plain text
"""
if isinstance(to, str):
to = [to]
payload = {
"message": {
"subject": subject,
"body": {
"contentType": "HTML" if html else "Text",
"content": body,
},
"toRecipients": [
{"emailAddress": {"address": addr}} for addr in to
],
},
"saveToSentItems": "true",
}
headers = {
"Authorization": f"Bearer {_get_token()}",
"Content-Type": "application/json",
}
r = requests.post(
f"https://graph.microsoft.com/v1.0/users/{SENDER}/sendMail",
headers=headers,
json=payload,
timeout=30,
)
if r.status_code != 202:
raise RuntimeError(
f"sendMail failed [{r.status_code}]: {r.text}"
)
+51
View File
@@ -0,0 +1,51 @@
# Medicus + Claude kontext projektu
## Přečti si prosím tyto soubory na začátku každé konverzace
1. `MedicusWithClaude/CLAUDE_NOTES.md` hlavní poznámky: DB připojení, klíčové tabulky, RTF formát, import pipeline
2. `MedicusWithClaudeSelects/SELECTS.md` SQL dotazy (registrovaní pacienti atd.)
3. `MedicusWithClaudeSelects/FakturaceADavky.md` tabulky FAK, FAKDET, FAKDAV, PORTAL, kódování dávek
## O projektu
Firebird DB lékařského SW **Medicus** pro praktického lékaře. Lékařka je **MUDr. Buzalková Michaela** (IDUZI=4). Vladimír Buzalka (IDUZI=6) je manžel a správce systému s ním probíhají tyto konverzace.
## Připojení k DB
```python
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
```
## Počítač "reporter"
Na tomto stroji běží pravidelná automatická tvorba reportů. Připojení k DB funguje stejně jako výše (ostrá DB, čerstvá data). Hlavní report skript: `MedicusWithClaudeFaktury/faktury_report.py`
### Co report dělá
- Generuje Excel s listy: **FAK**, **FAKDET**, **PORTAL**, **PORTAL_DATA**
- Ukládá do `u:\Dropbox\!!!Days\Downloads Z230\`
- Název souboru: `YYYY-MM-DD_HH-MM-SS_faktury.xlsx`
- Maže předchozí verze ze stejné složky
- Řazení: nejnovější záznamy nahoře
- Hyperlinky: FAK → FAKDET, PORTAL ↔ PORTAL_DATA
### Důležité kódování dávek
KDAVKA/FDAVKA jsou v **CP852** (DOS). fdb je vrací jako string dekódovaný win1250 nutno re-enkódovat:
```python
spravny_text = s.encode('cp1250', errors='replace').decode('cp852', errors='replace')
```
## Klíčové adresáře
| Adresář | Obsah |
|---|---|
| `MedicusWithClaude/` | průzkumné skripty, import pipeline (s03soubory.py) |
| `MedicusWithClaudeSelects/` | SQL dotazy, dokumentace tabulek |
| `MedicusWithClaudeFaktury/` | fakturační reporty |
## Co chceme na reporteru nastavit
Pravidelná automatická tvorba reportu `faktury_report.py` např. každý den ráno, aby byl vždy čerstvý Excel s fakturami v Dropboxu.
@@ -0,0 +1,226 @@
"""
msg_to_clipboard.py
Převede Outlook .msg email (i vlákno odpovědí) do formátu:
DD.MM.YYYY HH:MM od Jméno: text
a zkopíruje do schránky.
Použití:
python msg_to_clipboard.py "cesta/k/souboru.msg"
nebo spustit bez argumentu -> otevře dialog pro výběr souboru
"""
import sys
import re
import tkinter as tk
from tkinter import filedialog, messagebox
import extract_msg
from bs4 import BeautifulSoup
from dateutil import parser as dateparser
def decode_html(html_bytes: bytes) -> str:
"""Dekóduje HTML bytes zkouší UTF-8 přísně (selže na špatných sekvencích),
pak záložní windows-1250 a latin-1."""
for enc in ("utf-8", "windows-1250", "latin-1"):
try:
return html_bytes.decode(enc) # utf-8 je přísné selže pro non-UTF-8
except (UnicodeDecodeError, LookupError):
continue
return html_bytes.decode("utf-8", errors="replace")
def clean_text(text: str) -> str:
"""Smaže zbytečné bílé znaky a nbsp, vrátí jeden řádek."""
text = text.replace("\xa0", " ")
text = re.sub(r"\s+", " ", text)
return text.strip()
def is_separator(element) -> bool:
"""Vrátí True pokud je element Outlook oddělovač citace (border-top div)."""
if getattr(element, "name", None) != "div":
return False
inner = element.find("div", style=lambda s: s and "border-top" in s)
return inner is not None
def parse_separator(element) -> tuple[str, str | None]:
"""Z oddělovače vytáhne jméno odesílatele a řetězec data."""
text = element.get_text(separator="\n")
from_match = re.search(r"From:\s*(.+?)(?:\n|$)", text)
sent_match = re.search(r"Sent:\s*(.+?)(?:\n|$)", text)
sender_raw = from_match.group(1).strip() if from_match else "Neznámý"
# "Jméno Příjmení <email>" → "Jméno Příjmení"
name_only = re.match(r"^(.+?)\s*<", sender_raw)
sender = name_only.group(1).strip() if name_only else sender_raw
sent_str = sent_match.group(1).strip() if sent_match else None
return sender, sent_str
def parse_date(sent_str: str | None, fallback=None):
if not sent_str:
return fallback
try:
return dateparser.parse(sent_str)
except Exception:
return fallback
def extract_sections(soup, main_date, main_sender: str) -> list[dict]:
"""
Projde HTML tělo a rozdělí ho na sekce (každá zpráva ve vláknu).
Vrátí seznam diktů: {'sender', 'date', 'text'} seřazených od nejstaršího.
"""
body_div = soup.find("div", class_="WordSection1") or soup.body
if not body_div:
return []
sections = []
current = {"sender": main_sender, "date": main_date, "parts": []}
for child in body_div.children:
if is_separator(child):
# Ulož aktuální sekci a začni novou
text = clean_text(" ".join(current["parts"]))
sections.append({
"sender": current["sender"],
"date": current["date"],
"text": text,
})
sender, sent_str = parse_separator(child)
current = {
"sender": sender,
"date": parse_date(sent_str),
"parts": [],
}
elif hasattr(child, "get_text"):
t = clean_text(child.get_text(separator=" "))
if t:
current["parts"].append(t)
# Poslední sekce
text = clean_text(" ".join(current["parts"]))
sections.append({
"sender": current["sender"],
"date": current["date"],
"text": text,
})
# Otočit v e-mailu je nejnovější nahoře, chceme chronologicky
sections.reverse()
return sections
def parse_msg(msg_path: str) -> list[dict]:
msg = extract_msg.openMsg(msg_path)
main_date = msg.date
# Sender: preferuj display name před celým polem
main_sender = msg.sender or "Neznámý"
name_only = re.match(r"^(.+?)\s*<", main_sender)
if name_only:
main_sender = name_only.group(1).strip()
if msg.htmlBody:
html_text = decode_html(msg.htmlBody)
soup = BeautifulSoup(html_text, "html.parser")
return extract_sections(soup, main_date, main_sender)
# Fallback prostý text
return [{
"sender": main_sender,
"date": main_date,
"text": clean_text((msg.body or "").replace("\r\n", " ")),
}]
def format_sections(sections: list[dict]) -> str:
lines = []
for s in sections:
dt = s["date"]
if dt:
date_str = dt.strftime("%d.%m.%Y %H:%M")
else:
date_str = "??"
lines.append(f"{date_str} od {s['sender']}: {s['text']}")
return "\n".join(lines)
def copy_to_clipboard(text: str):
root = tk.Tk()
root.withdraw()
root.clipboard_clear()
root.clipboard_append(text)
root.update()
root.after(2000, root.destroy)
root.mainloop()
def show_result(text: str):
"""Zobrazí výsledek v malém okně a zkopíruje do schránky."""
root = tk.Tk()
root.title("Email → schránka")
root.resizable(True, True)
frame = tk.Frame(root, padx=10, pady=10)
frame.pack(fill=tk.BOTH, expand=True)
label = tk.Label(frame, text="Zkopírováno do schránky. Náhled:", anchor="w")
label.pack(fill=tk.X)
text_widget = tk.Text(frame, wrap=tk.WORD, height=12, width=80)
text_widget.insert(tk.END, text)
text_widget.config(state=tk.DISABLED)
text_widget.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
scrollbar = tk.Scrollbar(frame, command=text_widget.yview)
text_widget.config(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def copy_again():
root.clipboard_clear()
root.clipboard_append(text)
root.update()
btn_frame = tk.Frame(frame)
btn_frame.pack(fill=tk.X, pady=(8, 0))
tk.Button(btn_frame, text="Kopírovat znovu", command=copy_again).pack(side=tk.LEFT)
tk.Button(btn_frame, text="Zavřít", command=root.destroy).pack(side=tk.RIGHT)
# Auto-copy on open
root.clipboard_clear()
root.clipboard_append(text)
root.update()
root.mainloop()
def main():
if len(sys.argv) >= 2:
msg_path = sys.argv[1]
else:
root = tk.Tk()
root.withdraw()
msg_path = filedialog.askopenfilename(
title="Vyberte email (.msg)",
filetypes=[("Outlook Email", "*.msg"), ("Všechny soubory", "*.*")],
)
root.destroy()
if not msg_path:
return
try:
sections = parse_msg(msg_path)
result = format_sections(sections)
except Exception as exc:
messagebox.showerror("Chyba", f"Nepodařilo se zpracovat email:\n{exc}")
return
print(result)
show_result(result)
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
@echo off
:: Přetáhněte .msg soubor na tento soubor, nebo spusťte bez argumentu pro výběr souboru
python "%~dp0msg_to_clipboard.py" %1
@@ -0,0 +1,2 @@
06.03.2026 11:19 od pavel kraus: Dobrý den, přeposílám dokument z Fakultní nemocnice Královské Vinohrady Kardiologická klinika, dejte mi prosím vědět, kdy k vám mám můj tchán Antonín Mareček, r.č. 370701406 přijít. Děkuji, Kraus
08.03.2026 17:14 od Ordinace MUDr. Michaela Buzalková: Dobrý den, To záleží na termínu, kdy je TAVI plánováno. Nevidím ho nikde ve zprávě. Hezký den Vladimír Buzalka
+500
View File
@@ -0,0 +1,500 @@
# MedicusWithClaude poznámky pro Clauda
## Stav projektu
- Zahájeno: 2026-03-13
- Cíl: průzkum Firebird DB Medicus → analýzy a reporty
- Zatím: úspěšně čteme i zapisujeme do DB, rozumíme RTF formátu
- **2026-03-17**: Obnovena session Claude si přečetl poznámky, připraven pokračovat
- **2026-03-18**: Obnovena session Claude si přečetl poznámky, připraven pokračovat. Průběžně zapisuje do tohoto souboru.
- **2026-03-20**: Obnovena session merge logika z `test_import_single.py` integrována do `s03soubory.py`. Import pipeline kompletní.
- **2026-04-01**: Opravena chyba `BlobReader invalid BLOB handle` v `dekurz_report.py` viz níže.
## Bezpečnost
- Pracujeme na **místní kopii** poškození DB nevadí, obnova = 5 minut
- Lze bez obav experimentovat, zapisovat testovací data atd.
- Testovací záznamy nemusíme mazat
## Připojení k DB
```python
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
```
- fdb verze 2.0.4, Python na PATH (prostý `python` příkaz)
- Firebird instalace: `C:\Program Files\Firebird\Firebird_2_5_CGM\bin\`
- Jedná se o **HQbird** (IBSurgeon enhanced Firebird) port **3070** (ne standardní 3050)
- fbtracemgr trace nefungoval spolehlivě doporučeno použít **FBScanner** (stejný výrobce)
## O Medicusu
- Lékařský software pro praktického lékaře
- Vyvíjen od ~roku 2000, organicky rostl přidáváním funkcí
- Firebird 2.5 nikdy nepřejdou na nový (desetitisíce instalací, 993 tabulek)
- Žádné zabezpečení DB vše čitelné přes SYSDBA/masterkey
- Charset: win1250
## DB fakta
- 993 tabulek celkem
- Charset: win1250
## Klíčové tabulky
### KAR Kartotéka (základní kámen Medicusu)
- 6319 pacientů všichni kdo kdy prošli ordinací
- Primární klíč: **IDPAC** (interní ID, používá se jako FK v ostatních tabulkách)
- Identifikace pacienta v ČR/pojišťovna: **RODCIS** (rodné číslo, bez lomítka, např. 7309208104)
- **PRIJMENI**, **JMENO**, TITUL, TITULZA, DATNAR, POHLAVI
- **POJ** pojišťovna (3 znaky, např. 111 = VZP)
- Adresy: TRV* (trvalé), PRE* (přechodné)
- **VYRAZEN** příznak vyřazeného pacienta
- HLAVDGN hlavní diagnóza
### DEKURS záznamy z návštěv
- 172 068 záznamů
- **IDPAC** → vazba na KAR
- **DATUM** (date), **CAS** (time) kdy
- **DEKURS** text záznamu (BLOB, RTF nebo holý text)
- **DGN1** hlavní diagnóza (5 znaků, např. 'Z000 '), VDGN1-4 vedlejší
- **IDPRAC** který pracovník
- IDODD, IDUZI oddělení/uživatel (u Buzalky = 2, 2, 6)
- IDSKUPINA, IDTYP volitelné (NULL OK)
## Základní schéma
```
KAR (kartotéka) ←IDPAC→ DEKURS (záznamy z návštěv)
```
## Testovací pacient (uživatel)
- **IDPAC = 9742** Buzalka Vladimír, RC 7309208104, nar. 20.9.1973
- IDODD=2, IDPRAC=2, IDUZI=6 (použít při INSERT do DEKURS)
## RTF formát v DEKURS
### Základní struktura
```
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
{\info{\bookmarks "popis","Typ:ID",číslo}} ← metadata Medicusu
{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f2...Courier New;}{\f5...Symbol;}}
{\colortbl ;\red0\green0\blue255;\red255\green255\blue0;\red0\green128\blue0;\red192\green192\blue192;}
{\stylesheet{styly...}}
\uc1\pard\s10\plain... ← tělo záznamu
}
```
### Barvy (\cfX)
- `\cf1` = modrá (odkazy)
- `\cf2` = žlutá (highlight)
- `\cf3` = zelená
- `\cf4` = šedá (pozadí buněk tabulky `\clcbpat4`)
### Velikost písma (\fsXX = XX/2 bodů)
- `\fs18` = 9pt (tabulky)
- `\fs20` = 10pt (normální)
- `\fs24` = 12pt atd.
### Řezy a formátování
- `\b` = tučné, `\i` = kurzíva, `\ul` = podtržené
- `\ql` = vlevo, `\qr` = vpravo, `\qc` = střed
### Styly (stylesheet)
- `cs15/cs16` = Normální
- `cs20` = Vkládaný text
- `cs21` = Záhlaví (italic)
- `cs22/cs23` = Odkaz (underline, modrá)
- `s8/s10` = Vlevo (paragraph styl)
### Záložky (bookmarks v {\info})
- Klikatelné odkazy v Medicusu
- Formát: `"popis","Typ:ID",číslo`
- Typy: `Files:ID`, `MEDLAB:ID`, `Lab:ID`, `Ock:ID`, `VykA:ID`
### České znaky
- RTF hex escape v win1250: `\'e1`=á, `\'9e`=ž, `\'fd`=ý atd.
### Tabulky
- Plně funkční RTF tabulky (`\trowd`, `\cell`, `\cellx...`)
- Používají se např. pro laboratorní výsledky (viz ID=243082, 50KB)
### Holý text (starší záznamy)
- Některé starší záznamy jsou prostý text bez RTF obálky
- Nutno detekovat: začíná na `{\\rtf` = RTF, jinak plain text
### Příklad minimálního RTF záznamu
```
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
{\fonttbl{\f0\fnil\fcharset238 Arial;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
\pard\plain\f0\fs20 Text záznamu zde\par
}
```
## INSERT do DEKURS ověřený postup
```python
import fdb, datetime
conn = fdb.connect(...)
cur = conn.cursor()
cur.execute('SELECT MAX(ID) FROM DEKURS')
next_id = cur.fetchone()[0] + 1
text = r'{\rtf1\ansi\ansicpg1250...\par' + '\n}' # raw string pro RTF!
cur.execute("""
INSERT INTO DEKURS (ID, IDPAC, IDODD, IDPRAC, IDUZI, DATUM, CAS, DEKURS, DGN1)
VALUES (?, ?, 2, 2, 6, ?, ?, ?, ?)
""", (next_id, idpac, datum, cas, text, dgn1))
conn.commit()
```
- RTF string musí být **raw string** (r'...') kvůli zpětným lomítkům
- DGN1 = 5 znaků s mezerou: `'Z000 '`
- DATUM = datetime.date(...), CAS = datetime.time(...)
## Soubory v projektu
- `explore_db.py` výpis všech tabulek a jejich sloupců
- `inspect_table.py` detailní info o konkrétní tabulce (arg: název tabulky)
- `sample_dekurs.py` ukázky nejstarších a nejnovějších záznamů
- `sample_dekurs2.py` celý text jednoho záznamu
- `sample_rtf.py` nejdelší záznamy pacienta (dle délky RTF)
- `analyze_rtf.py` analýza RTF tagů v záznamu
- `insert_test.py` testovací INSERT do DEKURS
- `CLAUDE_NOTES.md` tento soubor
## Export FILES do externích DB (zjištěno 2026-03-17 přes FBScanner)
### Mechanismus přesunu
Medicus vytváří **měsíční** externí Firebird databáze ve formátu `MEDICUS_FILES_YYYYMM.fdb`.
**Postup exportu:**
1. SELECT dat z FILES po 10 záznamech: `select first 10 ID, BODY, DATUM from FILES where ID > X ORDER BY ID ASC`
2. Binární data (skeny) se zapíší do externího `.fdb` souboru
3. V hlavní DB se BODY přepíše referenčním číslem (ID v externím souboru): `update FILES set BODY = 379 where ID = 10009`
4. Do EXTERNI_DB se zapíše lokace: `insert into EXTERNI_DB (DBNAME, SERVER, PATH, HESLO) values ('DB202501', 'localhost/3053', 'u:\MEDICUS_FILES_202501.fdb', '<encrypted>')`
### Výsledek
- Hlavní DB: FILES.BODY obsahuje jen malé číslo (referenci) místo MB blobu
- Skenované soubory jsou fyzicky v `u:\MEDICUS_FILES_YYYYMM.fdb`
- Medicus načítá soubory přes EXTERNI_DB tabulku (SERVER + PATH + HESLO)
- Hesla v EXTERNI_DB jsou šifrovaná (base64 AES)
### FBScanner
- Nainstalován, funguje jako proxy: Medicus → [3050] FBScanner → [3053] Firebird
- Community Edition 2.5 stačí pro monitoring
- Export proveden na záloze (ne produkční DB)
## Přímý zápis do externí DB dokončeno (2026-03-17)
### Nový soubor: `funkce_ext.py`
Náhrada za `funkce.zapis_file()`. Ukládá PDF přímo do měsíční externí FDB
místo do hlavní medicus.fdb.
**Funkce:** `zapis_file_ext(vstupconnection, idpac, cesta, souborname, prvnizavorka, soubordate, souborfiledate, poznamka, ext_base_path=r'u:\\')`
- Parametry shodné s `funkce.zapis_file()` → jen prohodit import
- Vrací `fileid` stejně jako původní funkce
**Co dělá:**
1. Z `soubordate` odvodí `YYYYMM``DBNAME = 'DB202603'`
2. Načte PDF jako bytes
3. Vyhledá EXTERNI_DB pro daný měsíc:
- Nalezeno → připojí se k existující FDB (masterkey)
- Nenalezeno → vytvoří novou `MEDICUS_FILES_YYYYMM.fdb`, zaregistruje do EXTERNI_DB
4. Vygeneruje `UID = uuid.uuid4().hex` (32 znaků)
5. Zapíše do DATA tabulky: `UID, DATA (blob), DATASIZE, CHUNK=0`
6. Sestaví 48bajtovou BODY referenci
7. Získá `fileid` z Gen_Files
8. Vloží do FILES s BODY = reference (ne blob)
### Úprava `s03soubory.py`
- Přidán import `funkce_ext`
- Volání `funkce.zapis_file(...)` nahrazeno `funkce_ext.zapis_file_ext(...)`
### BODY reference binární formát (48 B)
```
magic 4 B = b'\xee\xbb\xaa\x0b'
uid 32 B = UUID4 hex (ASCII, 32 znaků)
dblen 4 B = len(DBNAME) jako little-endian uint32
dbname N B = DBNAME ASCII (typicky 'DB202603', 8 B)
```
Celkem typicky: 4 + 32 + 4 + 8 = **48 bajtů**
### Struktura DATA tabulky (externe DB)
```
UID VARCHAR(32) NOT NULL
DATA BLOB SUB_TYPE 0
DATASIZE INTEGER
CHUNK INTEGER
```
Generator: `GEN_UID` (Medicus ho asi nepoužívá přímo, UID je UUID string)
### EXTERNI_DB tabulka (hlavní DB)
```
DBNAME např. 'DB202603'
SERVER např. 'localhost/3053' (FBScanner proxy)
PATH např. 'u:\MEDICUS_FILES_202603.fdb'
HESLO šifrované heslo (Medicus AES) zkopírujeme z existující položky
```
Při vytváření nové DB: HESLO zkopírujeme z libovolné existující EXTERNI_DB položky
(všechny používají masterkey, jen jinak zašifrované ale stejný klíč).
### Poznámka k HESLO šifrování
Hesla v EXTERNI_DB jsou šifrovaná Medicusem. Při vytváření nové DB proto
zkopírujeme HESLO z existující položky (stejné heslo = stejný šifrovaný text).
Medicus pak dokáže novou DB otevřít standardně. Pokud by to nefungovalo,
lze heslo zadat manuálně přes správce Medicusu.
## Stav 2026-03-18 nastudováno po obnově session
### Co bylo dokončeno (včera večer)
- `funkce_ext.zapis_file_ext()` je **otestována a funkční**
- `s03soubory.py` volá `zapis_file_ext` místo původní `funkce.zapis_file`
- Celý import pipeline funguje end-to-end
### Celý import pipeline (s03soubory.py)
1. Scanuje složku `u:\NextcloudOrdinace\Dokumentace_ke_zpracování`
2. Ověří formát názvu souboru: `RC YYYY-MM-DD Jmeno, Jmeno. [prvnizavorka] [druhazavorka].pdf`
3. Ověří rodné číslo v KAR tabulce
4. Seskupí soubory podle RC (jeden pacient = jeden dekurs)
5. Pro každý PDF zavolá `funkce_ext.zapis_file_ext()` → zapíše do externí DB
6. Přesune soubor do `u:\NextcloudOrdinace\Dokumentace_zpracovaná`
7. Sestaví RTF se záložkami (klikatelné odkazy na soubory v Medicusu)
8. Zapíše dekurs do DEKURS tabulky
### Klíčové soubory
- `funkce_ext.py` zápis PDF do měsíční externí FDB (náhrada za funkce.zapis_file)
- `funkce.py` původní funkce (zapis_file, zapis_dekurs, get_files_id, get_idpac, convert_to1250)
- `s03soubory.py` hlavní import script (spouštět na Windows)
- `s04_presun_externi_db.py` utility: přesun ext DB souborů do u:\externi\ a update PATH v EXTERNI_DB
- `test_ext_db4.py` ověření BODY bajty z FILES tabulky (hex dump)
### Formát názvu souboru pro import
```
RC YYYY-MM-DD Prijmeni, Jmeno. [prvnizavorka] [druhazavorka].pdf
např: 7309208104 2026-03-17 Buzalka, Vladimír. [EKG] [normální nález].pdf
```
## Oprava RTF klikacích odkazů (2026-03-18)
### Problém
`s03soubory.py` generoval RTF s `\cs22` pro styl odkazu, ale Medicus vyžaduje `\cs32`.
Druhý+ bookmark neměl link styling vůbec. `poradi` se nikdy neinkrementoval.
### Zjištěno analýzou Medicusem vytvořeného DEKURS (ID=263480, AhojClaude.pdf)
Správný RTF formát klikacího odkazu:
```
{\*\cs32\f0\ul\fs20\cf1 Odkaz;} ← stylesheet: cs32, NE cs22
\uc1\pard\s10\plain\cs20\f0\i\fs20 Přílohy: {\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 TEXT{\*\bkmkend 0}\par
```
- "Přílohy:" je inline s prvním odkazem na stejném `\pard` řádku
- Každý další odkaz: `\pard\s10{\*\bkmkstart N}\plain\cs32\f0\ul\fs20\cf1 TEXT{\*\bkmkend N}\par`
### Opraveno v s03soubory.py + test_import_3files.py
1. `\cs22``\cs32` v stylesheet i těle RTF
2. Header `Vložené přílohy:` je na **samostatném** `\pard` řádku (RTF: `Vlo\'9een\'e9 p\'f8\'edlohy:`)
3. Všechny bookmarky mají stejný formát: `\pard\s10{\*\bkmkstart N}\plain\cs32\f0\ul\fs20\cf1 TEXT{\*\bkmkend N}\par`
4. Přidáno `poradi += 1` (bug: poradi se nikdy neinkrementoval)
5. Odstraněna proměnná `prvnibookmark` již nepotřebná
### RTF kódování českých znaků (win1250)
- ž = `\'9e`, é = `\'e9`, ř = `\'f8`, í = `\'ed`, á = `\'e1`
- "Vložené přílohy:" → `Vlo\'9een\'e9 p\'f8\'edlohy:`
## Merge do existujícího dekurzu (2026-03-18)
### Rozhodovací logika (3 případy)
1. Poslední dekurs pacienta je z dnešního dne A má `Vložené přílohy:`
**přidat soubor DO existující sekce** (`pridat_do_sekce_prilohy`)
2. Poslední dekurs je z dnešního dne, ale sekci nemá
**prepend nové sekce na začátek** (`merge_rtf_prepend`)
3. Poslední dekurs je z jiného dne / neexistuje
**nový dekurs pro dnešek**
- Funkce: `najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni)`
### pridat_do_sekce_prilohy (přidání do existující sekce)
1. Spočítat `"Files:"` v `{\info{\bookmarks}}` = N → nový bkmkstart = N
2. Posunout všechny bkmkstart/bkmkend ≥ N o +1 (uvolnit index N)
3. Vložit nový `\pard` před `\pard\s10\plain\cs15\f0\fs20 \par` (konec sekce)
4. Vložit bookmark na pozici N do `{\info{\bookmarks}}`
### merge_rtf_prepend (prepend nové sekce)
1. Posunout existující `\bkmkstart N` / `\bkmkend N` o počet nových souborů
2. Přidat naše bookmarky NA ZAČÁTEK `{\info{\bookmarks ...}}`
3. Vložit naše `\pard` tělo PŘED první `\uc1\pard` existujícího těla
### Detekce sekce
- Přítomnost: `PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"` hledáme v RTF
- Konec sekce: `PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'`
### Skripty
- `test_import_merge.py` 3 soubory → merge do dnešního dekurzu (bez sekce přílohy)
- `test_import_single.py` 1 soubor → merge DO existující sekce přílohy
- `test_import_3files.py` 3 soubory → vždy nový dekurs (bez merge logiky, starší)
### Stav testování (2026-03-18)
- `test_import_3files.py` ✅ ověřeno klikací odkazy fungují, "Vložené přílohy:" správně
- `test_import_merge.py` ✅ připraveno merge 3 souborů do dnešního dekurzu (bez sekce)
- `test_import_single.py` ✅ připraveno přidání 1 souboru DO existující sekce přílohy
### Stav s03soubory.py (2026-03-20) KOMPLETNÍ ✅
- Merge logika plně integrována z `test_import_single.py`
- Přidány konstanty `PRILOHY_HEADER`, `PRILOHY_CLOSING`
- Přidány funkce: `najdi_posledni_dekurs_dnes()`, `ma_sekci_prilohy()`, `pridat_do_sekce_prilohy()`, `merge_rtf_prepend()`
- RTF šablona přesunuta do konstanty `RTF_TEMPLATE`
- Odstraněn dead code: `prvnibookmark`, staré šablony, nepoužívané `convert_to1250`
- `pridat_do_sekce_prilohy()` podporuje **více souborů najednou** (oproti test_import_single.py, kde byl jen 1)
- `idpac` se bere z prvního záznamu skupiny (čistěji než z `row` po skončení loopu)
### Rozhodovací logika s03soubory.py 3 případy
1. Dnešní dekurs **má** sekci `Vložené přílohy` → soubory přidány **dovnitř** sekce (`pridat_do_sekce_prilohy`)
2. Dnešní dekurs **nemá** sekci příloh → nová sekce vložena **na začátek** (`merge_rtf_prepend`)
3. Žádný dnešní dekurs → **nový** záznam (INSERT)
### Stav testovacích skriptů
- `test_import_3files.py` ✅ ověřeno klikací odkazy fungují
- `test_import_merge.py` ✅ otestováno merge 3 souborů (jen 2 případy, bez detekce sekce)
- `test_import_single.py` ✅ otestováno všechny 3 případy, základ pro s03soubory.py
## Oprava BlobReader v dekurz_report.py (2026-04-01)
### Chyba
```
Exception ignored while calling deallocator <function BlobReader.__del__>:
fdb.fbcore.DatabaseError: BlobReader.close/isc_close_blob: invalid BLOB handle (-901)
```
Skript fungoval správně (exit code 0, Excel uložen), ale garbage collector házel warningy.
### Příčina
`fdb` vrací BLOB sloupce jako `BlobReader` objekty. Po `conn.close()` se při GC pokoušely
zavřít handle, který již neexistoval.
### Oprava
Explicitně zavřít `BlobReader` hned po `.read()`, ještě za živa spojení:
```python
# PŘED (špatně):
rtf = dekurs_blob.read() if hasattr(dekurs_blob, 'read') else (dekurs_blob or '')
# PO (správně):
if hasattr(dekurs_blob, 'read'):
rtf = dekurs_blob.read()
dekurs_blob.close()
else:
rtf = dekurs_blob or ''
```
### Obecné pravidlo
Kdykoli čteme BLOB z fdb, vždy `.close()` po `.read()` jinak GC po `conn.close()` hlásí chybu.
---
## Další postup (nápady)
- Otestovat `s03soubory.py` na Windows se skutečnými soubory (všechny 3 případy)
- Napsat `rtf_to_text()` pro extrakci čistého textu z dekurzů
- Prozkoumat tabulky: LECH/LECD (léky?), POU (poukazy?), AMBULEKY (výkony?)
- První report domluvit s uživatelem co chce vidět
---
## KAR.POZNAMKA automatické tagy (2026-04-09)
Pole `POZNAMKA` v tabulce KAR je **BLOB SUB_TYPE 1** (text blob, win1250)
Medicus ho renderuje jako RTF stejně jako DEKURS. Lze psát i prostý text.
### Konvence automatických tagů
Automaticky zapisované informace se ukládají na **začátek** POZNAMKA ve formátu:
```
[[klic:hodnota1, hodnota2]] #zapsáno DD-MM-YYYY HH:MM#
```
- Oddělovače `[[` a `]]` se v lékařských poznámkách přirozeně nevyskytují
- Tag lze programově najít a nahradit regexem
- Ruční text lékaře/sestry pod tagem zůstává nedotčen
### Implementovaný tag: prev_prohlidka
```
[[prev_prohlidka:15-05-2024 01022, 15-04-2026]] #zapsáno 09-04-2026 14:30#
```
- `15-05-2024` = datum poslední PP
- `01022` = kód výkonu (01022 = dospělí, 01021 = starší varianta)
- `15-04-2026` = nejdřívější možný termín příští PP (23 měsíců od poslední)
- Skript: `MedicusWithClaudeTest/prev_prohlidka.py`
---
## Tabulka VZPARC výkonové dávky pojišťovnám (zjištěno 2026-04-09)
Zachyceno přes Firebird trace log při exportu dávek z Medicusu.
### Struktura
- **1954 záznamů**, data od 2007 (s DAVKA blobem od 2013)
- Klíčové sloupce: `ID`, `ROK`, `DRUH`, `DATUMOD`, `DATUMDO`, `DAVKA` (blob), `ICP`, `ODB`, `IDICZ`
- Vazba na ICZ přes `IDICZ`
### Typy dávek (DRUH)
| DRUH | Popis |
|---|---|
| `98` | Výkonová dávka obsahuje výkony vykázané pojišťovně (DP98 formát) |
| `05` | Doplňková dávka (DP05) |
| `80` | Registrační dávka přihlášení/odhlášení pacientů (DP80) |
| `36` | Jiný typ |
### DAVKA blob formát (CP852)
Stejný formát jako KDAVKA v PORTAL, dekódování identické:
```python
text = raw.encode('cp1250', errors='replace').decode('cp852', errors='replace')
```
Struktura DP98 dávky:
```
DP98... hlavička (IČP, rok, měsíc, disk, počet případů, částka)
A ... ambulantní případ: RC pacienta na pozici 34, délka 10
V ... výkon: V + DDMM + YYYY + KOD(5) + ...
Z ... ZULP
L ... lék
```
### Parsování RC z A řádku
```python
aktualni_rc = line[34:44].strip() # pevná pozice, délka 10
```
### Parsování výkonu z V řádku
```python
dd = int(line[1:3])
mm = int(line[3:5])
yyyy = int(line[5:9])
kod = line[9:14].strip() # 5znakový kód výkonu
```
### Poznámka: PORTAL vs VZPARC
- **PORTAL** starší tabulka, KDAVKA blob, data jen do 2015
- **VZPARC** správná tabulka pro výkonové dávky, data od 2013 do dnes
- Pro hledání výkonů per pacient vždy používat **VZPARC**
---
## Skript prev_prohlidka.py (MedicusWithClaudeTest/)
Účel: najde datum poslední preventivní prohlídky (PP) pro každého registrovaného
pacienta a zapíše/aktualizuje tag v KAR.POZNAMKA.
### Co dělá
1. Načte registrované pacienty (přesný dotaz přes REGISTR + ICP, IČP=09305001)
2. Projde všechny VZPARC DRUH=98 dávky, hledá výkony `01022` nebo `01021`
3. Pro každého pacienta zachová jen **nejnovější** datum PP
4. Zapíše/přepíše tag `[[prev_prohlidka:...]]` na začátek KAR.POZNAMKA
### Výsledek posledního spuštění (2026-04-09)
- Registrovaných pacientů: 1621
- Dávek VZPARC DRUH=98: 778
- Pacientů s nalezenou PP: 1289
- Zapsáno do KAR: 1120
- Nenalezeno (odregistrovaní nebo bez PP): 169
### Závislosti
```
pip install python-dateutil
```
(fdb již musí být nainstalováno)
+37
View File
@@ -0,0 +1,37 @@
@echo off
:: Firebird SQL trace pro Medicus
:: Zachytí všechny SQL příkazy při exportu souborů do externích DB
::
:: INSTRUKCE:
:: 1. Spusť tento soubor jako Administrator (pravé tlačítko → Spustit jako správce)
:: 2. Nechej okno otevřené
:: 3. V Medicusu proveď export souborů do externích DB
:: 4. Vrať se sem a stiskni Ctrl+C pro zastavení
:: 5. Log najdeš v medicus_trace_output.txt
echo.
echo === MEDICUS SQL TRACE ===
echo Hledam Firebird fbtracemgr...
echo.
set FB_BIN=C:\Program Files\Firebird\Firebird_2_5_CGM\bin
if not exist "%FB_BIN%\fbtracemgr.exe" (
echo CHYBA: fbtracemgr.exe nenalezen v %FB_BIN%
echo Zkontroluj cestu a uprav tento soubor.
pause
exit /b 1
)
echo Nalezen: %FB_BIN%\fbtracemgr.exe
echo.
echo POZOR: Nyni PROVEĎ export v Medicusu, pak se vrat a stiskni Ctrl+C
echo Výstup se zapisuje do: medicus_trace_output.txt
echo.
powershell -Command "& '%FB_BIN%\fbtracemgr.exe' -se localhost:service_mgr -user SYSDBA -password masterkey -start -config '%~dp0medicus_trace.conf' | Tee-Object -FilePath '%~dp0medicus_trace_output.txt'"
echo.
echo === TRACE ZASTAVEN ===
echo Výsledky jsou v medicus_trace_output.txt
pause
+34
View File
@@ -0,0 +1,34 @@
import fdb, re
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute('SELECT DEKURS FROM DEKURS WHERE ID=243082')
text = cur.fetchone()[0]
text = text.read() if hasattr(text, 'read') else text
text = text.decode('windows-1250', errors='replace') if isinstance(text, bytes) else text
# colortbl
ct = re.search(r'\\colortbl[^}]+\}', text)
if ct: print('COLORTBL:', ct.group(0))
# stylesheet
ss = re.search(r'\\stylesheet\{.+?\}(?=\n)', text, re.DOTALL)
if ss: print('\nSTYLESHEET:', ss.group(0)[:600])
# Najdi různé styly použité v textu
print('\n--- Použité RTF tagy (unikátní) ---')
tags = re.findall(r'\\[a-z]+\d*', text)
from collections import Counter
for tag, count in Counter(tags).most_common(40):
print(f" {tag:<20} {count}x")
# Ukázka tabulky
tbl_start = text.find(r'\trowd')
if tbl_start > 0:
print('\n--- Začátek tabulky ---')
print(text[tbl_start:tbl_start+300])
conn.close()
@@ -0,0 +1,42 @@
"""check_last_dekurs.py zobrazí posledních 5 záznamů v DEKURS + FILES pro pacienta 9742"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250'
)
cur = conn.cursor()
print("=== Posledních 5 záznamů DEKURS pro Buzalka (idpac=9742) ===")
cur.execute("""
SELECT id, datum, cas, OCTET_LENGTH(dekurs) as delka
FROM dekurs WHERE idpac = 9742
ORDER BY id DESC ROWS 5
""")
for row in cur.fetchall():
print(f" ID={row[0]} datum={row[1]} cas={row[2]} délka={row[3]} B")
print()
print("=== Obsah posledního DEKURS záznamu ===")
cur.execute("""
SELECT dekurs FROM dekurs WHERE idpac = 9742
ORDER BY id DESC ROWS 1
""")
row = cur.fetchone()
if row:
text = row[0]
if isinstance(text, bytes):
text = text.decode('cp1250', errors='replace')
print(text)
print()
print("=== Posledních 5 záznamů FILES pro Buzalka (idpac=9742) ===")
cur.execute("""
SELECT id, filename, datum, OCTET_LENGTH(body) as bodylen
FROM files WHERE idpac = 9742
ORDER BY id DESC ROWS 5
""")
for row in cur.fetchall():
print(f" ID={row[0]} filename={row[1]} datum={row[2]} body={row[3]} B")
conn.close()
+70
View File
@@ -0,0 +1,70 @@
"""
Pomocný skript pro Clauda spusť na Windows: python db_query.py
Výsledky zapíše do db_query_result.txt
"""
import fdb
import json
import sys
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
results = {}
# 1. Tabulky s klíčovými slovy (soubory, dokumenty)
cur.execute("""
SELECT RDB$RELATION_NAME
FROM RDB$RELATIONS
WHERE RDB$SYSTEM_FLAG = 0 AND RDB$VIEW_BLR IS NULL
ORDER BY RDB$RELATION_NAME
""")
all_tables = [row[0].strip() for row in cur.fetchall()]
keywords = ['FILE', 'SOUB', 'DOC', 'SCAN', 'OBRAZ', 'IMG', 'ATTACH',
'EXTERN', 'PRILOHA', 'ZPRAV', 'ARCH', 'PRIL', 'FOTO']
matches = [t for t in all_tables if any(k in t.upper() for k in keywords)]
results['tables_matching'] = matches
results['total_tables'] = len(all_tables)
# 2. Podrobnosti o nalezených tabulkách (sloupce + počet řádků)
table_details = {}
for t in matches:
try:
cur.execute(f"SELECT COUNT(*) FROM {t}")
count = cur.fetchone()[0]
except:
count = "error"
cur.execute("""
SELECT r.RDB$FIELD_NAME, f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH
FROM RDB$RELATION_FIELDS r
JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = r.RDB$FIELD_SOURCE
WHERE r.RDB$RELATION_NAME = ?
ORDER BY r.RDB$FIELD_POSITION
""", (t,))
cols = [(row[0].strip(), row[1], row[2]) for row in cur.fetchall()]
table_details[t] = {"count": count, "columns": cols}
results['table_details'] = table_details
# 3. Uložit výsledky
output = []
output.append(f"=== VÝSLEDKY DB PRŮZKUMU ===\n")
output.append(f"Celkem tabulek v DB: {results['total_tables']}\n")
output.append(f"\nTabulky obsahující klíčová slova (soubory/dokumenty):\n")
for t in results['tables_matching']:
d = results['table_details'].get(t, {})
output.append(f"\n ** {t} ** (řádků: {d.get('count', '?')})")
for col in d.get('columns', []):
output.append(f" - {col[0]} (type={col[1]}, len={col[2]})")
with open('db_query_result.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(output))
print("Hotovo! Výsledky jsou v db_query_result.txt")
conn.close()
+75
View File
@@ -0,0 +1,75 @@
"""
Podrobný průzkum FILES a EXTERNI_DB spusť na Windows: python db_query2.py
"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
out = []
# 1. Obsah EXTERNI_DB
out.append("=== EXTERNI_DB (konfigurace externích databází) ===")
cur.execute("SELECT DBNAME, PATH, SERVER, HESLO FROM EXTERNI_DB")
for row in cur.fetchall():
out.append(f" DBNAME={row[0]} PATH={row[1]} SERVER={row[2]} HESLO={row[3]}")
# 2. FILES přehled po rocích
out.append("\n=== FILES počty záznamů po rocích ===")
cur.execute("""
SELECT EXTRACT(YEAR FROM DATUM) as ROK, COUNT(*) as POCET,
SUM(OCTET_LENGTH(BODY)) as CELKEM_BYTU
FROM FILES
GROUP BY EXTRACT(YEAR FROM DATUM)
ORDER BY ROK
""")
total_bytes = 0
for row in cur.fetchall():
rok, pocet, bytu = row
mb = (bytu or 0) / 1024 / 1024
total_bytes += (bytu or 0)
out.append(f" Rok {int(rok) if rok else '?'}: {pocet} souborů, {mb:.1f} MB")
out.append(f" CELKEM: {total_bytes/1024/1024:.1f} MB v BLOB datech FILES")
# 3. FILES ukázka typů souborů
out.append("\n=== FILES typy souborů (dle přípony FILENAME) ===")
cur.execute("""
SELECT LOWER(SUBSTRING(FILENAME FROM CHAR_LENGTH(FILENAME) - 3)) as EXT,
COUNT(*) as POCET
FROM FILES
WHERE FILENAME IS NOT NULL
GROUP BY 1
ORDER BY 2 DESC
""")
for row in cur.fetchall():
out.append(f" {row[0]} : {row[1]} souborů")
# 4. FILES ukázka prvních 5 záznamů (bez BODY)
out.append("\n=== FILES ukázka 5 záznamů (bez binárních dat) ===")
cur.execute("""
SELECT ID, IDPAC, FILENAME, DATUM, OCTET_LENGTH(BODY) as VELIKOST,
POZNAMKA, IDDOCTYP
FROM FILES
ORDER BY DATUM DESC
ROWS 5
""")
for row in cur.fetchall():
out.append(f" ID={row[0]} IDPAC={row[1]} soubor={row[2]} datum={row[3]}"
f" velikost={row[4]}B pozn={row[5]} typ={row[6]}")
# 5. DOCTYP typy dokumentů
out.append("\n=== DOCTYP číselník typů dokumentů ===")
cur.execute("SELECT ID, NAZEV, SYSID FROM DOCTYP ORDER BY ID")
for row in cur.fetchall():
out.append(f" ID={row[0]} nazev={row[1]} sysid={row[2]}")
result = '\n'.join(out)
print(result)
with open('db_query2_result.txt', 'w', encoding='utf-8') as f:
f.write(result)
print("\nUloženo do db_query2_result.txt")
conn.close()
@@ -0,0 +1,532 @@
=== VÝSLEDKY DB PRŮZKUMU ===
Celkem tabulek v DB: 993
Tabulky obsahující klíčová slova (soubory/dokumenty):
** CHAT_ZPRAVY ** (řádků: 0)
- ID (type=8, len=4)
- IDADRESAR (type=8, len=4)
- ODCHOZI (type=14, len=1)
- ODESLANO (type=35, len=8)
- TEXT (type=261, len=8)
- OTHER_ID (type=8, len=4)
- PRECTENO (type=35, len=8)
- SENT (type=35, len=8)
** CLICKDOC ** (řádků: 2)
- KEY (type=37, len=100)
- VALUE (type=37, len=100)
** CLICKDOC_CINNOSTI ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDCINNOSTI (type=8, len=4)
** CLICKDOC_KAR ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDPAC (type=8, len=4)
** CLICKDOC_OBJ ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDOBJ (type=8, len=4)
- REMOTECREATE (type=14, len=1)
- REMOTECHANGE (type=14, len=1)
- APPROVAL (type=7, len=2)
- SEND_REMINDER_SMS (type=14, len=1)
** CLICKDOC_UZIVATEL ** (řádků: 0)
- IDCLICKDOC (type=8, len=4)
- IDUZI (type=8, len=4)
** DEKIMG ** (řádků: 8)
- ID (type=8, len=4)
- HASH (type=37, len=32)
- REFCOUNT (type=8, len=4)
- DATA (type=261, len=8)
** DEKIMGREF ** (řádků: 7)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- IDDEKURS (type=8, len=4)
- IDDEKIMG (type=8, len=4)
** DOCLIST ** (řádků: 461429)
- ID (type=8, len=4)
- DATUM (type=12, len=4)
- TABULKA (type=37, len=20)
- IDREC (type=8, len=4)
- TYP (type=7, len=2)
- POPIS (type=37, len=40)
- IDPAC (type=8, len=4)
- IDODDEL (type=8, len=4)
** DOCTYP ** (řádků: 27)
- ID (type=8, len=4)
- NAZEV (type=37, len=32)
- SYSID (type=8, len=4)
** DOMPECEARCHIV ** (řádků: 0)
- ID (type=8, len=4)
- CREATED (type=35, len=8)
- DATA (type=261, len=8)
** ED_BOOKOFSUBMISSIONATTACH ** (řádků: 0)
- ID (type=8, len=4)
- BOOK_OF_SUBMISSION_ID (type=8, len=4)
- ORDER (type=8, len=4)
- HAS_ALREADY_BEEN_SENT (type=14, len=1)
- NAME (type=37, len=255)
- CONTENT (type=261, len=8)
** EET_ZPRAVARECORD ** (řádků: 0)
- ID (type=8, len=4)
- UUID_ZPRAVY (type=14, len=16)
- ID_TRZBA (type=8, len=4)
- GUID_TRZBA (type=14, len=16)
- DAT_ODESL (type=35, len=8)
- CERTTHUMBPRINT (type=37, len=64)
- REQUEST (type=261, len=8)
- RESPONSE (type=261, len=8)
- RESP_TYPE (type=8, len=4)
- RESP_UUID_ZPRAVY (type=37, len=50)
- RESP_DAT_PRIJ (type=35, len=8)
- RESP_DAT_ODMIT (type=35, len=8)
- RESP_BKP (type=37, len=44)
- RESP_FIK (type=37, len=39)
- RESP_KOD (type=8, len=4)
- RESP_CHYBA (type=37, len=100)
- PRVNI_ZASLANI (type=7, len=2)
- RESP_TEST (type=7, len=2)
- RESP_EXCEPTIONMESSAGE (type=261, len=8)
** ES_KONZULTACE_FILE ** (řádků: 0)
- ID (type=8, len=4)
- ID_KONZULTACE (type=8, len=4)
- CAS (type=35, len=8)
- NAME (type=37, len=256)
- CONTENT (type=261, len=8)
- MIME (type=37, len=100)
- PATIENT_DOC (type=14, len=1)
- PORADI (type=8, len=4)
** EXTERNI_DB ** (řádků: 1)
- DBNAME (type=37, len=12)
- PATH (type=37, len=120)
- SERVER (type=37, len=32)
- HESLO (type=37, len=64)
** FILES ** (řádků: 10305)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DOCID (type=8, len=4)
- TYP (type=8, len=4)
- FILENAME (type=37, len=254)
- BODY (type=261, len=8)
- DATUM (type=12, len=4)
- IDDOCTYP (type=8, len=4)
- IDPRAC (type=8, len=4)
- IDUZI (type=8, len=4)
- POZNAMKA (type=37, len=100)
- DATSOUBORU (type=35, len=8)
- ID_EDOKUMENT (type=8, len=4)
- EXT_ID (type=37, len=36)
** FNUSA_ATTACHMENT ** (řádků: 0)
- ID (type=8, len=4)
- DATA (type=261, len=8)
- ATTACH_INT_TYP (type=37, len=1)
- ATTACH_INT_ID (type=8, len=4)
- LAST_UPDATED (type=35, len=8)
** GRARCH ** (řádků: 0)
- ID (type=8, len=4)
- NAME (type=37, len=16)
- PATH (type=37, len=150)
** GRDOC ** (řádků: 0)
- ID (type=8, len=4)
- IDNEM (type=37, len=60)
- IDPAC (type=8, len=4)
- SKUPINA (type=8, len=4)
- DATUM (type=12, len=4)
- PREVIEW (type=261, len=8)
- PICT (type=261, len=8)
- LOCATION (type=37, len=16)
** HISTDOC ** (řádků: 32250)
- ID (type=8, len=4)
- TYP (type=37, len=7)
- DATUM (type=12, len=4)
- DATA (type=261, len=8)
- CENA (type=16, len=8)
- REPPOZN (type=37, len=60)
- REPDATUM (type=12, len=4)
- IDPACI (type=8, len=4)
- IDZAR (type=8, len=4)
- IDODD (type=8, len=4)
- IDPRAC (type=8, len=4)
- IDUZIV (type=8, len=4)
- IDLEK (type=8, len=4)
- IDZARPR (type=8, len=4)
- IDODDPR (type=8, len=4)
- IDPRACPR (type=8, len=4)
- IDLEKPR (type=8, len=4)
- STAV (type=14, len=1)
- REPSTAV (type=14, len=1)
- IDSABLONPRAC (type=37, len=55)
- REPTEXT (type=261, len=8)
- TYPCENY (type=14, len=1)
- CENAPACIENT (type=16, len=8)
- IDDOKLAD (type=8, len=4)
- POZNAMKA (type=37, len=100)
- PRINTED (type=14, len=1)
- PRIJALJINE (type=37, len=150)
- BMKDATA (type=261, len=8)
- PORCISLO (type=8, len=4)
- DRUHPOJ (type=8, len=4)
- IDLEKZPR (type=8, len=4)
- IDHLAV (type=8, len=4)
- CREATED (type=35, len=8)
- IDSOUHLASPACSABL (type=8, len=4)
- REPIDUZIVINS (type=8, len=4)
- REPIDUZIVUPD (type=8, len=4)
- CGMNUMBER_ODESILATEL (type=37, len=12)
- CGMNUMBER_PRIJEMCE (type=37, len=12)
- CLICKBOX_ATT_ID (type=8, len=4)
- CLICKBOX_MAILBOX (type=37, len=255)
- ID_EDOKUMENT (type=8, len=4)
** HISTDOCLAB ** (řádků: 27185)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDMETOD (type=8, len=4)
- POZN (type=261, len=8)
- IDPLADET (type=8, len=4)
** HISTDOCLABPARAMS ** (řádků: 0)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDHISTDOCLAB (type=8, len=4)
- IDPARAM (type=8, len=4)
- TYP (type=14, len=1)
- TEXT (type=14, len=8)
** HISTDOCSABL ** (řádků: 7)
- ID (type=8, len=4)
- TYP (type=37, len=7)
- DATA (type=261, len=8)
- IDZAR (type=8, len=4)
- IDODD (type=8, len=4)
- IDPRAC (type=8, len=4)
- IDUZIV (type=8, len=4)
- IDLEK (type=8, len=4)
- IDZARPR (type=8, len=4)
- IDODDPR (type=8, len=4)
- IDPRACPR (type=8, len=4)
- IDLEKPR (type=8, len=4)
- STAV (type=14, len=1)
- IDSABLONPRAC (type=37, len=55)
- NAZEV (type=37, len=48)
- REPSTAV (type=14, len=1)
- CENA (type=37, len=20)
- DOPLATEK (type=37, len=20)
- USEPRAC (type=8, len=4)
- USEUZIV (type=8, len=4)
** HISTDOCSABLLAB ** (řádků: 19)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDMETOD (type=8, len=4)
** HISTDOCVYK ** (řádků: 1868)
- IDHDVYK (type=8, len=4)
- ID (type=8, len=4)
- KOD (type=14, len=7)
- POCET (type=16, len=8)
- CENA (type=16, len=8)
- CENADOPL (type=16, len=8)
- POJ (type=14, len=3)
- ICZ (type=14, len=8)
- ICP (type=14, len=8)
- DATUM (type=12, len=4)
- REV (type=14, len=1)
- TYP (type=7, len=2)
- NOT_CENA_PRUMER (type=14, len=1)
- VARSYM (type=37, len=20)
- SKUPINA (type=14, len=2)
** HISTDOC_DEF ** (řádků: 0)
- ID (type=8, len=4)
- KOD (type=37, len=5)
- NAZEV (type=37, len=40)
- IDUZI (type=8, len=4)
- DATA (type=261, len=8)
- SKUPINA (type=8, len=4)
** HISTDOC_EPOUKAZ ** (řádků: 849)
- IDHISTDOC (type=8, len=4)
- ID_DOKLADU (type=14, len=9)
- IDUZI (type=8, len=4)
- IDPRAC (type=8, len=4)
- ODESLANO (type=35, len=8)
- CHYBA (type=14, len=1)
- SCHVALENI (type=14, len=1)
- MODIFIED (type=14, len=1)
- ODESLANO_LAST (type=35, len=8)
- ID_ZP (type=37, len=36)
- ID_DOKLADU_VYDEJ (type=37, len=9)
- ID_ZP_VYDEJ (type=37, len=36)
- VYDANO (type=35, len=8)
** HISTDOC_EPOUKAZ_ZMENY ** (řádků: 3)
- ID_LEKAR (type=37, len=36)
- LAST_DATE (type=12, len=4)
** HISTDOC_EPRILOHY ** (řádků: 17)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- NAZEV (type=37, len=255)
- POPIS (type=37, len=255)
- TYP (type=37, len=7)
- SOUBOR (type=261, len=8)
- ID_PRILOHY (type=37, len=36)
** HISTDOC_EZADANKA ** (řádků: 2195)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- TYP (type=37, len=10)
- DATUMVYS (type=12, len=4)
- CASVYS (type=13, len=4)
- CISLO (type=37, len=30)
- POZNAMKA (type=261, len=8)
- HOTOVO (type=14, len=1)
- IDZAD (type=8, len=4)
- VYSTEXT (type=261, len=8)
- DATUMEXP (type=12, len=4)
- CASEXP (type=13, len=4)
- SABLONY (type=261, len=8)
- PRINTED (type=35, len=8)
- SENDED (type=35, len=8)
- TEMPDAT (type=35, len=8)
- SEZNAMMETOD (type=261, len=8)
- SEZNAMSABLON (type=261, len=8)
- DIAG1 (type=37, len=10)
- DIAG2 (type=37, len=10)
- DIAG3 (type=37, len=10)
- STATIM (type=14, len=1)
- ODEBRANO (type=35, len=8)
- BEZODBERU (type=14, len=1)
- EXPORTOVAT (type=35, len=8)
- NAH1 (type=37, len=10)
- NAH2 (type=37, len=10)
- ODEBRAL (type=37, len=30)
- POCETZKUM (type=8, len=4)
- OS_NAZEV (type=37, len=100)
- OS_ID (type=37, len=20)
- PARAMETRY (type=261, len=8)
- POZNAMKY (type=261, len=8)
- STRINGPARAMETRY (type=261, len=8)
- VZOREK1 (type=37, len=25)
- VZOREK2 (type=37, len=25)
- VZOREK3 (type=37, len=25)
- VZOREK4 (type=37, len=25)
- PLATCE (type=37, len=30)
- OSVOBOZENDPH (type=14, len=1)
- PLATCEPOZNAMKA (type=37, len=100)
- KATALOGALIAS (type=37, len=20)
- KATALOGNAZEV (type=37, len=100)
- PRIMARNIVZORKY (type=37, len=256)
** HISTDOC_SCHVALENI ** (řádků: 0)
- ID (type=8, len=4)
- IDHISTDOC (type=8, len=4)
- IDVZPARC (type=8, len=4)
- PRILOHA (type=261, len=8)
- ODESLANO (type=14, len=1)
** HOSP_ZPRAVY ** (řádků: 0)
- ID (type=8, len=4)
- DATUM (type=12, len=4)
- CAS (type=13, len=4)
- ZPRAVA (type=261, len=8)
- IDUZI (type=8, len=4)
- IDPAC (type=8, len=4)
- ID_EDOKUMENT (type=8, len=4)
- IDHOSP (type=8, len=4)
** IZIPDOC ** (řádků: 0)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DATUM (type=12, len=4)
- STATUS (type=8, len=4)
- IDENT (type=8, len=4)
- ZAPIS (type=261, len=8)
- DEKID (type=8, len=4)
- DGN (type=14, len=5)
- CAS (type=13, len=4)
- IDZAR (type=8, len=4)
- IDODD (type=8, len=4)
- IDPRAC (type=8, len=4)
- AUTOR (type=8, len=4)
- LOCKED (type=14, len=1)
- TYP (type=14, len=1)
- VDGN1 (type=14, len=5)
- VDGN2 (type=14, len=5)
- VDGN3 (type=14, len=5)
- VDGN4 (type=14, len=5)
** IZIPFILES ** (řádků: 0)
- ID (type=8, len=4)
- DOCID (type=8, len=4)
- TYP (type=8, len=4)
- FILENAME (type=37, len=50)
- BODY (type=261, len=8)
- DATUM (type=12, len=4)
** LEKZPRAVY ** (řádků: 13748)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DEKURSID (type=8, len=4)
- DATUM (type=12, len=4)
- TEXT (type=261, len=8)
- IDUZI (type=8, len=4)
- IDNOSVYK (type=8, len=4)
- TYP (type=14, len=1)
- ODESILATEL (type=37, len=254)
- DEKURS_COPYED (type=35, len=8)
- EISPUBLIC (type=14, len=1)
- PRIJEMCE (type=37, len=254)
- IDPRAC (type=8, len=4)
- SIGNATURE (type=261, len=8)
- SIGNATURE_INFO (type=261, len=8)
- IDCERTIFICATE (type=8, len=4)
- TST (type=261, len=8)
- NAZEV (type=37, len=80)
- ID_EDOKUMENT (type=8, len=4)
- PRIJATO (type=14, len=1)
- JMENOLEK (type=37, len=50)
- ICP (type=14, len=8)
- ES_STAV (type=37, len=1)
- PR_TYP (type=14, len=1)
- PR_ID (type=8, len=4)
- STORNO (type=14, len=1)
- UID (type=37, len=16)
- PUVODNI (type=8, len=4)
- CAS (type=13, len=4)
- DATAKT (type=35, len=8)
- ODALIAS (type=37, len=8)
- PDF (type=261, len=8)
- PREPIS (type=8, len=4)
- OZNACENI_O (type=37, len=50)
- STAV (type=14, len=1)
- CLICKBOX_ATT_ID (type=8, len=4)
- CGMNUMBER_ODESILATEL (type=37, len=12)
- CLICKBOX_MAILBOX (type=37, len=255)
- CGMNUMBER_PRIJEMCE (type=37, len=12)
- CGMNUMBER (type=37, len=12)
** LEKZPRAVY_PR ** (řádků: 134)
- ID (type=8, len=4)
- LEKZPRID (type=8, len=4)
- PRID (type=8, len=4)
- IDUZI (type=8, len=4)
- TYP (type=14, len=1)
- EXT (type=14, len=4)
** LEKZPRAVY_SEND ** (řádků: 0)
- LEKZPRID (type=8, len=4)
- KDY (type=35, len=8)
- KOMU (type=37, len=80)
** MEDINETIN_ATTACHMENT ** (řádků: 120)
- ID (type=8, len=4)
- ID_MEDINETIN (type=8, len=4)
- FILE_NAME (type=37, len=254)
- FILE_EXT (type=37, len=5)
- FILE_TYPE (type=8, len=4)
- DATA (type=261, len=8)
- ID_FILES (type=8, len=4)
- ZPRACOVANO (type=14, len=1)
** MEDINETOUT_ATTACHMENT ** (řádků: 2236)
- ID (type=8, len=4)
- ID_MEDINETOUT (type=8, len=4)
- FILE_ID (type=37, len=36)
- FILE_NAME (type=37, len=254)
- FILE_EXT (type=37, len=5)
- FILE_TYPE (type=8, len=4)
- DATA (type=261, len=8)
- ATTACH_INT_TYP (type=37, len=1)
- ATTACH_INT_ID (type=8, len=4)
** OSE_ZPRAVY ** (řádků: 0)
- ID (type=8, len=4)
- IDPAC (type=8, len=4)
- DATUM (type=35, len=8)
- UZI (type=37, len=3)
- DATA (type=261, len=8)
- SIGNATURE (type=261, len=8)
- SIGNATURE_INFO (type=261, len=8)
- IDCERTIFICATE (type=8, len=4)
- STORNO (type=35, len=8)
- STORNOBY (type=14, len=3)
** SOC_FINPRIJEM_HISTDOC ** (řádků: 0)
- ID_SFH (type=8, len=4)
- ID_SFP (type=8, len=4)
- ID_HDC (type=8, len=4)
** ZPRAVY ** (řádků: 26)
- ID (type=8, len=4)
- IDZPRAVY (type=8, len=4)
- IDOD (type=8, len=4)
- IDKOMUZAR (type=8, len=4)
- IDKOMUODD (type=8, len=4)
- IDKOMUPRAC (type=8, len=4)
- IDKOMUUZIV (type=8, len=4)
- PREDMET (type=37, len=100)
- TEXT (type=261, len=8)
- DATUM (type=12, len=4)
- CAS (type=13, len=4)
- URGENTNI (type=8, len=4)
- PRECTENA (type=8, len=4)
- DATUMPRECT (type=12, len=4)
- CASPRECT (type=13, len=4)
- SMAZANA (type=8, len=4)
- OZNAMOPRECT (type=8, len=4)
- TYP (type=8, len=4)
- KOMU (type=261, len=8)
- PLATNOST (type=35, len=8)
- LZE_ODPOVED (type=14, len=1)
- ID_EXT (type=8, len=4)
** ZPRAVY_ANKETY ** (řádků: 0)
- ID (type=8, len=4)
- IDUZI (type=8, len=4)
- IDPRIJEMCE (type=8, len=4)
- ID_EXT (type=8, len=4)
- DATA (type=261, len=8)
- DATUM (type=12, len=4)
- ODESLANO (type=14, len=1)
** ZPRAVY_EXT ** (řádků: 18)
- ID (type=8, len=4)
- ID_EXT (type=8, len=4)
- ID_ODESILATEL (type=8, len=4)
- TYP (type=37, len=1)
- PRIORITA (type=37, len=1)
- DATUM (type=35, len=8)
- PREDMET (type=37, len=254)
- DATA (type=261, len=8)
- STAV (type=37, len=1)
- AKCE (type=37, len=20)
- PRIJEMCE (type=37, len=3)
- ODESLAL (type=37, len=20)
- ODB (type=37, len=3)
** ZPRAVY_LOCK ** (řádků: 1)
- IDUZI (type=8, len=4)
- BEGINTIME (type=35, len=8)
- ENDTIME (type=35, len=8)
+45
View File
@@ -0,0 +1,45 @@
"""
Průzkum Medicus databáze výpis tabulek a jejich sloupců.
"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
# Všechny uživatelské tabulky
cur.execute("""
SELECT rdb$relation_name
FROM rdb$relations
WHERE rdb$view_blr IS NULL
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
ORDER BY rdb$relation_name
""")
tables = [row[0].strip() for row in cur.fetchall()]
print(f"Počet tabulek: {len(tables)}\n")
print("=== SEZNAM TABULEK ===")
for t in tables:
print(f" {t}")
print("\n=== SLOUPCE KAŽDÉ TABULKY ===")
for table in tables:
cur.execute("""
SELECT rf.rdb$field_name, f.rdb$field_type, f.rdb$field_length
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE rf.rdb$relation_name = ?
ORDER BY rf.rdb$field_position
""", (table,))
cols = cur.fetchall()
col_names = [c[0].strip() for c in cols]
print(f"\n{table}")
print(" " + ", ".join(col_names))
cur.close()
conn.close()
+114
View File
@@ -0,0 +1,114 @@
import os,fdb,datetime
# conn = fdb.connect(
# dsn=r'localhost:u:\medicus 3\data\medicus.fdb', # Database path
# user='SYSDBA', # Username
# password="masterkey", # Password,
# charset="win1250")
# cur = conn.cursor()
def zapis_file(vstupconnection,idpac,cesta,souborname,prvnizavorka,soubordate,souborfiledate,poznamka):
import funkce
cur=vstupconnection.cursor()
fileid = funkce.get_files_id(vstupconnection)
with open(os.path.join(cesta,souborname), 'rb') as f:
daticka = f.read()
query = "insert into files (id,iduzi,iddoctyp,typ,idpac,filename,body,datum,datsouboru,poznamka) values(?,?,?,?,?,?,?,?,?,?)"
cur.execute(query,(fileid,6,1,1,idpac,prvnizavorka+".pdf",daticka,soubordate,souborfiledate,poznamka[:99]))
vstupconnection.commit()
return fileid
def zapis_dekurs(vstupconnection, idpac, idodd, iduzi, idprac, idfile, filename, text, datumzpravy,
datumsouboru):
import funkce
dekursid = funkce.get_dekurs_id(vstupconnection)
cur = vstupconnection.cursor()
print("Funkce zapis_dekurs hlasí OK")
print("idpac", idpac)
print("idodd", idodd)
print("iduzi", iduzi)
print("idfile", idfile)
print("filename", filename)
print("text", text)
print("datumzpravy", datumzpravy)
print("datumsouboru", datumsouboru)
print("dekursid", dekursid)
# rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks "BOOKMARKNAME","Files:FILEID",9}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
# {\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
# {\stylesheet{\s0\fi0\li0\ql\ri0\sb0\sa0 Norm\'e1ln\'ed;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
# \uc1\pard\s0\plain\cs20\f0\i\fs20 P\'f8\'edlohy: {\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 BOOKMARKNAME{\*\bkmkend 0}\par
# \pard\s0\plain\cs15\f0\fs20 \par
# }
# """
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks "BOOKMARKNAME","Files:FILEID",9}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs22\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 P\'f8\'edlohy:\par
\pard\s10{\*\bkmkstart 0}\plain\cs22\f0\ul\fs20\cf1 BOOKMARKNAME{\*\bkmkend 0}\par
\pard\s10\plain\cs15\f0\fs20 \par
}
"""
# id idpac filename body docid typ datum iddoctyp poznamka idpac=2 iduzi=2 datsouboru id_edokument ext_id
encodedbookmark = funkce.convert_to1250(filename)
print("Encodedbookmark", encodedbookmark)
rtf = rtf.replace("BOOKMARKNAME", encodedbookmark)
rtf = rtf.replace("FILEID", str(idfile))
rtf = rtf.replace("TEXTENTER", text)
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
print("Datumzapisu", datumzapisu)
print("Caszapisu", caszapisu)
print("RTF", rtf)
cur.execute("insert into dekurs (id,idpac,idodd,iduzi,idprac,datum,cas,dekurs) values(?,?,?,?,?,?,?,?)",
(dekursid, idpac, idodd, iduzi, idprac, datumzapisu, caszapisu, rtf))
vstupconnection.commit()
def convert_to1250(retezec):
retezec=retezec.encode("cp1250")
retezec=str(retezec)[2:]
retezec = retezec[:-1]
retezec=retezec.replace(r"\x",r"\'")
return retezec
# x=convert_to1250("Příloha")
# print(x,len(x))
def get_dekurs_id(connection):
try:
query = "SELECT GEN_ID(Gen_Dekurs, 1) FROM RDB$DATABASE"
cur = connection.cursor()
cur.execute(query)
newid=cur.fetchone()[0]
print("Funkce GET_DEKURS_ID přiřadila nové ID:",newid)
return(newid)
except:
print("Funkce GET_DEKURS_ID nepřiřadila nové ID")
return(None)
def get_files_id(connection):
try:
query = "SELECT GEN_ID(Gen_Files, 1) FROM RDB$DATABASE"
cur=connection.cursor()
cur.execute(query)
newid=cur.fetchone()[0]
print(newid)
return(newid)
except:
return(None)
def get_idpac(rodnecislo,connection):
try:
query = "SELECT idpac,prijmeni FROM kar where rodcis=?"
cur = connection.cursor()
cur.execute(query,(rodnecislo,))
tmp_nacteno=cur.fetchone()
tmp_id = tmp_nacteno[0]
tmp_jmeno=tmp_nacteno[1]
print(f"Pacient s rodným číslem {rodnecislo} má ID {tmp_id} a jméno {tmp_jmeno}")
return (tmp_id)
except:
return(None)
+184
View File
@@ -0,0 +1,184 @@
"""funkce_ext.py zápis PDF souborů přímo do měsíční externí Firebird DB
místo do hlavní medicus.fdb spouštět na Windows
Náhrada za funkce.zapis_file() v s03soubory.py.
Binární data souboru se uloží do MEDICUS_FILES_YYYYMM.fdb (DATA tabulka),
do hlavní FILES.BODY se vloží 48bajtová reference.
Formát FILES.BODY reference (48 B):
magic 4 B = b'\\xee\\xbb\\xaa\\x0b'
uid 32 B = UUID4 hex (ASCII, 32 znaků)
dblen 4 B = délka DBNAME jako little-endian uint32
dbname N B = DBNAME ASCII (např. 'DB202603', 8 B)
"""
import os
import struct
import uuid
import fdb
# Magic bajty identifikující referenci na externí DB
MAGIC = b'\xee\xbb\xaa\x0b'
# ─── Pomocné funkce ──────────────────────────────────────────────────────────
def make_body_ref(uid: str, dbname: str) -> bytes:
"""Sestaví 48bajtovou binární referenci pro FILES.BODY."""
uid_bytes = uid.encode('ascii') # 32 B
dbname_bytes = dbname.encode('ascii') # typicky 8 B ('DB202603')
return MAGIC + uid_bytes + struct.pack('<I', len(dbname_bytes)) + dbname_bytes
def _get_ext_server(vstupconnection):
"""Zjistí SERVER z existující EXTERNI_DB (např. 'localhost/3053')."""
cur = vstupconnection.cursor()
cur.execute(
"SELECT SERVER FROM EXTERNI_DB WHERE DBNAME LIKE 'DB%' ORDER BY DBNAME DESC ROWS 1"
)
row = cur.fetchone()
return row[0] if row else 'localhost/3053'
def _get_ext_heslo(vstupconnection):
"""Přečte šifrované heslo z existující EXTERNI_DB.
Všechny externí DB mají stejné heslo (masterkey, jen zašifrované Medicusem),
takže stačí vzít heslo z libovolné existující položky."""
cur = vstupconnection.cursor()
cur.execute(
"SELECT HESLO FROM EXTERNI_DB WHERE DBNAME LIKE 'DB%' ORDER BY DBNAME DESC ROWS 1"
)
row = cur.fetchone()
return row[0] if row else 'masterkey'
def _vytvor_externi_db(ext_path):
"""Vytvoří novou prázdnou externí Firebird DB s tabulkou DATA a generátorem GEN_UID.
Schéma odpovídá tomu, co vytváří Medicus sám při prvním exportu daného měsíce."""
print(f" Vytvářím novou ext DB: {ext_path}")
conn = fdb.create_database(
dsn=f'localhost:{ext_path}',
user='SYSDBA',
password='masterkey',
charset='WIN1250',
page_size=16384
)
cur = conn.cursor()
cur.execute("""
CREATE TABLE DATA (
UID VARCHAR(32) NOT NULL,
DATA BLOB SUB_TYPE 0,
DATASIZE INTEGER,
CHUNK INTEGER
)
""")
cur.execute("CREATE GENERATOR GEN_UID")
conn.commit()
print(f" Nová ext DB vytvořena: {ext_path}")
return conn
# ─── Hlavní funkce ───────────────────────────────────────────────────────────
def zapis_file_ext(vstupconnection, idpac, cesta, souborname, prvnizavorka,
soubordate, souborfiledate, poznamka,
ext_base_path=r'u:\externi'):
"""Zapíše PDF soubor do měsíční externí DB a vrátí fileid.
Parametry jsou shodné s funkce.zapis_file() stačí prohodit import.
Postup:
1. Určí měsíc z soubordate → DBNAME (např. 'DB202603')
2. Načte binární data PDF
3. Najde nebo vytvoří měsíční MEDICUS_FILES_YYYYMM.fdb
4. Zapíše data do DATA tabulky pod novým UID (UUID4 hex)
5. Sestaví 48bajtovou BODY referenci
6. Získá nové FILES ID z Gen_Files
7. Vloží záznam do hlavní FILES tabulky (BODY = reference, ne BLOB)
8. Vrátí fileid (shodné s funkce.zapis_file())
"""
import funkce # get_files_id ze stávajícího funkce.py
# 1. Měsíc a DBNAME
yyyymm = soubordate.strftime('%Y%m')
dbname = f'DB{yyyymm}'
print(f" DBNAME: {dbname}")
# 2. Binární data PDF
soubor_cesta = os.path.join(cesta, souborname)
with open(soubor_cesta, 'rb') as f:
data = f.read()
print(f" Načten soubor: {souborname} ({len(data):,} B)")
# 3. Připojení k externí DB
cur = vstupconnection.cursor()
cur.execute(
"SELECT SERVER, PATH FROM EXTERNI_DB WHERE DBNAME = ?", (dbname,)
)
row = cur.fetchone()
if row:
# Existující měsíční DB připojíme se
ext_server_raw, ext_path = row[0], row[1]
# SERVER je "localhost/3053" (FBScanner proxy) bereme jen hostname
ext_server = ext_server_raw.split('/')[0]
print(f" Existující ext DB: {dbname}{ext_path}")
conn_ext = fdb.connect(
dsn=f'{ext_server}:{ext_path}',
user='SYSDBA',
password='masterkey',
charset='WIN1250'
)
else:
# Nová měsíční DB vytvoříme a zaregistrujeme
ext_filename = f'MEDICUS_FILES_{yyyymm}.fdb'
ext_path = os.path.join(ext_base_path, ext_filename)
conn_ext = _vytvor_externi_db(ext_path)
# Zaregistrovat do EXTERNI_DB (heslo zkopírujeme z existující položky)
ext_server = _get_ext_server(vstupconnection)
heslo = _get_ext_heslo(vstupconnection)
cur.execute(
"INSERT INTO EXTERNI_DB (DBNAME, SERVER, PATH, HESLO) VALUES (?, ?, ?, ?)",
(dbname, ext_server, ext_path, heslo)
)
vstupconnection.commit()
print(f" Registrována v EXTERNI_DB: {dbname}, server={ext_server}")
# 4. Zápis do DATA tabulky
uid = uuid.uuid4().hex # 32 hex znaků, vždy unikátní
cur_ext = conn_ext.cursor()
cur_ext.execute(
"INSERT INTO DATA (UID, DATA, DATASIZE, CHUNK) VALUES (?, ?, ?, ?)",
(uid, data, len(data), 0)
)
conn_ext.commit()
conn_ext.close()
print(f" Zapsáno do ext DB: UID={uid}")
# 5. BODY reference (48 B)
body_ref = make_body_ref(uid, dbname)
print(f" BODY ref: {body_ref.hex()}")
# 6. Nové FILES ID
fileid = funkce.get_files_id(vstupconnection)
if fileid is None:
raise RuntimeError("Nepodařilo se získat nové FILES ID (Gen_Files selhal)!")
# 7. INSERT do hlavní FILES tabulky
cur.execute(
"INSERT INTO FILES "
"(id, iduzi, iddoctyp, typ, idpac, filename, body, datum, datsouboru, poznamka) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(fileid, 6, 1, 1, idpac,
prvnizavorka + '.pdf',
body_ref,
soubordate, souborfiledate,
poznamka[:99])
)
vstupconnection.commit()
print(f" FILES záznam vytvořen: ID={fileid}, idpac={idpac}, "
f"soubor={prvnizavorka}.pdf")
return fileid
+23
View File
@@ -0,0 +1,23 @@
import fdb
import datetime
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute('SELECT MAX(ID) FROM DEKURS')
next_id = cur.fetchone()[0] + 1
print('Next ID:', next_id)
text = r'{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\fonttbl{\f0\fnil\fcharset238 Arial;}}{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}' + '\n' + r'\pard\plain\f0\fs20 Test Claude AI - automaticky vlo\u382en\'fd z\'e1znam 13.03.2026\par' + '\n}'
cur.execute("""
INSERT INTO DEKURS (ID, IDPAC, IDODD, IDPRAC, IDUZI, DATUM, CAS, DEKURS, DGN1)
VALUES (?, 9742, 2, 2, 6, ?, ?, ?, 'Z000 ')
""", (next_id, datetime.date(2026, 3, 13), datetime.time(12, 0, 0), text))
conn.commit()
print('Vloženo OK, ID:', next_id)
conn.close()
@@ -0,0 +1,28 @@
import fdb
import sys
table = sys.argv[1] if len(sys.argv) > 1 else 'DEKURS'
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute("""
SELECT rf.rdb$field_name, f.rdb$field_type, f.rdb$field_length
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE rf.rdb$relation_name = ?
ORDER BY rf.rdb$field_position
""", (table,))
cols = cur.fetchall()
print(f"=== {table} sloupce ===")
for c in cols:
print(f" {c[0].strip():<30} typ: {c[1]} délka: {c[2]}")
cur.execute(f'SELECT COUNT(*) FROM {table}')
print(f"\nPočet záznamů: {cur.fetchone()[0]}")
cur.close()
conn.close()
@@ -0,0 +1,16 @@
# Firebird trace konfigurace zachytí SQL příkazy Medicusu
# Soubor: medicus_trace.conf
<database c:\\medicus 3\\data\\medicus.fdb>
enabled true
log_connections false
log_transactions false
log_statement_prepare false
log_statement_start false
log_statement_finish true
log_procedure_finish false
print_plan false
print_perf false
time_threshold 0
max_sql_length 32768
</database>
Binary file not shown.
@@ -0,0 +1,22 @@
"""read_last_dekurs.py jednorázový skript: vypíše obsah posledního DEKURS záznamu pro Buzalka"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, CAS, CHAR_LENGTH(DEKURS), DEKURS
FROM DEKURS
WHERE IDPAC = 9742
ORDER BY ID DESC
""")
row = cur.fetchone()
idek, datum, cas, delka, obsah = row
print(f"ID={idek} datum={datum} cas={cas} délka={delka} B")
print("=" * 60)
print(obsah)
conn.close()
+323
View File
@@ -0,0 +1,323 @@
import os, shutil, fdb, time
import re, datetime, funkce, funkce_ext
# Connect to the Firebird database
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password="masterkey",
charset="win1250")
cesta = r"u:\testimport"
cestazpracovana = r"u:\testimportzpracovana"
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─── Helper funkce ────────────────────────────────────────────────────────────
def restore_files_for_import(retezec):
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
if not os.path.exists(drop):
print(f"The directory '{drop}' does not exist.")
return
for item in os.listdir(drop):
item_path = os.path.join(drop, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
print(f"Deleted file: {item_path}")
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
print(f"Deleted directory: {item_path}")
for item in os.listdir(next):
item_path = os.path.join(next, item)
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
shutil.copy(item_path, os.path.join(drop, item))
print(f"Copied file: {item_path}")
def kontrola_rc(rc, connection):
cur = connection.cursor()
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
row = cur.fetchone()
if row:
return row[1]
else:
return False
def kontrola_struktury(souborname, connection):
if souborname.endswith('.pdf'):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
vpohode = True
if match and len(match.groups()) == 5:
datum = match.group(2)
try:
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
except:
vpohode = False
return vpohode
cur = connection.cursor()
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
row = cur.fetchone()[0]
if row != 1:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
return vpohode
def vrat_info_o_souboru(souborname, connection):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
rc = match.group(1)
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
jmeno = match.group(3)
prvnizavorka = match.group(4)
druhazavorka = match.group(5)
cur = connection.cursor()
cur.execute("select idpac from kar where rodcis=?", (rc,))
idpac = cur.fetchone()[0]
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
def prejmenuj_chybny_soubor(souborname, cesta):
if souborname[0] != "":
soubornovy = "" + souborname
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
def najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni):
"""Vrátí (id, rtf) posledního dekurzu pacienta pokud je z dnešního dne, jinak None."""
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum == datum_vlozeni:
print(f" → dnešní den ({datum_vlozeni}) ✓")
return (dekurs_id, dekurs_rtf)
else:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový")
return None
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
n_files = 0
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pards = ''
for i, fname in enumerate(filenameforbookmark_list):
idx = n_files + i
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
def append_bookmarks(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.extend(bookmark_list)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# Šablona RTF pro nový dekurs
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
info = []
for soubor in os.listdir(cesta):
if os.path.isfile(os.path.join(cesta, soubor)):
print(soubor)
if kontrola_struktury(soubor, conn):
info.append(vrat_info_o_souboru(soubor, conn))
else:
prejmenuj_chybny_soubor(soubor, cesta)
info = sorted(info, key=lambda x: (x[0], x[1]))
print(info)
skupiny = {}
for row in info:
skupiny[row[0]] = []
for row in info:
skupiny[row[0]].append(row)
for key in skupiny.keys():
print(f"\n{'='*60}")
print(f"RC: {key}, souborů: {len(skupiny[key])}")
cislo = 9
poradi = 0
bookmark_list = []
filenameforbookmark_list = []
bookmarks_body = ''
idpac = skupiny[key][0][1]
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
for row in skupiny[key]:
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=row[1],
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
print(f" → FILES.ID = {fileid} ({row[6]})")
# Přesun souboru do zpracovaných
for attempt in range(3):
try:
dest = os.path.join(cestazpracovana, row[6])
if not os.path.exists(dest):
shutil.move(os.path.join(cesta, row[6]), dest)
else:
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
shutil.move(os.path.join(cesta, row[6]),
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
print(" Přesun OK!")
break
except Exception as e:
print(f" Attempt {attempt + 1} failed: {e}")
if attempt < 2:
print(" Retrying in 5 seconds...")
time.sleep(5)
else:
print(" Max retries reached. Command failed.")
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
filenameforbookmark_list.append(filenameforbookmark)
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
cur = conn.cursor()
print(f"\n>>> Hledám poslední dekurs pro IDPAC={idpac}...")
existujici = najdi_posledni_dekurs_dnes(conn, idpac, datumzapisu)
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# Případ 1: dnešní dekurs má sekci příloh → přidáme soubory dovnitř
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubory DO existující sekce...")
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
else:
# Případ 2: dnešní dekurs existuje, ale sekci příloh nemá → prepend
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# Případ 3: žádný dnešní dekurs → vytvoříme nový
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
print("\n=== HOTOVO ===")
conn.close()
+397
View File
@@ -0,0 +1,397 @@
import os, shutil, fdb, time, threading
import re, datetime, funkce, funkce_ext
# Connect to the Firebird database
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password="masterkey",
charset="win1250")
cesta = r"u:\testimport"
cestazpracovana = r"u:\testimportzpracovana"
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─── Helper funkce ────────────────────────────────────────────────────────────
def restore_files_for_import(retezec):
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
if not os.path.exists(drop):
print(f"The directory '{drop}' does not exist.")
return
for item in os.listdir(drop):
item_path = os.path.join(drop, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
print(f"Deleted file: {item_path}")
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
print(f"Deleted directory: {item_path}")
for item in os.listdir(next):
item_path = os.path.join(next, item)
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
shutil.copy(item_path, os.path.join(drop, item))
print(f"Copied file: {item_path}")
def kontrola_rc(rc, connection):
cur = connection.cursor()
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
row = cur.fetchone()
if row:
return row[1]
else:
return False
def kontrola_struktury(souborname, connection):
if souborname.endswith('.pdf'):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
vpohode = True
if match and len(match.groups()) == 5:
datum = match.group(2)
try:
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
except:
vpohode = False
return vpohode
cur = connection.cursor()
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
row = cur.fetchone()[0]
if row != 1:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
return vpohode
def vrat_info_o_souboru(souborname, connection):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
rc = match.group(1)
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
jmeno = match.group(3)
prvnizavorka = match.group(4)
druhazavorka = match.group(5)
cur = connection.cursor()
cur.execute("select idpac from kar where rodcis=?", (rc,))
idpac = cur.fetchone()[0]
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
def prejmenuj_chybny_soubor(souborname, cesta):
if souborname[0] != "":
soubornovy = "" + souborname
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
def _pokus_o_zamek(dekurs_id, vysledek):
"""Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení.
Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}.
Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený.
"""
conn_t = None
try:
conn_t = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur_t = conn_t.cursor()
cur_t.execute(
"SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK",
(dekurs_id,)
)
cur_t.fetchone()
conn_t.rollback() # Uvolni zámek sloužil jen k ověření
vysledek['ok'] = True
except Exception as e:
vysledek['chyba'] = str(e)
finally:
if conn_t:
try:
conn_t.close()
except Exception:
pass
def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2):
"""Zjistí zda existuje dnešní dekurz a ověří že není zamčený.
Vrátí:
(id, rtf) dnešní dekurz existuje a není zamčený
None žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba)
Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený).
Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme
pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund
neskončí, záznam je zamčený a přeskočíme celou skupinu.
"""
cur = conn.cursor()
# Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT)
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
print(f" Žádný dekurz pro pacienta IDPAC={idpac}")
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum != datum_vlozeni:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový (INSERT)")
return None
# Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený
print(f" → dnešní den ({datum_vlozeni}) ✓ ověřuji zámek (timeout {timeout_sec}s)...")
vysledek = {}
t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True)
t.start()
t.join(timeout=timeout_sec)
if t.is_alive():
# Vlákno stále čeká na zámek = záznam drží Medicus
raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)")
if 'chyba' in vysledek:
raise fdb.DatabaseError(vysledek['chyba'])
print(f" → záznam volný, pokračuji se zápisem")
return (dekurs_id, dekurs_rtf)
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
n_files = 0
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pards = ''
for i, fname in enumerate(filenameforbookmark_list):
idx = n_files + i
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
def append_bookmarks(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.extend(bookmark_list)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# Šablona RTF pro nový dekurs
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
info = []
for soubor in os.listdir(cesta):
if os.path.isfile(os.path.join(cesta, soubor)):
print(soubor)
if kontrola_struktury(soubor, conn):
info.append(vrat_info_o_souboru(soubor, conn))
else:
prejmenuj_chybny_soubor(soubor, cesta)
info = sorted(info, key=lambda x: (x[0], x[1]))
print(info)
skupiny = {}
for row in info:
skupiny[row[0]] = []
for row in info:
skupiny[row[0]].append(row)
for key in skupiny.keys():
print(f"\n{'='*60}")
print(f"RC: {key}, souborů: {len(skupiny[key])}")
idpac = skupiny[key][0][1]
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
# ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ──────
print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...")
try:
existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu)
except RuntimeError as e:
# Vlákno nepřišlo do timeoutu = záznam drží Medicus
print(f"\n!!! DEKURZ ZAMČEN soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
except fdb.DatabaseError as e:
chyba = str(e).lower()
if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba:
print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
raise # jiná DB chyba propaguj dál
cislo = 9
poradi = 0
bookmark_list = []
filenameforbookmark_list = []
bookmarks_body = ''
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
for row in skupiny[key]:
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=row[1],
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
print(f" → FILES.ID = {fileid} ({row[6]})")
# Přesun souboru do zpracovaných
for attempt in range(3):
try:
dest = os.path.join(cestazpracovana, row[6])
if not os.path.exists(dest):
shutil.move(os.path.join(cesta, row[6]), dest)
else:
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
shutil.move(os.path.join(cesta, row[6]),
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
print(" Přesun OK!")
break
except Exception as e:
print(f" Attempt {attempt + 1} failed: {e}")
if attempt < 2:
print(" Retrying in 5 seconds...")
time.sleep(5)
else:
print(" Max retries reached. Command failed.")
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
filenameforbookmark_list.append(filenameforbookmark)
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
cur = conn.cursor()
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubory DO existující sekce...")
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
else:
# Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# Případ 3: žádný dnešní dekurz → vytvoříme nový
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
print("\n=== HOTOVO ===")
conn.close()
@@ -0,0 +1,397 @@
import os, shutil, fdb, time, threading
import re, datetime, funkce, funkce_ext
# Connect to the Firebird database
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password="masterkey",
charset="win1250")
cesta = r"u:\testimport"
cestazpracovana = r"u:\testimportzpracovana"
# Konstanty pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─── Helper funkce ────────────────────────────────────────────────────────────
def restore_files_for_import(retezec):
drop = r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
next = r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
if not os.path.exists(drop):
print(f"The directory '{drop}' does not exist.")
return
for item in os.listdir(drop):
item_path = os.path.join(drop, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
print(f"Deleted file: {item_path}")
elif os.path.isdir(item_path):
shutil.rmtree(item_path)
print(f"Deleted directory: {item_path}")
for item in os.listdir(next):
item_path = os.path.join(next, item)
if os.path.isfile(item_path) and item_path.endswith(".pdf") and retezec in item_path:
shutil.copy(item_path, os.path.join(drop, item))
print(f"Copied file: {item_path}")
def kontrola_rc(rc, connection):
cur = connection.cursor()
cur.execute("select count(*),idpac from kar where rodcis=? group by idpac", (rc,))
row = cur.fetchone()
if row:
return row[1]
else:
return False
def kontrola_struktury(souborname, connection):
if souborname.endswith('.pdf'):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
vpohode = True
if match and len(match.groups()) == 5:
datum = match.group(2)
try:
datetime.datetime.strptime(datum, "%Y-%m-%d").date()
except:
vpohode = False
return vpohode
cur = connection.cursor()
cur.execute("select count(*) from kar where rodcis=?", (match.group(1),))
row = cur.fetchone()[0]
if row != 1:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
else:
vpohode = False
return vpohode
return vpohode
def vrat_info_o_souboru(souborname, connection):
pattern = re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match = pattern.search(souborname)
rc = match.group(1)
datum = datetime.datetime.strptime(match.group(2), "%Y-%m-%d").date()
jmeno = match.group(3)
prvnizavorka = match.group(4)
druhazavorka = match.group(5)
cur = connection.cursor()
cur.execute("select idpac from kar where rodcis=?", (rc,))
idpac = cur.fetchone()[0]
datumsouboru = datetime.datetime.fromtimestamp(os.path.getctime(os.path.join(cesta, souborname)))
return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
def prejmenuj_chybny_soubor(souborname, cesta):
if souborname[0] != "":
soubornovy = "" + souborname
os.rename(os.path.join(cesta, souborname), os.path.join(cesta, soubornovy))
def _pokus_o_zamek(dekurs_id, vysledek):
"""Běží ve vlákně: pokusí se zamknout dekurz přes separátní spojení.
Výsledek zapíše do slovníku vysledek: {'ok': True} nebo {'chyba': str}.
Pokud vlákno stále běží po uplynutí timeoutu → záznam je zamčený.
"""
conn_t = None
try:
conn_t = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur_t = conn_t.cursor()
cur_t.execute(
"SELECT ID FROM DEKURS WHERE ID = ? FOR UPDATE WITH LOCK",
(dekurs_id,)
)
cur_t.fetchone()
conn_t.rollback() # Uvolni zámek sloužil jen k ověření
vysledek['ok'] = True
except Exception as e:
vysledek['chyba'] = str(e)
finally:
if conn_t:
try:
conn_t.close()
except Exception:
pass
def zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2):
"""Zjistí zda existuje dnešní dekurz a ověří že není zamčený.
Vrátí:
(id, rtf) dnešní dekurz existuje a není zamčený
None žádný dnešní dekurz (bude se dělat INSERT, zámek není potřeba)
Vyhodí RuntimeError pokud je záznam zamčený jiným uživatelem (Medicus ho má otevřený).
Poznámka: NOWAIT transakci fdb neumí spolehlivě nastavit, proto spustíme
pokus o zámek ve vlákně s timeoutem. Pokud vlákno do timeout_sec sekund
neskončí, záznam je zamčený a přeskočíme celou skupinu.
"""
cur = conn.cursor()
# Krok 1: přečti ID, datum a obsah posledního dekurzu (běžný SELECT)
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
print(f" Žádný dekurz pro pacienta IDPAC={idpac}")
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum != datum_vlozeni:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový (INSERT)")
return None
# Krok 2: ověř přes vlákno s timeoutem zda záznam není zamčený
print(f" → dnešní den ({datum_vlozeni}) ✓ ověřuji zámek (timeout {timeout_sec}s)...")
vysledek = {}
t = threading.Thread(target=_pokus_o_zamek, args=(dekurs_id, vysledek), daemon=True)
t.start()
t.join(timeout=timeout_sec)
if t.is_alive():
# Vlákno stále čeká na zámek = záznam drží Medicus
raise RuntimeError(f"DEKURZ ID={dekurs_id} je zamčený (Medicus má záznam otevřený)")
if 'chyba' in vysledek:
raise fdb.DatabaseError(vysledek['chyba'])
print(f" → záznam volný, pokračuji se zápisem")
return (dekurs_id, dekurs_rtf)
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list):
"""Přidá více souborů do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nové indexy začínají od N
2. Vloží nové \\pard řádky před uzavírací prázdný řádek sekce
3. Přidá nové bookmarky na konec {\\info{\\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
n_files = 0
print(f" Počet existujících Files odkazů: {n_files}, přidávám {len(bookmark_list)} nových")
# 2. Vložit nové \pard řádky před PRILOHY_CLOSING
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pards = ''
for i, fname in enumerate(filenameforbookmark_list):
idx = n_files + i
new_pards += (r'\pard\s10{\*\bkmkstart ' + str(idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + fname
+ r'{\*\bkmkend ' + str(idx) + r'}\par' + '\n')
rtf = rtf[:closing_pos] + new_pards + rtf[closing_pos:]
# 3. Přidat nové bookmarky na konec {\info{\bookmarks ...}}
def append_bookmarks(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.extend(bookmark_list)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', append_bookmarks, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží novou sekci příloh na ZAČÁTEK stávajícího dekurzu (sekce tam ještě není)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# Šablona RTF pro nový dekurs
RTF_TEMPLATE = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
# ─── Hlavní tělo skriptu ──────────────────────────────────────────────────────
info = []
for soubor in os.listdir(cesta):
if os.path.isfile(os.path.join(cesta, soubor)):
print(soubor)
if kontrola_struktury(soubor, conn):
info.append(vrat_info_o_souboru(soubor, conn))
else:
prejmenuj_chybny_soubor(soubor, cesta)
info = sorted(info, key=lambda x: (x[0], x[1]))
print(info)
skupiny = {}
for row in info:
skupiny[row[0]] = []
for row in info:
skupiny[row[0]].append(row)
for key in skupiny.keys():
print(f"\n{'='*60}")
print(f"RC: {key}, souborů: {len(skupiny[key])}")
idpac = skupiny[key][0][1]
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
# ── PRE-CHECK: zkus zamknout dnešní dekurz PŘED zpracováním souborů ──────
print(f"\n>>> Kontrola zámku dekurzu pro IDPAC={idpac}...")
try:
existujici = zkus_zamknout_dnesni_dekurs(conn, idpac, datumzapisu)
except RuntimeError as e:
# Vlákno nepřišlo do timeoutu = záznam drží Medicus
print(f"\n!!! DEKURZ ZAMČEN soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
except fdb.DatabaseError as e:
chyba = str(e).lower()
if 'deadlock' in chyba or 'lock conflict' in chyba or 'update conflict' in chyba:
print(f"\n!!! DEKURZ ZAMČEN (DB konflikt) soubory skupiny RC={key} přeskočeny.")
print(" Spusťte skript znovu až bude záznam volný.")
continue
raise # jiná DB chyba propaguj dál
cislo = 9
poradi = 0
bookmark_list = []
filenameforbookmark_list = []
bookmarks_body = ''
# ── Krok 1: vložit každý soubor do ext DB + přesunout do zpracovaných ────
for row in skupiny[key]:
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=row[1],
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
print(f" → FILES.ID = {fileid} ({row[6]})")
# Přesun souboru do zpracovaných
for attempt in range(3):
try:
dest = os.path.join(cestazpracovana, row[6])
if not os.path.exists(dest):
shutil.move(os.path.join(cesta, row[6]), dest)
else:
ts = datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")
shutil.move(os.path.join(cesta, row[6]),
os.path.join(cestazpracovana, row[6][:-4] + " " + ts + ".pdf"))
print(" Přesun OK!")
break
except Exception as e:
print(f" Attempt {attempt + 1} failed: {e}")
if attempt < 2:
print(" Retrying in 5 seconds...")
time.sleep(5)
else:
print(" Max retries reached. Command failed.")
filenameforbookmark = row[2].strftime('%Y-%m-%d') + ' ' + row[4] + ': ' + row[5]
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
filenameforbookmark_list.append(filenameforbookmark)
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# ── Krok 2: sestavit tělo nové sekce příloh ───────────────────────────────
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 3: rozhodovací logika (3 případy) ────────────────────────────────
cur = conn.cursor()
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# Případ 1: dnešní dekurz má sekci příloh → přidáme soubory dovnitř
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubory DO existující sekce...")
merged_rtf = pridat_do_sekce_prilohy(existing_rtf, bookmark_list, filenameforbookmark_list)
else:
# Případ 2: dnešní dekurz existuje, ale sekci příloh nemá → prepend
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → vkládám sekci na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(skupiny[key]))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# Případ 3: žádný dnešní dekurz → vytvoříme nový
print(f"\n>>> Žádný dekurs pro dnešek → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = RTF_TEMPLATE.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(dekursid, 6, 2, 2, idpac, datumzapisu, caszapisu, rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
print("\n=== HOTOVO ===")
conn.close()
@@ -0,0 +1,226 @@
# s03soubory_01_FINAL.py dokumentace
**Finální verze importního skriptu pro vkládání PDF dokumentů do dekurzů Medicusu.**
Datum finalizace: 2026-04-04
Autor: Vladimír Buzalka + Claude (Anthropic)
---
## Co skript dělá
Zpracuje PDF soubory v určené složce (`cesta`) a pro každý soubor:
1. Ověří správnost názvu souboru (formát, RC, datum)
2. Zkontroluje, zda cílový dekurz není zamčený v Medicusu
3. Zapíše soubor do externí databáze souborů (tabulka FILES)
4. Přesune soubor do složky zpracovaných
5. Vloží odkaz (bookmark) do dekurzu pacienta jako RTF záznam
---
## Adresáře
| Proměnná | Cesta (testovací) | Popis |
|---|---|---|
| `cesta` | `u:\testimport` | Vstupní složka sem patří soubory ke zpracování |
| `cestazpracovana` | `u:\testimportzpracovana` | Cílová složka sem se přesouvají zpracované soubory |
> V produkci tyto cesty nahradit skutečnými složkami (Nextcloud/Dropbox).
---
## Formát názvu souboru
Každý PDF soubor musí mít název ve tvaru:
```
RC YYYY-MM-DD Příjmení, Jméno [typ dokumentu] [poznámka].pdf
```
**Příklad:**
```
7309208104 2020-10-16 Buzalka, Vladimír [LZ ortopedie] [VAS LS páteře, obstřik].pdf
```
| Část | Popis |
|---|---|
| `RC` | Rodné číslo pacienta (9 nebo 10 číslic) musí existovat v tabulce KAR |
| `YYYY-MM-DD` | Datum dokumentu |
| `Příjmení, Jméno` | Jméno pacienta (jen pro čitelnost, nepoužívá se k vyhledání) |
| `[typ dokumentu]` | První závorka druh nálezu (LZ ortopedie, EKG, Lab. nález…) |
| `[poznámka]` | Druhá závorka krátký popis obsahu (může být prázdná `[]`) |
**Chybný soubor** (špatný název, RC nenalezeno v DB) je přejmenován přidáním prefixu `♥`:
```
♥chybny soubor.pdf
```
Skript ho přeskočí a nechá v složce pro ruční opravu.
---
## Databázové tabulky
| Tabulka | DB | Popis |
|---|---|---|
| `KAR` | Medicus (`medicus.fdb`) | Kartotéka pacientů lookup RC → IDPAC |
| `DEKURS` | Medicus (`medicus.fdb`) | Dekurzy čtení a zápis RTF záznamu |
| `FILES` | Externí DB (`MEDICUS_FILES_YYYYMM.fdb`) | Binární uložení PDF souborů |
---
## Klíčová novinka oproti s03soubory.py ochrana před zamčeným dekurzem
### Problém
Medicus drží **exkluzivní zámek** (Firebird row lock) na záznamu tabulky DEKURS po celou dobu, kdy má lékařka pacienta otevřeného. Kdyby skript provedl `UPDATE DEKURS` do zamčeného záznamu, přepsal by lékařčiny neuložené změny.
### Řešení detekce zámku pomocí vlákna s timeoutem
Firebird neumí NOWAIT nastavit per-statement v SQL (syntaxe `FOR UPDATE WITH LOCK NOWAIT` není platná). Nastavení NOWAIT je vlastnost transakce, nikoliv dotazu. Knihovna `fdb` navíc toto nastavení spolehlivě nepodporuje.
**Zvolené řešení:** spuštění pokusu o zámek ve vedlejším vlákně s timeoutem 2 sekundy.
```
hlavní vlákno vedlejší vlákno (_pokus_o_zamek)
───────────────── ─────────────────────────────────
t.start() ──────► fdb.connect() [nové spojení]
t.join(timeout=2s) SELECT ... FOR UPDATE WITH LOCK
├── záznam volný → fetchone() → rollback() → konec
└── záznam zamčený → čeká (blokuje)...
─────────────────
po 2 sekundách:
t.is_alive()?
ANO → záznam zamčený → RuntimeError → přeskoč skupinu
NE → záznam volný → pokračuj se zápisem
```
### Důležitý detail pořadí operací
**Kontrola zámku probíhá PŘED zápisem do FILES a PŘED přesunem souboru.**
Kdyby se pořadí obrátilo, mohlo by dojít k situaci:
- soubor zapsán do FILES ✓
- soubor přesunut do zpracovaných ✓
- dekurz zamčen → UPDATE selže
- soubor je pryč ze vstupní složky, ale odkaz v dekurzu chybí
Správné pořadí:
```
1. Zkontroluj zámek dekurzu (NOWAIT)
└── zamčeno → přeskoč (soubory zůstanou v cesta)
2. Zapiš soubory do ext. DB (FILES)
3. Přesuň soubory do zpracovaných
4. Sestav RTF
5. UPDATE nebo INSERT do DEKURS
```
---
## Logika vkládání do dekurzu 3 případy
Po úspěšné kontrole zámku skript rozhodne, co s dekurzem udělat:
```
Existuje dnešní dekurz pro pacienta?
├── ANO → obsahuje sekci "Vložené přílohy"?
│ ├── ANO → Případ 1: přidá nové soubory DOVNITŘ sekce
│ └── NE → Případ 2: vloží celou sekci na ZAČÁTEK dekurzu (prepend)
└── NE → Případ 3: vytvoří NOVÝ dekurz ze šablony RTF_TEMPLATE
```
### Případ 1 `pridat_do_sekce_prilohy()`
Dnešní dekurz **existuje a má** sekci „Vložené přílohy".
- Spočítá kolik odkazů (Files:) už sekce obsahuje → nové indexy bookmarků navazují
- Nové `\pard` řádky vloží **před** uzavírací prázdný řádek sekce (`PRILOHY_CLOSING`)
- Nové bookmarky přidá na **konec** `{\info{\bookmarks ...}}`
- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?`
### Případ 2 `merge_rtf_prepend()`
Dnešní dekurz **existuje, ale nemá** sekci příloh (lékařka do něj napsala text).
- Přečísluje existující bookmarky (posunutí o počet nových souborů)
- Novou sekci „Vložené přílohy" vloží **na začátek** těla RTF (před `\uc1\pard`)
- Nové bookmarky předřadí před existující v `{\info{\bookmarks ...}}`
- Lékařčin text zůstane zachován, jen se posune níž
- Provede `UPDATE DEKURS SET DEKURS = ? WHERE ID = ?`
### Případ 3 nový INSERT
Pro dnešní datum **neexistuje žádný dekurz**.
- Vyplní `RTF_TEMPLATE` (bookmarky + tělo sekce příloh)
- Provede `INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)`
- `iduzi=6` (Vladimír Buzalka), `idprac=2`, `idodd=2`
---
## RTF formát dekurzu
### Struktura bookmarku
Každý přiložený soubor je reprezentován jako:
1. **Bookmark entry** v `{\info{\bookmarks ...}}`:
```
"2020-10-16 LZ ortopedie: VAS LS páteře, obstřik","Files:21923",9
```
- `"popis"` zobrazený text odkazu
- `"Files:ID"` odkaz na záznam v tabulce FILES (slouží Medicusu k načtení souboru)
- `9` číslo fontu/stylu (od 9, každý další +7)
2. **Vizuální řádek** v těle RTF:
```rtf
\pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2020-10-16 LZ ortopedie: VAS LS páteře, obstřik{\*\bkmkend 0}\par
```
- `\bkmkstart N` / `\bkmkend N` index bookmarku (0, 1, 2…)
- `\cs32\ul\cf1` styl „Odkaz" (modrý podtržený text)
### Konstanty pro detekci sekce příloh (win1250 RTF escape)
```python
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:" # "Vložené přílohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par' # uzavírací prázdný řádek
```
---
## Funkce přehled
| Funkce | Popis |
|---|---|
| `restore_files_for_import(retezec)` | Debug utilita vrátí soubory z Nextcloudu zpět do Dropboxu. Nepoužívá se v produkci. |
| `kontrola_rc(rc, connection)` | Ověří zda RC existuje v KAR, vrátí IDPAC nebo False. |
| `kontrola_struktury(souborname, connection)` | Ověří formát názvu souboru a existenci RC v DB. |
| `vrat_info_o_souboru(souborname, connection)` | Parsuje název souboru, dohledá IDPAC, vrátí tuple s metadaty. |
| `prejmenuj_chybny_soubor(souborname, cesta)` | Přidá prefix `` k chybnému souboru. |
| `_pokus_o_zamek(dekurs_id, vysledek)` | Interní běží ve vlákně, zkouší zamknout dekurz. |
| `zkus_zamknout_dnesni_dekurs(conn, idpac, datum_vlozeni, timeout_sec=2)` | Zjistí zda dnešní dekurz existuje a není zamčený. Vyhodí RuntimeError pokud je zamčený. |
| `ma_sekci_prilohy(rtf)` | Vrátí True pokud RTF obsahuje sekci „Vložené přílohy". |
| `pridat_do_sekce_prilohy(rtf, bookmark_list, filenameforbookmark_list)` | Případ 1 přidá soubory do existující sekce příloh. |
| `merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new)` | Případ 2 vloží sekci příloh na začátek existujícího dekurzu. |
---
## Ošetření chyb
| Situace | Chování |
|---|---|
| Soubor má chybný název | Přejmenován na `♥soubor.pdf`, přeskočen |
| RC nenalezeno v KAR | Přejmenován na `♥soubor.pdf`, přeskočen |
| Dekurz zamčený (timeout vlákna) | Skupina přeskočena, soubory zůstanou v `cesta` |
| DB konflikt při zamykání (-913 deadlock) | Skupina přeskočena, soubory zůstanou v `cesta` |
| Přesun souboru selže | 3 pokusy s 5s pauzou, poté varování |
| Jiná DB chyba | Výjimka se propaguje, skript havaruje |
---
## Vývoj a testování
| Verze | Soubor | Co přibilo |
|---|---|---|
| Prototyp | `test_import_FINAL.py` | Ruční zadání IDPAC a DATUM, ověření RTF logiky |
| v1 | `s03soubory.py` | Automatický parsing RC z názvu, dávkování po skupinách |
| **v1 FINAL** | `s03soubory_01_FINAL.py` | Ochrana před zamčeným dekurzem (threading + timeout) |
### Jak byl objeven problém se zámky
Experimentem bylo ověřeno, že Medicus drží Firebird row lock na záznamu DEKURS po celou dobu, kdy má lékařka pacienta otevřeného (`SELECT FIRST 1 ... FOR UPDATE WITH LOCK` z Pythonu čekalo dokud lékařka neuložila). NOWAIT nelze nastavit přes SQL syntaxi ani spolehlivě přes fdb TPB bajty, proto bylo zvoleno řešení přes vlákno s timeoutem.
@@ -0,0 +1,108 @@
"""s04_presun_externi_db.py Přesun externích DB souborů z u:\ do u:\externi\
Spustit na Windows s ZAVŘENÝM Medicusem!
Co dělá:
1. Připojí se k hlavní medicus.fdb
2. Načte všechny záznamy z EXTERNI_DB
3. Pro každý záznam zkopíruje FDB soubor z původní lokace do u:\externi\
4. Aktualizuje EXTERNI_DB.PATH na novou lokaci
5. Vytiskne přehled výsledků
Po spuštění zkus v Medicusu otevřít přílohu pacienta mělo by to fungovat.
Pokud ne, zálohuj si databázi a spusť znovu (skript je idempotentní).
"""
import os
import shutil
import fdb
# ── Konfigurace ──────────────────────────────────────────────────────────────
MAIN_DB = r'c:\medicus 3\data\medicus.fdb'
CÍL_SLOŽKA = r'u:\externi'
# ─────────────────────────────────────────────────────────────────────────────
def main():
os.makedirs(CÍL_SLOŽKA, exist_ok=True)
print(f"Cílová složka: {CÍL_SLOŽKA}")
print()
print("Připojuji se k hlavní DB...")
conn = fdb.connect(
dsn=f'localhost:{MAIN_DB}',
user='SYSDBA',
password='masterkey',
charset='WIN1250'
)
cur = conn.cursor()
# Načti všechny záznamy z EXTERNI_DB
cur.execute("SELECT DBNAME, SERVER, PATH, HESLO FROM EXTERNI_DB ORDER BY DBNAME")
zaznamy = cur.fetchall()
print(f"Nalezeno {len(zaznamy)} záznamů v EXTERNI_DB:")
print()
ok = 0
preskoceno = 0
chyba = 0
for dbname, server, path_puvodni, heslo in zaznamy:
print(f" [{dbname}] {path_puvodni}")
# Název souboru (jen basename, bez cesty)
basename = os.path.basename(path_puvodni)
# Normalize: Medicus někdy ukládá s malým .fdb, někdy velkým .FDB
path_cil = os.path.join(CÍL_SLOŽKA, basename)
# Zkontroluj, jestli zdrojový soubor existuje
# (zkus obě varianty přípony)
path_src = None
for kandidat in [path_puvodni,
path_puvodni.replace('.fdb', '.FDB'),
path_puvodni.replace('.FDB', '.fdb')]:
if os.path.isfile(kandidat):
path_src = kandidat
break
if path_src is None:
print(f" ⚠ ZDROJOVÝ SOUBOR NENALEZEN: {path_puvodni} přeskakuji")
chyba += 1
continue
# Pokud je soubor už v cíli, přeskoč kopírování
if os.path.isfile(path_cil):
velikost = os.path.getsize(path_cil)
print(f" → Soubor již existuje v cíli ({velikost:,} B), přeskakuji kopírování")
else:
velikost = os.path.getsize(path_src)
print(f" → Kopíruji ({velikost:,} B)...", end=' ', flush=True)
shutil.copy2(path_src, path_cil)
print("OK")
# Aktualizuj PATH v EXTERNI_DB pokud se liší
if path_puvodni != path_cil:
cur.execute(
"UPDATE EXTERNI_DB SET PATH = ? WHERE DBNAME = ?",
(path_cil, dbname)
)
print(f" → EXTERNI_DB.PATH aktualizováno: {path_cil}")
else:
print(f" → PATH již ukazuje na cíl, není třeba měnit")
preskoceno += 1
ok += 1
conn.commit()
conn.close()
print()
print("=" * 60)
print(f"Hotovo! Zpracováno: {ok}, přeskočeno: {preskoceno}, chyb: {chyba}")
print()
if chyba > 0:
print("⚠ Některé soubory nebyly nalezeny viz výpis výše.")
print(" Pokud jsou to staré DB které již neexistují, nevadí to.")
print("Nyní spusť Medicus a zkus otevřít přílohu pacienta.")
if __name__ == '__main__':
main()
@@ -0,0 +1,37 @@
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
print("=== NEJSTARŠÍ záznamy ===")
cur.execute("""
SELECT FIRST 5 ID, DATUM, DEKURS
FROM DEKURS
WHERE DEKURS IS NOT NULL
ORDER BY DATUM ASC
""")
for row in cur.fetchall():
text = row[2].read() if hasattr(row[2], 'read') else row[2]
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
print(f"\n--- ID={row[0]} DATUM={row[1]} ---")
print(repr(text[:300]))
print("\n\n=== NEJNOVĚJŠÍ záznamy ===")
cur.execute("""
SELECT FIRST 5 ID, DATUM, DEKURS
FROM DEKURS
WHERE DEKURS IS NOT NULL
ORDER BY DATUM DESC
""")
for row in cur.fetchall():
text = row[2].read() if hasattr(row[2], 'read') else row[2]
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
print(f"\n--- ID={row[0]} DATUM={row[1]} ---")
print(repr(text[:300]))
conn.close()
@@ -0,0 +1,25 @@
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS
FROM DEKURS
WHERE DEKURS IS NOT NULL
ORDER BY DATUM DESC
""")
row = cur.fetchone()
text = row[2].read() if hasattr(row[2], 'read') else row[2]
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
print(f"ID={row[0]} DATUM={row[1]}")
print(f"Délka: {len(text)} znaků")
print("\n--- CELÝ TEXT ---")
print(text)
conn.close()
+35
View File
@@ -0,0 +1,35 @@
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
# Načti všechny záznamy pacienta a zjisti délku v Pythonu
cur.execute("""
SELECT ID, DATUM, DEKURS
FROM DEKURS
WHERE IDPAC = 9742 AND DEKURS IS NOT NULL
ORDER BY DATUM DESC
""")
rows = cur.fetchall()
def read_blob(val):
text = val.read() if hasattr(val, 'read') else val
if isinstance(text, bytes):
text = text.decode('windows-1250', errors='replace')
return text
# Seřaď podle délky
data = [(r[0], r[1], read_blob(r[2])) for r in rows]
data.sort(key=lambda x: len(x[2]), reverse=True)
print("Top 10 nejdelších záznamů:")
for d in data[:10]:
print(f" ID={d[0]} datum={d[1]} délka={len(d[2])}")
print("\n--- Nejdelší záznam (celý RTF) ---")
print(data[0][2])
conn.close()
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
"""Test připojení k externí DB a prozkoumání její struktury - spusť na Windows"""
import fdb, os, glob
# Najde první existující externí FDB soubor
soubory = glob.glob(r'u:\MEDICUS_FILES_*.fdb')
if not soubory:
soubory = glob.glob(r'u:\externi\MEDICUS_FILES_*.fdb')
if not soubory:
print("Žádné externí FDB soubory nenalezeny!")
exit()
soubor = sorted(soubory)[-1] # vezme nejnovější
print(f"Zkouším: {soubor}")
# Test 1: masterkey
try:
conn = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='masterkey', charset='win1250')
print("✓ Připojení s masterkey FUNGUJE!")
cur = conn.cursor()
# Jaké tabulky jsou v externí DB?
cur.execute("SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$SYSTEM_FLAG=0 AND RDB$VIEW_BLR IS NULL")
tabulky = [r[0].strip() for r in cur.fetchall()]
print(f"Tabulky v externí DB: {tabulky}")
# Kolik záznamů v FILES?
if 'FILES' in tabulky:
cur.execute("SELECT COUNT(*), MIN(ID), MAX(ID) FROM FILES")
row = cur.fetchone()
print(f"FILES: {row[0]} záznamů, ID od {row[1]} do {row[2]}")
cur.execute("SELECT ID, IDPAC, FILENAME, DATUM, OCTET_LENGTH(BODY) FROM FILES ORDER BY ID ROWS 3")
for r in cur.fetchall():
print(f" ID={r[0]} IDPAC={r[1]} FILE={r[2]} DATUM={r[3]} BODY={r[4]}B")
# Zjistit generátor
cur.execute("SELECT RDB$GENERATOR_NAME, RDB$GENERATOR_ID FROM RDB$GENERATORS WHERE RDB$SYSTEM_FLAG=0")
for r in cur.fetchall():
print(f"Generátor: {r[0].strip()} = {r[1]}")
conn.close()
except Exception as e:
print(f"✗ masterkey NEFUNGUJE: {e}")
# Test s jiným heslem - zkus prázdné
try:
conn = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='', charset='win1250')
print("✓ Připojení s prázdným heslem FUNGUJE!")
conn.close()
except Exception as e2:
print(f"✗ Prázdné heslo také nefunguje: {e2}")
+46
View File
@@ -0,0 +1,46 @@
"""Prozkoumá strukturu tabulky DATA v externí DB - spusť na Windows"""
import fdb, glob
soubory = glob.glob(r'u:\MEDICUS_FILES_*.fdb') + glob.glob(r'u:\MEDICUS_FILES_*.FDB')
if not soubory:
soubory = glob.glob(r'u:\externi\MEDICUS_FILES_*.fdb') + glob.glob(r'u:\externi\MEDICUS_FILES_*.FDB')
soubor = sorted(soubory)[-1]
print(f"Soubor: {soubor}\n")
conn = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='masterkey', charset='win1250')
cur = conn.cursor()
# Sloupce tabulky DATA
print("=== Sloupce tabulky DATA ===")
cur.execute("""
SELECT r.RDB$FIELD_NAME, f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH, f.RDB$SEGMENT_LENGTH
FROM RDB$RELATION_FIELDS r
JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = r.RDB$FIELD_SOURCE
WHERE r.RDB$RELATION_NAME = 'DATA'
ORDER BY r.RDB$FIELD_POSITION
""")
for row in cur.fetchall():
typ = {7:'SMALLINT',8:'INTEGER',12:'DATE',13:'TIME',14:'CHAR',16:'INT64',35:'TIMESTAMP',37:'VARCHAR',261:'BLOB'}.get(row[1], str(row[1]))
print(f" {row[0].strip():30} {typ} len={row[2]}")
# Generátory
print("\n=== Generátory ===")
cur.execute("SELECT RDB$GENERATOR_NAME, RDB$GENERATOR_ID FROM RDB$GENERATORS WHERE RDB$SYSTEM_FLAG=0")
for row in cur.fetchall():
print(f" {row[0].strip()} = {row[1]}")
# Počet záznamů a ukázka
print("\n=== Počet záznamů ===")
cur.execute("SELECT COUNT(*) FROM DATA")
print(f" {cur.fetchone()[0]} záznamů")
print("\n=== Ukázka 3 záznamů (bez BLOB) ===")
cur.execute("""
SELECT ID, OCTET_LENGTH(BODY) as VELIKOST
FROM DATA ORDER BY ID ROWS 3
""")
for row in cur.fetchall():
print(f" ID={row[0]} BODY={row[1]}B")
conn.close()
+47
View File
@@ -0,0 +1,47 @@
"""Zjistí propojení mezi hlavní DB a externí DB - spusť na Windows"""
import fdb, glob
# Hlavní DB
conn_main = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250')
cur = conn_main.cursor()
# Najít exportované záznamy v FILES (BODY není velký BLOB)
print("=== FILES záznamy po exportu (malý BODY = reference na externí DB) ===")
cur.execute("""
SELECT ID, IDPAC, FILENAME, DATUM, EXT_ID, DOCID,
OCTET_LENGTH(BODY) as BODY_LEN,
CAST(BODY AS VARCHAR(100)) as BODY_TEXT
FROM FILES
WHERE OCTET_LENGTH(BODY) < 100
ORDER BY ID DESC ROWS 5
""")
for r in cur.fetchall():
print(f" ID={r[0]} IDPAC={r[1]} FILE={r[2]}")
print(f" DATUM={r[3]} EXT_ID={r[4]} DOCID={r[5]}")
print(f" BODY_LEN={r[6]} BODY_TEXT='{r[7]}'")
# Ukázka záznamu s plným BODY (ještě ne exportovaný)
print("\n=== FILES záznamy PŘED exportem (velký BODY = binary data) ===")
cur.execute("""
SELECT ID, FILENAME, DATUM, EXT_ID, OCTET_LENGTH(BODY) as BODY_LEN
FROM FILES
WHERE OCTET_LENGTH(BODY) > 1000
ORDER BY ID DESC ROWS 3
""")
for r in cur.fetchall():
print(f" ID={r[0]} FILE={r[1]} DATUM={r[2]} EXT_ID={r[3]} BODY={r[4]}B")
conn_main.close()
# Porovnat s externí DB
print("\n=== DATA záznamy v externí DB ===")
soubory = glob.glob(r'u:\MEDICUS_FILES_*.fdb') + glob.glob(r'u:\MEDICUS_FILES_*.FDB')
soubor = sorted(soubory)[0] # vezmeme nejstarší
print(f"Soubor: {soubor}")
conn_ext = fdb.connect(dsn=f'localhost:{soubor}', user='SYSDBA', password='masterkey', charset='win1250')
cur_ext = conn_ext.cursor()
cur_ext.execute("SELECT UID, DATASIZE, CHUNK FROM DATA ORDER BY CHUNK ROWS 5")
for r in cur_ext.fetchall():
print(f" UID='{r[0]}' DATASIZE={r[1]} CHUNK={r[2]}")
conn_ext.close()
+20
View File
@@ -0,0 +1,20 @@
"""Čte přesné bajty BODY z exportovaného záznamu FILES - spusť na Windows"""
import fdb
conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250')
cur = conn.cursor()
# Vezmi jeden exportovaný záznam
cur.execute("SELECT ID, BODY FROM FILES WHERE ID = 10487")
row = cur.fetchone()
fileid = row[0]
body_bytes = row[1] # fdb vrací BLOB jako bytes přímo
print(f"FILE ID: {fileid}")
print(f"BODY délka: {len(body_bytes)} bajtů")
print(f"BODY hex: {body_bytes.hex()}")
print(f"BODY repr: {repr(body_bytes)}")
print(f"BODY jako string (latin1): {body_bytes.decode('latin1')}")
conn.close()
@@ -0,0 +1,108 @@
"""test_import_3files.py jednorázový test: 3 soubory jednoho pacienta → 1 dekurs s klikacími odkazy
Testuje opravenou RTF logiku (cs32, Přílohy: inline, poradi++)
Spustit na Windows.
"""
import datetime
import os
import fdb
import funkce
import funkce_ext
CESTA = r'u:\\'
IDPAC = 9742 # Buzalka Vladimír, RC 7309208104
SOUBORY = [
{
'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy1].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude copy1',
'datum': datetime.date(2026, 3, 18),
},
{
'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy2].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude copy2',
'datum': datetime.date(2026, 3, 18),
},
{
'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude',
'datum': datetime.date(2026, 3, 18),
},
]
# ── Připojení ─────────────────────────────────────────────────────────────────
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250'
)
# ── Krok 1: vložit každý soubor do ext DB ─────────────────────────────────────
bookmark = ''
bookmarks = ''
cislo = 9
poradi = 0
for s in SOUBORY:
cesta_souboru = os.path.join(CESTA, s['souborname'])
datumsouboru = datetime.datetime.fromtimestamp(os.path.getmtime(cesta_souboru))
print(f"\n>>> Zpracovávám: {s['souborname']}")
fileid = funkce_ext.zapis_file_ext(
vstupconnection = conn,
idpac = IDPAC,
cesta = CESTA,
souborname = s['souborname'],
prvnizavorka = s['prvnizavorka'],
soubordate = s['datum'],
souborfiledate = datumsouboru,
poznamka = s['druhazavorka'],
)
print(f" → FILES.ID = {fileid}")
# {\info{\bookmarks ...}} sekce
filenameforbookmark = s['datum'].strftime('%Y-%m-%d') + ' ' + s['prvnizavorka'] + ': ' + s['druhazavorka']
bookmark += '"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo) + ';'
cislo += 7
# tělo RTF klikatelné záložky (cs32, všechny stejný formát)
bookmarks += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark +
r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
bookmark = bookmark[:-1] # odstranit poslední ;
# ── Krok 2: sestavit RTF ──────────────────────────────────────────────────────
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
rtf = rtf.replace('BOOKMARKNAMES', bookmark)
rtf = rtf.replace('BOOKMARKSTEXT', bookmarks)
print('\n=== Výsledný RTF ===')
print(rtf)
# ── Krok 3: zapsat dekurs ─────────────────────────────────────────────────────
dekursid = funkce.get_dekurs_id(conn)
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
cur = conn.cursor()
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?,?,?,?,?,?,?,?)",
(dekursid, 6, 2, 2, IDPAC, datumzapisu, caszapisu, rtf)
)
conn.commit()
conn.close()
print(f'\n=== HOTOVO ===')
print(f'DEKURS.ID = {dekursid}')
print('Otevři Medicus → karta Buzalka Vladimír → najdi dnešní záznam → klikej na odkazy!')
@@ -0,0 +1,238 @@
"""test_import_single.py vloží 1 soubor do dekurzu s rozšířenou logikou:
1. Poslední dekurs pacienta je z dnešního dne A má sekci 'Vložené přílohy'
→ soubor se přidá DO té sekce (ne nová sekce)
2. Poslední dekurs je z dnešního dne, ale sekci 'Vložené přílohy' nemá
→ prepend nové sekce na začátek
3. Poslední dekurs je z jiného dne / neexistuje
→ nový dekurs pro dnešek
Spustit na Windows.
"""
import datetime, os, re, fdb
import funkce, funkce_ext
CESTA = r'u:\\'
IDPAC = 9742
DATUM = datetime.date(2026, 3, 18)
SOUBORY = [
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude - zařazení].pdf',
'prvnizavorka': 'vyšetření',
'druhazavorka': 'ahoj Claude - zařazení',
'datum': DATUM},
]
# Vzor pro detekci sekce Vložené přílohy (RTF kódování win1250)
PRILOHY_HEADER = r"Vlo\'9een\'e9 p\'f8\'edlohy:"
PRILOHY_CLOSING = r'\pard\s10\plain\cs15\f0\fs20 \par'
# ─────────────────────────────────────────────────────────────────────────────
def najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni):
"""Vrátí (id, rtf) posledního dekurzu pacienta pokud je z dnešního dne."""
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum == datum_vlozeni:
print(f" → dnešní den ({datum_vlozeni}) ✓")
return (dekurs_id, dekurs_rtf)
else:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový")
return None
def ma_sekci_prilohy(rtf):
return PRILOHY_HEADER in rtf
def pridat_do_sekce_prilohy(rtf, new_bkm_entry, filenameforbookmark):
"""Přidá soubor do EXISTUJÍCÍ sekce 'Vložené přílohy'.
Postup:
1. Spočítá počet Files: odkazů = N → nový index = N
2. Posune bkmkstart/bkmkend >= N o +1 (uvolní místo pro nový)
3. Vloží nový \pard před uzavírací prázdný řádek sekce
4. Vloží bookmark na pozici N do {\info{\bookmarks ...}}
"""
# 1. Počet existujících Files: odkazů v bookmarks listu
bkm_match = re.search(r'\{\\info\{\\bookmarks ([^}]*)\}\}', rtf)
if bkm_match:
bkm_entries = [e for e in bkm_match.group(1).split(';') if e.strip()]
n_files = sum(1 for e in bkm_entries if '"Files:' in e)
else:
bkm_entries = []
n_files = 0
new_idx = n_files
print(f" Počet existujících Files odkazů: {n_files} → nový bkmkstart={new_idx}")
# 2. Posunout bkmkstart/bkmkend >= n_files o +1
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + (
str(int(m.group(1)) + 1) if int(m.group(1)) >= n_files
else m.group(1)),
rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + (
str(int(m.group(1)) + 1) if int(m.group(1)) >= n_files
else m.group(1)),
rtf)
# 3. Najít uzavírající prázdný řádek sekce (první výskyt po hlavičce)
prilohy_pos = rtf.find(PRILOHY_HEADER)
closing_pos = rtf.find(PRILOHY_CLOSING, prilohy_pos)
if closing_pos == -1:
raise RuntimeError("Nenalezen uzavírací řádek sekce Vložené přílohy!")
new_pard = (r'\pard\s10{\*\bkmkstart ' + str(new_idx) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(new_idx) + r'}\par')
rtf = rtf[:closing_pos] + new_pard + '\n' + rtf[closing_pos:]
# 4. Vložit bookmark na pozici n_files do {\info{\bookmarks}}
def insert_bookmark(m):
entries = [e for e in m.group(1).split(';') if e.strip()]
entries.insert(n_files, new_bkm_entry)
return '{\\info{\\bookmarks ' + ';'.join(entries) + '}}'
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', insert_bookmark, rtf)
return rtf
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží nový obsah na ZAČÁTEK stávajícího dekurzu (žádná existující sekce)."""
rtf = existing_rtf
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = match.start()
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
return rtf
# ─────────────────────────────────────────────────────────────────────────────
conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250')
# ── Krok 1: vložit soubory do ext DB ─────────────────────────────────────────
bookmark_list = []
bookmarks_body = ''
cislo = 9
poradi = 0
for s in SOUBORY:
cesta_souboru = os.path.join(CESTA, s['souborname'])
datumsouboru = datetime.datetime.fromtimestamp(os.path.getmtime(cesta_souboru))
print(f"\n>>> Zpracovávám: {s['souborname']}")
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=IDPAC,
cesta=CESTA, souborname=s['souborname'],
prvnizavorka=s['prvnizavorka'],
soubordate=s['datum'], souborfiledate=datumsouboru,
poznamka=s['druhazavorka'],
)
print(f" → FILES.ID = {fileid}")
filenameforbookmark = (s['datum'].strftime('%Y-%m-%d') + ' '
+ s['prvnizavorka'] + ': ' + s['druhazavorka'])
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 2: rozhodovací logika ────────────────────────────────────────────────
print(f"\n>>> Hledám poslední dekurs pro IDPAC={IDPAC}...")
existujici = najdi_posledni_dekurs_dnes(conn, IDPAC, DATUM)
cur = conn.cursor()
now = datetime.datetime.now()
if existujici:
dekurs_id, existing_rtf = existujici
if ma_sekci_prilohy(existing_rtf):
# ── Případ 1: dnešní dekurs s existující sekcí → přidáme do ní ──────
print(f"\n>>> Sekce 'Vložené přílohy' nalezena v DEKURS ID={dekurs_id}")
print(">>> Přidávám soubor DO existující sekce...")
filenameforbookmark = (SOUBORY[0]['datum'].strftime('%Y-%m-%d') + ' '
+ SOUBORY[0]['prvnizavorka'] + ': ' + SOUBORY[0]['druhazavorka'])
merged_rtf = pridat_do_sekce_prilohy(
existing_rtf,
bookmark_list[0],
filenameforbookmark
)
else:
# ── Případ 2: dnešní dekurs bez sekce → prepend ───────────────────────
print(f"\n>>> DEKURS ID={dekurs_id} nemá sekci příloh → prepend nové sekce")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(SOUBORY))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
# ── Případ 3: žádný dnešní dekurs → nový ─────────────────────────────────
print(f"\n>>> Žádný dekurs pro {DATUM} → vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
rtf = rtf.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?,?,?,?,?,?,?,?)",
(dekursid, 6, 2, 2, IDPAC, now.date(), now.time(), rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
conn.close()
print("\n=== HOTOVO ===")
print("Otevři Medicus → karta Buzalka → zkontroluj dekurs!")
@@ -0,0 +1,156 @@
# test_import_FINAL.py detailní dokumentace
## Co skript dělá
Importuje PDF soubory (lékařské zprávy) do Medicus DB. Konkrétně:
1. Uloží fyzický soubor do **externí Firebird DB** (tabulka FILES)
2. Vloží nebo aktualizuje **dekurs pacienta** (tabulka DEKURS) s klikacím RTF odkazem na soubor
---
## Vstupní data (konfigurace nahoře)
```python
CESTA = r'u:\\' # adresář se zdrojovými PDF soubory
IDPAC = 9742 # ID pacienta v DB
DATUM = datetime.date(2026, 3, 18) # datum zprávy (ne dnešek!)
SOUBORY = [
{
'souborname': 'název souboru.pdf',
'prvnizavorka': 'typ zprávy', # např. "vyšetření"
'druhazavorka': 'poznámka', # volný text
'datum': DATUM,
},
...
]
```
Pozor: `DATUM` je datum zprávy (ne dnešek). Podle tohoto data se hledá existující dekurs.
---
## Rozhodovací logika 3 scénáře
```
Poslední dekurs pacienta
├─ z JINÉHO dne / neexistuje
│ └─→ SCÉNÁŘ 3: vytvoří nový dekurs
└─ z DNEŠNÍHO dne (= DATUM)
├─ MÁ sekci "Vložené přílohy"
│ └─→ SCÉNÁŘ 1: přidá odkaz DO existující sekce
└─ NEMÁ sekci "Vložené přílohy"
└─→ SCÉNÁŘ 2: prepend nové sekce na začátek
```
Klíčová funkce pro detekci: `ma_sekci_prilohy(rtf)` hledá RTF string `Vlo\'9een\'e9 p\'f8\'edlohy:` (= „Vložené přílohy:" zakódováno win1250).
---
## Krok 1 uložení souboru do ext DB
Volá `funkce_ext.zapis_file_ext(...)` pro každý soubor. Vrátí `fileid` (ID záznamu v tabulce FILES).
Z každého souboru se postaví:
- **bookmark entry** pro `{\info{\bookmarks ...}}` blok RTF:
`"2026-03-18 vyšetření: poznámka","Files:1234",9`
- **RTF pard** (klikací odkaz) pro tělo dekurzu:
`\pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2026-03-18 vyšetření: poznámka{\*\bkmkend 0}\par`
Číslo `cislo` začíná na 9 a roste po 7 (interní Medicus konvence). Index `poradi` (bkmkstart) začíná na 0 a roste po 1.
---
## Krok 2 práce s dekurzem
### Scénář 1: přidání DO existující sekce (`pridat_do_sekce_prilohy`)
Situace: dnešní dekurs již má blok „Vložené přílohy" s nějakými odkazy.
Postup:
1. Spočítá počet existujících `Files:` odkazů v `{\info{\bookmarks}}` → to je index nového (`new_idx`)
2. Posune všechny `\bkmkstart N` / `\bkmkend N` kde `N >= new_idx` o +1 (uvolní místo)
3. Vloží nový `\pard` řádek **před** uzavírací `\pard\s10\plain\cs15\f0\fs20 \par` sekce
4. Vloží nový bookmark na pozici `new_idx` v `{\info{\bookmarks}}`
Výsledek: soubor se přidá na konec existujícího seznamu příloh, indexy zůstanou konzistentní.
### Scénář 2: prepend nové sekce (`merge_rtf_prepend`)
Situace: dnešní dekurs existuje, ale ještě nemá blok příloh.
Postup:
1. Posune všechny existující `\bkmkstart N` / `\bkmkend N` o +n_new (počet nových souborů)
2. Přidá nové bookmarky **na začátek** `{\info{\bookmarks}}` bloku
- Pokud `{\info{\bookmarks}}` neexistuje, vloží ho za `\deflang1029`
3. Vloží nové tělo (záhlaví „Vložené přílohy:" + řádky s odkazy) **před** první `\uc1\pard` těla stávajícího dekurzu
Výsledek: sekce příloh je viditelně nahoře, stávající text dekurzu zůstane pod ní.
### Scénář 3: nový dekurs
Situace: žádný dnešní dekurs neexistuje.
Sestaví RTF šablonu s:
- `{\info{\bookmarks ...}}` všechny bookmarky
- záhlaví „Vložené přílohy:" + klikací řádky
- uzavírací prázdný řádek
Vloží jako nový řádek do tabulky DEKURS s `iduzi=6, idprac=2, idodd=2` (Vladimír Buzalka, ordinace).
---
## RTF formát dekurzu
```rtf
{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029
{\info{\bookmarks "2026-03-18 vyšetření: poznámka","Files:1234",9}}
{\fonttbl{\f0\fnil\fcharset238 Arial;} ...}
{\colortbl ;\red0\green0\blue255; ...}
{\stylesheet ... {\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 Vložené přílohy:\par
\pard\s10{\*\bkmkstart 0}\plain\cs32\f0\ul\fs20\cf1 2026-03-18 vyšetření: poznámka{\*\bkmkend 0}\par
\pard\s10\plain\cs15\f0\fs20 \par
}
```
- **cs20** = kurzíva (záhlaví sekce)
- **cs32** = podtržený modrý text (klikací odkaz)
- **cs15** = normální text
- `\cf1` = modrá barva (první v colortbl)
---
## Závislosti
| Import | Odkud | Co dělá |
|--------|-------|---------|
| `funkce_ext.zapis_file_ext` | `funkce_ext.py` | Uloží soubor do ext DB (tabulka FILES), vrátí fileid |
| `funkce.get_dekurs_id` | `funkce.py` | Vrátí nové ID pro INSERT do tabulky DEKURS |
| `fdb` | pip | Připojení k Firebird DB |
---
## Tabulky v DB
| Tabulka | DB | Popis |
|---------|----|-------|
| `DEKURS` | hlavní (`medicus.fdb`) | Záznamy dekurzu, pole `DEKURS` obsahuje RTF text |
| `FILES` | ext DB (`MEDICUS_FILES_*.fdb`) | Binární obsah souborů |
---
## Jak spustit
Skript se spouští jednorázově na Windows stroji s přístupem k Firebird DB. Před spuštěním:
1. Upravit `SOUBORY` seznam PDF souborů ke zpracování
2. Zkontrolovat `IDPAC`, `DATUM`, `CESTA`
3. Ověřit, že PDF soubory fyzicky existují na `CESTA`
Po spuštění ověřit v Medicus: karta pacienta → záložka Dekurzy → kliknout na odkaz.
@@ -0,0 +1,47 @@
"""test_import_april2026.py jednorázový testovací import do DB202604
Spustit na Windows (Medicus může být spuštěný, jen ne na kartě tohoto pacienta).
"""
import datetime
import os
import fdb
import funkce_ext
SOUBOR_CESTA = r'u:\\'
SOUBOR_NAZEV = '7309208104 Buzalka, Vladimír 2026-04-01 [testovaci zprava] [všechno OK].pdf'
IDPAC = 9742 # Buzalka Vladimír (RC 7309208104)
PRVNI_ZAVORKA = 'testovaci zprava'
POZNAMKA = 'všechno OK'
DATUM_ZPRAVY = datetime.date(2026, 4, 1)
DATUM_SOUBORU = datetime.datetime.fromtimestamp(
os.path.getmtime(os.path.join(SOUBOR_CESTA, SOUBOR_NAZEV)))
print(f"Soubor: {os.path.join(SOUBOR_CESTA, SOUBOR_NAZEV)}")
print(f"Datum zprávy: {DATUM_ZPRAVY}")
print(f"Datum souboru:{DATUM_SOUBORU}")
print()
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='WIN1250'
)
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn,
idpac=IDPAC,
cesta=SOUBOR_CESTA,
souborname=SOUBOR_NAZEV,
prvnizavorka=PRVNI_ZAVORKA,
soubordate=DATUM_ZPRAVY,
souborfiledate=DATUM_SOUBORU,
poznamka=POZNAMKA,
)
conn.close()
print()
print(f"Hotovo! FILES.ID = {fileid}")
print("Otevři Medicus, přejdi na kartu Buzalka Vladimír a zkontroluj záložku Soubory.")
@@ -0,0 +1,182 @@
"""test_import_merge.py vloží přílohy do dekurzu daného dne.
Pokud dekurs pro daný den existuje → vloží přílohy NAHORU před stávající text.
Pokud neexistuje → vytvoří nový dekurs.
Spustit na Windows.
"""
import datetime, os, re, fdb
import funkce, funkce_ext
CESTA = r'u:\\'
IDPAC = 9742 # Buzalka Vladimír
DATUM = datetime.date(2026, 3, 18)
SOUBORY = [
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy1].pdf',
'prvnizavorka': 'vyšetření', 'druhazavorka': 'ahoj Claude copy1', 'datum': DATUM},
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude copy2].pdf',
'prvnizavorka': 'vyšetření', 'druhazavorka': 'ahoj Claude copy2', 'datum': DATUM},
{'souborname': '7309208104 2026-03-18 Buzalka, Vladimír [vyšetření] [ahoj Claude].pdf',
'prvnizavorka': 'vyšetření', 'druhazavorka': 'ahoj Claude', 'datum': DATUM},
]
# ─────────────────────────────────────────────────────────────────────────────
def najdi_posledni_dekurs_dnes(conn, idpac, datum_vlozeni):
"""Najde POSLEDNÍ dekurs pacienta (jakýkoli datum) a vrátí (id, rtf)
pouze pokud je ze stejného dne jako datum_vlozeni. Jinak vrátí None.
Logika: zajímá nás jen poslední zápis pokud je z dnešního dne,
vložíme přílohy do něj. Pokud je starší, zakládáme nový dnešní záznam.
"""
cur = conn.cursor()
cur.execute("""
SELECT FIRST 1 ID, DATUM, DEKURS FROM DEKURS
WHERE IDPAC = ?
ORDER BY ID DESC
""", (idpac,))
row = cur.fetchone()
if row is None:
return None
dekurs_id, dekurs_datum, dekurs_rtf = row
print(f" Poslední dekurs: ID={dekurs_id}, datum={dekurs_datum}")
if dekurs_datum == datum_vlozeni:
print(f" → shoduje se s dneškem ({datum_vlozeni}), budeme mergovat")
return (dekurs_id, dekurs_rtf)
else:
print(f" → jiný den ({dekurs_datum}{datum_vlozeni}), vytvoříme nový")
return None
def merge_rtf_prepend(existing_rtf, new_bkm_list, new_body_pards, n_new):
"""Vloží nový obsah (přílohy) na ZAČÁTEK stávajícího dekurzu.
existing_rtf stávající RTF string z DB
new_bkm_list list stringů typu '"popis","Files:123",9'
new_body_pards RTF string: header \par + \pard řádky s odkazy
n_new počet nových bkmkstart indexů (= počet souborů)
"""
rtf = existing_rtf
# 1. Posunout stávající bkmkstart/bkmkend indexy o n_new,
# aby nedošlo ke kolizi s našimi novými (0, 1, 2 …)
rtf = re.sub(r'\\bkmkstart (\d+)',
lambda m: '\\bkmkstart ' + str(int(m.group(1)) + n_new), rtf)
rtf = re.sub(r'\\bkmkend (\d+)',
lambda m: '\\bkmkend ' + str(int(m.group(1)) + n_new), rtf)
# 2. Přidat naše bookmarky na ZAČÁTEK {\info{\bookmarks ...}}
new_bkm_str = ';'.join(new_bkm_list)
def merge_bkm(m):
existing = m.group(1).strip()
combined = new_bkm_str + (';' + existing if existing else '')
return '{\\info{\\bookmarks ' + combined + '}}'
if re.search(r'\{\\info\{\\bookmarks', rtf):
rtf = re.sub(r'\{\\info\{\\bookmarks ([^}]*)\}\}', merge_bkm, rtf)
else:
# žádný {\info} blok vložíme za \deflang...
rtf = re.sub(r'(\\deflang\d+)',
r'\1{\\info{\\bookmarks ' + new_bkm_str + '}}', rtf, count=1)
# 3. Vložit naše tělo před první \uc1\pard těla stávajícího dekurzu
match = re.search(r'\\uc1\\pard', rtf)
if match:
pos = rtf.index(r'\uc1\pard', match.start())
rtf = rtf[:pos] + new_body_pards + '\n' + rtf[pos:]
else:
# fallback připojit před poslední }
rtf = rtf.rstrip()
if rtf.endswith('}'):
rtf = rtf[:-1] + new_body_pards + '\n}'
return rtf
# ─────────────────────────────────────────────────────────────────────────────
conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='WIN1250')
# ── Krok 1: vložit soubory do ext DB ─────────────────────────────────────────
bookmark_list = []
bookmarks_body = ''
cislo = 9
poradi = 0
for s in SOUBORY:
cesta_souboru = os.path.join(CESTA, s['souborname'])
datumsouboru = datetime.datetime.fromtimestamp(os.path.getmtime(cesta_souboru))
print(f"\n>>> Zpracovávám: {s['souborname']}")
fileid = funkce_ext.zapis_file_ext(
vstupconnection=conn, idpac=IDPAC,
cesta=CESTA, souborname=s['souborname'],
prvnizavorka=s['prvnizavorka'],
soubordate=s['datum'], souborfiledate=datumsouboru,
poznamka=s['druhazavorka'],
)
print(f" → FILES.ID = {fileid}")
filenameforbookmark = (s['datum'].strftime('%Y-%m-%d') + ' '
+ s['prvnizavorka'] + ': ' + s['druhazavorka'])
bookmark_list.append('"' + filenameforbookmark + '","Files:' + str(fileid) + '",' + str(cislo))
cislo += 7
bookmarks_body += (r'\pard\s10{\*\bkmkstart ' + str(poradi) + r'}'
r'\plain\cs32\f0\ul\fs20\cf1 ' + filenameforbookmark
+ r'{\*\bkmkend ' + str(poradi) + r'}\par')
poradi += 1
# Tělo přílohy (záhlaví + odkaz řádky + prázdný řádek)
new_body = (r'\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9een\'e9 p\'f8\'edlohy:\par' + '\n'
+ bookmarks_body + '\n'
+ r'\pard\s10\plain\cs15\f0\fs20 \par')
# ── Krok 2: existující dekurs pro tento den? ──────────────────────────────────
print(f"\n>>> Hledám poslední dekurs pro IDPAC={IDPAC}...")
existujici = najdi_posledni_dekurs_dnes(conn, IDPAC, DATUM)
cur = conn.cursor()
now = datetime.datetime.now()
if existujici:
dekurs_id, existing_rtf = existujici
print(f"\n>>> Nalezen existující dekurs pro {DATUM}: ID={dekurs_id}")
print(">>> Vkládám přílohy na začátek...")
merged_rtf = merge_rtf_prepend(existing_rtf, bookmark_list, new_body, len(SOUBORY))
print("\n=== Výsledný RTF ===")
print(merged_rtf)
cur.execute("UPDATE DEKURS SET DEKURS = ? WHERE ID = ?", (merged_rtf, dekurs_id))
conn.commit()
print(f"\n>>> UPDATE DEKURS ID={dekurs_id} hotovo!")
else:
print(f"\n>>> Žádný dekurs pro {DATUM} nenalezen vytvářím nový...")
bookmark_str = ';'.join(bookmark_list)
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES}}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs32\f0\ul\fs20\cf1 Odkaz;}}
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
rtf = rtf.replace('BOOKMARKNAMES', bookmark_str)
rtf = rtf.replace('BOOKMARKSTEXT', new_body)
print("\n=== Výsledný RTF ===")
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
cur.execute(
"INSERT INTO DEKURS (id, iduzi, idprac, idodd, idpac, datum, cas, dekurs)"
" VALUES (?,?,?,?,?,?,?,?)",
(dekursid, 6, 2, 2, IDPAC, now.date(), now.time(), rtf)
)
conn.commit()
print(f"\n>>> Nový DEKURS ID={dekursid}")
conn.close()
print("\n=== HOTOVO ===")
print("Otevři Medicus → karta Buzalka → zkontroluj dekurs!")
@@ -0,0 +1,140 @@
# Dekurzy report dokumentace
## Co report dělá
Generuje Excel soubor s přehledem všech dekurzů z ordinace MUDr. Buzalkové za zadané období.
Hlavní list **Dekurz** zobrazuje každý dekurz jako jeden řádek. Čísla v sloupcích jsou klikatelné hyperlinkové zkratky, které přeskočí na příslušný detailní list (Recepty, Výkony, Soubory apod.).
---
## Spuštění
```
python dekurz_report.py
```
**Vstupní parametry** (nastavit přímo v souboru):
| Proměnná | Výchozí hodnota | Popis |
|---|---|---|
| `DATUM_OD` | `2025-01-01` | Začátek období |
| `DATUM_DO` | dnešní datum | Konec období (automaticky) |
| `VYSTUPNI_ADRESAR` | `u:\Dropbox\Ordinace\Reporty` | Kam se ukládá |
| `NAZEV_REPORTU` | `Dekurzy` | Část názvu souboru |
**Výstupní soubor:** `YYYY-MM-DD HH-MM-SS Dekurzy.xlsx`
Starý soubor se stejným názvem je automaticky smazán.
---
## Zdroj dat
Databáze Firebird: `localhost:c:\medicus 3\data\medicus.fdb`
Připojení: SYSDBA / masterkey, charset win1250
### Hlavní dotaz
```sql
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 >= '2025-01-01' AND d.DATUM <= dnes
ORDER BY d.DATUM DESC, d.CAS DESC, k.PRIJMENI, k.JMENO
```
Řazení: nejnovější záznamy nahoře.
---
## Jak funguje parsování RTF bookmarků
Každý dekurz je uložen jako RTF blob ve sloupci `DEKURS.DEKURS`.
Medicus do RTF hlavičky zapisuje **bookmarky** hypertextové odkazy na propojené záznamy:
```
{\info{\bookmarks "ATORIS","Rec:322528",17;"01543","VykA:189603",8}}
```
Formát: `"název","TYP:ID",číslo_stylu`
Skript parsuje regex `"([^"]+)","([A-Za-z]+):(\d+)"` a extrahuje typ a ID záznamu.
### Typy bookmarků a jejich tabulky
| Bookmark | List v Excelu | Tabulka v DB | PK | Zobrazované sloupce |
|---|---|---|---|---|
| `Rec` | Recepty | `RECEPT` | `ID` | LEK, DSIG (lék, dávkování) |
| `VykA` | Výkony | `DOKLADD` | `ID` | KOD, DDGN (kód výkonu, diagnóza) |
| `Files` | Soubory | `FILES` | `ID` | FILENAME, DATUM |
| `MEDLAB` | MedLab | `HISTDOC` | `ID` | DATUM, TYP (žádanka do laboratoře) |
| `Lab` | Lab | `LABVH` | `IDVH` | DATUM, CISLO (výsledky laboratoře) |
| `Ock` | Očkování | `OCKZAZ` | `ID` | DATUM, LATKA (vakcína) |
| `Nes` | Neschop. | `NES` | `ID` | ZACNES, KONNES (od do) |
| `Lec` | Léčiva | `LECD` | `ID` | KOD, DATOSE (léčivo podané v ordinaci) |
| `SpecVys` | SpecVys | `SPECVYS` | `IDSPECVYS` | TYP, DATUM (Tonotrack, holter…) |
| `PlaPac` | Platby | `PLA` | `IDPLA` | DATUM, CENA, DOKLAD |
| ostatní | Ostatní | | | TYP, ID, Název (formuláře, poukazy…) |
**Ostatní typy** (méně časté):
`ORTOPE` ePoukaz na ortopedickou pomůcku
`ZDRINF` Žádost o předání zdravotních informací
`PROHLAS` Prohlášení
`POTDPN` Potvrzení DPN
`MOTORVO` Posudek motorového vozidla
`LAZPEC` Lázně
`VypZdrD` Výpis ze zdravotní dokumentace
`VYMLIST` Výměnný list
`PouRTG` Poukaz RTG
`ZPUPRN` Způsobilost k práci/řízení
`EPOSMRO` ePosudek MRO
`ZNESUP` Potvrzení neschopnosti uchazeče o zaměstnání
> Všechny výše uvedené jdou do listu **Ostatní** s uvedením typu, ID a názvu.
---
## Struktura Excel souboru
### List Dekurz (hlavní)
| Sloupec | Zdroj | Popis |
|---|---|---|
| Datum | `DEKURS.DATUM` | Datum dekurzu |
| Čas | `DEKURS.CAS` | Čas (HH:MM) |
| Lékař | `UZIVATEL.ZKRATKA` | MBU / VBU / ISE |
| Jméno | `KAR.PRIJMENI + JMENO` | Formát: `Příjmení, I.` |
| Rodné číslo | `KAR.RODCIS` | |
| Pojišťovna | `KAR.POJ` | Kód pojišťovny (111, 201…) |
| Rec … PlaPac | RTF bookmark | Počet záznamů **klikací hyperlink** na detailní list |
| Ostatní | RTF bookmark | Počet ostatních typů hyperlink na list Ostatní |
### Detailní listy
Každý list má:
- Záhlaví s vlastní barevnou kombinací
- Sloupce: Datum, Jméno + specifické sloupce dle typu
- Střídání bílých a barevných řádků
- Tenké šedé ohraničení všech buněk
- Zmrazený první řádek
- Autofiltr
---
## Technické poznámky
- Firebird limit `IN (...)` je 1500 hodnot dotazy na detaily se automaticky dělí do dávek po 1000
- RTF blob je čten přes `blob.read()` nebo přímo jako string
- Jméno pacienta: `Příjmení, I.` (iniciála prvního písmene jména)
- Chybová hláška `BlobReader.close: invalid BLOB handle` je neškodná GC uzavírá handlery po odpojení DB
---
## Scheduled Task
Spouštěcí příkaz:
```
python "C:\Users\vlado\PycharmProjects\Medicus\MedicusWithClaudeDekurz\dekurz_report.py"
```
Doporučené spouštění: každý den ráno (např. 6:00), aby byl vždy čerstvý soubor v Dropboxu.
@@ -0,0 +1,352 @@
import sys, io, re, os, glob
from datetime import date, datetime, timedelta
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
import fdb
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
VYSTUPNI_ADRESAR = r'u:\Dropbox\Ordinace\Reporty'
NAZEV_REPORTU = 'Dekurzy_TEST'
DATUM_OD = (date.today() - timedelta(days=100)).strftime('%Y-%m-%d')
DATUM_DO = date.today().strftime('%Y-%m-%d')
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
cur.execute(f"""
SELECT d.DATUM, d.CAS, u.ZKRATKA, k.PRIJMENI, k.JMENO, k.RODCIS, k.POJ, d.DEKURS
FROM DEKURS d
JOIN KAR k ON k.IDPAC = d.IDPAC
LEFT JOIN UZIVATEL u ON u.IDUZI = d.IDUZI
WHERE d.DATUM >= '{DATUM_OD}' AND d.DATUM <= '{DATUM_DO}'
ORDER BY d.DATUM DESC, d.CAS DESC, k.PRIJMENI, k.JMENO
""")
raw_rows = cur.fetchall()
TOP_TYPY = ['VykA', 'Rec', 'Files', 'MEDLAB', 'Lab', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
def rtf_na_text(rtf):
"""Jednoduchý převod RTF na čistý text."""
# Odstraň info blok (bookmarky apod.)
text = re.sub(r'\{\\info.*?\}', '', rtf, flags=re.DOTALL)
# Odstraň ostatní skupiny v {} rekurzivně (fonty, barvy apod.)
for _ in range(6):
text = re.sub(r'\{[^{}]*\}', '', text)
# Nový řádek za \par \line
text = re.sub(r'\\par\b\s*', '\n', text)
text = re.sub(r'\\line\b\s*', '\n', text)
# Dekóduj RTF hex escape sekvence (\'xx) jako cp1250 → správná čeština
def decode_hex(m):
try:
return bytes.fromhex(m.group(1)).decode('cp1250')
except Exception:
return ''
text = re.sub(r"\\'([0-9a-fA-F]{2})", decode_hex, text)
# Odstraň ostatní RTF příkazy
text = re.sub(r'\\[a-zA-Z]+\-?[0-9]*\s?', '', text)
text = re.sub(r'[{}\\]', '', text)
# Vyčisti prázdné řádky a whitespace
lines = [l.strip() for l in text.splitlines()]
lines = [l for l in lines if l]
return '\n'.join(lines)
# Parse dekurzů
rows = []
for datum, cas, zkratka, prijmeni, jmeno, rodcis, poj, dekurs_blob in raw_rows:
rtf = dekurs_blob.read() if hasattr(dekurs_blob, 'read') else (dekurs_blob or '')
pocty = {}
ids_by_typ = {t: [] for t in TOP_TYPY}
ids_ostatni = []
for nazev, typ, rid in re.findall(r'"([^"]+)","([A-Za-z]+):(\d+)"', rtf):
pocty[typ] = pocty.get(typ, 0) + 1
if typ in ids_by_typ:
ids_by_typ[typ].append(int(rid))
else:
ids_ostatni.append((typ, int(rid), nazev))
top = [pocty.get(t, 0) for t in TOP_TYPY]
ostatni = sum(v for k, v in pocty.items() if k not in TOP_TYPY)
iniciala = jmeno[0] + '.' if jmeno and jmeno.strip() else ''
jmeno_cel = f"{prijmeni.strip()}, {iniciala}" if prijmeni else iniciala
text_dekurzu = rtf_na_text(rtf)
rows.append((datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni, text_dekurzu))
# ── Načtení detailů z DB ────────────────────────────────────────────────────
def fetch_details(cur, table, pk, id_col, fields, ids):
if not ids:
return {}
result = {}
batch_size = 1000
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
ph = ','.join('?' * len(batch))
cur.execute(f"SELECT {pk}, {','.join(fields)} FROM {table} WHERE {id_col} IN ({ph})", batch)
for row in cur.fetchall():
result[row[0]] = row[1:]
return result
def get_ids(rows, typ):
return list({rid for *_, ids_by_typ, _, _text in rows for rid in ids_by_typ[typ]})
rec_det = fetch_details(cur, 'RECEPT', 'ID', 'ID', ['LEK','DSIG'], get_ids(rows,'Rec'))
vyka_det = fetch_details(cur, 'DOKLADD', 'ID', 'ID', ['KOD','DDGN','BODY'], get_ids(rows,'VykA'))
files_det = fetch_details(cur, 'FILES', 'ID', 'ID', ['FILENAME','DATUM'], get_ids(rows,'Files'))
medlab_det = fetch_details(cur, 'HISTDOC', 'ID', 'ID', ['DATUM','TYP'], get_ids(rows,'MEDLAB'))
lab_det = fetch_details(cur, 'LABVH', 'IDVH', 'IDVH', ['DATUM','CISLO'], get_ids(rows,'Lab'))
ock_det = fetch_details(cur, 'OCKZAZ', 'ID', 'ID', ['DATUM','LATKA'], get_ids(rows,'Ock'))
nes_det = fetch_details(cur, 'NES', 'ID', 'ID', ['ZACNES','KONNES'], get_ids(rows,'Nes'))
lec_det = fetch_details(cur, 'LECD', 'ID', 'ID', ['KOD','DATOSE'], get_ids(rows,'Lec'))
spec_det = fetch_details(cur, 'SPECVYS', 'IDSPECVYS','IDSPECVYS',['TYP','DATUM'], get_ids(rows,'SpecVys'))
pla_det = fetch_details(cur, 'PLA', 'IDPLA', 'IDPLA', ['DATUM','CENA','DOKLAD'], get_ids(rows,'PlaPac'))
conn.close()
print(f"Načteno {len(rows)} dekurzů (období: {DATUM_OD} {DATUM_DO})")
# ── Styly ──────────────────────────────────────────────────────────────────
tenka_cara = Side(style='thin', color='AAAAAA')
ohraniceni = Border(left=tenka_cara, right=tenka_cara, top=tenka_cara, bottom=tenka_cara)
hl_font = Font(bold=True, color="FFFFFF")
hl_fill = PatternFill("solid", fgColor="2E75B6")
r_fill = [PatternFill("solid", fgColor="FFFFFF"), PatternFill("solid", fgColor="DCE6F1")]
BARVY_LISTU = {
'Recepty': ('1F6B33', 'E2EFDA'),
'Výkony': ('2E4057', 'D6E4F0'),
'Soubory': ('7B3F00', 'FAE5D3'),
'Žádanky': ('4A235A', 'F5EEF8'),
'Lab výsl.': ('145A32', 'D5F5E3'),
'Očkování': ('7E5109', 'FDEBD0'),
'Neschop.': ('922B21', 'FADBD8'),
'Léčiva': ('1A5276', 'D6EAF8'),
'SpecVys': ('0B5345', 'D1F2EB'),
'Platby': ('4D5656', 'EAECEE'),
'Ostatní': ('2C3E50', 'EBF5FB'),
}
def zapis_hlavicku(ws, sloupce, sirky, barva_hex):
hl_fill_l = PatternFill("solid", fgColor=barva_hex)
for col, (nazev, sirka) in enumerate(zip(sloupce, sirky), start=1):
cell = ws.cell(row=1, column=col, value=nazev)
cell.font = hl_font
cell.fill = hl_fill_l
cell.alignment = Alignment(horizontal='center')
cell.border = ohraniceni
ws.column_dimensions[cell.column_letter].width = sirka
def zapis_radek(ws, row_i, hodnoty, zarovnani, barva_hex):
fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \
else PatternFill("solid", fgColor=barva_hex)
for col_i, (val, align) in enumerate(zip(hodnoty, zarovnani), start=1):
cell = ws.cell(row=row_i, column=col_i, value=val)
cell.fill = fill
cell.border = ohraniceni
cell.alignment = Alignment(horizontal=align)
if col_i == 1 and isinstance(val, __import__('datetime').date):
cell.number_format = 'DD.MM.YYYY'
def hyperlink_cell(ws, row_i, col_i, cil_list, cil_radek, text, barva_hex):
fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \
else PatternFill("solid", fgColor=barva_hex)
cell = ws.cell(row=row_i, column=col_i)
cell.value = str(text)
# Název listu s mezerou musí být v apostrofech
sheet_ref = f"'{cil_list}'" if ' ' in cil_list else cil_list
cell.hyperlink = f'#{sheet_ref}!A{cil_radek}'
cell.font = Font(color="0000FF", underline='single')
cell.fill = fill
cell.border = ohraniceni
cell.alignment = Alignment(horizontal='center')
# ── Workbook ───────────────────────────────────────────────────────────────
wb = openpyxl.Workbook()
LISTY = [
('Recepty', 'Rec', rec_det, ['Datum','Jméno','Recept','Dávkování'], [12,25,25,12], None),
('Výkony', 'VykA', vyka_det, ['Datum','Jméno','Kód výkonu','Diagnóza','Body'], [12,25,14,10,8], None),
('Soubory', 'Files', files_det, ['Datum','Jméno','Soubor','Datum souboru'], [12,25,35,14], None),
('Žádanky', 'MEDLAB', medlab_det, ['Datum','Jméno','Typ'], [12,25,15], None),
('Lab výsl.', 'Lab', lab_det, ['Datum','Jméno','Číslo'], [12,25,20], None),
('Očkování', 'Ock', ock_det, ['Datum','Jméno','Datum očkování','Vakcína'], [12,25,14,30], None),
('Neschop.', 'Nes', nes_det, ['Datum','Jméno','Od','Do'], [12,25,12,12], None),
('Léčiva', 'Lec', lec_det, ['Datum','Jméno','Kód','Datum výkonu'], [12,25,12,14], None),
('SpecVys', 'SpecVys', spec_det, ['Datum','Jméno','Typ vyšetření','Datum vyšetření'], [12,25,25,14], None),
('Platby', 'PlaPac', pla_det, ['Datum','Jméno','Datum platby','Částka','Doklad'], [12,25,14,12,15], None),
('Ostatní', None, None, ['Datum','Jméno','Typ','ID','Název'], [12,25,12,10,30], None),
]
ws_d = wb.active
ws_d.title = "Dekurz"
# List "Text dekurzu" hned za Dekurzem
ws_text = wb.create_sheet("Text dekurzu")
ws_listy = {}
for nazev, *_ in LISTY:
ws_listy[nazev] = wb.create_sheet(nazev)
for nazev, typ, det, sloupce, sirky, _ in LISTY:
barva_hl, _ = BARVY_LISTU[nazev]
zapis_hlavicku(ws_listy[nazev], sloupce, sirky, barva_hl)
ws_listy[nazev].freeze_panes = 'A2'
# Záhlaví listu "Text dekurzu"
zapis_hlavicku(ws_text,
['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Text dekurzu'],
[12, 8, 8, 25, 14, 100],
'2E75B6')
ws_text.freeze_panes = 'A2'
row_ptr_text = 2
# Zobrazované názvy sloupců pro TOP_TYPY (stejné pořadí)
NAZVY_TYPY = ['VykA', 'Rec', 'Files', 'Žádanky', 'Lab výsl.', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
# Záhlaví Dekurz sloupce AR (bez textu)
nazvy_d = ['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Pojišťovna'] + ['HodVyk'] + NAZVY_TYPY + ['Ostatní']
sirky_d = [12, 8, 8, 25, 14, 12 ] + [10] + [38, 8, 8, 8, 8, 8, 8, 8, 8, 8] + [8]
zapis_hlavicku(ws_d, nazvy_d, sirky_d, '2E75B6')
ws_d.freeze_panes = 'A2'
ws_d.auto_filter.ref = f"A1:R{len(rows)+1}"
row_ptr = {nazev: 2 for nazev, *_ in LISTY}
# ── Plnění dat ─────────────────────────────────────────────────────────────
def get_det_hodnoty(typ, rid, datum, jmeno_cel):
if typ == 'Rec':
d = rec_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1] or '']
elif typ == 'VykA':
d = vyka_det.get(rid, ('', '', 0))
return [datum, jmeno_cel, d[0] or '', (d[1] or '').strip(), d[2] or 0]
elif typ == 'Files':
d = files_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1]]
elif typ == 'MEDLAB':
d = medlab_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[1] or '']
elif typ == 'Lab':
d = lab_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[1] or '']
elif typ == 'Ock':
d = ock_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0], d[1] or '']
elif typ == 'Nes':
d = nes_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0], d[1]]
elif typ == 'Lec':
d = lec_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1]]
elif typ == 'SpecVys':
d = spec_det.get(rid, ('', ''))
return [datum, jmeno_cel, d[0] or '', d[1]]
elif typ == 'PlaPac':
d = pla_det.get(rid, ('', '', ''))
return [datum, jmeno_cel, d[0], d[1], d[2] or '']
return []
ZAROVNANI = {
'Recepty': ['left','left','left','center'],
'Výkony': ['left','left','center','center','right'],
'Soubory': ['left','left','left','left'],
'MedLab': ['left','left','center'],
'Lab': ['left','left','center'],
'Očkování': ['left','left','left','left'],
'Neschop.': ['left','left','left','left'],
'Léčiva': ['left','left','center','left'],
'SpecVys': ['left','left','left','left'],
'Platby': ['left','left','left','right','center'],
'Ostatní': ['left','left','center','center','left'],
}
for row_i, (datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni, text_dekurzu) in enumerate(rows, start=2):
fill_d = r_fill[row_i % 2]
for col_i in range(1, len(nazvy_d) + 1):
ws_d.cell(row=row_i, column=col_i).fill = fill_d
ws_d.cell(row=row_i, column=col_i).border = ohraniceni
ws_d.cell(row=row_i, column=1, value=datum).number_format = 'DD.MM.YYYY'
ws_d.cell(row=row_i, column=2, value=str(cas)[:5] if cas else '').alignment = Alignment(horizontal='center')
ws_d.cell(row=row_i, column=3, value=zkratka or '').alignment = Alignment(horizontal='center')
ws_d.cell(row=row_i, column=4, value=jmeno_cel)
# Sloupec 5 Rodné číslo jako hyperlink na list "Text dekurzu"
hyperlink_cell(ws_d, row_i, 5, 'Text dekurzu', row_ptr_text, rodcis or '', 'DCE6F1')
ws_d.cell(row=row_i, column=6, value=poj or '').alignment = Alignment(horizontal='center')
# Sloupec G HodVyk (součet bodů všech výkonů dekurzu)
hodvyk = sum((vyka_det.get(rid, ('', '', 0))[2] or 0) for rid in ids_by_typ['VykA'])
cell_hv = ws_d.cell(row=row_i, column=7, value=hodvyk if hodvyk else '')
cell_hv.alignment = Alignment(horizontal='center')
for col_off, (typ, pocet) in enumerate(zip(TOP_TYPY, top)):
col_i = 8 + col_off
if pocet == 0:
continue
nazev_listu = next((n for n, t, *_ in LISTY if t == typ), None)
if nazev_listu and ids_by_typ[typ]:
_, barva_ll = BARVY_LISTU[nazev_listu]
# Pro výkony (VykA) přidej kódy do závorky
if typ == 'VykA':
kody = [str(vyka_det.get(rid, ('',))[0] or '').strip() for rid in ids_by_typ[typ]]
kody_str = ', '.join(k for k in kody if k)
display_text = f"{pocet} ({kody_str})" if kody_str else pocet
else:
display_text = pocet
hyperlink_cell(ws_d, row_i, col_i, nazev_listu, row_ptr[nazev_listu], display_text, barva_ll[1:] if len(barva_ll) > 6 else 'DCE6F1')
ws_det = ws_listy[nazev_listu]
barva_hl, barva_r = BARVY_LISTU[nazev_listu]
for rid in ids_by_typ[typ]:
hodnoty = get_det_hodnoty(typ, rid, datum, jmeno_cel)
zarovnani_l = ZAROVNANI.get(nazev_listu, ['left']*10)
zapis_radek(ws_det, row_ptr[nazev_listu], hodnoty, zarovnani_l, barva_r)
row_ptr[nazev_listu] += 1
else:
ws_d.cell(row=row_i, column=col_i, value=pocet).alignment = Alignment(horizontal='center')
if ostatni:
ws_det = ws_listy['Ostatní']
barva_hl, barva_r = BARVY_LISTU['Ostatní']
hyperlink_cell(ws_d, row_i, 18, 'Ostatní', row_ptr['Ostatní'], ostatni, barva_r)
for typ, rid, nazev in ids_ostatni:
zapis_radek(ws_det, row_ptr['Ostatní'],
[datum, jmeno_cel, typ, rid, nazev],
ZAROVNANI['Ostatní'], barva_r)
row_ptr['Ostatní'] += 1
# Zápis do listu "Text dekurzu"
fill_t = r_fill[row_ptr_text % 2]
for col_i in range(1, 7):
ws_text.cell(row=row_ptr_text, column=col_i).fill = fill_t
ws_text.cell(row=row_ptr_text, column=col_i).border = ohraniceni
c1 = ws_text.cell(row=row_ptr_text, column=1, value=datum)
c1.number_format = 'DD.MM.YYYY'
c1.alignment = Alignment(horizontal='left', vertical='center')
ws_text.cell(row=row_ptr_text, column=2, value=str(cas)[:5] if cas else '').alignment = Alignment(horizontal='center', vertical='center')
ws_text.cell(row=row_ptr_text, column=3, value=zkratka or '').alignment = Alignment(horizontal='center', vertical='center')
ws_text.cell(row=row_ptr_text, column=4, value=jmeno_cel).alignment = Alignment(horizontal='left', vertical='center')
ws_text.cell(row=row_ptr_text, column=5, value=rodcis or '').alignment = Alignment(horizontal='left', vertical='center')
cell_txt = ws_text.cell(row=row_ptr_text, column=6, value=text_dekurzu)
cell_txt.alignment = Alignment(horizontal='left', vertical='top', wrap_text=True)
row_ptr_text += 1
# Autofiltr na detailních listech
for nazev, *_ in LISTY:
ws = ws_listy[nazev]
max_col = ws.max_column
max_row = ws.max_row
if max_row > 1:
ws.auto_filter.ref = f"A1:{ws.cell(row=1, column=max_col).column_letter}{max_row}"
# Smazat starý TEST report
for stary in glob.glob(os.path.join(VYSTUPNI_ADRESAR, f'* {NAZEV_REPORTU}.xlsx')):
os.remove(stary)
print(f"Smazán: {stary}")
# Uložit nový
os.makedirs(VYSTUPNI_ADRESAR, exist_ok=True)
casova_znacka = datetime.now().strftime('%Y-%m-%d %H-%M-%S')
vystup = os.path.join(VYSTUPNI_ADRESAR, f'{casova_znacka} {NAZEV_REPORTU}.xlsx')
wb.save(vystup)
print(f"Uloženo: {vystup}")
for nazev, *_ in LISTY:
print(f" {nazev}: {row_ptr[nazev]-2} řádků")
@@ -0,0 +1,357 @@
import sys, io, re, os, glob
from datetime import date, datetime
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'z:\Dropbox\Ordinace\Reporty'
NAZEV_REPORTU = 'Dekurzy'
DATUM_OD = '2025-01-01'
DATUM_DO = date.today().strftime('%Y-%m-%d')
conn = fdb.connect(
host='192.168.1.10', database=r'm:\MEDICUS\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:
if hasattr(dekurs_blob, 'read'):
rtf = dekurs_blob.read()
dekurs_blob.close()
else:
rtf = 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ů")
# ── Styly ──────────────────────────────────────────────────────────────────
tenka_cara = Side(style='thin', color='AAAAAA')
ohraniceni = Border(left=tenka_cara, right=tenka_cara, top=tenka_cara, bottom=tenka_cara)
hl_font = Font(bold=True, color="FFFFFF")
hl_fill = PatternFill("solid", fgColor="2E75B6")
r_fill = [PatternFill("solid", fgColor="FFFFFF"), PatternFill("solid", fgColor="DCE6F1")]
BARVY_LISTU = {
'Recepty': ('1F6B33', 'E2EFDA'),
'Výkony': ('2E4057', 'D6E4F0'),
'Soubory': ('7B3F00', 'FAE5D3'),
'Žádanky': ('4A235A', 'F5EEF8'),
'Lab výsl.': ('145A32', 'D5F5E3'),
'Očkování': ('7E5109', 'FDEBD0'),
'Neschop.': ('922B21', 'FADBD8'),
'Léčiva': ('1A5276', 'D6EAF8'),
'SpecVys': ('0B5345', 'D1F2EB'),
'Platby': ('4D5656', 'EAECEE'),
'Ostatní': ('2C3E50', 'EBF5FB'),
}
def zapis_hlavicku(ws, sloupce, sirky, barva_hex):
hl_fill_l = PatternFill("solid", fgColor=barva_hex)
for col, (nazev, sirka) in enumerate(zip(sloupce, sirky), start=1):
cell = ws.cell(row=1, column=col, value=nazev)
cell.font = hl_font
cell.fill = hl_fill_l
cell.alignment = Alignment(horizontal='center')
cell.border = ohraniceni
ws.column_dimensions[cell.column_letter].width = sirka
def zapis_radek(ws, row_i, hodnoty, zarovnani, barva_hex):
fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \
else PatternFill("solid", fgColor=barva_hex)
for col_i, (val, align) in enumerate(zip(hodnoty, zarovnani), start=1):
cell = ws.cell(row=row_i, column=col_i, value=val)
cell.fill = fill
cell.border = ohraniceni
cell.alignment = Alignment(horizontal=align)
if col_i == 1 and isinstance(val, __import__('datetime').date):
cell.number_format = 'DD.MM.YYYY'
def hyperlink_cell(ws, row_i, col_i, cil_list, cil_radek, text, barva_hex):
fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \
else PatternFill("solid", fgColor=barva_hex)
cell = ws.cell(row=row_i, column=col_i)
cell.value = str(text)
# Název listu s mezerou musí být v apostrofech
sheet_ref = f"'{cil_list}'" if ' ' in cil_list else cil_list
cell.hyperlink = f'#{sheet_ref}!A{cil_radek}'
cell.font = Font(color="0000FF", underline='single')
cell.fill = fill
cell.border = ohraniceni
cell.alignment = Alignment(horizontal='center')
# ── Workbook ───────────────────────────────────────────────────────────────
wb = openpyxl.Workbook()
LISTY = [
('Recepty', 'Rec', rec_det, ['Datum','Jméno','Recept','Dávkování'], [12,25,25,12], None),
('Výkony', 'VykA', vyka_det, ['Datum','Jméno','Kód výkonu','Diagnóza','Body'], [12,25,14,10,8], None),
('Soubory', 'Files', files_det, ['Datum','Jméno','Soubor','Datum souboru'], [12,25,35,14], None),
('Žádanky', 'MEDLAB', medlab_det, ['Datum','Jméno','Typ'], [12,25,15], None),
('Lab výsl.', 'Lab', lab_det, ['Datum','Jméno','Číslo'], [12,25,20], None),
('Očkování', 'Ock', ock_det, ['Datum','Jméno','Datum očkování','Vakcína'], [12,25,14,30], None),
('Neschop.', 'Nes', nes_det, ['Datum','Jméno','Od','Do'], [12,25,12,12], None),
('Léčiva', 'Lec', lec_det, ['Datum','Jméno','Kód','Datum výkonu'], [12,25,12,14], None),
('SpecVys', 'SpecVys', spec_det, ['Datum','Jméno','Typ vyšetření','Datum vyšetření'], [12,25,25,14], None),
('Platby', 'PlaPac', pla_det, ['Datum','Jméno','Datum platby','Částka','Doklad'], [12,25,14,12,15], None),
('Ostatní', None, None, ['Datum','Jméno','Typ','ID','Název'], [12,25,12,10,30], None),
]
ws_d = wb.active
ws_d.title = "Dekurz"
# List "Text dekurzu" hned za Dekurzem
ws_text = wb.create_sheet("Text dekurzu")
ws_listy = {}
for nazev, *_ in LISTY:
ws_listy[nazev] = wb.create_sheet(nazev)
for nazev, typ, det, sloupce, sirky, _ in LISTY:
barva_hl, _ = BARVY_LISTU[nazev]
zapis_hlavicku(ws_listy[nazev], sloupce, sirky, barva_hl)
ws_listy[nazev].freeze_panes = 'A2'
# Záhlaví listu "Text dekurzu"
zapis_hlavicku(ws_text,
['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Text dekurzu'],
[12, 8, 8, 25, 14, 100],
'2E75B6')
ws_text.freeze_panes = 'A2'
row_ptr_text = 2
# Zobrazované názvy sloupců pro TOP_TYPY (stejné pořadí)
NAZVY_TYPY = ['VykA', 'Rec', 'Files', 'Žádanky', 'Lab výsl.', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
# Záhlaví Dekurz sloupce AR
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'],
'Žá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, 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ý 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ů")
@@ -0,0 +1,274 @@
import firebirdsql as fdb
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
from datetime import datetime
import os
import sys
# --- Připojení ---
conn = fdb.connect(
host='192.168.1.10',
port=3050,
database=r'm:\Medicus\data\MEDICUS.FDB',
user='SYSDBA', password='masterkey', charset='WIN1250'
)
cur = conn.cursor()
# --- Výstupní soubor ---
output_dir = r'z:\Dropbox\Ordinace\Reporty'
now = datetime.now()
filename = now.strftime('%Y-%m-%d %H-%M-%S') + ' faktury.xlsx'
output_path = os.path.join(output_dir, filename)
# --- Smazání předchozích verzí ---
for f in os.listdir(output_dir):
if f.endswith(' faktury.xlsx'):
os.remove(os.path.join(output_dir, f))
wb = openpyxl.Workbook()
# =====================
# Pomocné funkce
# =====================
HEADER_FILL = PatternFill('solid', fgColor='2F5496')
HEADER_FONT = Font(bold=True, color='FFFFFF')
LINK_FONT = Font(color='0563C1', underline='single')
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
def style_header(ws):
for cell in ws[1]:
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal='center')
def autofit(ws):
for col in ws.columns:
max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col)
ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 40)
def fmt(val):
if val is None:
return ''
return val
# =====================
# List 1 FAK
# =====================
ws1 = wb.active
ws1.title = 'FAK'
cur.execute('''
SELECT
ID, CISFAK, POJ, DATUMOD, DATUMDO, DATVYS, DATODE,
VYKONY, KAPITACE, ZALOHA, CENA, ZAPLACENO, ZUM, HOSPAUSAL,
PROPLACENO, SPLAT, DRUH, TYP, ROK, OBDOB,
NAZFAK, POZFAK, OBDFAK,
ICO, BANKA, UCET,
ODJMENO, ODULICE, ODMISTO, ODPSC,
PLNAZEV, PLULICE, PLMISTO, PLPSC,
ICZ, ICZ1, IDICZ, PORCISLO, DRUHPOJ, POZNAMKA
FROM FAK
ORDER BY ID DESC
''')
fak_cols = [d[0] for d in cur.description]
fak_rows = cur.fetchall()
ws1.append(fak_cols)
for i, row in enumerate(fak_rows, start=2):
ws1.append([fmt(v) for v in row])
if i % 2 == 0:
for cell in ws1[i]:
cell.fill = ZEBRA_FILL
style_header(ws1)
ws1.freeze_panes = 'A2'
autofit(ws1)
# =====================
# List 2 FAKDET
# =====================
ws2 = wb.create_sheet('FAKDET')
cur.execute('''
SELECT
fd.ID, fd.IDFAK,
f.CISFAK, f.POJ, f.DATUMOD, f.DATUMDO, f.ROK,
fd.ICP, fd.ODB, fd.IDUZI,
u.PRIJMENI, u.JMENO,
fd.CENAVYK, fd.CENALEC, fd.CENAKAP
FROM FAKDET fd
JOIN FAK f ON f.ID = fd.IDFAK
LEFT JOIN UZIVATEL u ON u.IDUZI = fd.IDUZI
ORDER BY fd.IDFAK DESC, fd.ID DESC
''')
det_cols = [d[0] for d in cur.description]
det_rows = cur.fetchall()
ws2.append(det_cols)
for i, row in enumerate(det_rows, start=2):
ws2.append([fmt(v) for v in row])
if i % 2 == 0:
for cell in ws2[i]:
cell.fill = ZEBRA_FILL
style_header(ws2)
ws2.freeze_panes = 'A2'
autofit(ws2)
# =====================
# List 3 PORTAL (krátké sloupce)
# =====================
ws3 = wb.create_sheet('PORTAL')
cur.execute('''
SELECT ID, IDFAK, ODESLANO, CHYBA, STAV, ID_PODANI, IDPODANI, IDCERT,
DAVKA_ROK, DAVKA_DISK, DAVKA_IDICZ, DAVKA_DATUMOD, DAVKA_DATUMDO,
DAVKA_CASTKA, BB_DAVKA, BB_FAKTURA
FROM PORTAL
ORDER BY ID DESC
''')
portal_cols = [d[0] for d in cur.description]
portal_rows = cur.fetchall()
ws3.append(portal_cols)
for i, row in enumerate(portal_rows, start=2):
ws3.append([fmt(v) for v in row])
if i % 2 == 0:
for cell in ws3[i]:
cell.fill = ZEBRA_FILL
style_header(ws3)
ws3.freeze_panes = 'A2'
autofit(ws3)
# =====================
# List 4 PORTAL_DATA (BLOBy)
# =====================
ws4 = wb.create_sheet('PORTAL_DATA')
cur.execute('''
SELECT ID, ID_PODANI, DAVKA_ROK, DAVKA_DISK, DAVKA_DATUMOD, DAVKA_DATUMDO,
KDAVKA, FDAVKA, DATA
FROM PORTAL
ORDER BY ID DESC
''')
pdata_rows = cur.fetchall()
pdata_cols = ['ID', 'ID_PODANI', 'DAVKA_ROK', 'DAVKA_DISK', 'DAVKA_DATUMOD', 'DAVKA_DATUMDO',
'KDAVKA', 'FDAVKA', 'DATA']
ws4.append(pdata_cols)
WRAP = Alignment(wrap_text=True, vertical='top')
# Indexy BLOB sloupců: KDAVKA=6, FDAVKA=7, DATA=8
# DATA je XML v win1250, KDAVKA/FDAVKA jsou latin2
BLOB_ENCODINGS = {6: 'cp852', 7: 'cp852', 8: 'cp1250'}
def decode_blob(v, enc):
if hasattr(v, 'read'):
raw = v.read()
else:
raw = v
if not raw:
return ''
if isinstance(raw, str):
# fdb dekódoval jako win1250 vrátíme zpět na bytes, pak dekódujeme správně
raw = raw.encode('cp1250', errors='replace')
return raw.decode(enc, errors='replace')
for i, row in enumerate(pdata_rows, start=2):
out = []
for col_idx, v in enumerate(row):
if col_idx in BLOB_ENCODINGS:
enc = BLOB_ENCODINGS[col_idx]
out.append(decode_blob(v, enc))
else:
out.append(fmt(v))
ws4.append(out)
if i % 2 == 0:
for cell in ws4[i]:
cell.fill = ZEBRA_FILL
for cell in ws4[i]:
cell.alignment = WRAP
ws4.row_dimensions[i].height = 80
style_header(ws4)
ws4.freeze_panes = 'A2'
# Šířky sloupců PORTAL_DATA
for col, width in zip(['A','B','C','D','E','F','G','H','I'], [6, 26, 8, 6, 12, 12, 80, 20, 60]):
ws4.column_dimensions[col].width = width
# =====================
# Hyperlinky PORTAL ↔ PORTAL_DATA
# =====================
# Mapa: portal_id → řádek v každém listu
portal_id_to_ws3_row = {}
for i, row in enumerate(portal_rows, start=2):
portal_id_to_ws3_row[row[0]] = i # row[0] = ID
portal_id_to_ws4_row = {}
for i, row in enumerate(pdata_rows, start=2):
portal_id_to_ws4_row[row[0]] = i # row[0] = ID
# PORTAL → PORTAL_DATA (sloupec A = ID)
for i, row in enumerate(portal_rows, start=2):
pid = row[0]
cell = ws3.cell(row=i, column=1)
if pid in portal_id_to_ws4_row:
cell.hyperlink = f'#PORTAL_DATA!A{portal_id_to_ws4_row[pid]}'
cell.font = LINK_FONT
# PORTAL_DATA → PORTAL (sloupec A = ID)
for i, row in enumerate(pdata_rows, start=2):
pid = row[0]
cell = ws4.cell(row=i, column=1)
if pid in portal_id_to_ws3_row:
cell.hyperlink = f'#PORTAL!A{portal_id_to_ws3_row[pid]}'
cell.font = LINK_FONT
cell.alignment = WRAP
# =====================
# Hyperlinky FAK → FAKDET
# =====================
# Mapa: IDFAK → první řádek na listu FAKDET (řádek 1 = záhlaví, data od 2)
idfak_to_row = {}
for i, row in enumerate(det_rows, start=2):
idfak = row[1] # IDFAK je druhý sloupec
if idfak not in idfak_to_row:
idfak_to_row[idfak] = i
# Přidat sloupec "→FAKDET" jako první sloupec na listu FAK
ws1.insert_cols(1)
ws1.cell(row=1, column=1, value='FAKDET').fill = HEADER_FILL
ws1.cell(row=1, column=1).font = HEADER_FONT
ws1.cell(row=1, column=1).alignment = Alignment(horizontal='center')
ws1.column_dimensions['A'].width = 9
for i, row in enumerate(fak_rows, start=2):
fak_id = row[0] # ID je první sloupec z DB
cell = ws1.cell(row=i, column=1)
if fak_id in idfak_to_row:
target_row = idfak_to_row[fak_id]
cell.value = '>> det'
cell.hyperlink = f'#FAKDET!A{target_row}'
cell.font = LINK_FONT
else:
cell.value = ''
# =====================
# Uložení
# =====================
conn.close()
wb.save(output_path)
sys.stdout.buffer.write(f'Ulozeno: {output_path}\n'.encode('utf-8'))
sys.stdout.buffer.write(f'FAK: {len(fak_rows)} radku, FAKDET: {len(det_rows)} radku, PORTAL: {len(portal_rows)} radku\n'.encode('utf-8'))
@@ -0,0 +1,281 @@
import os,shutil,fdb,time, socket
import re,datetime,funkce
def get_medicus_connection():
"""
Connect to Firebird 'medicus.fdb' depending on computer name.
Returns fdb.Connection or raises RuntimeError if unknown or connection fails.
"""
computer_name = socket.gethostname().upper()
try:
if computer_name == "Z230":
print("Computer name is Z230")
cesta = r"u:\dropboxtest\Ordinace\Dokumentace_ke_zpracování"
cestazpracovana = r"u:\Dropboxtest\Ordinace\Dokumentace_zpracovaná"
return fdb.connect(dsn=r"localhost:c:\medicus 3\data\medicus.fdb", user="SYSDBA", password="masterkey", charset="win1250"),cesta,cestazpracovana
elif computer_name == "LEKAR":
print("Computer name is LEKAR")
cesta = r"z:\dropbox\Ordinace\Dokumentace_ke_zpracování"
cestazpracovana = r"z:\Dropbox\Ordinace\Dokumentace_zpracovaná"
return fdb.connect(dsn=r"localhost:m:\medicus\data\medicus.fdb", user="SYSDBA", password="masterkey", charset="win1250"),cesta,cestazpracovana
elif computer_name in ("SESTRA", "POHODA"):
print("Computer name is SESTRA or POHODA")
cesta = r"z:\dropbox\Ordinace\Dokumentace_ke_zpracování"
cestazpracovana = r"z:\Dropbox\Ordinace\Dokumentace_zpracovaná"
return fdb.connect(dsn=r"192.168.1.10:m:\medicus\data\medicus.fdb", user="SYSDBA", password="masterkey", charset="win1250"),cesta,cestazpracovana
else:
raise RuntimeError(f"❌ Unknown computer name: {computer_name}")
except Exception as e:
print(f"⚠️ Error connecting to Medicus on {computer_name}: {e}")
raise
#toto volání současně nadefinuje cesty do dropboxu
conn,cesta,cestazpracovana=get_medicus_connection()
def is_encodable_win1250(text: str) -> bool:
try:
text.encode("cp1250")
return True
except UnicodeEncodeError:
return False
def make_win1250_safe(text: str) -> str:
return text.encode("cp1250", errors="replace").decode("cp1250").replace("?", "_")
def restore_files_for_import(retezec):
drop=r"u:\Dropbox\!!!Days\Downloads Z230\Dokumentace"
next=r"u:\NextcloudOrdinace\Dokumentace_ke_zpracování"
# Check if the directory exists
if not os.path.exists(drop):
print(f"The directory '{drop}' does not exist.")
return
# Iterate over all files and subdirectories in the directory
for item in os.listdir(drop):
item_path = os.path.join(drop, item)
# If it's a file or a symbolic link, delete it
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
print(f"Deleted file: {item_path}")
# If it's a directory, delete it recursively
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 it's a file finished with PDF, copy it
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'):
#kontrola struktury
pattern=re.compile(r'(^\d{9,10}) (\d{4}-\d{2}-\d{2}) (\w+, \w.+?) \[(.+?)\] \[(.*?)\]')
match=pattern.search(souborname)
# print(souborname)
vpohode=True
if match and len(match.groups())==5:
datum=match.group(2)
try:
datum_object = datetime.datetime.strptime(datum,"%Y-%m-%d").date()
# print(datum_object)
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))
# print(kontrola_struktury(ss))
# info=vrat_info_o_souboru(ss)
# print(kontrola_rc(info[0],conn))
# restore_files_for_import("")
# restore_files_for_import("346204097")
info=[]
for soubor in os.listdir(cesta):
plna_cesta = os.path.join(cesta, soubor)
if not os.path.isfile(plna_cesta):
continue # skip folders or anything thats not a regular fil
if not is_encodable_win1250(soubor):
safe_name = make_win1250_safe(soubor)
novy_plna_cesta = os.path.join(cesta, safe_name)
print(f"⚠️ Renaming invalid filename:\n {soubor}{safe_name}")
os.rename(plna_cesta, novy_plna_cesta)
# Update variable for later processing
soubor = safe_name
plna_cesta = novy_plna_cesta
print(soubor)
if kontrola_struktury(soubor,conn):
info.append(vrat_info_o_souboru(soubor,conn))
# os.remove(os.path.join(cesta,soubor))
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)
# print(skupiny)
# rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES }}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
# {\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
# {\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs22\f0\ul\fs20\cf1 Odkaz;}}
# \uc1\pard\s10\plain\cs20\f0\i\fs20 P\'f8\'edlohy:\par
# \pard\s10{\*\bkmkstart 0}\plain\cs22\f0\ul\fs20\cf1 BOOKMARKNAMESTEXT{\*\bkmkend 0}\par
# \pard\s10\plain\cs15\f0\fs20 \par
# }"""
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES }}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs22\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 P\'f8\'edlohy:\par
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20 \par
}"""
for key in skupiny.keys():
rtf = r"""{\rtf1\ansi\ansicpg1250\uc1\deff0\deflang1029{\info{\bookmarks BOOKMARKNAMES }}{\fonttbl{\f0\fnil\fcharset238 Arial;}{\f5\fnil\fcharset238 Symbol;}}
{\colortbl ;\red0\green0\blue255;\red0\green128\blue0;\red0\green0\blue0;}
{\stylesheet{\s10\fi0\li0\ql\ri0\sb0\sa0 Vlevo;}{\*\cs15\f0\fs20 Norm\'e1ln\'ed;}{\*\cs20\f0\i\fs20 Z\'e1hlav\'ed;}{\*\cs22\f0\ul\fs20\cf1 Odkaz;}}
\uc1\pard\s10\plain\cs20\f0\i\fs20 Vlo\'9eena skenovan\'e1 dokumentace:\par
BOOKMARKSTEXT
\pard\s10\plain\cs15\f0\fs20\par
}"""
# if key=="8257300425": #346204097
if True:
prvnibookmark=True
print(key,len(skupiny[key]))
cislo=9
poradi=0
bookmark=""
bookmarks=""
for row in skupiny[key]:
# print(row)
pacid=row[1]
filename=row[6]
fileid=funkce.zapis_file(vstupconnection=conn, idpac=row[1],
cesta=cesta, souborname=row[6], prvnizavorka=row[4],
soubordate=row[2], souborfiledate=row[7], poznamka=row[5])
for attempt in range(3):
try:
# Replace this with the command that might raise an error
if not os.path.exists(os.path.join(cestazpracovana,row[6])):
shutil.move(os.path.join(cesta,row[6]), os.path.join(cestazpracovana,row[6]))
print("Command succeeded!")
break # Exit the loop if the command succeeds
else:
now = datetime.datetime.now()
datetime_string = now.strftime("%Y-%m-%d %H-%M-%S")
print(os.path.join(cestazpracovana,row[6][:-4]+" "+datetime_string+".pdf"))
shutil.move(os.path.join(cesta,row[6]),os.path.join(cestazpracovana,row[6][:-4]+" "+datetime_string+".pdf"))
print("Command succeeded!")
break # Exit the loop if the command succeeds
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt < 3 - 1:
print(f"Retrying in {5} seconds...")
time.sleep(5)
else:
print("Max retries reached. Command failed.")
filename=funkce.convert_to1250(filename)
print("Encodedfilename", filename)
filenameforbookmark=row[2].strftime('%Y-%m-%d')+" "+row[4]+": "+row[5]
bookmark=bookmark+'"'+filenameforbookmark+'","Files:'+str(fileid)+'",'+str(cislo)+";"
cislo+=7
# print(bookmark)
if prvnibookmark:
bookmarks=bookmarks+r'\pard\s10{\*\bkmkstart '+str(poradi)+r"}\plain\cs22\f0\ul\fs20\cf1 "+filenameforbookmark+r"{\*\bkmkend "+str(poradi)+r"}\par"
prvnibookmark=False
else:
bookmarks=bookmarks+r'\pard\s10{\*\bkmkstart '+str(poradi)+r"}" + filenameforbookmark + r"{\*\bkmkend " + str(poradi) + r"}\par"
bookmark=bookmark[:-1]
# bookmarks=bookmarks[:-2]
print(bookmark)
print(bookmarks)
rtf = rtf.replace("BOOKMARKNAMES", bookmark)
rtf=rtf.replace("BOOKMARKSTEXT",bookmarks)
print(rtf)
dekursid = funkce.get_dekurs_id(conn)
datumzapisu = datetime.datetime.now().date()
caszapisu = datetime.datetime.now().time()
cur=conn.cursor()
cur.execute("insert into dekurs (id,iduzi,idprac,idodd,idpac,datum,cas,dekurs)"
" values(?,?,?,?,?,?,?,?)",
(dekursid,6,2,2, row[1],datumzapisu,caszapisu, rtf))
conn.commit()
# rtf = rtf.replace("FILEID", str(idfile))
#Zde zapisujeme soubor
# fileid=funkce.zapis_file(conn,row[1],cesta,row[6],row[4],row[2],row[7],row[5])
# zapis_dekurs(vstupconnection, idpac, idodd, iduzi, idprac, idfile, filename, text, datumzpravy,datumsouboru)
# return (rc, idpac, datum, jmeno, prvnizavorka, druhazavorka, souborname, datumsouboru)
# Zde zapisujeme dekurs
# text=row[2].strftime("%Y-%m-%d")+" "+row[4].strip()+": "+row[5].strip()
# funkce.zapis_dekurs(conn, row[1], 2, 6, 2, fileid, text, text, row[7], row[2])
# os.remove(os.path.join(cesta, soubor))
@@ -0,0 +1,379 @@
import os
import fdb
import csv,time,pandas as pd
import openpyxl
PathToSaveCSV=r"z:\Dropbox\Ordinace\Reporty"
timestr = time.strftime("%Y-%m-%d %H-%M-%S ")
CSVname="Pacienti.xlsx"
# ================= DELETE OLD REPORTS (KEEP TODAY) ==================
from datetime import datetime
today = datetime.now().strftime("%Y-%m-%d")
for fname in os.listdir(PathToSaveCSV):
if fname.endswith("Pacienti.xlsx"):
file_date = fname[:10] # first 10 chars = YYYY-MM-DD
if file_date != today: # delete only older files
try:
os.remove(os.path.join(PathToSaveCSV, fname))
print(f"🗑️ Deleted old report: {fname}")
except Exception as e:
print(f"⚠️ Could not delete {fname}: {e}")
con = fdb.connect(
host='192.168.1.10', database=r'm:\MEDICUS\data\medicus.FDB',
user='sysdba', password='masterkey',charset='WIN1250')
#Server=192.168.1.10
#Path=M:\Medicus\Data\Medicus.fdb
# Create a Cursor object that operates in the context of Connection con:
cur = con.cursor()
# import openpyxl module
import openpyxl
import xlwings as xw
wb = openpyxl.Workbook()
sheet = wb.active
# wb.save("sample.xlsx")
#Načtení očkování registrovaných pacientů
cur.execute("select rodcis,prijmeni,jmeno,ockzaz.datum,kodmz,ockzaz.poznamka,latka,nazev,expire from registr join kar on registr.idpac=kar.idpac join ockzaz on registr.idpac=ockzaz.idpac where datum_zruseni is null and kar.vyrazen!='A' and kar.rodcis is not null and idicp!=0 order by ockzaz.datum desc")
nacteno=cur.fetchall()
print(len(nacteno))
sheet.title="Očkování"
sheet.append(["Rodne cislo","Prijmeni","Jmeno","Datum ockovani","Kod MZ","Sarze","Latka","Nazev","Expirace"])
#nacteno jsou ockovani
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Načtení registrovaných pacientů
cur.execute("select rodcis,prijmeni,jmeno,datum_registrace,registr.idpac,poj from registr join kar on registr.idpac=kar.idpac where kar.vyrazen!='A' and kar.rodcis is not null and idicp!=0 and datum_zruseni is null")
nacteno=cur.fetchall()
print(len(nacteno))
wb.create_sheet('Registrovani',0)
sheet=wb['Registrovani']
sheet.append(["Rodne cislo","Prijmeni","Jmeno","Datum registrace","ID pacienta","Pojistovna"])
#nacteno jsou registrovani
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Načtení receptů
cur.execute("""select
kar.rodcis,
TRIM(kar.prijmeni) ||' '|| substring(kar.jmeno from 1 for 1) ||'.' as jmeno,
recept.datum,
TRIM(recept.lek) ||' '|| trim(recept.dop) as lek,
recept.expori AS Poc,
CASE
WHEN recept.opakovani is null THEN 1
ELSE recept.opakovani
END AS OP,
recept.uhrada,
recept.dsig,
recept.NOTIFIKACE_KONTAKT as notifikace,
recept_epodani.erp,
recept_epodani.vystavitel_jmeno,
recept.atc,
recept.CENAPOJ,
recept.cenapac
from recept LEFT Join RECEPT_EPODANI on recept.id_epodani=recept_epodani.id
LEFT join kar on recept.idpac=kar.idpac
order by datum desc,erp desc"""
)
nacteno=cur.fetchall()
print(len(nacteno))
wb.create_sheet('Recepty',0)
sheet=wb['Recepty']
sheet.title="Recepty"
sheet.append(["Rodné číslo","Jméno","Datum vystavení","Název leku","Poč.","Op.","Úhr.","Da signa","Notifikace","eRECEPT","Vystavil","ATC","Cena pojišťovna","Cena pacient"])
#nacteno jsou ockovani
for row in nacteno:
try:
sheet.append(row)
except:
continue
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Načtení vykony vsech
cur.execute("select dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,dokladd.pocvyk,dokladd.ddgn,dokladd.body,vykony.naz "
"from kar join dokladd on kar.rodcis=dokladd.rodcis join vykony on dokladd.kod=vykony.kod where (datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null) order by dokladd.datose desc,dokladd.rodcis")
wb.create_sheet('Vykony',0)
sheet=wb['Vykony']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum vykonu","Kod","Pocet","Dg.","Body","Nazev"])
#nacteno jsou ockovani
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Načtení neschopenek
import datetime
def pocet_dni(zacnes,konnes,pracne):
dnes=datetime.date.today()
if pracne=='A':
return (dnes-zacnes).days
if pracne=='N' and zacnes is not None and konnes is not None and zacnes<=konnes:
return (konnes-zacnes).days
else:
return "NA"
cur.execute("select nes.idpac, "
"kar.rodcis, "
"TRIM(prijmeni) ||', '|| TRIM(jmeno), "
"nes.datnes, "
"nes.ecn, "
"nes.zacnes, "
"nes.pracne, "
"nes.konnes, "
"nes.diagno, "
"nes.kondia, "
"nes.updated "
"from nes "
"left join kar on nes.idpac=kar.idpac where nes.datnes<=current_date "
"order by datnes desc")
tmpnacteno_vse=[]
nacteno_vse=cur.fetchall()
cur.execute("select nes.idpac, "
"kar.rodcis, "
"TRIM(prijmeni) ||', '|| TRIM(jmeno), "
"nes.datnes, "
"nes.ecn, "
"nes.zacnes, "
"nes.pracne, "
"nes.konnes, "
"nes.diagno, "
"nes.kondia, "
"nes.updated "
"from nes "
"left join kar on nes.idpac=kar.idpac where nes.datnes<=current_date and pracne='A'"
"order by datnes desc")
tmpnacteno_aktivni=[]
nacteno_aktivni=cur.fetchall()
for row in nacteno_vse:
tmpnacteno_vse.append((row[0],row[1],row[2],row[3],row[4],row[5],row[6],row[7],pocet_dni(row[5],row[7],row[6]),row[8],row[9],row[10]))
for row in nacteno_aktivni:
(tmpnacteno_aktivni.append((row[0],row[1],row[2],row[3],row[4],row[5],row[6],row[7],pocet_dni(row[5],row[7],row[6]),row[8],row[9],row[10])))
wb.create_sheet('Neschopenky všechny',0)
sheet=wb["Neschopenky všechny"]
sheet.append(["ID pac","Rodne cislo","Jmeno","Datum neschopenky","Číslo neschopenky","Zacatek","Aktivní?","Konec","Pocet dni","Diagnoza zacatel","Diagnoza konec","Aktualizovano"])
for row in tmpnacteno_vse:
sheet.append(row)
wb.create_sheet('Neschopenky aktivní',0)
sheet=wb["Neschopenky aktivní"]
sheet.append(["ID pac","Rodne cislo","Jmeno","Datum neschopenky","Číslo neschopenky","Zacatek","Aktivní?","Konec","Pocet dni","Diagnoza zacatel","Diagnoza konec","Aktualizovano"])
for row in tmpnacteno_aktivni:
sheet.append(row)
#Načtení preventivni prohlidky
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=1022 or dokladd.kod=1021) "
"order by datose desc")
wb.create_sheet('Preventivni prohlidky',0)
sheet=wb['Preventivni prohlidky']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Nacteni INR
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=01443) "
"order by datose desc")
wb.create_sheet('INR',0)
sheet=wb['INR']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Nacteni CRP
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=02230 or dokladd.kod=09111) "
"order by datose desc,dokladd.rodcis,dokladd.kod")
wb.create_sheet('CRP',0)
sheet=wb['CRP']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Nacteni Holter
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=17129) "
"order by datose desc,dokladd.rodcis,dokladd.kod")
wb.create_sheet('Holter',0)
sheet=wb['Holter']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Nacteni prostata
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=01130 or dokladd.kod=01131 or dokladd.kod=01132 or dokladd.kod=01133 or dokladd.kod=01134) "
"order by datose desc,dokladd.rodcis,dokladd.kod")
wb.create_sheet('Prostata',0)
sheet=wb['Prostata']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Nacteni TOKS
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and "
"(dokladd.kod=15118 or dokladd.kod=15119 or dokladd.kod=15120 or dokladd.kod=15121) "
"order by datose desc,dokladd.rodcis,dokladd.kod")
wb.create_sheet('TOKS',0)
sheet=wb['TOKS']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Nacteni COVID
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and "
"(dokladd.kod=01306) "
"order by datose desc,dokladd.rodcis,dokladd.kod")
wb.create_sheet('COVID',0)
sheet=wb['COVID']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
#Nacteni Streptest
cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body "
"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where "
"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and "
"(dokladd.kod=02220) "
"order by datose desc,dokladd.rodcis,dokladd.kod")
wb.create_sheet('Streptest',0)
sheet=wb['Streptest']
nacteno=cur.fetchall()
print(len(nacteno))
sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"])
for row in nacteno:
sheet.append(row)
# autofilter
for ws in wb.worksheets:
# Get the maximum number of rows and columns
max_row = ws.max_row
max_column = ws.max_column
ws.auto_filter.ref = f"A1:{openpyxl.utils.get_column_letter(max_column)}{max_row}"
# ws.auto_filter.ref = ws.dimensions
wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname))
# Tento modul je pouze na autofit jednotlivych sloupcu na vsech listech workbooku
file = os.path.join(PathToSaveCSV ,timestr+CSVname)
with xw.App(visible=False) as app:
wb = xw.Book(file)
for sheet in range(len(wb.sheets)):
ws = wb.sheets[sheet]
ws.autofit()
# centrování receptů
sheet = wb.sheets['Recepty']
for sloupec in ["C:C", "E:E", "F:F", "G:G", "I:I", "M:M", "N:N"]:
sheet.range(sloupec).api.HorizontalAlignment = 3 # 3 = Center
wb.save()
wb.close()
@@ -0,0 +1,188 @@
# MedicusWithClaudeKomplexniReport CLAUDE_NOTES
## Co skript dělá
`komplexni_report.py` generuje komplexní Excel přehled ordinace.
Soubor se ukládá do `u:\Dropbox\!!!Days\Downloads Z230\YYYY-MM-DD_HH-MM-SS_Pacienti.xlsx`.
Předchozí verze (`*Pacienti.xlsx`) se před zápisem automaticky smažou.
## Spuštění
```
C:\Python\python.exe komplexni_report.py
```
Trvá cca **10 minut** (kvůli xlwings autofit přes celý Excel).
Spouští se automaticky v noci → nevadí.
## Připojení k DB
```python
fdb.connect(
host='localhost',
database=r'c:\MEDICUS 3\data\medicus.FDB',
user='sysdba', password='masterkey', charset='WIN1250'
)
```
## Závislosti
```
pip install fdb openpyxl xlwings extract-msg beautifulsoup4 python-dateutil
```
---
## Listy v Excelu (pořadí)
| List | Zdroj | Popis |
|---|---|---|
| `Registrovani` | registr + kar | Aktivní registrovaní pacienti |
| `Očkování` | ockzaz + registr + kar | Záznamy o očkování registrovaných |
| `Recepty` | recept + recept_epodani + kar | Všechny recepty, eRECEPT čísla, ceny |
| `Vykony` | dokladd + kar + vykony | Všechny výkony (s platným číselníkem) |
| `Neschopenky všechny` | nes + kar | Všechny neschopenky |
| `Neschopenky aktivní` | nes + kar | Pouze aktivní (pracne='A') |
| `Preventivni prohlidky` | dokladd + vykony | Kódy 1021, 1022 |
| `INR` | dokladd + vykony | Kód 1443 |
| `CRP` | dokladd + vykony | Kódy 2230, 9111 |
| `Holter` | dokladd + vykony | Kód 17129 |
| `Prostata` | dokladd + vykony | Kódy 11301134 |
| `TOKS` | dokladd + vykony | Kódy 1511815121 |
| `COVID` | dokladd + vykony | Kód 1306 |
| `Streptest` | dokladd + vykony | Kód 2220 |
| `Posudky řidičák` | HISTDOC (TYP=MOTORVO) + KAR | Ruční posudky k řízení MV |
| `ePosudky registr` | HISTDOC (TYP=EPOSMRO) + HISTDOC_EPOSUDEK + KAR | Elektronická podání do centrálního registru |
---
## Pomocné funkce
### `sanitize(val)`
Opraví znaky neplatné pro Excel:
- `µ``u`
- řídící znaky (ord < 32, kromě tab/LF/CR) → `_`
- náhradní znaky Unicode (0xFFFE, 0xFFFF, surrogáty) → `_`
Použito ve všech listech kde hrozí problematická data z DB.
### `fmt(val)`
Vrátí `''` pro None, jinak zavolá `sanitize()`.
### `add_vykony_sheet(sheet_name, kody)`
Helper pro listy s výkony. Přijme název listu a seznam kódů výkonů.
SQL: `dokladd JOIN kar JOIN vykony WHERE kod IN (...) AND platnost kódu platí`.
Řazení: datum DESC, rodcis, kod.
### `pocet_dni(zacnes, konnes, pracne)`
Výpočet délky neschopenky:
- `pracne='A'` (aktivní) → dny od začátku do dnes
- `pracne='N'` → dny od začátku do konce
- jinak → `"NA"`
### `parse_data(data_str)`
Parsuje `key=value` text z pole `HISTDOC.DATA` do slovníku.
Každý řádek = jeden klíč/hodnota oddělené `=`.
### `parse_date(val)`
Převede formát `D:DD.MM.YYYY` (jak ho ukládá Medicus) na `datetime.date`.
### `style_header(ws)` / `autofit_ws(ws)`
Styl záhlaví (modrý fill, bílý tučný text, centrování) a šířky sloupců (max 50 znaků).
Používají se jen na listech s posudky (ostatní listy řeší xlwings).
---
## Listy s posudky detail
### `Posudky řidičák` (MOTORVO)
Data jsou uložena v `HISTDOC.DATA` jako `key=value` text.
Parsovaná pole:
| Sloupec | Zdroj v DATA |
|---|---|
| PorCislo | `PorCislo` nebo `HISTDOC.PORCISLO` |
| DatumVyd | `DatumVyd` (formát `D:DD.MM.YYYY`) |
| DatKonec | `DatKonec` (formát `D:DD.MM.YYYY`) |
| DruhProh | `DruhProh` |
| Posouzeni | odvozeno z `Posouzeni`, `Posouzeni2`, `ZpusobPodminka` |
| ZpusobPodminka | `ZpusobPodminka` |
| SkupinaPodminka | `SkupinaPodminka` |
| Skupiny | `ZpusobJe` |
**Logika Posouzeni:**
- `Posouzeni2 = T``nezpůsobilý`
- `ZpusobPodminka = B:1``způsobilý s podmínkou`
- `Posouzeni = T``způsobilý`
**Sloupec ePosudek:**
`ANO` pokud existuje záznam v HISTDOC s `TYP='EPOSMRO'` pro stejného pacienta (IDPACI) a stejné datum.
Párování: `(IDPACI, DATUM)` přímá FK vazba mezi MOTORVO a EPOSMRO neexistuje.
Buňka s ANO je zelená (fill + font).
**Zebra pruhování:** liché řádky bílé, sudé světle modré (`DCE6F1`).
### `ePosudky registr` (EPOSMRO)
Elektronická podání do centrálního registru způsobilosti.
Stát tuto funkci zavedl přibližně od aktualizace Medicusu (03/2026).
Data parsovaná z `HISTDOC.DATA`:
| Sloupec | Zdroj v DATA |
|---|---|
| DatumVyd | `DatumVystaveni` |
| DatKonec | `PlatnostDo` |
| DruhProhlidky | `DruhProhlidkyNazev` |
| DruhPosudku | `DruhPosudkuNazev` |
| Vysledek | `VysledekNazev` |
| StavPosudku | `StavPosudkuNazev` |
| TypAkce | `TypAkceNazev` |
Stavová pole z `HISTDOC_EPOSUDEK`:
- `ID_PODANI` ID podání do registru
- `ODESLANO` timestamp odeslání
- `STATUS_ODESL` stav odpovědi z registru (`O` = odesláno)
**Zneplatnění:** `StavPosudku = zneplatneny` = lékař aktivně odvolal způsobilost
(např. pacient prodělal mrtvici, epileptický záchvat atp.).
Zneplatnění je samostatný EPOSMRO záznam, ne modifikace původního.
---
## xlwings závěrečný krok
Po `wb.save()` se soubor otevře přes xlwings (vyžaduje plný Excel):
1. `sheet.autofit()` na všech listech správné šířky sloupců
2. Na listu `Recepty`: centrování sloupců C, E, F, G, I, M, N
3. `wb_xw.save()` + zavření
xlwings je nutný pro spolehlivý autofit (openpyxl ho neumí přesně).
Trvá ~10 minut, spouští se v noci.
---
## Pořadí zpracování (pro debugování)
```
DB connect
→ smazání starých souborů
→ SQL dotazy (Registrovani, Očkování, Recepty, Výkony, Neschopenky)
→ add_vykony_sheet × 8
→ MOTORVO + EPOSMRO listy (s parsováním DATA)
→ autofilter na všech listech
→ con.close() + wb.save()
→ xlwings autofit + centrování
→ Hotovo.
```
Print výstup v konzoli ukazuje počty řádků každého listu užitečné pro kontrolu.
---
## Rozšíření v budoucnu
- Přidat další typy posudků (pracovní, vstupní, sportovní...) ze `VS_POSUDKY`
- Případně sledovat stav podání EPOSMRO v čase (datum odeslání vs. datum posudku)
- Automatické spouštění přes Windows Task Scheduler (jako `faktury_report.py`)
@@ -0,0 +1,410 @@
import os
import time
import fdb
import openpyxl
import xlwings as xw
from datetime import datetime, date
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font, PatternFill, Alignment
# --- Konfigurace ---
PathToSaveCSV = r"u:\Dropbox\!!!Days\Downloads Z230"
timestr = time.strftime("%Y-%m-%d_%H-%M-%S_")
output_path = os.path.join(PathToSaveCSV, timestr + "Pacienti.xlsx")
# --- Smazání předchozích verzí ---
for fname in os.listdir(PathToSaveCSV):
if fname.endswith("Pacienti.xlsx"):
try:
os.remove(os.path.join(PathToSaveCSV, fname))
except Exception as e:
print(f"Nelze smazat {fname}: {e}")
# --- Připojení k DB ---
con = fdb.connect(
host='localhost', database=r'c:\MEDICUS 3\data\medicus.FDB',
user='sysdba', password='masterkey', charset='WIN1250'
)
cur = con.cursor()
wb = openpyxl.Workbook()
# =====================
# Pomocné funkce
# =====================
# Styly pro posudky
HEADER_FILL = PatternFill('solid', fgColor='2F5496')
HEADER_FONT = Font(bold=True, color='FFFFFF')
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
GREEN_FILL = PatternFill('solid', fgColor='C6EFCE')
GREEN_FONT = Font(bold=True, color='276221')
def style_header(ws):
for cell in ws[1]:
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal='center')
def autofit_ws(ws):
for col in ws.columns:
max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col)
ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 50)
def sanitize(val):
"""Nahradí znaky neplatné pro Excel: µ → u, ostatní → _"""
if not isinstance(val, str):
return val
result = []
for ch in val:
if ch == 'µ':
result.append('u')
elif ord(ch) < 32 and ch not in '\t\n\r':
result.append('_')
elif ord(ch) in (0xFFFE, 0xFFFF) or 0xD800 <= ord(ch) <= 0xDFFF:
result.append('_')
else:
result.append(ch)
return ''.join(result)
def fmt(val):
return '' if val is None else sanitize(val)
def parse_data(data_str):
"""Parsuje key=value text z HISTDOC.DATA do slovníku."""
result = {}
if not data_str:
return result
for line in data_str.splitlines():
if '=' in line:
key, _, val = line.partition('=')
result[key.strip()] = val.strip()
return result
def parse_date(val):
"""Převede 'D:DD.MM.YYYY' na datetime.date."""
if val and val.startswith('D:'):
try:
return datetime.strptime(val[2:], '%d.%m.%Y').date()
except ValueError:
return val
return val
VYKONY_CONDITION = """
(datose >= vykony.platiod AND datose <= vykony.platido)
OR (datose >= vykony.platiod AND vykony.platido IS NULL)
"""
VYKONY_HEADERS = ["Rodne cislo", "Jmeno", "Datum vykonu", "Kod", "Název", "Dg.", "Body"]
def add_vykony_sheet(sheet_name, kody):
"""Přidá list s výkony filtrovanými podle seznamu kódů."""
kod_list = ", ".join(str(k) for k in kody)
cur.execute(f"""
SELECT dokladd.rodcis,
TRIM(prijmeni) || ', ' || TRIM(jmeno),
dokladd.datose, dokladd.kod, vykony.naz, dokladd.ddgn, dokladd.body
FROM dokladd
LEFT JOIN kar ON dokladd.rodcis = kar.rodcis
JOIN vykony ON dokladd.kod = vykony.kod
WHERE ({VYKONY_CONDITION})
AND dokladd.kod IN ({kod_list})
ORDER BY datose DESC, dokladd.rodcis, dokladd.kod
""")
rows = cur.fetchall()
print(f"{sheet_name}: {len(rows)}")
ws = wb.create_sheet(sheet_name)
ws.append(VYKONY_HEADERS)
for row in rows:
ws.append(list(row))
# =====================
# List: Registrovaní
# =====================
cur.execute("""
SELECT rodcis, prijmeni, jmeno, datum_registrace, registr.idpac, poj
FROM registr
JOIN kar ON registr.idpac = kar.idpac
WHERE kar.vyrazen != 'A'
AND kar.rodcis IS NOT NULL
AND idicp != 0
AND datum_zruseni IS NULL
""")
rows = cur.fetchall()
print(f"Registrovaní: {len(rows)}")
ws = wb.active
ws.title = 'Registrovani'
ws.append(["Rodne cislo", "Prijmeni", "Jmeno", "Datum registrace", "ID pacienta", "Pojistovna"])
for row in rows:
ws.append(list(row))
# =====================
# List: Očkování
# =====================
cur.execute("""
SELECT rodcis, prijmeni, jmeno, ockzaz.datum, kodmz, ockzaz.poznamka, latka, nazev, expire
FROM registr
JOIN kar ON registr.idpac = kar.idpac
JOIN ockzaz ON registr.idpac = ockzaz.idpac
WHERE datum_zruseni IS NULL
AND kar.vyrazen != 'A'
AND kar.rodcis IS NOT NULL
AND idicp != 0
ORDER BY ockzaz.datum DESC
""")
rows = cur.fetchall()
print(f"Očkování: {len(rows)}")
ws = wb.create_sheet("Očkování")
ws.append(["Rodne cislo", "Prijmeni", "Jmeno", "Datum ockovani", "Kod MZ", "Sarze", "Latka", "Nazev", "Expirace"])
for row in rows:
ws.append(list(row))
# =====================
# List: Recepty
# =====================
cur.execute("""
SELECT kar.rodcis,
TRIM(kar.prijmeni) || ' ' || SUBSTRING(kar.jmeno FROM 1 FOR 1) || '.' AS jmeno,
recept.datum,
TRIM(recept.lek) || ' ' || TRIM(recept.dop) AS lek,
recept.expori AS Poc,
CASE WHEN recept.opakovani IS NULL THEN 1 ELSE recept.opakovani END AS OP,
recept.uhrada,
recept.dsig,
recept.NOTIFIKACE_KONTAKT AS notifikace,
recept_epodani.erp,
recept_epodani.vystavitel_jmeno,
recept.atc,
recept.CENAPOJ,
recept.cenapac
FROM recept
LEFT JOIN RECEPT_EPODANI ON recept.id_epodani = recept_epodani.id
LEFT JOIN kar ON recept.idpac = kar.idpac
ORDER BY datum DESC, erp DESC
""")
rows = cur.fetchall()
print(f"Recepty: {len(rows)}")
ws = wb.create_sheet("Recepty")
ws.append(["Rodné číslo", "Jméno", "Datum vystavení", "Název leku", "Poč.", "Op.", "Úhr.",
"Da signa", "Notifikace", "eRECEPT", "Vystavil", "ATC", "Cena pojišťovna", "Cena pacient"])
for row in rows:
ws.append([sanitize(v) if isinstance(v, str) else v for v in row])
# =====================
# List: Výkony všechny
# =====================
cur.execute(f"""
SELECT dokladd.rodcis,
TRIM(prijmeni) || ', ' || TRIM(jmeno),
dokladd.datose, dokladd.kod, dokladd.pocvyk, dokladd.ddgn, dokladd.body, vykony.naz
FROM kar
JOIN dokladd ON kar.rodcis = dokladd.rodcis
JOIN vykony ON dokladd.kod = vykony.kod
WHERE {VYKONY_CONDITION}
ORDER BY dokladd.datose DESC, dokladd.rodcis
""")
rows = cur.fetchall()
print(f"Výkony: {len(rows)}")
ws = wb.create_sheet("Vykony")
ws.append(["Rodne cislo", "Jmeno", "Datum vykonu", "Kod", "Pocet", "Dg.", "Body", "Nazev"])
for row in rows:
ws.append(list(row))
# =====================
# Listy: Neschopenky
# =====================
def pocet_dni(zacnes, konnes, pracne):
dnes = date.today()
if pracne == 'A':
return (dnes - zacnes).days if zacnes else "NA"
if pracne == 'N' and zacnes and konnes and zacnes <= konnes:
return (konnes - zacnes).days
return "NA"
def nes_row(r):
return (r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7],
pocet_dni(r[5], r[7], r[6]), r[8], r[9], r[10])
NES_HEADERS = ["ID pac", "Rodne cislo", "Jmeno", "Datum neschopenky", "Číslo neschopenky",
"Zacatek", "Aktivní?", "Konec", "Pocet dni", "Diagnoza zacatel", "Diagnoza konec", "Aktualizovano"]
cur.execute("""
SELECT nes.idpac, kar.rodcis,
TRIM(prijmeni) || ', ' || TRIM(jmeno),
nes.datnes, nes.ecn, nes.zacnes, nes.pracne, nes.konnes,
nes.diagno, nes.kondia, nes.updated
FROM nes
LEFT JOIN kar ON nes.idpac = kar.idpac
WHERE nes.datnes <= CURRENT_DATE
ORDER BY datnes DESC
""")
vse = cur.fetchall()
aktivni = [r for r in vse if r[6] == 'A']
print(f"Neschopenky: {len(vse)} celkem, {len(aktivni)} aktivních")
ws = wb.create_sheet("Neschopenky všechny")
ws.append(NES_HEADERS)
for r in vse:
ws.append(list(nes_row(r)))
ws = wb.create_sheet("Neschopenky aktivní")
ws.append(NES_HEADERS)
for r in aktivni:
ws.append(list(nes_row(r)))
# =====================
# Výkonové listy jednotlivé typy výkonů
# =====================
add_vykony_sheet('Preventivni prohlidky', [1022, 1021])
add_vykony_sheet('INR', [1443])
add_vykony_sheet('CRP', [2230, 9111])
add_vykony_sheet('Holter', [17129])
add_vykony_sheet('Prostata', [1130, 1131, 1132, 1133, 1134])
add_vykony_sheet('TOKS', [15118, 15119, 15120, 15121])
add_vykony_sheet('COVID', [1306])
add_vykony_sheet('Streptest', [2220])
# =====================
# List: Posudky řidičák MOTORVO (ruční)
# =====================
cur.execute("SELECT IDPACI, DATUM FROM HISTDOC WHERE TYP = 'EPOSMRO'")
eposmro_keys = set((r[0], r[1]) for r in cur.fetchall())
cur.execute("""
SELECT h.ID, h.DATUM, h.IDPACI,
k.PRIJMENI, k.JMENO, k.RODCIS,
h.DATA, h.PORCISLO, h.STAV, h.PRINTED, h.IDUZIV, h.CREATED
FROM HISTDOC h
JOIN KAR k ON k.IDPAC = h.IDPACI
WHERE h.TYP = 'MOTORVO'
ORDER BY h.ID DESC
""")
motorvo_rows = cur.fetchall()
print(f"MOTORVO: {len(motorvo_rows)}")
motorvo_headers = [
'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS',
'PorCislo', 'DatumVyd', 'DatKonec', 'DruhProh',
'Posouzeni', 'ZpusobPodminka', 'SkupinaPodminka', 'Skupiny',
'ePosudek', 'STAV', 'PRINTED', 'IDUZIV', 'CREATED'
]
ws = wb.create_sheet("Posudky řidičák")
ws.append(motorvo_headers)
epos_col_idx = motorvo_headers.index('ePosudek') + 1
for i, row in enumerate(motorvo_rows, start=2):
(hid, datum, idpac, prijmeni, jmeno, rodcis,
data_blob, porcislo, stav, printed, iduziv, created) = row
data = parse_data(data_blob)
if data.get('Posouzeni2') == 'T':
posouzeni = 'nezpůsobilý'
elif data.get('ZpusobPodminka') == 'B:1':
posouzeni = 'způsobilý s podmínkou'
elif data.get('Posouzeni') == 'T':
posouzeni = 'způsobilý'
else:
posouzeni = ''
ws.append([
hid, fmt(datum), idpac, fmt(prijmeni), fmt(jmeno), fmt(rodcis),
fmt(porcislo or data.get('PorCislo', '')),
parse_date(data.get('DatumVyd', '')),
parse_date(data.get('DatKonec', '')),
fmt(data.get('DruhProh', '')),
posouzeni,
fmt(data.get('ZpusobPodminka', '')),
fmt(data.get('SkupinaPodminka', '')),
fmt(data.get('ZpusobJe', '')),
'ANO' if (idpac, datum) in eposmro_keys else 'NE',
fmt(stav), fmt(printed), fmt(iduziv), fmt(created),
])
if i % 2 == 0:
for cell in ws[i]:
cell.fill = ZEBRA_FILL
cell = ws.cell(row=i, column=epos_col_idx)
if cell.value == 'ANO':
cell.fill = GREEN_FILL
cell.font = GREEN_FONT
style_header(ws)
ws.freeze_panes = 'A2'
autofit_ws(ws)
# =====================
# List: Posudky řidičák EPOSMRO (elektronická podání)
# =====================
cur.execute("""
SELECT h.ID, h.DATUM, h.IDPACI,
k.PRIJMENI, k.JMENO, k.RODCIS,
h.DATA, h.STAV, h.CREATED,
e.ID_PODANI, e.ODESLANO, e.STATUS
FROM HISTDOC h
JOIN KAR k ON k.IDPAC = h.IDPACI
LEFT JOIN HISTDOC_EPOSUDEK e ON e.ID_HISTDOC = h.ID
WHERE h.TYP = 'EPOSMRO'
ORDER BY h.ID DESC
""")
epos_rows = cur.fetchall()
print(f"EPOSMRO: {len(epos_rows)}")
ws = wb.create_sheet("ePosudky registr")
ws.append([
'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS',
'DatumVyd', 'DatKonec', 'DruhProhlidky', 'DruhPosudku',
'Vysledek', 'StavPosudku', 'TypAkce',
'STAV', 'CREATED', 'ID_PODANI', 'ODESLANO', 'STATUS_ODESL'
])
for i, row in enumerate(epos_rows, start=2):
(hid, datum, idpac, prijmeni, jmeno, rodcis,
data_blob, stav, created, id_podani, odeslano, status_odesl) = row
data = parse_data(data_blob)
ws.append([
hid, fmt(datum), idpac, fmt(prijmeni), fmt(jmeno), fmt(rodcis),
parse_date(data.get('DatumVystaveni', '')),
parse_date(data.get('PlatnostDo', '')),
fmt(data.get('DruhProhlidkyNazev', '')),
fmt(data.get('DruhPosudkuNazev', '')),
fmt(data.get('VysledekNazev', '')),
fmt(data.get('StavPosudkuNazev', '')),
fmt(data.get('TypAkceNazev', '')),
fmt(stav), fmt(created), fmt(id_podani), fmt(odeslano), fmt(status_odesl),
])
if i % 2 == 0:
for cell in ws[i]:
cell.fill = ZEBRA_FILL
style_header(ws)
ws.freeze_panes = 'A2'
autofit_ws(ws)
# =====================
# Autofilter na všech listech
# =====================
for ws in wb.worksheets:
ws.auto_filter.ref = f"A1:{get_column_letter(ws.max_column)}{ws.max_row}"
# =====================
# Uložení
# =====================
con.close()
wb.save(output_path)
print(f"Uloženo: {output_path}")
# =====================
# xlwings: autofit + centrování Recepty
# =====================
with xw.App(visible=False) as app:
wb_xw = xw.Book(output_path)
for sheet in wb_xw.sheets:
sheet.autofit()
for sloupec in ["C:C", "E:E", "F:F", "G:G", "I:I", "M:M", "N:N"]:
wb_xw.sheets['Recepty'].range(sloupec).api.HorizontalAlignment = 3
wb_xw.save()
wb_xw.close()
print("Hotovo.")
+321
View File
@@ -0,0 +1,321 @@
# MedicusWithClaudePN Pracovní neschopnosti
## Účel
Report aktivních pracovních neschopností pro MUDr. Buzalkovou Michaelu.
Generuje PDF a odesílá na výchozí tiskárnu.
## Spuštění
```bash
# Test otevře PDF v prohlížeči, netiskne:
python pn_report.py --no-print
# Ostrý provoz vytiskne rovnou na výchozí tiskárnu:
python pn_report.py
```
## Požadavky
```bash
pip install reportlab pywin32
```
## SQL dotaz aktivní PN
Zachycen přes Firebird trace přímo z Medicusu (přesná kopie logiky aplikace),
doplněn o podotaz na poslední 14denní potvrzení z tabulky HPN.
```sql
SELECT
nes.id,
nes.idpac,
TRIM(kar.prijmeni) || ', ' || TRIM(kar.jmeno) AS jmeno,
kar.rodcis,
nes.zacnes,
nes.konnes,
nes.diagno,
COALESCE(nes.ecn, nes.cisnes) AS cisnes,
(SELECT MAX(h.datum) FROM hpn h
WHERE h.idnes = nes.id AND h.typ = '2' AND h.storno = 'F') AS posl_potvrzeni
FROM nes, kar
WHERE nes.zacnes <= current_date
AND nes.konnes IS NULL
AND nes.idpac = kar.idpac
AND nes.pracne = 'A'
AND nes.storno <> 'T'
AND (
NOT EXISTS (SELECT id FROM nesd WHERE nesd.idnes = nes.id)
OR (SELECT FIRST 1 kam FROM nesd WHERE nesd.idnes = nes.id
ORDER BY nesd.datum DESC, nesd.id DESC) = 'N'
)
ORDER BY kar.prijmeni ASC, kar.jmeno ASC
```
### Klíčové podmínky
| Podmínka | Význam |
|---|---|
| `pracne = 'A'` | Pouze pracovní neschopnosti (ne jiné typy) |
| `storno <> 'T'` | Vyřazení stornovaných záznamů |
| `zacnes <= current_date` | PN již začala |
| `konnes IS NULL` | PN dosud neskončila datum konce nemůže být v budoucnosti (pravidlo ČSSZ), aktivní PN má vždy `konnes = NULL` |
| `nesd` subquery | PN nebyla předána dál poslední záznam `Kam = 'N'` = stále u pacienta |
| `COALESCE(ecn, cisnes)` | Použije ECN (elektronické), jinak starší CISNES |
## Sloupce reportu
| Sloupec | Zdroj | Poznámka |
|---|---|---|
| # | | Pořadové číslo |
| Příjmení a jméno | KAR | |
| Rod. číslo | KAR | |
| Začátek PN | NES.ZACNES | |
| Dnů | výpočet | Počet dní od začátku PN do dnes |
| Diagnóza | NES.DIAGNO | |
| Posl. potvrzení | HPN (TYP='2') | Datum posledního 14denního potvrzení |
| Dní od potvr. | výpočet | Červeně pokud > 14 dní |
## Tabulka HPN typy podání
| TYP | Význam |
|---|---|
| `H` | Hlášení neschopnosti (vznik PN) |
| `1` | První zpráva |
| `P` | **Průběžná zpráva = 14denní potvrzení trvání PN** |
| `2` | Neznámý typ (2033 záznamů v DB, ale ne pro průběžná potvrzení) |
| `C`, `Y`, `Z` | Vzácné typy (jednotky záznamů) |
Vazba: `HPN.IDNES → NES.ID`
---
## Jak Medicus zobrazuje PN daného pacienta (zachyceno z trace)
### 1. Seznam všech PN pacienta
```sql
SELECT
ID, IDPAC, DATNES, CISNES, PODNIK, ADRESA, PROFES, ZACNES,
KONNES, PRACNE, PRICINA, DIAGNO, KONDIA, PREDAN, IDUZI,
VYSTAVIL, DATUKONNES, IDODD, IDPRAC, STORNO,
DATVYCHOD, DATVYCHDO, VYCH1OD, VYCH1DO, VYCH2OD, VYCH2DO, VYCH3OD, VYCH3DO,
DATOSETRENI, DATPRINAV, OMLUVENKA, RODCISNES,
DatNastUstPece, DatUkonUstPece, ICPE, ECN, EPODANI,
ADR_OBEC, ADR_CP, ADR_CO, ADR_DOD, ADR_PSC, ADR_STAT,
ZAM_ADRESA, DATUKON_OSSZ, ZAMDRUH, STATDPNKOD,
SOUHLAS_SSZKOD, SOUHLAS_SSZNAZ, SOUHLAS_DATUM,
DGZMENA, CIZI, OSSZ, DUVOD_UKONCENI, UKON_OSSZ, PORUS_REZIMU,
UKON_OSSZNAZ, ADR_ZMENA, coalesce(ECN, CISNES) as CISLO,
ADR_ZMENA_DO, POTVRZENI_VYDANO, DATNAR, SPRAVCE_POJ,
ZAM_OBEC, ZAM_CO, ZAM_CP, ZAM_DOD, ZAM_PSC, ZAM_STAT, ZAM_CCSZ_ID, ZAM_CSSZ_VARSYM,
VYCHINDIVIDUAL, VERZE_DPN, LEKAR_VYSTAVIL, LEKAR_VYSTAVIL_ICPE,
USEDATNAR, KONTAKT_TEL, KONTAKT_EMAIL,
case when KONTAKT_TEL is not NULL then 'S'
when KONTAKT_EMAIL is not null then 'E'
else NULL end as NOTIFIKACE,
coalesce(KONTAKT_TEL, KONTAKT_EMAIL) as NOTIFIKACE_KONTAKT
FROM NES
WHERE IDPAC = ?
ORDER BY DATNES ASC, ID ASC
```
### 2. Formuláře eNeschopenky pro vybranou PN (záložka "Formuláře eNeschopenky")
Zobrazuje pouze TYP `H`, `1`, `2` (ne `P` = propuštění).
Pouze záznamy s vazbou na HISTDOC (`IDHISTDOC IS NOT NULL`).
```sql
SELECT
HD.ID AS HISTDOCID,
H.TYP AS HISTDOCTYP, -- H=hlášení, 1=první zpráva, 2=průběžná/potvrzení
HD.DATUM AS DATZAD, -- Datum vystavení (z HISTDOC)
H.DATUM AS DATPOD, -- Datum podání (z HPN)
H.STAV,
H.ODBAVENO,
HD.TYP
FROM HPN H
JOIN HISTDOC HD ON H.IDHISTDOC = HD.ID
WHERE H.IDNES = ?
ORDER BY HD.DATUM ASC
```
### 3. Rychlý přehled formulářů (bez HISTDOC)
Používá se pro zjištění stavu vrací všechny záznamy TYP `H`, `1`, `2`:
```sql
SELECT * FROM HPN
WHERE IDNES = ?
AND STORNO = 'F'
AND TYP IN ('1', '2', 'H')
ORDER BY DATUM DESC, CAS DESC, ID DESC
```
### 4. TFHpnHistorie formulář "Historie HPN" (acHpnHistorie)
Kompletní přehled všech HPN záznamů pro vybranou PN. Spouští se akcí `acHpnHistorie`.
```sql
select h.ID, h.IDNES, h.IDPODANI, h.TYP, h.DATA, h.DATUM, h.CAS, h.ODPOVED,
h.IDPRAC, h.IDUZI, h.UPRAVENO, h.OPRAVA_ID, h.STAV, h.STORNO,
h.POR_CISLO, h.ID_CHYBY, h.OSSZ,
n.CisNes, n.Ecn, coalesce(n.CisNes, n.Ecn) as Cislo, n.IdPac,
k.Prijmeni, k.Jmeno, k.Titul, coalesce(n.RODCISNES, k.RODCIS) as RODCIS,
u.Zkratka, hp.Odeslano, hp.CorelationId,
(select first 1 h2.ID from HPN h2 where h2.OPRAVA_ID = h.ID) as IdOpravy,
h.verze_dpn,
n.SPRAVCE_POJ,
n.ADRESA as ULICE, n.ADR_CP, n.ADR_CO, n.ADR_DOD, n.ADR_OBEC, n.ADR_PSC, n.ADR_STAT,
n.PODNIK, n.PROFES, n.ZAM_ADRESA, n.ZAM_CP, n.ZAM_CO, n.ZAM_OBEC, n.ZAM_PSC, n.ZAM_STAT,
n.ZACNES, n.DIAGNO, n.DATNES, n.PRICINA,
n.KONNES, n.KONDIA, n.DATUKONNES,
n.DATVYCHOD, n.VYCH1OD, n.VYCH1DO, n.VYCH2OD, n.VYCH2DO,
n.PRICINA, n.ICPE, n.VYCH3OD, n.VYCH3DO, n.KONTAKT_TEL,
h.IDHISTDOC, h.ODBAVENO,
IIF((H.IDHISTDOC is not null),
(select HD.TYP from HISTDOC HD where HD.ID = H.IDHISTDOC), null) as HISTDOCTYP
from HPN h
left join NES n on (n.id = h.idnes)
left join KAR k on (k.IdPac = n.IdPac)
left join UZIVATEL u on (u.IdUzi = h.IdUzi)
left join HPN_PODANI hp on (hp.ID = h.IdPodani)
where h.IdNes = ?
ORDER BY h.Datum ASC, h.Cas ASC, h.Id ASC
```
### 5. Kontrola čekajících podání (PN s neodeslanými HPN záznamy)
```sql
select nes.id from nes
where nes.idpac = ?
and nes.storno = 'F'
and nes.epodani = 'T'
and nes.icpe = ?
and coalesce(nes.verze_dpn, '') not in ('', 'p', 'o')
and exists (
select 1 from hpn
where hpn.idnes = nes.id
and hpn.storno = 'F'
and hpn.typ in ('1', '2', 'H')
and hpn.idpodani is null -- dosud neodesláno
and hpn.stav <> 99
and hpn.stav <> 10
)
```
### 6. Kontrola potvrzení vydaného tento měsíc (POTVRZENI_VYDANO)
Medicus kontroluje zda pro daného pacienta existuje PN aktivní alespoň 10 dní,
u které ještě nebylo vydáno potvrzení v aktuálním měsíci:
```sql
select first 1 ZACNES, CISNES, ID, POTVRZENI_VYDANO
from NES
where (IDPAC = ?)
and (? >= ZACNES + 10) -- PN trvá alespoň 10 dní
and ((KONNES is NULL) or (KONNES > ?))
and (STORNO = 'F')
and (
extract(month from POTVRZENI_VYDANO) || extract(year from POTVRZENI_VYDANO)
=
extract(month from cast(? as date)) || extract(year from cast(? as date))
)
```
### 7. Vyhledání HPN záznamu podle ICPE v XML datech
Číslo `11031812` (ICPE lékaře) se hledá přímo v obsahu XML blobu `HPN.DATA`.
Medicus takto identifikuje konkrétní HPN záznam při opravě nebo ověření stavu:
```sql
-- Neodeslané nebo čekající záznamy:
select h.id from HPN h
left join HPN h2 on (h2.OPRAVA_ID = h.ID)
where (h.storno = 'F')
and ((h.stav in (0,1)) or (h.stav is NULL))
and (h2.OPRAVA_ID is null)
and (H.DATA containing '11031812') -- hledá ICPE v XML obsahu
and (h.IDNES = ?)
-- Úspěšně odeslané záznamy (stav=1):
select h.id from HPN h
left join HPN h2 on (h2.OPRAVA_ID = h.ID)
where (h.storno = 'F')
and (h.stav = 1)
and h2.OPRAVA_ID is null
and (H.DATA containing '11031812')
and h.IDNES = ?
```
### 8. Předání/Převzetí (záložka "Předání/Převzetí")
```sql
SELECT ID, IDNES, KAMODKUD, DATUM, KAM, ICZ, ICPE, ICO, JMENO_LEKARE
FROM NESD
WHERE IDNES = ?
ORDER BY DATUM ASC, ID ASC
```
### Poznámky
- **HISTDOC** každé odeslání formuláře vytváří záznam v HISTDOC; HPN bez IDHISTDOC se v UI nezobrazí
- **HPN.STAV** stav podání (1 = odesláno/přijato)
- **HPN.ODBAVENO** příznak zpracování (`'F'` = ne, `'T'` = ano)
- HPN záznamy TYP='2' (průběžná potvrzení) **nemají IDHISTDOC** JOIN s HISTDOC by je odfiltroval. Pro datum posledního potvrzení v reportu proto používáme prostý MAX bez JOINu. HISTDOC mají pouze TYP='P' (ukončení PN).
---
## Vazby tabulky NES (zjištěno z DB)
### Formální FK constrainty
| Směr | Vazba | Popis |
|---|---|---|
| NES → KAR | `NES.IDPAC → KAR.IDPAC` | Každá neschopenka patří pacientovi v kartotéce |
| HPN → NES | `HPN.IDNES → NES.ID` | Formuláře/hlášení HPN odkazují na konkrétní neschopenku |
### Logické vazby (bez FK constraintu)
| Tabulka | Pole | Poznámka |
|---|---|---|
| NESD | `NESD.IDNES → NES.ID` | Předání/převzetí PN vazba jen kódem, ne constraintem |
| HISTDOC | `HPN.IDHISTDOC → HISTDOC.ID` | Dokumenty k formulářům vazba přes HPN |
### Indexy na NES
| Index | Unique | Pole |
|---|---|---|
| PK_NES | Ano | ID |
| FK_NES_KAR | Ne | IDPAC |
| NES_POTVRZENI_VYDANO | Ne | POTVRZENI_VYDANO |
### Poznámka
Medicus obecně používá minimum DB constraintů většina vazeb je řešena aplikačním kódem.
`NESD` a `HISTDOC` nemají formální FK na `NES`, přesto jsou klíčové pro zobrazení PN v UI.
---
## Červené zvýraznění
Sloupce "Posl. potvrzení" a "Dní od potvr." jsou červeně zvýrazněny pokud:
- Od posledního potvrzení uplynulo více než 14 dní, nebo
- PN nemá žádné potvrzení a trvá déle než 14 dní (zobrazí se s `(!)`)
## Tisk
Skript používá `win32api.ShellExecute` s příkazem `'print'` odešle PDF
na výchozí tiskárnu Windows.
## Automatizace
Plánované spouštění každé pondělí a pátek ráno přes Windows Task Scheduler
zatím nenastaveno, připravit až bude skript stabilní.
## Soubory
| Soubor | Obsah |
|---|---|
| `pn_report.py` | Hlavní skript DB dotaz, generování PDF, tisk |
| `PN.md` | Tento soubor dokumentace |
+320
View File
@@ -0,0 +1,320 @@
"""
pn_report.py Report aktivních pracovních neschopností
Generuje PDF a posílá na výchozí tiskárnu.
Spuštění:
python pn_report.py # vytvoří PDF a vytiskne
python pn_report.py --no-print # jen vytvoří PDF (pro testování)
Požadavky:
pip install reportlab pywin32
"""
import fdb
import sys
import os
import tempfile
from datetime import date, datetime
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# ---------------------------------------------------------------------------
# Konfigurace
# ---------------------------------------------------------------------------
DB_DSN = r'localhost:c:\medicus 3\data\medicus.fdb'
DB_USER = 'SYSDBA'
DB_PASS = 'masterkey'
DB_CHARSET = 'win1250'
# Pokud chcete soubor uložit trvale, nastavte výstupní adresář:
OUTPUT_DIR = None # None = dočasný soubor, smaže se po tisku
#OUTPUT_DIR = r'u:\Dropbox\!!!Days\Downloads Z230'
# ---------------------------------------------------------------------------
# Dotaz aktivní PN (konnes IS NULL nebo v budoucnosti, storno='F')
# ---------------------------------------------------------------------------
SQL = """
SELECT
nes.id AS idnes,
nes.idpac,
TRIM(kar.prijmeni) || ', ' || TRIM(kar.jmeno) AS jmeno,
kar.rodcis,
nes.zacnes,
nes.konnes,
nes.diagno,
COALESCE(nes.ecn, nes.cisnes) AS cisnes,
(SELECT MAX(h.datum) FROM hpn h
WHERE h.idnes = nes.id AND h.typ = 'P' AND h.storno = 'F') AS posl_potvrzeni
FROM nes, kar
WHERE nes.zacnes <= current_date
AND nes.konnes IS NULL
AND nes.idpac = kar.idpac
AND nes.pracne = 'A'
AND nes.storno <> 'T'
AND (
NOT EXISTS (SELECT id FROM nesd WHERE nesd.idnes = nes.id)
OR (SELECT FIRST 1 kam FROM nesd WHERE nesd.idnes = nes.id
ORDER BY nesd.datum DESC, nesd.id DESC) = 'N'
)
ORDER BY kar.prijmeni ASC, kar.jmeno ASC
"""
# ---------------------------------------------------------------------------
# Pomocné funkce
# ---------------------------------------------------------------------------
def fmt_date(val):
"""Datum → DD.MM.YYYY nebo prázdný řetězec."""
if val is None:
return ''
if isinstance(val, (date, datetime)):
return val.strftime('%d.%m.%Y')
return str(val)
def fmt_str(val):
if val is None:
return ''
return str(val).strip()
def delka_pn(zacnes, konnes):
"""Počet dnů PN (od začátku do dnes / do konce)."""
if zacnes is None:
return ''
end = konnes if konnes else date.today()
if isinstance(zacnes, datetime):
zacnes = zacnes.date()
if isinstance(end, datetime):
end = end.date()
if isinstance(zacnes, date) and isinstance(end, date):
days = (end - zacnes).days + 1
return str(days)
return ''
# ---------------------------------------------------------------------------
# Načtení fontu s českou diakritikou
# ---------------------------------------------------------------------------
def register_font():
"""
Zkusí zaregistrovat DejaVuSans (umí win1250 znaky).
Fallback: Helvetica (bez diakritiky nouzové řešení).
"""
font_paths = [
r'C:\Windows\Fonts\DejaVuSans.ttf',
r'C:\Windows\Fonts\arial.ttf',
r'C:\Windows\Fonts\segoeui.ttf',
]
for path in font_paths:
if os.path.exists(path):
name = os.path.splitext(os.path.basename(path))[0]
try:
pdfmetrics.registerFont(TTFont(name, path))
pdfmetrics.registerFont(TTFont(name + '-Bold',
path.replace('.ttf', 'bd.ttf') if 'arial' in path.lower()
else path.replace('.ttf', '-Bold.ttf')
if os.path.exists(path.replace('.ttf', '-Bold.ttf'))
else path
))
return name, name + '-Bold'
except Exception:
continue
return 'Helvetica', 'Helvetica-Bold'
# ---------------------------------------------------------------------------
# Generování PDF
# ---------------------------------------------------------------------------
def build_pdf(rows, output_path, font_name, font_bold):
today_str = date.today().strftime('%d.%m.%Y')
weekday_cs = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle']
weekday = weekday_cs[date.today().weekday()]
doc = SimpleDocTemplate(
output_path,
pagesize=A4,
topMargin=1.5*cm,
bottomMargin=1.5*cm,
leftMargin=1.5*cm,
rightMargin=1.5*cm,
)
styles = getSampleStyleSheet()
def para(text, size=10, bold=False, align='LEFT', color=colors.black):
fn = font_bold if bold else font_name
al = {'LEFT': 0, 'CENTER': 1, 'RIGHT': 2}.get(align, 0)
from reportlab.platypus import Paragraph as P
from reportlab.lib.styles import ParagraphStyle
st = ParagraphStyle('x', fontName=fn, fontSize=size,
textColor=color, alignment=al, leading=size*1.3)
return P(text, st)
story = []
# Záhlaví
story.append(para('MUDr. Buzalková Michaela ordinace praktického lékaře',
size=9, color=colors.grey))
story.append(para(f'Aktivní pracovní neschopnosti',
size=16, bold=True))
story.append(para(f'Vytištěno: {weekday} {today_str} | Počet záznamů: {len(rows)}',
size=9, color=colors.grey))
story.append(Spacer(1, 0.4*cm))
if not rows:
story.append(para('Žádné aktivní pracovní neschopnosti.', size=12))
doc.build(story)
return
# Záhlaví tabulky
headers = ['#', 'Příjmení a jméno', 'Rod. číslo', 'Začátek PN',
'Dnů', 'Diagnóza', 'Posl. potvrzení', 'Dní od potvr.']
col_widths = [0.7*cm, 5.5*cm, 2.8*cm, 2.4*cm, 1.4*cm, 2.2*cm, 3.0*cm, 2.2*cm]
table_data = [headers]
overdue_rows = [] # indexy řádků kde je potvrzení po splatnosti
for idx, row in enumerate(rows, start=1):
idnes, idpac, jmeno, rodcis, zacnes, konnes, diagno, cisnes, posl_potvrzeni = row
# Počet dní od posledního potvrzení
if posl_potvrzeni is not None:
pp = posl_potvrzeni.date() if isinstance(posl_potvrzeni, datetime) else posl_potvrzeni
dni_od = (date.today() - pp).days
dni_od_str = str(dni_od)
if dni_od > 14:
overdue_rows.append(idx + 1) # +1 kvůli záhlaví
else:
# Žádné potvrzení počítáme od začátku PN
zac = zacnes.date() if isinstance(zacnes, datetime) else zacnes
if zac:
dni_od = (date.today() - zac).days
dni_od_str = str(dni_od) + ' (!)'
if dni_od > 14:
overdue_rows.append(idx + 1)
else:
dni_od_str = ''
table_data.append([
str(idx),
fmt_str(jmeno),
fmt_str(rodcis),
fmt_date(zacnes),
delka_pn(zacnes, konnes),
fmt_str(diagno),
fmt_date(posl_potvrzeni) if posl_potvrzeni else '',
dni_od_str,
])
tbl = Table(table_data, colWidths=col_widths, repeatRows=1)
style = TableStyle([
# Záhlaví
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2F5496')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), font_bold),
('FONTSIZE', (0, 0), (-1, 0), 8),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('BOTTOMPADDING',(0, 0), (-1, 0), 5),
('TOPPADDING', (0, 0), (-1, 0), 5),
# Data
('FONTNAME', (0, 1), (-1, -1), font_name),
('FONTSIZE', (0, 1), (-1, -1), 8),
('ALIGN', (0, 1), (0, -1), 'CENTER'), # #
('ALIGN', (5, 1), (5, -1), 'RIGHT'), # Dnů
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('TOPPADDING', (0, 1), (-1, -1), 3),
('BOTTOMPADDING',(0, 1), (-1, -1), 3),
# Mřížka
('GRID', (0, 0), (-1, -1), 0.3, colors.HexColor('#AAAAAA')),
('LINEBELOW', (0, 0), (-1, 0), 1, colors.HexColor('#2F5496')),
# Zebra
*[('BACKGROUND', (0, i), (-1, i), colors.HexColor('#DCE6F1'))
for i in range(2, len(table_data), 2)],
# Červené zvýraznění potvrzení po splatnosti (> 14 dní)
*[('BACKGROUND', (6, i), (7, i), colors.HexColor('#F4CCCC'))
for i in overdue_rows],
*[('TEXTCOLOR', (6, i), (7, i), colors.HexColor('#CC0000'))
for i in overdue_rows],
*[('FONTNAME', (6, i), (7, i), font_bold)
for i in overdue_rows],
])
tbl.setStyle(style)
story.append(tbl)
# Patička
story.append(Spacer(1, 0.5*cm))
story.append(para(f'--- konec reportu ({len(rows)} záznamů) ---',
size=8, color=colors.grey, align='CENTER'))
doc.build(story)
# ---------------------------------------------------------------------------
# Tisk
# ---------------------------------------------------------------------------
def print_pdf(path):
"""Pošle PDF na výchozí tiskárnu přes Windows ShellExecute."""
try:
import win32api
win32api.ShellExecute(0, 'print', path, None, '.', 0)
print(f'Odesláno na tiskárnu: {path}')
except ImportError:
# Fallback otevře soubor v PDF prohlížeči (ruční tisk)
print('pywin32 není nainstalován, otevírám PDF...')
os.startfile(path)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
no_print = '--no-print' in sys.argv
# Připojení k DB
print('Připojuji se k DB...')
conn = fdb.connect(dsn=DB_DSN, user=DB_USER, password=DB_PASS, charset=DB_CHARSET)
cur = conn.cursor()
print('Načítám aktivní PN...')
cur.execute(SQL)
rows = cur.fetchall()
conn.close()
print(f'Nalezeno {len(rows)} aktivních PN.')
# Font
font_name, font_bold = register_font()
print(f'Font: {font_name}')
# Výstupní soubor
if OUTPUT_DIR:
os.makedirs(OUTPUT_DIR, exist_ok=True)
out_path = os.path.join(OUTPUT_DIR,
date.today().strftime('%Y-%m-%d') + '_pn_report.pdf')
else:
fd, out_path = tempfile.mkstemp(suffix='_pn_report.pdf')
os.close(fd)
print(f'Generuji PDF: {out_path}')
build_pdf(rows, out_path, font_name, font_bold)
print('PDF hotovo.')
if no_print:
print('(tisk přeskočen --no-print)')
# Otevřeme pro náhled
os.startfile(out_path)
else:
print_pdf(out_path)
# Dočasný soubor necháme tiskárna ho potřebuje přečíst
# Windows ho smaže sám po zpracování tisku (temp adresář)
if __name__ == '__main__':
main()
+28
View File
@@ -0,0 +1,28 @@
import fdb, sys
sys.stdout.reconfigure(encoding='utf-8')
conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb', user='SYSDBA', password='masterkey', charset='win1250')
cur = conn.cursor()
sql = """
SELECT nes.id, TRIM(kar.prijmeni) || ', ' || TRIM(kar.jmeno) AS jmeno,
nes.zacnes,
(SELECT MAX(h.datum) FROM hpn h
WHERE h.idnes = nes.id AND h.typ = 'P' AND h.storno = 'F') AS posl_potvrzeni
FROM nes, kar
WHERE nes.zacnes <= current_date
AND nes.konnes IS NULL
AND nes.idpac = kar.idpac
AND nes.pracne = 'A'
AND nes.storno <> 'T'
AND (
NOT EXISTS (SELECT id FROM nesd WHERE nesd.idnes = nes.id)
OR (SELECT FIRST 1 kam FROM nesd WHERE nesd.idnes = nes.id
ORDER BY nesd.datum DESC, nesd.id DESC) = 'N'
)
ORDER BY kar.prijmeni ASC
"""
cur.execute(sql)
for row in cur.fetchall():
print(row)
conn.close()
@@ -0,0 +1,117 @@
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
import fdb
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from datetime import date, timedelta
import os
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
zacatek = date(2025, 1, 1)
konec = date.today()
dny = []
d = zacatek
while d <= konec:
dny.append(d)
d += timedelta(days=1)
print(f"Počítám {len(dny)} dní ({zacatek} {konec})...")
vysledky = []
for i, den in enumerate(dny):
# Počet registrovaných
cur.execute(f"""
SELECT COUNT(*) FROM KAR
WHERE vyrazen = 'N'
AND EXISTS (
SELECT id FROM registr r
JOIN icp i ON r.idicp = i.idicp
WHERE r.idpac = kar.idpac
AND r.datum <= '{den}'
AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= '{den}')
AND r.priznak IN ('V','D','A')
AND i.icp = '09305001'
AND i.odb = '001'
)
""")
pocet = cur.fetchone()[0]
# Zaregistrovaní tento den
cur.execute(f"""
SELECT k.RODCIS, k.PRIJMENI, k.JMENO
FROM REGISTR r JOIN KAR k ON k.IDPAC = r.IDPAC
WHERE r.datum = '{den}'
AND r.priznak IN ('V','D','A')
ORDER BY k.PRIJMENI, k.JMENO
""")
zaregistrovani = [f"{row[0]} {row[1].strip()} {row[2]}" for row in cur.fetchall()]
# Odregistrovaní tento den
cur.execute(f"""
SELECT k.RODCIS, k.PRIJMENI, k.JMENO
FROM REGISTR r JOIN KAR k ON k.IDPAC = r.IDPAC
WHERE r.datum_zruseni = '{den}'
ORDER BY k.PRIJMENI, k.JMENO
""")
odregistrovani = [f"{row[0]} {row[1].strip()} {row[2]}" for row in cur.fetchall()]
vysledky.append((den, pocet, zaregistrovani, odregistrovani))
if (i + 1) % 30 == 0:
print(f" {i+1}/{len(dny)}: {den}{pocet}")
conn.close()
# Excel
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Registrace"
hlavicka_font = Font(bold=True, color="FFFFFF")
hlavicka_fill = PatternFill("solid", fgColor="2E75B6")
ws.column_dimensions['A'].width = 14
ws.column_dimensions['B'].width = 14
ws.column_dimensions['C'].width = 10
ws.column_dimensions['D'].width = 45
ws.column_dimensions['E'].width = 45
for col, nazev in enumerate(['Datum', 'Registrovaných', 'Změna', 'Zaregistrováno', 'Odregistrováno'], start=1):
cell = ws.cell(row=1, column=col, value=nazev)
cell.font = hlavicka_font
cell.fill = hlavicka_fill
cell.alignment = Alignment(horizontal='center')
predchozi = None
for row_i, (den, pocet, zaregistrovani, odregistrovani) in enumerate(vysledky, start=2):
ws.cell(row=row_i, column=1, value=den).number_format = 'DD.MM.YYYY'
ws.cell(row=row_i, column=2, value=pocet).alignment = Alignment(horizontal='center')
if predchozi is not None:
zmena = pocet - predchozi
cell = ws.cell(row=row_i, column=3, value=zmena)
cell.alignment = Alignment(horizontal='center')
if zmena > 0:
cell.font = Font(color="00AA00", bold=True)
elif zmena < 0:
cell.font = Font(color="CC0000", bold=True)
predchozi = pocet
if zaregistrovani:
cell = ws.cell(row=row_i, column=4, value="\n".join(zaregistrovani))
cell.alignment = Alignment(wrap_text=True, vertical='top')
cell.font = Font(color="00AA00")
if odregistrovani:
cell = ws.cell(row=row_i, column=5, value="\n".join(odregistrovani))
cell.alignment = Alignment(wrap_text=True, vertical='top')
cell.font = Font(color="CC0000")
ws.freeze_panes = 'A2'
vystup = os.path.join(os.path.dirname(__file__), 'registrace_2025_dnes.xlsx')
wb.save(vystup)
print(f"\nUloženo: {vystup}")
@@ -0,0 +1,143 @@
# MedicusWithClaudePosudek poznámky pro Clauda
## O co jde
Lékařské posudky vystavované MUDr. Buzalkovou. Prozatím řešíme posudky k řízení motorových vozidel.
Nový zákon ukládá povinnost odesílat posudky k řízení do **centrálního registru** tuto funkci Medicus přidal v aktualizaci z konce března 2026.
---
## Tabulky
### HISTDOC hlavní tabulka pro všechny posudky
Všechny posudky jsou záznamy v `HISTDOC`, lišící se hodnotou sloupce `TYP`.
Klíčové sloupce:
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `TYP` | typ dokumentu (viz níže) |
| `DATUM` | datum vystavení posudku |
| `IDPACI` | FK → KAR.IDPAC (pacient) |
| `DATA` | obsah posudku text ve formátu key=value (viz níže) |
| `PORCISLO` | pořadové číslo posudku (= PorCislo v DATA) |
| `STAV` | stav záznamu (Z = zavřeno) |
| `PRINTED` | T/F byl vytištěn |
| `IDUZIV` | FK → UZIVATEL.IDUZI kdo vystavil (4 = MUDr. Buzalková) |
| `CREATED` | timestamp vytvoření záznamu |
**Vazba:** žádná přímá vazba na jiné tabulky (vyšetření, dekurz apod.) posudek je svébytný dokument.
### TYP hodnoty relevantní pro posudky řidičů
| TYP | Popis | Počet (k 2026-03-31) |
|---|---|---|
| `MOTORVO` | ruční posudek k řízení motorových vozidel | 1530 |
| `EPOSMRO` | elektronické podání posudku do centrálního registru | 2 |
Ostatní typy posudků v HISTDOC (pro referenci):
- `ZBROJPR`, `ZBROJP2` zbrojní průkaz
- `ZPUPRN` způsobilost pro práci
- `ZDRSTA3``ZDRSTA5`, `ZDRSTAV`, `ZDRINF` zdravotní stav (různé varianty)
- ... (celkem desítky typů)
### HISTDOC_EPOSUDEK evidence odeslání do registru
Doplňková tabulka k EPOSMRO záznamům v HISTDOC.
| Sloupec | Popis |
|---|---|
| `ID_HISTDOC` | FK → HISTDOC.ID (záznam EPOSMRO) |
| `ID_PODANI` | UUID přidělené centrálním registrem |
| `ODESLANO` | timestamp odeslání |
| `STATUS` | O = odesláno |
| `VERZE` | verze záznamu (base64 interní hodnota) |
### VS_POSUDKY prázdná, zatím nepoužívaná
Sloupce: ID, IDPAC, DATA (BLOB), DATUM, POSTYPE. Pravděpodobně připravena pro budoucí využití.
---
## Workflow: ruční posudek → elektronické podání
1. Lékař v Medicusu vyplní posudek → vznikne `HISTDOC` TYP=`MOTORVO`
2. Medicus automaticky odešle do centrálního registru → vznikne `HISTDOC` TYP=`EPOSMRO` + záznam v `HISTDOC_EPOSUDEK`
3. Oba záznamy mají stejné `IDPACI` + `DATUM` → podle toho je párujeme
Příklad (pacient Vráček, 30.3.2026):
- HISTDOC ID=34743, TYP=MOTORVO, CREATED=13:12
- HISTDOC ID=34746, TYP=EPOSMRO, CREATED=13:21
- HISTDOC_EPOSUDEK: STATUS=O, ODESLANO=13:21
---
## Formát DATA (key=value) MOTORVO
```
JmenoPac=Radomil Vráček
DatNar=D:27.03.1956
Prukaz=207069669 ← číslo řidičského průkazu
DatKonec=D:30.03.2028 ← platnost posudku do
DatumVyd=D:30.03.2026 ← datum vydání
Bydliste=K Šafránce 507/16, 19000 Praha 9-Střížkov
DruhProh=periodická ← druh prohlídky
Posouzeni=T ← T = způsobilý (F = nezpůsobilý?)
Posouzeni2=F ← T = nezpůsobilý (druhá volba)
ZpusobJe=B:0 ← skupiny bez podmínky
ZpusobPodminka=B:1 ← B:1 = má podmínku
SkupinaPodminka=sk. B brýle
PorCislo=2600037
KonecDleZakona=D
DatumPrevzeti=D:30.03.2026
```
**Výsledek posouzení** (kombinace Posouzeni + Posouzeni2 + ZpusobPodminka):
- `Posouzeni=T` + `Posouzeni2=F` + `ZpusobPodminka=B:0` → způsobilý
- `Posouzeni=T` + `Posouzeni2=F` + `ZpusobPodminka=B:1` → způsobilý s podmínkou
- `Posouzeni=T` + `Posouzeni2=T` → nezpůsobilý
## Formát DATA (key=value) EPOSMRO
```
Lekar=MUDr. Michaela Buzalková
KRZPID=130153584 ← ID lékaře v registru
ICO=68366370
ICP=09305001
Pacient=Radomil Vráček
RID=8705636888 ← číslo řidičáku
DatumNarozeni=D:27.03.1956
StavPosudkuKodVerze=zneplatneny|1.0.0
StavPosudkuNazev=Zneplatněný ← stav posudku v registru
TypAkceNazev=vytvoření
TypAkceKodVerze=akce_ro_1|1.0.0
DruhProhlidkyNazev=pravidelná
DruhProhlidkyKodVerze=Pravidelna|1.0.0
DruhPosudkuNazev=řidičské oprávnění pro seniory
DruhPosudkuKodVerze=SenioriRo|1.0.0
SkupinaZadatelRidicNazev=skupina 1
SkupinyRidicskehoOpravneniSeznam=B
HarmonizovaneNarodniKody=$:~HNK1:011:01.01 Brýle5:01.012:HK1:B0: ← kódy omezení (brýle)
VysledekKodVerze=ZpusobilySPodminkou|1.0.0
VysledekNazev=způsobilý s podmínkou
DatumVystaveni=D:30.03.2026
PlatnostDo=D:30.03.2028
```
**StavPosudku = "Zneplatněný"** neznamená chybu jde o akci, kdy lékař odvolá způsobilost pacienta (např. po mrtvici, epileptickém záchvatu apod.). Medicus pak odešle do registru zneplatnění existujícího posudku.
---
## Soubory v projektu
- `posudky_report.py` generuje Excel s listy MOTORVO a EPOSMRO
- `CLAUDE_NOTES.md` tento soubor
## Report (posudky_report.py)
- Výstup: `u:\Dropbox\!!!Days\Downloads Z230\YYYY-MM-DD_HH-MM-SS_Přehled posudků řidičák.xlsx`
- Maže předchozí verzi před zápisem nové
- List MOTORVO: 1530 záznamů, sloupec `ePosudek` = ANO (zeleně) / NE podle toho, zda byl odeslán ePosudek (párování IDPACI + DATUM)
- List EPOSMRO: 2 záznamy, detail elektronického podání
@@ -0,0 +1,237 @@
import fdb
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
from datetime import datetime
import os
import sys
# --- Připojení ---
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
cur = conn.cursor()
# --- Výstupní soubor ---
output_dir = r'u:\Dropbox\!!!Days\Downloads Z230'
now = datetime.now()
filename = now.strftime('%Y-%m-%d_%H-%M-%S') + '_Přehled posudků řidičák.xlsx'
output_path = os.path.join(output_dir, filename)
# --- Smazání předchozích verzí ---
for f in os.listdir(output_dir):
if f.endswith('_Přehled posudků řidičák.xlsx'):
os.remove(os.path.join(output_dir, f))
wb = openpyxl.Workbook()
# =====================
# Pomocné funkce
# =====================
HEADER_FILL = PatternFill('solid', fgColor='2F5496')
HEADER_FONT = Font(bold=True, color='FFFFFF')
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
GREEN_FILL = PatternFill('solid', fgColor='C6EFCE')
GREEN_FONT = Font(bold=True, color='276221')
def style_header(ws):
for cell in ws[1]:
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal='center')
def autofit(ws):
for col in ws.columns:
max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col)
ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 50)
def fmt(val):
if val is None:
return ''
return val
def parse_data(data_str):
"""Parsuje key=value text z HISTDOC.DATA do slovníku."""
result = {}
if not data_str:
return result
for line in data_str.splitlines():
if '=' in line:
key, _, val = line.partition('=')
result[key.strip()] = val.strip()
return result
def parse_date(val):
"""Převede 'D:DD.MM.YYYY' na datetime.date, nebo vrátí původní hodnotu."""
if val and val.startswith('D:'):
try:
return datetime.strptime(val[2:], '%d.%m.%Y').date()
except ValueError:
return val
return val
# =====================
# List 1 MOTORVO (ruční posudky k řízení)
# =====================
ws1 = wb.active
ws1.title = 'MOTORVO'
# Množina (IDPACI, DATUM) kde existuje EPOSMRO
cur.execute("""
SELECT IDPACI, DATUM FROM HISTDOC WHERE TYP = 'EPOSMRO'
""")
eposmro_keys = set((r[0], r[1]) for r in cur.fetchall())
cur.execute("""
SELECT h.ID, h.DATUM, h.IDPACI,
k.PRIJMENI, k.JMENO, k.RODCIS,
h.DATA, h.PORCISLO, h.STAV, h.PRINTED, h.IDUZIV, h.CREATED
FROM HISTDOC h
JOIN KAR k ON k.IDPAC = h.IDPACI
WHERE h.TYP = 'MOTORVO'
ORDER BY h.ID DESC
""")
raw_rows = cur.fetchall()
headers = [
'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS',
'PorCislo', 'DatumVyd', 'DatKonec', 'DruhProh',
'Posouzeni', 'ZpusobPodminka', 'SkupinaPodminka',
'Skupiny',
'ePosudek',
'STAV', 'PRINTED', 'IDUZIV', 'CREATED'
]
ws1.append(headers)
for i, row in enumerate(raw_rows, start=2):
(hid, datum, idpac, prijmeni, jmeno, rodcis,
data_blob, porcislo, stav, printed, iduziv, created) = row
data = parse_data(data_blob)
posouzeni = ''
if data.get('Posouzeni') == 'T':
if data.get('Posouzeni2') == 'T':
posouzeni = 'nezpůsobilý'
elif data.get('ZpusobPodminka') == 'B:1':
posouzeni = 'způsobilý s podmínkou'
else:
posouzeni = 'způsobilý'
skupiny = data.get('SkupinyRidicskehoOpravneniSeznam', '')
if not skupiny:
# MOTORVO nemá SkupinyRidicskehoOpravneniSeznam, zkusíme ZpusobJe
skupiny = data.get('ZpusobJe', '')
ws1.append([
hid,
fmt(datum),
idpac,
fmt(prijmeni),
fmt(jmeno),
fmt(rodcis),
fmt(porcislo or data.get('PorCislo', '')),
parse_date(data.get('DatumVyd', '')),
parse_date(data.get('DatKonec', '')),
fmt(data.get('DruhProh', '')),
posouzeni,
fmt(data.get('ZpusobPodminka', '')),
fmt(data.get('SkupinaPodminka', '')),
fmt(skupiny),
'ANO' if (idpac, datum) in eposmro_keys else 'NE',
fmt(stav),
fmt(printed),
fmt(iduziv),
fmt(created),
])
if i % 2 == 0:
for cell in ws1[i]:
cell.fill = ZEBRA_FILL
# Sloupec ePosudek zvýraznit ANO zeleně
epos_col = headers.index('ePosudek') + 1
cell = ws1.cell(row=i, column=epos_col)
if cell.value == 'ANO':
cell.fill = GREEN_FILL
cell.font = GREEN_FONT
style_header(ws1)
ws1.freeze_panes = 'A2'
autofit(ws1)
# =====================
# List 2 EPOSMRO (elektronická podání do registru)
# =====================
ws2 = wb.create_sheet('EPOSMRO')
cur.execute("""
SELECT h.ID, h.DATUM, h.IDPACI,
k.PRIJMENI, k.JMENO, k.RODCIS,
h.DATA, h.STAV, h.CREATED,
e.ID_PODANI, e.ODESLANO, e.STATUS
FROM HISTDOC h
JOIN KAR k ON k.IDPAC = h.IDPACI
LEFT JOIN HISTDOC_EPOSUDEK e ON e.ID_HISTDOC = h.ID
WHERE h.TYP = 'EPOSMRO'
ORDER BY h.ID DESC
""")
epos_rows = cur.fetchall()
headers2 = [
'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS',
'DatumVyd', 'DatKonec', 'DruhProhlidky', 'DruhPosudku',
'Vysledek', 'StavPosudku', 'TypAkce',
'STAV', 'CREATED',
'ID_PODANI', 'ODESLANO', 'STATUS_ODESL'
]
ws2.append(headers2)
for i, row in enumerate(epos_rows, start=2):
(hid, datum, idpac, prijmeni, jmeno, rodcis,
data_blob, stav, created,
id_podani, odeslano, status_odesl) = row
data = parse_data(data_blob)
ws2.append([
hid,
fmt(datum),
idpac,
fmt(prijmeni),
fmt(jmeno),
fmt(rodcis),
parse_date(data.get('DatumVystaveni', '')),
parse_date(data.get('PlatnostDo', '')),
fmt(data.get('DruhProhlidkyNazev', '')),
fmt(data.get('DruhPosudkuNazev', '')),
fmt(data.get('VysledekNazev', '')),
fmt(data.get('StavPosudkuNazev', '')),
fmt(data.get('TypAkceNazev', '')),
fmt(stav),
fmt(created),
fmt(id_podani),
fmt(odeslano),
fmt(status_odesl),
])
if i % 2 == 0:
for cell in ws2[i]:
cell.fill = ZEBRA_FILL
style_header(ws2)
ws2.freeze_panes = 'A2'
autofit(ws2)
# =====================
# Uložení
# =====================
conn.close()
wb.save(output_path)
sys.stdout.buffer.write(f'Ulozeno: {output_path}\n'.encode('utf-8'))
sys.stdout.buffer.write(f'MOTORVO: {len(raw_rows)} radku, EPOSMRO: {len(epos_rows)} radku\n'.encode('utf-8'))
@@ -0,0 +1,252 @@
# Fakturace a dávky poznámky pro Clauda
## Přehled tabulek
### FAK faktury pojišťovnám
- **844 záznamů** (k 2026-03-28)
- Primární klíč: `ID`
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `CISFAK` | číslo faktury (10 znaků) |
| `POJ` | pojišťovna (3 znaky, např. 111=VZP, 205=ČPZP, 207=OZP) |
| `ICZ`, `ICZ1` | IČZ ordinace |
| `IDICZ` | FK → ICZ.IDICZ (jen jedna ordinace, neřešit) |
| `PORCISLO` | pořadové číslo |
| `DATUMOD`, `DATUMDO` | období faktury (oddo) |
| `DATVYS` | datum vystavení |
| `DATODE` | datum odeslání |
| `OBDOB` | období (5 znaků, např. "20260") |
| `VYKONY` | částka za výkony (Kč) |
| `KAPITACE` | kapitační platba (Kč) |
| `ZALOHA` | záloha |
| `CENA` | celková cena faktury (Kč) |
| `DRUH` | druh faktury (např. "konečná") |
| `TYP` | typ (1 znak) |
| `ROK` | rok |
| `SPLAT` | datum splatnosti |
| `PROPLACENO` | datum proplacení (NULL = nezaplaceno) |
| `ZAPLACENO` | zaplacená částka |
| `ZUM` | ? |
| `HOSPAUSAL` | hospitalizační/paušální? |
| `FDAVKA` | BLOB odkaz na dávku |
| `KAPDETAIL`, `AGRDETAIL` | BLOBy detaily kapitace/agregace |
| `NAZFAK`, `POZFAK`, `OBDFAK` | název, poznámka, období faktury (texty) |
| `ICO`, `BANKA`, `UCET` | IČO, banka, účet |
| `ODJMENO/ULICE/MISTO/PSC` | adresa odesílatele |
| `PLNAZEV/ULICE/MISTO/PSC` | adresa plátce (pojišťovny) |
| `DRUHPOJ` | druh pojištění |
---
### FAKDET finanční detail faktury
- **1021 záznamů**
- Vazba: `IDFAK` → FAK.ID
- Typicky 12 řádky na fakturu (breakdown podle lékaře/IDUZI)
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `IDFAK` | FK → FAK.ID |
| `ICP` | IČP ordinace (např. 09305001) |
| `ODB` | odbornost (001 = praktický lékař) |
| `IDUZI` | FK → UZIVATEL.IDUZI který lékař |
| `CENAVYK` | výkony (Kč) |
| `CENALEC` | léky (Kč) |
| `CENAKAP` | kapitace (Kč) |
**Poznámka:** IDUZI=0 = systémový záznam (kapitace bez konkrétního lékaře).
---
### FAKDAV dávky zahrnuté ve faktuře
- **1296 záznamů**
- Vazba: `IDFAK` → FAK.ID
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `IDFAK` | FK → FAK.ID |
| `CISDAV` | číslo dávky |
| `DRUH` | druh dávky |
| `CENA` | celková cena dávky |
| `CENAVYK` | výkony |
| `CENALEC` | léky |
| `CENAPAU` | paušál |
| `ODB` | odbornost |
| `ICP` | IČP |
---
### PORTAL elektronické podání pojišťovně
- **180 záznamů**
- Vazba: `IDFAK` → FAK.ID
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `IDFAK` | FK → FAK.ID |
| `ODESLANO` | timestamp odeslání |
| `DATA`, `KDAVKA`, `FDAVKA` | BLOBy pravděpodobně XML dávky |
| `CHYBA` | příznak chyby (1 znak) |
| `STAV` | stav podání (smallint) |
| `ID_PODANI` | ID podání u pojišťovny (32 znaků, UUID) |
| `IDPODANI` | interní ID podání |
| `IDCERT` | certifikát použitý pro podání |
| `DAVKA_ROK`, `DAVKA_DISK`, `DAVKA_IDICZ` | metadata dávky |
| `BB_DAVKA`, `BB_FAKTURA` | ? |
| `DAVKA_DATUMOD`, `DAVKA_DATUMDO` | období dávky |
| `DAVKA_CASTKA` | částka dávky |
---
## Schéma vazeb
```
FAK (faktura)
├── FAKDET (IDFAK → FAK.ID) finanční breakdown podle lékaře
├── FAKDAV (IDFAK → FAK.ID) seznam dávek ve faktuře
├── PORTAL (IDFAK → FAK.ID) viz níže, IDFAK bývá NULL
└── ICZ (IDICZ → ICZ.IDICZ) ordinace (jen jedna, neřešit)
```
---
## UZIVATEL lékaři a uživatelé
| IDUZI | Příjmení | Jméno | Zkratka | Aktivní |
|---|---|---|---|---|
| 2 | Jourová | Jana | JOU | F (neaktivní) |
| 3 | Sestra | — | SES | F (neaktivní) |
| 4 | Buzalková | Michaela | MBU | T **lékařka, manželka** |
| 5 | Ševčíková | Ivana | ISE | T |
| 6 | Buzalka | Vladimír | VBU | T **uživatel (já)** |
---
## Ukázkové záznamy
### FAK ID=856 (poslední)
- Faktura č. 260026, pojišťovna **205 (ČPZP)**, prosinec 2025
- Vystavena: 2026-03-12, druh: konečná
- VYKONY: 2 528,99 Kč, KAPITACE: 0, CENA: **2 528,99 Kč**
- ZAPLACENO: NULL, PROPLACENO: NULL
FAKDET k FAK ID=856:
| ID | IDUZI | CENAVYK | CENALEC | CENAKAP |
|---|---|---|---|---|
| 1034 | 0 (systém) | 0 | 0 | 1 902,54 |
| 1035 | 4 (Buzalková) | 2 528,99 | 0 | 0 |
### FAK ID=855 (předposlední)
- Faktura č. 260026, pojišťovna **207 (OZP)**, prosinec 2025
- Vystavena: 2026-03-01, druh: konečná
- VYKONY: 85 Kč, KAPITACE: 0, CENA: **85 Kč**
- ZAPLACENO: NULL, PROPLACENO: NULL
FAKDET k FAK ID=855:
| ID | IDUZI | CENAVYK | CENALEC | CENAKAP |
|---|---|---|---|---|
| 1033 | 6 (Buzalka Vladimír) | 85 | 0 | 0 |
---
## PORTAL upřesnění (zjištěno 2026-03-28)
PORTAL **nesouvisí s fakturací**. Je to samostatná agenda evidence **registračních dávek** odeslaných pojišťovně.
### Co PORTAL skutečně je
- Medicus přes PORTAL odesílá pojišťovně dávky s přihlášením/odhlášením pacientů k lékaři (MUDr. Buzalková jako praktický lékař)
- Bez peněz čistě administrativní agenda registrací
- **IDFAK = NULL** u všech registračních podání (není vazba na fakturu)
### Klíčové sloupce
| Sloupec | Popis |
|---|---|
| `KDAVKA` | odeslaná dávka pevný formát (hlavička + řádky I = pacienti, RC, datum) |
| `DATA` | odpověď pojišťovny jako XML (`<statuscode>100</statuscode>` = OK) |
| `ID_PODANI` | ID přidělené pojišťovnou (např. `D01F260118593316.D01`) |
| `DAVKA_DISK` | číslo dávky v roce (sekvenční) |
| `DAVKA_ROK` | rok dávky |
| `DAVKA_DATUMOD/DO` | období registrací v dávce |
| `CHYBA` | F = bez chyby |
### Ukázka KDAVKA (hlavička dávky)
```
DP80093050000900202506 15 1 0 0.00180:6.2.46
H21109305001 179202506001
I 1Příjmení Jméno RC datum_registrace
I 2...
```
### Ukázka DATA (odpověď pojišťovny)
```xml
<ekomunikace timestamp="20260127 063105">
<session id="ff59b436-...">
<statusline>Přijetí registrací proběhlo úspěšně</statusline>
<statuscode>100</statuscode>
<idpodani>D01F260118593316.D01</idpodani>
</session>
</ekomunikace>
```
---
## PORTAL dva typy dávek
### Typ 1 Registrační dávka (DP80)
- KDAVKA začíná `DP80`
- **IDFAK = NULL** bez vazby na fakturu
- Bez peněz (`DAVKA_CASTKA` = NULL)
- Přihlašuje/odhlašuje pacienty k lékaři (MUDr. Buzalková)
- Řádky `I` = pacient (RC, datum registrace)
### Typ 2 Výkonová dávka (DP98)
- KDAVKA začíná `DP98`
- **IDFAK = ID faktury** napojeno na FAK
- Má peníze (`DAVKA_CASTKA`, např. 15 452,91 Kč)
- `STAV = 10` (registrační mají NULL)
- Obsahuje výkony vykázané pojišťovně za dané období
**Struktura výkonové dávky (KDAVKA):**
```
DP98... hlavička dávky (IČP, rok, měsíc, disk, počet případů, částka)
A ... ambulantní případ (RC pacienta, diagnóza)
V ... výkon (datum, kód výkonu, body)
Z ... ZULP (zvlášť účtovaný léčivý přípravek)
L ... lékový řádek (kód, množství, cena)
DP05... druhá část dávky (prevence / jiný typ)
P ... preventivní prohlídka
```
**Odpověď pojišťovny pro výkonovou dávku:**
```xml
<statusline>Přijetí faktury proběhlo úspěšně</statusline>
<statuscode>100</statuscode>
```
---
## Poznámky
- Faktury jsou vystavovány zvlášť pro každou pojišťovnu
- FAKDET má řádky zvlášť pro každého lékaře (IDUZI)
- IDUZI=0 v FAKDET = systémový záznam pro kapitaci
- Kapitace se v FAK.KAPITACE neukazuje (je 0), ale v FAKDET.CENAKAP ano nutno ověřit
- PORTAL = registrační dávky, nesouvisí s fakturací, IDFAK bývá NULL
## Kódování KDAVKA/FDAVKA důležité!
Dávkové soubory (KDAVKA, FDAVKA) jsou uloženy v **CP852** (DOS Latin-2, prahistorické kódování).
fdb je čte přes connection charset win1250 a vrací je jako Python `str` (špatně dekódované).
**Správný postup dekódování:**
```python
# fdb vrátil string dekódovaný jako win1250 musíme to zvrátit
raw_bytes = s.encode('cp1250', errors='replace')
spravny_text = raw_bytes.decode('cp852', errors='replace')
```
- **KDAVKA, FDAVKA** → cp852
- **DATA** (XML odpověď pojišťovny) → cp1250 (win1250), fdb dekóduje správně
+316
View File
@@ -0,0 +1,316 @@
# MedicusWithClaudeSelects SQL dotazy
## Registrovaní pacienti
Přesný select který Medicus používá pro záložku **Registrovaní** (zachycen přes FBScanner, dotaz č. 143).
### Počet registrovaných pacientů
```sql
SELECT COUNT(*) FROM KAR
WHERE (vyrazen = 'N')
AND EXISTS (
SELECT id FROM registr r
JOIN icp i ON r.idicp = i.idicp
WHERE r.idpac = kar.idpac
AND (r.datum <= '2026-03-20')
AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= '2026-03-20')
AND (r.priznak IN ('V','D','A'))
AND (i.icp = '09305001')
AND (i.odb = '001')
)
```
Vrátí: **1618 pacientů** (ověřit na Windows).
### Podmínky registrace vysvětlení
- `vyrazen = 'N'` pacient není vyřazen z kartotéky
- `r.datum <= dnes` registrace již začala
- `r.datum_zruseni IS NULL OR r.datum_zruseni >= dnes` registrace dosud platí
- `r.priznak IN ('V','D','A')` aktivní příznak (ne 'Z' = zrušen, ne 'N')
- `i.icp = '09305001'` IČP naší ordinace
- `i.odb = '001'` odbornost praktický lékař
### Skript pro Python
Viz `count_registrovani.py` v této složce spustit na Windows.
### Plný select Medicusu (seznam pacientů s metadaty)
```sql
SELECT
KAR.DATNAR,
KAR.IDPAC,
KAR.INFORMACE,
KAR.INFORMACE_COL,
KAR.JMENO,
KAR.POHLAVI,
GPP.POJ,
KAR.POZNAMKA,
KAR.PRIJMENI,
KAR.PRIJMENI_UP,
(SELECT DATUM_REGISTRACE FROM SP_GETREGDAT(kar.IDPAC)) AS REGDATUM,
KAR.REGISTROVAL,
(SELECT PRIZNAK FROM SP_GETREGDAT(kar.IDPAC)) AS REGPRIZNAK,
KAR.RODCIS,
KAR.ROZENA,
KAR.TITUL,
KAR.TITULZA,
KAR.TRVOBEC,
KAR.TRVPSC,
KAR.TRVULICE,
KAR.VYRAZEN
FROM KAR
LEFT JOIN GETPACPOJ(KAR.IDPAC, '2026-03-20') GPP ON GPP.IDPAC = KAR.IDPAC
WHERE (vyrazen = 'N')
AND EXISTS (
SELECT id FROM registr r
JOIN icp i ON r.idicp = i.idicp
WHERE r.idpac = kar.idpac
AND (r.datum <= '2026-03-20')
AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= '2026-03-20')
AND (r.priznak IN ('V','D','A'))
AND (i.icp = '09305001')
AND (i.odb = '001')
)
ORDER BY KAR.PRIJMENI_UP ASC, KAR.RODCIS ASC
```
Poznámka: `GETPACPOJ` a `SP_GETREGDAT` jsou uložené procedury Medicusu
fungují v kontextu Firebird připojení přes SYSDBA/masterkey.
---
## Panel pacienta hlavní UNION dotaz
Zachycen přes Firebird AuditTrace (`default_trace.log`) při otevření karty pacienta
(IDPAC=9733, datum 28.03.2026). Dotaz vrací **28 UNION částí** každá část má pevnou
strukturu sloupců: `ID, VAR1, VAR2, DATE1, DATE2, TIME1, INT1, TEXT1, NUM1, NUM2`.
Parametry při volání:
- `:IDPAC` ID pacienta (např. 9733)
- `:RODCIS` rodné číslo pacienta (např. '0308020152')
- `:DATUM` dnešní datum ve formátu YYYY-MM-DD (např. '2026-03-28')
- `:DATUM_CZ` dnešní datum ve formátu DD.MM.YYYY (např. '28.03.2026')
- `:ROK` aktuální rok (např. '2026')
### Mapování UNION části → položka panelu Medicusu
| ID (UNION část) | Tabulka | Položka v panelu Medicusu |
|------------------|--------------------|--------------------------------------------------|
| `BalickyPac` | BALICKYPAC | Balíčky pacienta (aktivní/budoucí) |
| `Dluh` | PLA / PLADET | Dluh (nezaplacené faktury po splatnosti) |
| `SouhlasPac` | HISTDOC / SOUHLASPACSABL | Souhlasy pacienta |
| `sCenaVykZUM` | DOKLADD / LECD | Cena výkonů v aktuálním roce (ZUM) |
| `Registrl` | KAR | Kdo registroval pacienta |
| `OseLekPrak` | KARUZIV_SEL | Ošetřující lékař (odb. 001/002) |
| `SledLek` | SLEDLEK | Sledující lékař (specialista) |
| `HistDoc` | HISTDOC | Historie dokumentů (posledních 10) |
| `LastSms` | SMS | Datum poslední odeslané SMS |
| `PozadLekar` | DOKLADH | Požadující lékař (EICZ) z posledního dokladu |
| `Prilohy` | FILES | Přílohy pacienta (posledních 10) |
| `Objednavky` | OBJOBJ | Objednávky od dnešního dne |
| `OseLek` | KARUZIV_SEL | Všichni lékaři přiřazení ke kartě |
| `PeProhlidky` | PREH / PREINIH | Preventivní a examinační prohlídky |
| `Medikace` | MEDIKACE | Aktuální medikace (platná k dnešku) |
| `NextDispenz` | DISPAC / DISSKU | Příští dispenzarizace (s termínem) |
| `Dispenz` | DISPAC / DISSKU | Dispenzarizace všechny záznamy |
| `Prohlidky` | PREPRI / PREINIH | Preventivní prohlídky záznamy |
| `NextOck` | OCKPRI / KLK | Příští očkování (plánované) |
| `LastVykon` | DOKLADD / DOKLADH | Poslední výkon (kód + datum) |
| `LastDekurs` | DEKURS | Poslední dekurz (datum) |
| `Karta` | KAR | Informace a poznámka z karty |
| `Saldo` | PACIENT_SALDO(SP) | Saldo pacienta (funkce) |
| `Anamneza` | ANAMNEZA | Anamnéza + krevní skupina |
| `Ockovani` | OCKZAZ | Očkovací záznamy (max 20, seskupeno po látce) |
| `NeschopenOd` | NES | Aktuální neschopenka (od do) |
| `Alergie` | ANAMNEZA | Alergie (z posledního záznamu anamnézy) |
| `Pojistovna` | ICP / ICZ | IDICP naší ordinace pro pojišťovnu pacienta |
### SQL dotaz (parametrizovaný)
```sql
SELECT
cast('BalickyPac' as varchar(11)) as ID, substring(cast(BPAC.KOD as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(BPAC.CENPASMO as VARCHAR(70)) from 1 for 30) as VAR2, cast(BPAC.DATUMOD as DATE) as DATE1, cast(BPAC.DATUMDO as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from BALICKYPAC BPAC left join SP_BALICKYPAC_PRIZNAK(BPAC.ID, ':DATUM_CZ') PRI on 1 = 1 where BPAC.IDPAC = :IDPAC and PRI.PRIZNAK in ('A', 'B')
UNION SELECT
cast('Dluh' as varchar(11)) as ID, substring(cast(P.MENA as VARCHAR(254)) from 1 for 250) 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast((SUM(P.CENA - P.SLEVAC) - (SUM(P.PLATBA) + SUM((COALESCE((select SUM(case ZD.TYP when 'R' then ZD.CELKEM else -ZD.CELKEM end) from PLADET ZD where ZD.IDPLA = P.IDPLA and (ZD.TYP <> P.DOKLADTYP) and (ZD.TYP <> '|') and ((ZD.CENA < 0) or (ZD.TYP = 'R'))), 0))))) as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 FROM PLA P WHERE (P.IDPAC = :IDPAC) AND (P.DOKLADTYP = 'F') AND (P.STORNO IS NULL) AND (P.NENISALDO = 'F') AND ((P.SPLATNOST IS NULL) OR (P.SPLATNOST < ':DATUM')) AND (P.VALID = 'F') GROUP BY P.MENA
UNION SELECT
cast('SouhlasPac' as varchar(11)) as ID, substring(cast(case when S.NAZEV is null then case when H.TYP = 'ZSOUPOS' then 'Souhlas/Nesouhlas s poskytnutím zdravotních služeb nezletilému' when H.TYP = 'ZSOUPOZ' then 'Souhlas zákonného zástupce nezletilého pacienta staršího 15ti let' when H.TYP = 'ZSOUPO2' then 'Nesouhlas s poskytnutím zdravotních služeb - povinné očkování' when H.TYP = 'ZPOSIN2' then 'Určení osoby oprávněné dle zákona o zdravotních službách' when H.TYP = 'OdmPece' then 'Prohlášení o odmítnutí zdravotní péče pacientem - Negativní revers' end else S.NAZEV end as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(H.DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from HISTDOC H left join SOUHLASPACSABL S on H.IDSOUHLASPACSABL = S.ID where H.TYP in ('IndSou', 'ZSOUPOS', 'ZSOUPOZ', 'ZSOUPO2', 'ZPOSIN2', 'OdmPece') and H.IDPACI = :IDPAC
UNION SELECT
cast('sCenaVykZUM' 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(coalesce(sum(cenabod),0) + coalesce(sum(cenamat),0) as NUMERIC(15,2)) as NUM1, cast((select coalesce(sum(cena),0) from LECD d where d.RODCIS = ':RODCIS' and extract(year from d.DATOSE) = ':ROK' and ((d.KAT is null) or (d.KAT <> 'N')) and exists (select h.IDLEC from LECH h where h.IDLEC = d.IDLEC and h.POJ = '207' and h.ICZ in ('09305001'))) as NUMERIC(15,2)) as NUM2 from DOKLADD d where d.RODCIS = ':RODCIS' and extract(year from d.DATOSE) = ':ROK' and ((d.KAT is null) or (d.KAT <> 'N' and d.KAT <> 'K' and d.KAT <> 'A')) and exists (select h.IDHLAV from DOKLADH h where h.IDHLAV = d.IDHLAV and h.POJ = '207' and h.ICZ in ('09305001'))
UNION SELECT
cast('Registrl' as varchar(11)) as ID, substring(cast(REGISTROVAL as VARCHAR(254)) from 1 for 250) 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from KAR where IDPAC = :IDPAC
UNION SELECT
cast('OseLekPrak' as varchar(11)) as ID, substring(cast(F_CONCAT(PRIJMENI, F_CONCAT(JMENO, TITUL, ', '), ' ') as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(TITUL2 as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(CAST(ODBORN as INTEGER) as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 FROM KARUZIV_SEL(:IDPAC, 'T') WHERE ODBORN in ('001', '002')
UNION SELECT
cast('SledLek' as varchar(11)) as ID, substring(cast(KOD as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(LEK as VARCHAR(70)) from 1 for 30) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from SLEDLEK where IDPAC = :IDPAC and DATUM <= ':DATUM'
UNION SELECT
first 10 cast('HistDoc' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from HISTDOC where IDPACI = :IDPAC and STAV is NULL and IDZARPR = 2 and IDODDPR = 2 and IDPRACPR = 2
UNION SELECT
first 1 cast('LastSms' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(MAX(SENDTIME) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from SMS where PACID = :IDPAC and SENDTIME is not NULL and not(STATUS in (100,1000))
UNION SELECT
first 1 cast('PozadLekar' as varchar(11)) as ID, substring(cast(H.EICZ as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(H.EODZ as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DOKLADH H where H.IDHLAV = (select first 1 I.IDHLAV from DOKLADH I where I.RODCIS = ':RODCIS' and I.EICZ is not NULL order by I.IDHLAV desc)
UNION SELECT
first 10 cast('Prilohy' as varchar(11)) as ID, substring(cast(FILENAME as VARCHAR(254)) from 1 for 250) 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from FILES where IDPAC = :IDPAC
UNION SELECT
first 10 cast('Objednavky' as varchar(11)) as ID, substring(cast(F_CONCAT(U.PRIJMENI, F_CONCAT(U.JMENO, U.TITUL, ', '), ' ') as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(PRAC as VARCHAR(70)) from 1 for 30) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(CAS as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from OBJOBJ O join PRACOVISTE P on (P.ID = O.IDPRAC) join UZIVATEL U on (U.IDUZI = O.IDUZI) where IDPAC = :IDPAC and DATUM >= ':DATUM_CZ'
UNION SELECT
cast('OseLek' as varchar(11)) as ID, substring(cast(F_CONCAT(PRIJMENI, F_CONCAT(JMENO, TITUL, ', '), ' ') as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(TITUL2 as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 FROM KARUZIV_SEL(:IDPAC, 'T')
UNION SELECT
cast('PeProhlidky' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUM as DATE) as DATE1, cast(TERMIN as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from PREH join PREINIH on (PREH.IDPREINI = PREINIH.IDPREINI) where IDPAC = :IDPAC
UNION SELECT
cast('Medikace' as varchar(11)) as ID, substring(cast(NAZ as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(PLATI_OD as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from MEDIKACE where IDPAC = :IDPAC and PLATI_OD <= ':DATUM_CZ' and (PLATI_DO >= ':DATUM_CZ' or PLATI_DO is NULL)
UNION SELECT
cast('NextDispenz' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(SKUPINA as VARCHAR(70)) from 1 for 30) as VAR2, cast(PRISTI as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DISPAC join DISSKU on (DISSKU.IDDIS = DISPAC.IDDIS) where IDPAC = :IDPAC and PRISTI is not NULL
UNION SELECT
cast('Dispenz' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(SKUPINA as VARCHAR(70)) from 1 for 30) as VAR2, cast(DATZAR as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DISPAC join DISSKU on (DISSKU.IDDIS = DISPAC.IDDIS) where IDPAC = :IDPAC
UNION SELECT
cast('Prohlidky' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from PREPRI join PREINIH on (PREPRI.IDPREINI = PREINIH.IDPREINI) where IDPAC = :IDPAC and datum is not null
UNION SELECT
cast('NextOck' as varchar(11)) as ID, substring(cast(coalesce(NAZ,ZKRATKA) as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUMD as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from OCKPRI o left join KLK k on o.ZKRATKA = k.KOD where IDPAC = :IDPAC
UNION SELECT
first 1 cast('LastVykon' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, substring(cast(D.KOD as VARCHAR(70)) from 1 for 30) as VAR2, cast(D.DATOSE as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DOKLADD D where D.ID = (select first 1 dd.id from dokladd dd join dokladh dh on (dh.idhlav = dd.idhlav) where dd.rodcis = ':RODCIS' and (dh.hodb = '001' or dh.hodb is null) order by dd.datose desc)
UNION SELECT
first 1 cast('LastDekurs' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(MAX(DATUM) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DEKURS where IDPAC = :IDPAC and (IDPRAC = 2 or IDPRAC = -1)
UNION SELECT
first 1 cast('Karta' as varchar(11)) as ID, substring(cast(INFORMACE as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(CIZINEC as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(INFORMACE_COL as INTEGER) as INT1, POZNAMKA as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from KAR where IDPAC = :IDPAC
UNION SELECT
first 1 cast('Saldo' 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(SALDO as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from PACIENT_SALDO(:IDPAC, 1, 0, 0)
UNION SELECT
first 1 cast('Anamneza' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, substring(cast(KREVSKUP as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, ANAMNEZA as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from ANAMNEZA where ID = (select first 1 ID from ANAMNEZA where IDPAC = :IDPAC order by DATUM DESC, ID desc)
UNION SELECT
first 20 cast('Ockovani' as varchar(11)) as ID, substring(cast(ockzaz.LATKA as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(ockzaz.ZKRATKA as VARCHAR(70)) from 1 for 30) as VAR2, cast(max(ockzaz.DATUM) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from ockzaz where ockzaz.idpac = :IDPAC group by ockzaz.ZKRATKA, ockzaz.LATKA
UNION SELECT
first 1 cast('NeschopenOd' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(MAX(ZACNES) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from NES where (IDPAC = :IDPAC) and (ZACNES <= ':DATUM_CZ') and ((KONNES is NULL) or (KONNES > ':DATUM_CZ')) and (STORNO = 'F')
UNION SELECT
first 1 cast('Alergie' 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(NULL as INTEGER) as INT1, ALERGIE as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from ANAMNEZA where IDPAC = :IDPAC and ID = (select first 1 ID from ANAMNEZA where IDPAC = :IDPAC and DATUM <= ':DATUM_CZ' order by DATUM desc, ID desc)
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,83 @@
SELECT
cast('BalickyPac' as varchar(11)) as ID, substring(cast(BPAC.KOD as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(BPAC.CENPASMO as VARCHAR(70)) from 1 for 30) as VAR2, cast(BPAC.DATUMOD as DATE) as DATE1, cast(BPAC.DATUMDO as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from BALICKYPAC BPAC left join SP_BALICKYPAC_PRIZNAK(BPAC.ID, ':DATUM_CZ') PRI on 1 = 1 where BPAC.IDPAC = :IDPAC and PRI.PRIZNAK in ('A', 'B')
UNION SELECT
cast('Dluh' as varchar(11)) as ID, substring(cast(P.MENA as VARCHAR(254)) from 1 for 250) 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast((SUM(P.CENA - P.SLEVAC) - (SUM(P.PLATBA) + SUM((COALESCE((select SUM(case ZD.TYP when 'R' then ZD.CELKEM else -ZD.CELKEM end) from PLADET ZD where ZD.IDPLA = P.IDPLA and (ZD.TYP <> P.DOKLADTYP) and (ZD.TYP <> '|') and ((ZD.CENA < 0) or (ZD.TYP = 'R'))), 0))))) as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 FROM PLA P WHERE (P.IDPAC = :IDPAC) AND (P.DOKLADTYP = 'F') AND (P.STORNO IS NULL) AND (P.NENISALDO = 'F') AND ((P.SPLATNOST IS NULL) OR (P.SPLATNOST < ':DATUM')) AND (P.VALID = 'F') GROUP BY P.MENA
UNION SELECT
cast('SouhlasPac' as varchar(11)) as ID, substring(cast(case when S.NAZEV is null then case when H.TYP = 'ZSOUPOS' then 'Souhlas/Nesouhlas s poskytnutím zdravotních služeb nezletilému' when H.TYP = 'ZSOUPOZ' then 'Souhlas zákonného zástupce nezletilého pacienta staršího 15ti let' when H.TYP = 'ZSOUPO2' then 'Nesouhlas s poskytnutím zdravotních služeb - povinné oèkování' when H.TYP = 'ZPOSIN2' then 'Urèení osoby oprávnìné dle zákona o zdravotních službách' when H.TYP = 'OdmPece' then 'Prohlášení o odmítnutí zdravotní péèe pacientem - Negativní revers' end else S.NAZEV end as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(H.DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from HISTDOC H left join SOUHLASPACSABL S on H.IDSOUHLASPACSABL = S.ID where H.TYP in ('IndSou', 'ZSOUPOS', 'ZSOUPOZ', 'ZSOUPO2', 'ZPOSIN2', 'OdmPece') and H.IDPACI = :IDPAC
UNION SELECT
cast('sCenaVykZUM' 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(coalesce(sum(cenabod),0) + coalesce(sum(cenamat),0) as NUMERIC(15,2)) as NUM1, cast((select coalesce(sum(cena),0) from LECD d where d.RODCIS = ':RODCIS' and extract(year from d.DATOSE) = ':ROK' and ((d.KAT is null) or (d.KAT <> 'N')) and exists (select h.IDLEC from LECH h where h.IDLEC = d.IDLEC and h.POJ = '207' and h.ICZ in ('09305001'))) as NUMERIC(15,2)) as NUM2 from DOKLADD d where d.RODCIS = ':RODCIS' and extract(year from d.DATOSE) = ':ROK' and ((d.KAT is null) or (d.KAT <> 'N' and d.KAT <> 'K' and d.KAT <> 'A')) and exists (select h.IDHLAV from DOKLADH h where h.IDHLAV = d.IDHLAV and h.POJ = '207' and h.ICZ in ('09305001'))
UNION SELECT
cast('Registrl' as varchar(11)) as ID, substring(cast(REGISTROVAL as VARCHAR(254)) from 1 for 250) 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from KAR where IDPAC = :IDPAC
UNION SELECT
cast('OseLekPrak' as varchar(11)) as ID, substring(cast(F_CONCAT(PRIJMENI, F_CONCAT(JMENO, TITUL, ', '), ' ') as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(TITUL2 as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(CAST(ODBORN as INTEGER) as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 FROM KARUZIV_SEL(9733, 'T') WHERE ODBORN in ('001', '002')
UNION SELECT
cast('SledLek' as varchar(11)) as ID, substring(cast(KOD as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(LEK as VARCHAR(70)) from 1 for 30) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from SLEDLEK where IDPAC = :IDPAC and DATUM <= ':DATUM'
UNION SELECT
first 10 cast('HistDoc' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from HISTDOC where IDPACI = :IDPAC and STAV is NULL and IDZARPR = 2 and IDODDPR = 2 and IDPRACPR = 2
UNION SELECT
first 1 cast('LastSms' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(MAX(SENDTIME) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from SMS where PACID = :IDPAC and SENDTIME is not NULL and not(STATUS in (100,1000))
UNION SELECT
first 1 cast('PozadLekar' as varchar(11)) as ID, substring(cast(H.EICZ as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(H.EODZ as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DOKLADH H where H.IDHLAV = (select first 1 I.IDHLAV from DOKLADH I where I.RODCIS = ':RODCIS' and I.EICZ is not NULL order by I.IDHLAV desc)
UNION SELECT
first 10 cast('Prilohy' as varchar(11)) as ID, substring(cast(FILENAME as VARCHAR(254)) from 1 for 250) 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from FILES where IDPAC = :IDPAC
UNION SELECT
first 10 cast('Objednavky' as varchar(11)) as ID, substring(cast(F_CONCAT(U.PRIJMENI, F_CONCAT(U.JMENO, U.TITUL, ', '), ' ') as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(PRAC as VARCHAR(70)) from 1 for 30) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(CAS as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from OBJOBJ O join PRACOVISTE P on (P.ID = O.IDPRAC) join UZIVATEL U on (U.IDUZI = O.IDUZI) where IDPAC = :IDPAC and DATUM >= ':DATUM_CZ'
UNION SELECT
cast('OseLek' as varchar(11)) as ID, substring(cast(F_CONCAT(PRIJMENI, F_CONCAT(JMENO, TITUL, ', '), ' ') as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(TITUL2 as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 FROM KARUZIV_SEL(9733, 'T')
UNION SELECT
cast('PeProhlidky' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUM as DATE) as DATE1, cast(TERMIN as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from PREH join PREINIH on (PREH.IDPREINI = PREINIH.IDPREINI) where IDPAC = :IDPAC
UNION SELECT
cast('Medikace' as varchar(11)) as ID, substring(cast(NAZ as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(PLATI_OD as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from MEDIKACE where IDPAC = :IDPAC and PLATI_OD <= ':DATUM_CZ' and (PLATI_DO >= ':DATUM_CZ' or PLATI_DO is NULL)
UNION SELECT
cast('NextDispenz' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(SKUPINA as VARCHAR(70)) from 1 for 30) as VAR2, cast(PRISTI as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DISPAC join DISSKU on (DISSKU.IDDIS = DISPAC.IDDIS) where IDPAC = :IDPAC and PRISTI is not NULL
UNION SELECT
cast('Dispenz' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(SKUPINA as VARCHAR(70)) from 1 for 30) as VAR2, cast(DATZAR as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DISPAC join DISSKU on (DISSKU.IDDIS = DISPAC.IDDIS) where IDPAC = :IDPAC
UNION SELECT
cast('Prohlidky' as varchar(11)) as ID, substring(cast(NAZEV as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUM as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from PREPRI join PREINIH on (PREPRI.IDPREINI = PREINIH.IDPREINI) where IDPAC = :IDPAC and datum is not null
UNION SELECT
cast('NextOck' as varchar(11)) as ID, substring(cast(coalesce(NAZ,ZKRATKA) as VARCHAR(254)) from 1 for 250) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(DATUMD as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from OCKPRI o left join KLK k on o.ZKRATKA = k.KOD where IDPAC = :IDPAC
UNION SELECT
first 1 cast('LastVykon' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, substring(cast(D.KOD as VARCHAR(70)) from 1 for 30) as VAR2, cast(D.DATOSE as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DOKLADD D where D.ID = (select first 1 dd.id from dokladd dd join dokladh dh on (dh.idhlav = dd.idhlav) where dd.rodcis = ':RODCIS' and (dh.hodb = '001' or dh.hodb is null) order by dd.datose desc)
UNION SELECT
first 1 cast('LastDekurs' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(MAX(DATUM) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from DEKURS where IDPAC = :IDPAC and (IDPRAC = 2 or IDPRAC = -1)
UNION SELECT
first 1 cast('Karta' as varchar(11)) as ID, substring(cast(INFORMACE as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(CIZINEC as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(INFORMACE_COL as INTEGER) as INT1, POZNAMKA as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from KAR where IDPAC = :IDPAC
UNION SELECT
first 1 cast('Saldo' 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(NULL as INTEGER) as INT1, NULL as TEXT1, cast(SALDO as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from PACIENT_SALDO(9733, 1, 0, 0)
UNION SELECT
first 1 cast('Anamneza' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, substring(cast(KREVSKUP as VARCHAR(70)) from 1 for 30) as VAR2, cast(NULL as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, ANAMNEZA as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from ANAMNEZA where ID = (select first 1 ID from ANAMNEZA where IDPAC=9733 order by DATUM DESC, ID desc)
UNION SELECT
first 20 cast('Ockovani' as varchar(11)) as ID, substring(cast(ockzaz.LATKA as VARCHAR(254)) from 1 for 250) as VAR1, substring(cast(ockzaz.ZKRATKA as VARCHAR(70)) from 1 for 30) as VAR2, cast(max(ockzaz.DATUM) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from ockzaz where ockzaz.idpac = :IDPAC group by ockzaz.ZKRATKA, ockzaz.LATKA
UNION SELECT
first 1 cast('NeschopenOd' as varchar(11)) as ID, cast(NULL as VARCHAR(254)) as VAR1, cast(NULL as VARCHAR(70)) as VAR2, cast(MAX(ZACNES) as DATE) as DATE1, cast(NULL as DATE) as DATE2, cast(NULL as TIMESTAMP) as TIME1, cast(NULL as INTEGER) as INT1, NULL as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from NES where (IDPAC = :IDPAC) and (ZACNES <= ':DATUM_CZ') and ((KONNES is NULL) or (KONNES > ':DATUM_CZ')) and (STORNO = 'F')
UNION SELECT
first 1 cast('Alergie' 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(NULL as INTEGER) as INT1, ALERGIE as TEXT1, cast(NULL as NUMERIC(15,2)) as NUM1, cast(NULL as NUMERIC(15,2)) as NUM2 from ANAMNEZA where IDPAC = :IDPAC and ID = (select first 1 ID from ANAMNEZA where IDPAC = :IDPAC and DATUM <= ':DATUM_CZ' order by DATUM desc, ID desc)
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'
@@ -0,0 +1,27 @@
import fdb, datetime
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250')
cur = conn.cursor()
dnes = datetime.date.today().isoformat()
cur.execute("""
SELECT COUNT(*) FROM KAR
WHERE (vyrazen = 'N')
AND EXISTS (
SELECT id FROM registr r
JOIN icp i ON r.idicp = i.idicp
WHERE r.idpac = kar.idpac
AND (r.datum <= ?)
AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= ?)
AND (r.priznak IN ('V','D','A'))
AND (i.icp = '09305001')
AND (i.odb = '001')
)
""", (dnes, dnes))
pocet = cur.fetchone()[0]
print(f'Registrovaných pacientů: {pocet}')
conn.close()
@@ -0,0 +1,78 @@
"""db_bridge_vm.py VM strana souborového mostu k Medicusu.
Použití z Linuxu:
from db_bridge_vm import query
rows, columns = query("SELECT COUNT(*) FROM KAR")
print(columns, rows)
"""
import json, time, os, uuid
BRIDGE_DIR = os.path.dirname(os.path.abspath(__file__))
REQUEST = os.path.join(BRIDGE_DIR, 'query_request.json')
RESPONSE = os.path.join(BRIDGE_DIR, 'query_response.json')
TIMEOUT_SEC = 30
POLL_SEC = 0.3
def query(sql, params=None, timeout=TIMEOUT_SEC):
"""Pošle SQL dotaz přes souborový most a vrátí (rows, columns).
Raises:
TimeoutError watchdog neodpověděl do timeout sekund
RuntimeError Firebird vrátil chybu
"""
# Smaž případnou starou response
if os.path.exists(RESPONSE):
os.remove(RESPONSE)
req_id = uuid.uuid4().hex
req = {'id': req_id, 'sql': sql, 'params': params or []}
with open(REQUEST, 'w', encoding='utf-8') as f:
json.dump(req, f, ensure_ascii=False)
# Čekej na odpověď
waited = 0.0
while waited < timeout:
time.sleep(POLL_SEC)
waited += POLL_SEC
if os.path.exists(RESPONSE):
with open(RESPONSE, 'r', encoding='utf-8') as f:
resp = json.load(f)
os.remove(RESPONSE)
if resp.get('status') == 'error':
raise RuntimeError(f"DB chyba: {resp.get('error')}")
return resp.get('rows', []), resp.get('columns', [])
# Timeout smaž request aby watchdog nezpracoval zastaralý dotaz
if os.path.exists(REQUEST):
os.remove(REQUEST)
raise TimeoutError(f'Watchdog neodpověděl do {timeout}s běží db_bridge_windows.py?')
def query_print(sql, params=None):
"""Spustí dotaz a vypíše výsledek přehledně."""
rows, cols = query(sql, params)
if not cols:
print('(žádné sloupce)')
return rows, cols
col_w = [max(len(str(c)), max((len(str(r[i])) for r in rows), default=0))
for i, c in enumerate(cols)]
sep = '+' + '+'.join('-' * (w + 2) for w in col_w) + '+'
fmt = '|' + '|'.join(f' {{:<{w}}} ' for w in col_w) + '|'
print(sep)
print(fmt.format(*cols))
print(sep)
for row in rows:
print(fmt.format(*[str(v) if v is not None else 'NULL' for v in row]))
print(sep)
print(f'{len(rows)} řádků')
return rows, cols
if __name__ == '__main__':
# Rychlý test
print('Testuji spojení...')
rows, cols = query('SELECT COUNT(*) AS POCET FROM KAR')
print(f'OK pacientů v KAR: {rows[0][0]}')
@@ -0,0 +1,90 @@
"""db_bridge_windows.py Windows watchdog pro dotazy z Linux VM.
Spusť jednou na Windows:
python db_bridge_windows.py
Skript sleduje soubor query_request.json ve stejné složce.
Jakmile ho najde, spustí SQL dotaz proti Medicusu a zapíše výsledek
do query_response.json. Pak čeká na další dotaz.
Ukonči: Ctrl+C
"""
import fdb, json, time, os, traceback, datetime
# ── Konfigurace ───────────────────────────────────────────────────────────────
DSN = r'localhost:c:\medicus 3\data\medicus.fdb'
USER = 'SYSDBA'
PASSWORD = 'masterkey'
CHARSET = 'win1250'
BRIDGE_DIR = os.path.dirname(os.path.abspath(__file__))
REQUEST = os.path.join(BRIDGE_DIR, 'query_request.json')
RESPONSE = os.path.join(BRIDGE_DIR, 'query_response.json')
POLL_SEC = 0.5
# ── Pomocné funkce ────────────────────────────────────────────────────────────
def serialize(val):
"""Převede Python hodnoty na JSON-serializovatelné typy."""
if isinstance(val, (datetime.date, datetime.datetime)):
return val.isoformat()
if isinstance(val, datetime.time):
return val.isoformat()
if isinstance(val, bytes):
return f'<bytes len={len(val)}>'
return val
def run_query(sql, params=None):
conn = fdb.connect(dsn=DSN, user=USER, password=PASSWORD, charset=CHARSET)
try:
cur = conn.cursor()
cur.execute(sql, params or [])
columns = [d[0] for d in cur.description] if cur.description else []
rows = [[serialize(v) for v in row] for row in cur.fetchall()]
return {'status': 'ok', 'columns': columns, 'rows': rows, 'error': None}
except Exception as e:
return {'status': 'error', 'columns': [], 'rows': [], 'error': str(e)}
finally:
conn.close()
# ── Hlavní smyčka ─────────────────────────────────────────────────────────────
print(f'DB Bridge spuštěn. Sleduju: {REQUEST}')
print('Ukončení: Ctrl+C\n')
while True:
try:
if os.path.exists(REQUEST):
print(f'[{datetime.datetime.now().strftime("%H:%M:%S")}] Přijat dotaz...')
with open(REQUEST, 'r', encoding='utf-8') as f:
req = json.load(f)
os.remove(REQUEST)
sql = req.get('sql', '')
params = req.get('params', [])
req_id = req.get('id', '')
result = run_query(sql, params)
result['id'] = req_id
result['sql'] = sql
with open(RESPONSE, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
if result['status'] == 'ok':
print(f' → OK, {len(result["rows"])} řádků')
else:
print(f' → CHYBA: {result["error"]}')
time.sleep(POLL_SEC)
except KeyboardInterrupt:
print('\nDB Bridge ukončen.')
break
except Exception as e:
print(f'Neočekávaná chyba: {e}')
traceback.print_exc()
time.sleep(2)
@@ -0,0 +1,30 @@
"""get_kar_sortby_idlist.py přečte definici stored procedure KAR_SORTBY_IDLIST z Firebirdu.
Spustit na Windows.
"""
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250')
cur = conn.cursor()
cur.execute("""
SELECT RDB$PROCEDURE_SOURCE FROM RDB$PROCEDURES
WHERE RDB$PROCEDURE_NAME = 'KAR_SORTBY_IDLIST'
""")
row = cur.fetchone()
if row and row[0]:
print(row[0])
else:
# Možná je to funkce (FUNCTION), ne procedure
cur.execute("""
SELECT RDB$FUNCTION_SOURCE FROM RDB$FUNCTIONS
WHERE RDB$FUNCTION_NAME = 'KAR_SORTBY_IDLIST'
""")
row = cur.fetchone()
if row and row[0]:
print(row[0])
else:
print("Nenalezeno ani jako PROCEDURE ani jako FUNCTION.")
conn.close()
File diff suppressed because one or more lines are too long
@@ -0,0 +1,106 @@
"""
precti_trace.py přehledné SQL dotazy z Firebird audit trace logu
Čte od konce (nejnovější nahoře).
Použití:
python precti_trace.py # posledních 50 dotazů
python precti_trace.py 100 # posledních 100
python precti_trace.py 50 SELECT # filtr jen SELECT dotazy
"""
import re, sys, io
LOG_PATH = r'C:\Program Files\Firebird\Firebird_2_5_CGM\default_trace.log'
SEPARATOR = '-' * 80
TS_RE = re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+)\s+\([\w:]+\)\s+(\w+)')
ATT_RE = re.compile(r'\(ATT_\d+,\s*([\w]+):')
SQL_S_RE = re.compile(r'^-{70,}$')
SQL_E_RE = re.compile(r'^\^{70,}$')
# ── Parametry ──────────────────────────────────────────────────────────────
limit = int(sys.argv[1]) if len(sys.argv) > 1 else 50
filtr = sys.argv[2].upper() if len(sys.argv) > 2 else ''
# ── Čtení logu ─────────────────────────────────────────────────────────────
with open(LOG_PATH, 'r', encoding='cp1252', errors='replace') as f:
lines = f.readlines()
# ── Rozděl log na bloky podle timestamp řádků ──────────────────────────────
# Každý blok = od jednoho timestamp řádku do dalšího
blocks = [] # (start_line_idx, ts, event, block_lines[])
i = 0
n = len(lines)
while i < n:
m = TS_RE.match(lines[i].rstrip())
if m:
ts = m.group(1)
event = m.group(2)
start = i
i += 1
block = []
while i < n and not TS_RE.match(lines[i].rstrip()):
block.append(lines[i].rstrip())
i += 1
blocks.append((ts, event, block))
else:
i += 1
# ── Extrahuj SQL z PREPARE_STATEMENT bloků ─────────────────────────────────
results = []
for ts, event, block in blocks:
if event != 'PREPARE_STATEMENT':
continue
# Uživatel
user = '?'
for line in block:
att = ATT_RE.search(line)
if att:
user = att.group(1)
break
# SQL mezi --- a ^^^
sql_lines = []
plan_lines = []
in_sql = False
after_sql = False
for line in block:
if SQL_S_RE.match(line):
in_sql = True
after_sql = False
continue
if SQL_E_RE.match(line):
in_sql = False
after_sql = True
continue
if in_sql:
sql_lines.append(line)
elif after_sql and line.startswith('PLAN'):
plan_lines.append(line)
sql = '\n'.join(sql_lines).strip()
plan = ' '.join(plan_lines).strip()
if sql:
results.append((ts, user, sql, plan))
# ── Výstup nejnovější nahoře ─────────────────────────────────────────────
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
filtered = [r for r in results if filtr in r[2].upper()] if filtr else results
filtered = list(reversed(filtered))[:limit]
print(f'Firebird trace posledních {len(filtered)} dotazu'
+ (f' [filtr: {filtr}]' if filtr else ''))
print(SEPARATOR)
for ts, user, sql, plan in filtered:
print(f'[{ts}] {user}')
print(sql)
if plan:
print(f' --> {plan}')
print(SEPARATOR)
@@ -0,0 +1,74 @@
{
"status": "ok",
"columns": [
"ID",
"DATUM",
"CAS",
"UZIVATEL",
"IDPAC",
"TABULKA",
"IDREC",
"AKCE",
"DETAIL"
],
"rows": [
[
2882053,
"2026-03-20",
"17:43:50.523000",
"VBU ",
3234,
10000,
3234,
"V",
null
],
[
2882052,
"2026-03-20",
"17:43:46.435000",
"VBU ",
3234,
10000,
3234,
"V",
null
],
[
2882051,
"2026-03-20",
"17:24:19.777000",
"VBU ",
4757,
10000,
4757,
"V",
null
],
[
2882050,
"2026-03-20",
"17:24:15.866000",
"VBU ",
3234,
10000,
3234,
"V",
null
],
[
2882049,
"2026-03-20",
"07:11:41.434000",
"VBU ",
4757,
10000,
4757,
"V",
null
]
],
"error": null,
"id": "11859d529a784bedb653030ba60131de",
"sql": "SELECT FIRST 5 * FROM LOG ORDER BY ID DESC"
}
@@ -0,0 +1,144 @@
# trace_report.py Excel report z Firebird audit trace logu
# Ulozi do u:\Dropbox\!!!Days\Downloads Z230\, smaze predchozi verzi.
import re, sys, io, os, openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
from datetime import datetime
LOG_PATH = r'C:\Program Files\Firebird\Firebird_2_5_CGM\default_trace.log'
OUTPUT_DIR = r'u:\Dropbox\!!!Days\Downloads Z230'
TS_RE = re.compile(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+)\s+\([\w:]+\)\s+(\w+)')
ATT_RE = re.compile(r'\(ATT_\d+,\s*([\w]+):')
SQL_S_RE = re.compile(r'^-{70,}$')
SQL_E_RE = re.compile(r'^\^{70,}$')
HEADER_FILL = PatternFill('solid', fgColor='2F5496')
HEADER_FONT = Font(bold=True, color='FFFFFF')
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
WRAP_TOP = Alignment(wrap_text=True, vertical='top')
# ── Smazání předchozích verzí ───────────────────────────────────────────────
for f in os.listdir(OUTPUT_DIR):
if f.endswith('_trace.xlsx'):
os.remove(os.path.join(OUTPUT_DIR, f))
# ── Výstupní soubor ─────────────────────────────────────────────────────────
now = datetime.now()
filename = now.strftime('%Y-%m-%d_%H-%M-%S') + '_trace.xlsx'
out_path = os.path.join(OUTPUT_DIR, filename)
# ── Čtení logu ─────────────────────────────────────────────────────────────
with open(LOG_PATH, 'r', encoding='cp1252', errors='replace') as f:
lines = f.readlines()
# ── Rozděl na bloky ─────────────────────────────────────────────────────────
blocks = []
i, n = 0, len(lines)
while i < n:
m = TS_RE.match(lines[i].rstrip())
if m:
ts, event = m.group(1), m.group(2)
i += 1
block = []
while i < n and not TS_RE.match(lines[i].rstrip()):
block.append(lines[i].rstrip())
i += 1
blocks.append((ts, event, block))
else:
i += 1
# ── Extrahuj SQL z PREPARE_STATEMENT bloků ─────────────────────────────────
results = []
for ts, event, block in blocks:
if event != 'PREPARE_STATEMENT':
continue
user = '?'
for line in block:
att = ATT_RE.search(line)
if att:
user = att.group(1)
break
sql_lines, plan_lines = [], []
in_sql, after_sql = False, False
for line in block:
if SQL_S_RE.match(line):
in_sql, after_sql = True, False
continue
if SQL_E_RE.match(line):
in_sql, after_sql = False, True
continue
if in_sql:
sql_lines.append(line)
elif after_sql and line.startswith('PLAN'):
plan_lines.append(line)
sql = '\n'.join(sql_lines).strip()
plan = ' '.join(plan_lines).strip()
if sql:
# Detekuj typ dotazu
first = sql.lstrip().upper()
if first.startswith('SELECT'):
typ = 'SELECT'
elif first.startswith('INSERT'):
typ = 'INSERT'
elif first.startswith('UPDATE'):
typ = 'UPDATE'
elif first.startswith('DELETE'):
typ = 'DELETE'
elif first.startswith('EXECUTE'):
typ = 'EXECUTE'
else:
typ = 'OTHER'
results.append((ts, user, typ, sql, plan))
# Nejnovější nahoře
results = list(reversed(results))
# ── Excel ───────────────────────────────────────────────────────────────────
wb = openpyxl.Workbook()
ws = wb.active
ws.title = 'TRACE'
cols = ['CAS', 'UZIVATEL', 'TYP', 'SQL', 'PLAN']
ws.append(cols)
for i, (ts, user, typ, sql, plan) in enumerate(results, start=2):
ws.append([ts, user, typ, sql, plan])
if i % 2 == 0:
for cell in ws[i]:
cell.fill = ZEBRA_FILL
for cell in ws[i]:
cell.alignment = WRAP_TOP
# Záhlaví
for cell in ws[1]:
cell.fill = HEADER_FILL
cell.font = HEADER_FONT
cell.alignment = Alignment(horizontal='center')
# Šířky sloupců
for col, width in zip(['A','B','C','D','E'], [22, 12, 10, 80, 60]):
ws.column_dimensions[col].width = width
# Výška řádků SQL může být víceřádkový
for row in ws.iter_rows(min_row=2):
lines_count = max(row[3].value.count('\n') + 1 if row[3].value else 1, 1)
ws.row_dimensions[row[0].row].height = min(lines_count * 15, 120)
ws.freeze_panes = 'A2'
# Autofiltr
ws.auto_filter.ref = f'A1:E{len(results)+1}'
wb.save(out_path)
sys.stdout.buffer.write(f'Ulozeno: {out_path}\n'.encode('utf-8'))
sys.stdout.buffer.write(f'Dotazu: {len(results)}\n'.encode('utf-8'))
@@ -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,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Find the latest Medicus backup (.fbk or .zip) in the given folder.
If the newest backup is a ZIP, extract the .fbk into Z:\Medicus 3\restore.
Before extraction, the restore folder is fully cleared.
Prints the full path to the resulting .fbk file (for batch usage).
"""
import re
import zipfile
import shutil
from pathlib import Path
from datetime import datetime
# ========================================
# 🔧 CONFIGURATION
# ========================================
BACKUP_DIR = Path(r"G:\OnedriveOrdinace\OneDrive\MedicusBackup") # ✅ your real backup location
RESTORE_DIR = Path(r"Z:\\Medicus 3\\restore") # destination folder for .fbk
RESTORE_DIR.mkdir(parents=True, exist_ok=True)
# ========================================
# 🧹 1) Clean restore folder
# ========================================
print(f"🧹 Cleaning folder: {RESTORE_DIR}")
for item in RESTORE_DIR.iterdir():
try:
if item.is_file() or item.is_symlink():
item.unlink()
elif item.is_dir():
shutil.rmtree(item)
except Exception as e:
print(f"⚠️ Cannot delete {item}: {e}")
# ========================================
# 🔍 2) Find newest backup file
# ========================================
pattern = re.compile(r"Medicus_(\d{6})_(\d{4})\.(fbk|zip)$", re.IGNORECASE)
candidates = []
for f in BACKUP_DIR.iterdir():
m = pattern.match(f.name)
if m:
yymmdd, hhmm, ext = m.groups()
ts = datetime.strptime("20" + yymmdd + hhmm, "%Y%m%d%H%M")
candidates.append((ts, f))
if not candidates:
raise SystemExit("❌ No Medicus backup files found in folder!")
candidates.sort(key=lambda x: x[0], reverse=True)
latest_time, latest_file = candidates[0]
print(f"🕒 Latest backup: {latest_file.name} ({latest_time})")
# ========================================
# 📦 3) Extract or use directly
# ========================================
if latest_file.suffix.lower() == ".zip":
with zipfile.ZipFile(latest_file, "r") as zf:
fbk_files = [n for n in zf.namelist() if n.lower().endswith(".fbk")]
if not fbk_files:
raise SystemExit("❌ ZIP file does not contain any .fbk!")
fbk_name = Path(fbk_files[0]).name
out_path = RESTORE_DIR / fbk_name
print(f"📂 Extracting {fbk_name}{out_path}")
zf.extract(fbk_files[0], RESTORE_DIR)
final_fbk = out_path
else:
out_path = RESTORE_DIR / latest_file.name
print(f"📋 Copying {latest_file}{out_path}")
shutil.copy2(latest_file, out_path)
final_fbk = out_path
# ========================================
# ✅ 4) Output final .fbk path for the batch file
# ========================================
print(final_fbk)
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Force-kill all running Medicus processes instantly.
No graceful wait, no exceptions if already closed.
"""
import psutil
TARGETS = ["Medicus.exe", "MedicusServer.exe", "MedicusUpdater.exe"]
for proc in psutil.process_iter(["name", "pid"]):
try:
name = proc.info["name"]
if name and name.lower() in [t.lower() for t in TARGETS]:
print(f"💀 Killing {name} (PID {proc.pid})")
proc.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
print("✅ All Medicus processes terminated.")
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Step 2: Restart Firebird service (FirebirdServerCGM)
Forces all connections to close before database restore.
"""
import subprocess
import time
SERVICE_NAME = "FirebirdServerCGM"
def run_cmd(cmd):
"""Run a command and return (success, output)."""
try:
result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
return result.returncode == 0, result.stdout.strip() + result.stderr.strip()
except Exception as e:
return False, str(e)
print(f"🔧 Restarting service: {SERVICE_NAME}")
# --- Stop service
ok, out = run_cmd(f'net stop "{SERVICE_NAME}"')
if ok:
print(f"🛑 Service {SERVICE_NAME} stopped.")
else:
print(f"⚠️ Failed to stop service (it may already be stopped):\n{out}")
# --- Small delay
time.sleep(5)
# --- Start service again
ok, out = run_cmd(f'net start "{SERVICE_NAME}"')
if ok:
print(f"▶️ Service {SERVICE_NAME} started.")
else:
print(f"❌ Failed to start service:\n{out}")
print("✅ Firebird restart sequence complete.")
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Simplified Medicus Firebird database restore
✅ Works locally with Firebird service running
✅ Handles spaces in paths
✅ Restores newest .fbk backup from restore folder
"""
import subprocess
from pathlib import Path
# =========================================
# 🔧 CONFIGURATION
# =========================================
FIREBIRD_GBAK = Path(r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe")
RESTORE_DIR = Path(r"Z:\Medicus 3\restore")
TARGET_DB = Path(r"Z:\Medicus 3\data\MEDICUS.FDB")
USER = "SYSDBA"
PASSWORD = "masterkey"
# =========================================
# 🔍 Find newest .fbk file
# =========================================
fbk_files = list(RESTORE_DIR.glob("*.fbk"))
if not fbk_files:
raise SystemExit(f"❌ No .fbk files found in {RESTORE_DIR}")
latest_fbk = max(fbk_files, key=lambda f: f.stat().st_mtime)
print(f"🕒 Latest FBK: {latest_fbk.name}")
# =========================================
# 🧩 Build gbak command (direct restore)
# =========================================
cmd = f'"{FIREBIRD_GBAK}" -rep "{latest_fbk}" "{TARGET_DB}" -user {USER} -pas {PASSWORD}'
print("\n🧩 Running restore command:")
print(cmd)
# =========================================
# ▶️ Execute restore
# =========================================
result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
if result.returncode == 0:
print("✅ Medicus database successfully restored!")
else:
print("❌ Restore failed:")
print(result.stderr.strip() or result.stdout.strip())
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Nightly Medicus automatic restore
---------------------------------
Workflow:
1️⃣ Kill all Medicus processes
2️⃣ Restart Firebird service (to release all open connections)
3️⃣ Clean restore folder
4️⃣ Find and extract newest Medicus backup (.fbk or .zip)
5️⃣ Restore database using gbak (Firebird service kept running)
6️⃣ Restart Firebird again to refresh state
7️⃣ Log all actions with timestamps
"""
import os
import psutil
import shutil
import zipfile
import subprocess
from pathlib import Path
from datetime import datetime
import time
# =========================================
# 🔧 CONFIGURATION
# =========================================
FIREBIRD_GBAK = Path(r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe")
BACKUP_DIR = Path(r"G:\OnedriveOrdinace\OneDrive\MedicusBackup")
RESTORE_DIR = Path(r"Z:\Medicus 3\restore")
TARGET_DB = Path(r"Z:\Medicus 3\data\MEDICUS.FDB")
SERVICE_NAME = "FirebirdServerCGM"
USER = "SYSDBA"
PASSWORD = "masterkey"
LOG_FILE = RESTORE_DIR / f"nightly_restore_{datetime.now():%Y%m%d_%H%M%S}.log"
# =========================================
# 🪓 1) Kill Medicus processes
# =========================================
def kill_medicus():
targets = ["Medicus.exe", "MedicusServer.exe", "MedicusUpdater.exe"]
with open(LOG_FILE, "w", encoding="utf-8") as log:
log.write(f"==== NIGHTLY RESTORE START {datetime.now():%Y-%m-%d %H:%M:%S} ====\n")
for proc in psutil.process_iter(["name", "pid"]):
try:
if proc.info["name"] and proc.info["name"].lower() in [t.lower() for t in targets]:
log.write(f"Killing {proc.info['name']} (PID {proc.pid})\n")
proc.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
log.write("All Medicus processes terminated.\n")
# =========================================
# ⚙️ 2) Firebird service control
# =========================================
def service_cmd(action):
cmd = f'net {action} "{SERVICE_NAME}"'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.returncode == 0, result.stdout + result.stderr
def restart_firebird(label):
ok, out = service_cmd("stop")
with open(LOG_FILE, "a", encoding="utf-8") as log:
log.write(f"\n--- Restart Firebird ({label}) ---\n")
log.write(f"Stopping {SERVICE_NAME}...\n{out}\n")
time.sleep(5)
ok, out = service_cmd("start")
with open(LOG_FILE, "a", encoding="utf-8") as log:
log.write(f"Starting {SERVICE_NAME}...\n{out}\n")
# =========================================
# 🧹 3) Clean restore folder
# =========================================
def clean_restore_folder():
for item in RESTORE_DIR.glob("*"):
try:
if item.is_file():
item.unlink()
elif item.is_dir():
shutil.rmtree(item)
except Exception as e:
with open(LOG_FILE, "a", encoding="utf-8") as log:
log.write(f"⚠️ Could not delete {item}: {e}\n")
# =========================================
# 💾 4) Find newest backup and prepare .fbk
# =========================================
def prepare_backup():
backups = list(BACKUP_DIR.glob("Medicus_*.fbk")) + list(BACKUP_DIR.glob("Medicus_*.zip"))
if not backups:
raise SystemExit(f"❌ No Medicus backups found in {BACKUP_DIR}")
latest = max(backups, key=lambda f: f.stat().st_mtime)
fbk_path = RESTORE_DIR / (latest.stem + ".fbk")
with open(LOG_FILE, "a", encoding="utf-8") as log:
log.write(f"\nUsing backup: {latest}\n")
if latest.suffix.lower() == ".fbk":
shutil.copy2(latest, fbk_path)
else:
with zipfile.ZipFile(latest, "r") as zf:
fbk_files = [n for n in zf.namelist() if n.lower().endswith(".fbk")]
if not fbk_files:
raise SystemExit("❌ ZIP file does not contain .fbk!")
zf.extract(fbk_files[0], RESTORE_DIR)
extracted = RESTORE_DIR / fbk_files[0]
if extracted != fbk_path:
shutil.move(extracted, fbk_path)
return fbk_path
# =========================================
# 🧩 5) Restore database using gbak
# =========================================
def restore_database(fbk_path):
cmd = f'"{FIREBIRD_GBAK}" -rep "{fbk_path}" "{TARGET_DB}" -user {USER} -pas {PASSWORD}'
with open(LOG_FILE, "a", encoding="utf-8") as log:
log.write(f"\nRunning restore command:\n{cmd}\n")
result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
with open(LOG_FILE, "a", encoding="utf-8") as log:
log.write(result.stdout)
log.write(result.stderr)
log.write(f"Return code: {result.returncode}\n")
if result.returncode == 0:
print("✅ Medicus database successfully restored!")
else:
print("❌ Restore failed! Check log for details.")
# =========================================
# 🚀 MAIN WORKFLOW
# =========================================
if __name__ == "__main__":
kill_medicus()
restart_firebird("before restore")
clean_restore_folder()
fbk = prepare_backup()
restore_database(fbk)
restart_firebird("after restore")
with open(LOG_FILE, "a", encoding="utf-8") as log:
log.write(f"\n==== RESTORE COMPLETED {datetime.now():%Y-%m-%d %H:%M:%S} ====\n")
print(f"📝 Full log saved to {LOG_FILE}")
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['05 FullCodeNightlyRestore.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='05 FullCodeNightlyRestore',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
@@ -0,0 +1,865 @@
(['C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\05 '
'FullCodeNightlyRestore.py'],
['C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22'],
[],
[('C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\stdhooks',
-1000),
('C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\_pyinstaller_hooks_contrib',
-1000)],
{},
[],
[],
False,
{},
0,
[],
[],
'3.12.0 (tags/v3.12.0:0fb18b0, Oct 2 2023, 13:03:39) [MSC v.1935 64 bit '
'(AMD64)]',
[('pyi_rth_inspect',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('05 FullCodeNightlyRestore',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\NighMedicusRestoreTW22\\05 '
'FullCodeNightlyRestore.py',
'PYSOURCE')],
[('inspect',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\inspect.py',
'PYMODULE'),
('importlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\__init__.py',
'PYMODULE'),
('importlib._bootstrap_external',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.metadata',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\__init__.py',
'PYMODULE'),
('typing',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\typing.py',
'PYMODULE'),
('importlib.abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\abc.py',
'PYMODULE'),
('importlib.resources.abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\abc.py',
'PYMODULE'),
('importlib.resources',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\__init__.py',
'PYMODULE'),
('importlib.resources._legacy',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_legacy.py',
'PYMODULE'),
('importlib.resources._common',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_common.py',
'PYMODULE'),
('importlib.resources._adapters',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_adapters.py',
'PYMODULE'),
('tempfile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tempfile.py',
'PYMODULE'),
('random',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\random.py',
'PYMODULE'),
('statistics',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\statistics.py',
'PYMODULE'),
('decimal',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\decimal.py',
'PYMODULE'),
('_pydecimal',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_pydecimal.py',
'PYMODULE'),
('contextvars',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\contextvars.py',
'PYMODULE'),
('fractions',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\fractions.py',
'PYMODULE'),
('numbers',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\numbers.py',
'PYMODULE'),
('hashlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\hashlib.py',
'PYMODULE'),
('logging',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\logging\\__init__.py',
'PYMODULE'),
('pickle',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\pickle.py',
'PYMODULE'),
('pprint',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\pprint.py',
'PYMODULE'),
('dataclasses',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\dataclasses.py',
'PYMODULE'),
('copy',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\copy.py',
'PYMODULE'),
('_compat_pickle',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_compat_pickle.py',
'PYMODULE'),
('struct',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\struct.py',
'PYMODULE'),
('threading',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\threading.py',
'PYMODULE'),
('_threading_local',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_threading_local.py',
'PYMODULE'),
('string',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\string.py',
'PYMODULE'),
('bisect',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\bisect.py',
'PYMODULE'),
('importlib._abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\_abc.py',
'PYMODULE'),
('importlib.metadata._itertools',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_itertools.py',
'PYMODULE'),
('importlib.metadata._functools',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_functools.py',
'PYMODULE'),
('importlib.metadata._collections',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_collections.py',
'PYMODULE'),
('importlib.metadata._meta',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_meta.py',
'PYMODULE'),
('importlib.metadata._adapters',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_adapters.py',
'PYMODULE'),
('importlib.metadata._text',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_text.py',
'PYMODULE'),
('email.message',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\message.py',
'PYMODULE'),
('email.policy',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\policy.py',
'PYMODULE'),
('email.contentmanager',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\contentmanager.py',
'PYMODULE'),
('email.quoprimime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\quoprimime.py',
'PYMODULE'),
('email.headerregistry',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\headerregistry.py',
'PYMODULE'),
('email._header_value_parser',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_header_value_parser.py',
'PYMODULE'),
('urllib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\urllib\\__init__.py',
'PYMODULE'),
('email.iterators',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\iterators.py',
'PYMODULE'),
('email.generator',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\generator.py',
'PYMODULE'),
('email._encoded_words',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_encoded_words.py',
'PYMODULE'),
('base64',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\base64.py',
'PYMODULE'),
('getopt',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\getopt.py',
'PYMODULE'),
('gettext',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\gettext.py',
'PYMODULE'),
('email.charset',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\charset.py',
'PYMODULE'),
('email.encoders',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\encoders.py',
'PYMODULE'),
('email.base64mime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\base64mime.py',
'PYMODULE'),
('email._policybase',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_policybase.py',
'PYMODULE'),
('email.header',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\header.py',
'PYMODULE'),
('email.errors',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\errors.py',
'PYMODULE'),
('email.utils',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\utils.py',
'PYMODULE'),
('email._parseaddr',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_parseaddr.py',
'PYMODULE'),
('calendar',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\calendar.py',
'PYMODULE'),
('urllib.parse',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\urllib\\parse.py',
'PYMODULE'),
('ipaddress',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ipaddress.py',
'PYMODULE'),
('socket',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\socket.py',
'PYMODULE'),
('selectors',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\selectors.py',
'PYMODULE'),
('quopri',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\quopri.py',
'PYMODULE'),
('contextlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\contextlib.py',
'PYMODULE'),
('textwrap',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\textwrap.py',
'PYMODULE'),
('email',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\__init__.py',
'PYMODULE'),
('email.parser',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\parser.py',
'PYMODULE'),
('email.feedparser',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\feedparser.py',
'PYMODULE'),
('csv',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\csv.py',
'PYMODULE'),
('importlib.readers',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\readers.py',
'PYMODULE'),
('importlib.resources.readers',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\readers.py',
'PYMODULE'),
('importlib.resources._itertools',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_itertools.py',
'PYMODULE'),
('importlib._bootstrap',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('argparse',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\argparse.py',
'PYMODULE'),
('token',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\token.py',
'PYMODULE'),
('tokenize',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tokenize.py',
'PYMODULE'),
('importlib.machinery',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\machinery.py',
'PYMODULE'),
('dis',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\dis.py',
'PYMODULE'),
('opcode',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\opcode.py',
'PYMODULE'),
('ast',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ast.py',
'PYMODULE'),
('tracemalloc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tracemalloc.py',
'PYMODULE'),
('fnmatch',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\fnmatch.py',
'PYMODULE'),
('_py_abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_py_abc.py',
'PYMODULE'),
('stringprep',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\stringprep.py',
'PYMODULE'),
('bz2',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\bz2.py',
'PYMODULE'),
('_compression',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_compression.py',
'PYMODULE'),
('_strptime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_strptime.py',
'PYMODULE'),
('datetime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\datetime.py',
'PYMODULE'),
('_pydatetime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_pydatetime.py',
'PYMODULE'),
('pathlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\pathlib.py',
'PYMODULE'),
('subprocess',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\subprocess.py',
'PYMODULE'),
('signal',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\signal.py',
'PYMODULE'),
('zipfile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\__init__.py',
'PYMODULE'),
('zipfile.__main__',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\__main__.py',
'PYMODULE'),
('zipfile._path',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\_path\\__init__.py',
'PYMODULE'),
('zipfile._path.glob',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\_path\\glob.py',
'PYMODULE'),
('py_compile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\py_compile.py',
'PYMODULE'),
('lzma',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\lzma.py',
'PYMODULE'),
('importlib.util',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\util.py',
'PYMODULE'),
('shutil',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\shutil.py',
'PYMODULE'),
('tarfile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tarfile.py',
'PYMODULE'),
('gzip',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\gzip.py',
'PYMODULE'),
('psutil',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\__init__.py',
'PYMODULE'),
('psutil._pswindows',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\_pswindows.py',
'PYMODULE'),
('psutil._common',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\_common.py',
'PYMODULE'),
('ctypes',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ctypes\\__init__.py',
'PYMODULE'),
('ctypes._endian',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ctypes\\_endian.py',
'PYMODULE')],
[('python312.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll',
'BINARY'),
('_decimal.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_decimal.pyd',
'EXTENSION'),
('_hashlib.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_hashlib.pyd',
'EXTENSION'),
('unicodedata.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\unicodedata.pyd',
'EXTENSION'),
('select.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\select.pyd',
'EXTENSION'),
('_socket.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_socket.pyd',
'EXTENSION'),
('_bz2.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_bz2.pyd',
'EXTENSION'),
('_lzma.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_lzma.pyd',
'EXTENSION'),
('psutil\\_psutil_windows.pyd',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\_psutil_windows.pyd',
'EXTENSION'),
('_ctypes.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_ctypes.pyd',
'EXTENSION'),
('VCRUNTIME140.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\VCRUNTIME140.dll',
'BINARY'),
('libcrypto-3.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libcrypto-3.dll',
'BINARY'),
('python3.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\python3.dll',
'BINARY'),
('libffi-8.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libffi-8.dll',
'BINARY')],
[],
[],
[('base_library.zip',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\base_library.zip',
'DATA')],
[('re._parser',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\re\\_parser.py',
'PYMODULE'),
('re._constants',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\re\\_constants.py',
'PYMODULE'),
('re._compiler',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\re\\_compiler.py',
'PYMODULE'),
('re._casefix',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\re\\_casefix.py',
'PYMODULE'),
('re',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\re\\__init__.py',
'PYMODULE'),
('sre_compile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\sre_compile.py',
'PYMODULE'),
('warnings',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\warnings.py',
'PYMODULE'),
('reprlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\reprlib.py',
'PYMODULE'),
('heapq',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\heapq.py',
'PYMODULE'),
('traceback',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\traceback.py',
'PYMODULE'),
('weakref',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\weakref.py',
'PYMODULE'),
('linecache',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\linecache.py',
'PYMODULE'),
('copyreg',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\copyreg.py',
'PYMODULE'),
('sre_constants',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\sre_constants.py',
'PYMODULE'),
('collections.abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\collections\\abc.py',
'PYMODULE'),
('collections',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\collections\\__init__.py',
'PYMODULE'),
('abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\abc.py',
'PYMODULE'),
('operator',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\operator.py',
'PYMODULE'),
('_collections_abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_collections_abc.py',
'PYMODULE'),
('genericpath',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\genericpath.py',
'PYMODULE'),
('stat',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\stat.py',
'PYMODULE'),
('functools',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\functools.py',
'PYMODULE'),
('locale',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\locale.py',
'PYMODULE'),
('codecs',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\codecs.py',
'PYMODULE'),
('sre_parse',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\sre_parse.py',
'PYMODULE'),
('encodings.zlib_codec',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\zlib_codec.py',
'PYMODULE'),
('encodings.uu_codec',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\uu_codec.py',
'PYMODULE'),
('encodings.utf_8_sig',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_8_sig.py',
'PYMODULE'),
('encodings.utf_8',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_8.py',
'PYMODULE'),
('encodings.utf_7',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_7.py',
'PYMODULE'),
('encodings.utf_32_le',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_32_le.py',
'PYMODULE'),
('encodings.utf_32_be',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_32_be.py',
'PYMODULE'),
('encodings.utf_32',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_32.py',
'PYMODULE'),
('encodings.utf_16_le',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_16_le.py',
'PYMODULE'),
('encodings.utf_16_be',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_16_be.py',
'PYMODULE'),
('encodings.utf_16',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\utf_16.py',
'PYMODULE'),
('encodings.unicode_escape',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\unicode_escape.py',
'PYMODULE'),
('encodings.undefined',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\undefined.py',
'PYMODULE'),
('encodings.tis_620',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\tis_620.py',
'PYMODULE'),
('encodings.shift_jisx0213',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\shift_jisx0213.py',
'PYMODULE'),
('encodings.shift_jis_2004',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\shift_jis_2004.py',
'PYMODULE'),
('encodings.shift_jis',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\shift_jis.py',
'PYMODULE'),
('encodings.rot_13',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\rot_13.py',
'PYMODULE'),
('encodings.raw_unicode_escape',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\raw_unicode_escape.py',
'PYMODULE'),
('encodings.quopri_codec',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\quopri_codec.py',
'PYMODULE'),
('encodings.punycode',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\punycode.py',
'PYMODULE'),
('encodings.ptcp154',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\ptcp154.py',
'PYMODULE'),
('encodings.palmos',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\palmos.py',
'PYMODULE'),
('encodings.oem',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\oem.py',
'PYMODULE'),
('encodings.mbcs',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mbcs.py',
'PYMODULE'),
('encodings.mac_turkish',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_turkish.py',
'PYMODULE'),
('encodings.mac_romanian',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_romanian.py',
'PYMODULE'),
('encodings.mac_roman',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_roman.py',
'PYMODULE'),
('encodings.mac_latin2',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_latin2.py',
'PYMODULE'),
('encodings.mac_iceland',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_iceland.py',
'PYMODULE'),
('encodings.mac_greek',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_greek.py',
'PYMODULE'),
('encodings.mac_farsi',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_farsi.py',
'PYMODULE'),
('encodings.mac_cyrillic',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_cyrillic.py',
'PYMODULE'),
('encodings.mac_croatian',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_croatian.py',
'PYMODULE'),
('encodings.mac_arabic',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\mac_arabic.py',
'PYMODULE'),
('encodings.latin_1',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\latin_1.py',
'PYMODULE'),
('encodings.kz1048',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\kz1048.py',
'PYMODULE'),
('encodings.koi8_u',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\koi8_u.py',
'PYMODULE'),
('encodings.koi8_t',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\koi8_t.py',
'PYMODULE'),
('encodings.koi8_r',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\koi8_r.py',
'PYMODULE'),
('encodings.johab',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\johab.py',
'PYMODULE'),
('encodings.iso8859_9',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_9.py',
'PYMODULE'),
('encodings.iso8859_8',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_8.py',
'PYMODULE'),
('encodings.iso8859_7',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_7.py',
'PYMODULE'),
('encodings.iso8859_6',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_6.py',
'PYMODULE'),
('encodings.iso8859_5',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_5.py',
'PYMODULE'),
('encodings.iso8859_4',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_4.py',
'PYMODULE'),
('encodings.iso8859_3',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_3.py',
'PYMODULE'),
('encodings.iso8859_2',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_2.py',
'PYMODULE'),
('encodings.iso8859_16',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_16.py',
'PYMODULE'),
('encodings.iso8859_15',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_15.py',
'PYMODULE'),
('encodings.iso8859_14',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_14.py',
'PYMODULE'),
('encodings.iso8859_13',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_13.py',
'PYMODULE'),
('encodings.iso8859_11',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_11.py',
'PYMODULE'),
('encodings.iso8859_10',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_10.py',
'PYMODULE'),
('encodings.iso8859_1',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso8859_1.py',
'PYMODULE'),
('encodings.iso2022_kr',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso2022_kr.py',
'PYMODULE'),
('encodings.iso2022_jp_ext',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso2022_jp_ext.py',
'PYMODULE'),
('encodings.iso2022_jp_3',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso2022_jp_3.py',
'PYMODULE'),
('encodings.iso2022_jp_2004',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso2022_jp_2004.py',
'PYMODULE'),
('encodings.iso2022_jp_2',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso2022_jp_2.py',
'PYMODULE'),
('encodings.iso2022_jp_1',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso2022_jp_1.py',
'PYMODULE'),
('encodings.iso2022_jp',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\iso2022_jp.py',
'PYMODULE'),
('encodings.idna',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\idna.py',
'PYMODULE'),
('encodings.hz',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\hz.py',
'PYMODULE'),
('encodings.hp_roman8',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\hp_roman8.py',
'PYMODULE'),
('encodings.hex_codec',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\hex_codec.py',
'PYMODULE'),
('encodings.gbk',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\gbk.py',
'PYMODULE'),
('encodings.gb2312',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\gb2312.py',
'PYMODULE'),
('encodings.gb18030',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\gb18030.py',
'PYMODULE'),
('encodings.euc_kr',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\euc_kr.py',
'PYMODULE'),
('encodings.euc_jp',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\euc_jp.py',
'PYMODULE'),
('encodings.euc_jisx0213',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\euc_jisx0213.py',
'PYMODULE'),
('encodings.euc_jis_2004',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\euc_jis_2004.py',
'PYMODULE'),
('encodings.cp950',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp950.py',
'PYMODULE'),
('encodings.cp949',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp949.py',
'PYMODULE'),
('encodings.cp932',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp932.py',
'PYMODULE'),
('encodings.cp875',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp875.py',
'PYMODULE'),
('encodings.cp874',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp874.py',
'PYMODULE'),
('encodings.cp869',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp869.py',
'PYMODULE'),
('encodings.cp866',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp866.py',
'PYMODULE'),
('encodings.cp865',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp865.py',
'PYMODULE'),
('encodings.cp864',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp864.py',
'PYMODULE'),
('encodings.cp863',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp863.py',
'PYMODULE'),
('encodings.cp862',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp862.py',
'PYMODULE'),
('encodings.cp861',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp861.py',
'PYMODULE'),
('encodings.cp860',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp860.py',
'PYMODULE'),
('encodings.cp858',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp858.py',
'PYMODULE'),
('encodings.cp857',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp857.py',
'PYMODULE'),
('encodings.cp856',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp856.py',
'PYMODULE'),
('encodings.cp855',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp855.py',
'PYMODULE'),
('encodings.cp852',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp852.py',
'PYMODULE'),
('encodings.cp850',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp850.py',
'PYMODULE'),
('encodings.cp775',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp775.py',
'PYMODULE'),
('encodings.cp737',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp737.py',
'PYMODULE'),
('encodings.cp720',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp720.py',
'PYMODULE'),
('encodings.cp500',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp500.py',
'PYMODULE'),
('encodings.cp437',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp437.py',
'PYMODULE'),
('encodings.cp424',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp424.py',
'PYMODULE'),
('encodings.cp273',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp273.py',
'PYMODULE'),
('encodings.cp1258',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1258.py',
'PYMODULE'),
('encodings.cp1257',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1257.py',
'PYMODULE'),
('encodings.cp1256',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1256.py',
'PYMODULE'),
('encodings.cp1255',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1255.py',
'PYMODULE'),
('encodings.cp1254',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1254.py',
'PYMODULE'),
('encodings.cp1253',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1253.py',
'PYMODULE'),
('encodings.cp1252',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1252.py',
'PYMODULE'),
('encodings.cp1251',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1251.py',
'PYMODULE'),
('encodings.cp1250',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1250.py',
'PYMODULE'),
('encodings.cp1140',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1140.py',
'PYMODULE'),
('encodings.cp1125',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1125.py',
'PYMODULE'),
('encodings.cp1026',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1026.py',
'PYMODULE'),
('encodings.cp1006',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp1006.py',
'PYMODULE'),
('encodings.cp037',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\cp037.py',
'PYMODULE'),
('encodings.charmap',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\charmap.py',
'PYMODULE'),
('encodings.bz2_codec',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\bz2_codec.py',
'PYMODULE'),
('encodings.big5hkscs',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\big5hkscs.py',
'PYMODULE'),
('encodings.big5',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\big5.py',
'PYMODULE'),
('encodings.base64_codec',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\base64_codec.py',
'PYMODULE'),
('encodings.ascii',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\ascii.py',
'PYMODULE'),
('encodings.aliases',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\aliases.py',
'PYMODULE'),
('encodings',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\encodings\\__init__.py',
'PYMODULE'),
('keyword',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\keyword.py',
'PYMODULE'),
('posixpath',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\posixpath.py',
'PYMODULE'),
('types',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\types.py',
'PYMODULE'),
('ntpath',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ntpath.py',
'PYMODULE'),
('io',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\io.py',
'PYMODULE'),
('enum',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\enum.py',
'PYMODULE'),
('_weakrefset',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_weakrefset.py',
'PYMODULE'),
('os',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\os.py',
'PYMODULE')])
@@ -0,0 +1,123 @@
('C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\dist\\05 '
'FullCodeNightlyRestore.exe',
True,
False,
False,
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico',
None,
False,
False,
b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<assembly xmlns='
b'"urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">\n <trustInfo x'
b'mlns="urn:schemas-microsoft-com:asm.v3">\n <security>\n <requested'
b'Privileges>\n <requestedExecutionLevel level="asInvoker" uiAccess='
b'"false"/>\n </requestedPrivileges>\n </security>\n </trustInfo>\n '
b'<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">\n <'
b'application>\n <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f'
b'0}"/>\n <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>\n '
b' <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>\n <s'
b'upportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>\n <supporte'
b'dOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>\n </application>\n <'
b'/compatibility>\n <application xmlns="urn:schemas-microsoft-com:asm.v3">'
b'\n <windowsSettings>\n <longPathAware xmlns="http://schemas.micros'
b'oft.com/SMI/2016/WindowsSettings">true</longPathAware>\n </windowsSett'
b'ings>\n </application>\n <dependency>\n <dependentAssembly>\n <ass'
b'emblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version='
b'"6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" langua'
b'ge="*"/>\n </dependentAssembly>\n </dependency>\n</assembly>',
True,
False,
None,
None,
None,
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\05 FullCodeNightlyRestore.pkg',
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\PYZ-00.pyz',
'PYZ'),
('struct',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth_inspect',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('05 FullCodeNightlyRestore',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\NighMedicusRestoreTW22\\05 '
'FullCodeNightlyRestore.py',
'PYSOURCE'),
('python312.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll',
'BINARY'),
('_decimal.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_decimal.pyd',
'EXTENSION'),
('_hashlib.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_hashlib.pyd',
'EXTENSION'),
('unicodedata.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\unicodedata.pyd',
'EXTENSION'),
('select.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\select.pyd',
'EXTENSION'),
('_socket.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_socket.pyd',
'EXTENSION'),
('_bz2.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_bz2.pyd',
'EXTENSION'),
('_lzma.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_lzma.pyd',
'EXTENSION'),
('psutil\\_psutil_windows.pyd',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\_psutil_windows.pyd',
'EXTENSION'),
('_ctypes.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_ctypes.pyd',
'EXTENSION'),
('VCRUNTIME140.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\VCRUNTIME140.dll',
'BINARY'),
('libcrypto-3.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libcrypto-3.dll',
'BINARY'),
('python3.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\python3.dll',
'BINARY'),
('libffi-8.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libffi-8.dll',
'BINARY'),
('base_library.zip',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\base_library.zip',
'DATA')],
[],
False,
False,
1762244851,
[('run.exe',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\run.exe',
'EXECUTABLE')],
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll')
@@ -0,0 +1,100 @@
('C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\05 FullCodeNightlyRestore.pkg',
{'BINARY': True,
'DATA': True,
'EXECUTABLE': True,
'EXTENSION': True,
'PYMODULE': True,
'PYSOURCE': True,
'PYZ': False,
'SPLASH': True,
'SYMLINK': False},
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\PYZ-00.pyz',
'PYZ'),
('struct',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\struct.pyc',
'PYMODULE'),
('pyimod01_archive',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth_inspect',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('05 FullCodeNightlyRestore',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\NighMedicusRestoreTW22\\05 '
'FullCodeNightlyRestore.py',
'PYSOURCE'),
('python312.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\python312.dll',
'BINARY'),
('_decimal.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_decimal.pyd',
'EXTENSION'),
('_hashlib.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_hashlib.pyd',
'EXTENSION'),
('unicodedata.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\unicodedata.pyd',
'EXTENSION'),
('select.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\select.pyd',
'EXTENSION'),
('_socket.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_socket.pyd',
'EXTENSION'),
('_bz2.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_bz2.pyd',
'EXTENSION'),
('_lzma.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_lzma.pyd',
'EXTENSION'),
('psutil\\_psutil_windows.pyd',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\_psutil_windows.pyd',
'EXTENSION'),
('_ctypes.pyd',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\_ctypes.pyd',
'EXTENSION'),
('VCRUNTIME140.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\VCRUNTIME140.dll',
'BINARY'),
('libcrypto-3.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libcrypto-3.dll',
'BINARY'),
('python3.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\python3.dll',
'BINARY'),
('libffi-8.dll',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\DLLs\\libffi-8.dll',
'BINARY'),
('base_library.zip',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\base_library.zip',
'DATA')],
'python312.dll',
False,
False,
False,
[],
None,
None,
None)
@@ -0,0 +1,326 @@
('C:\\Users\\vlado\\PycharmProjects\\medicus\\nighmedicusrestoretw22\\build\\05 '
'FullCodeNightlyRestore\\PYZ-00.pyz',
[('_compat_pickle',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_compat_pickle.py',
'PYMODULE'),
('_compression',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_compression.py',
'PYMODULE'),
('_py_abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_py_abc.py',
'PYMODULE'),
('_pydatetime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_pydatetime.py',
'PYMODULE'),
('_pydecimal',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_pydecimal.py',
'PYMODULE'),
('_strptime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_strptime.py',
'PYMODULE'),
('_threading_local',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\_threading_local.py',
'PYMODULE'),
('argparse',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\argparse.py',
'PYMODULE'),
('ast',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ast.py',
'PYMODULE'),
('base64',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\base64.py',
'PYMODULE'),
('bisect',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\bisect.py',
'PYMODULE'),
('bz2',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\bz2.py',
'PYMODULE'),
('calendar',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\calendar.py',
'PYMODULE'),
('contextlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\contextlib.py',
'PYMODULE'),
('contextvars',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\contextvars.py',
'PYMODULE'),
('copy',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\copy.py',
'PYMODULE'),
('csv',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\csv.py',
'PYMODULE'),
('ctypes',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ctypes\\__init__.py',
'PYMODULE'),
('ctypes._endian',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ctypes\\_endian.py',
'PYMODULE'),
('dataclasses',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\dataclasses.py',
'PYMODULE'),
('datetime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\datetime.py',
'PYMODULE'),
('decimal',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\decimal.py',
'PYMODULE'),
('dis',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\dis.py',
'PYMODULE'),
('email',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\__init__.py',
'PYMODULE'),
('email._encoded_words',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_encoded_words.py',
'PYMODULE'),
('email._header_value_parser',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_header_value_parser.py',
'PYMODULE'),
('email._parseaddr',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_parseaddr.py',
'PYMODULE'),
('email._policybase',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\_policybase.py',
'PYMODULE'),
('email.base64mime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\base64mime.py',
'PYMODULE'),
('email.charset',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\charset.py',
'PYMODULE'),
('email.contentmanager',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\contentmanager.py',
'PYMODULE'),
('email.encoders',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\encoders.py',
'PYMODULE'),
('email.errors',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\errors.py',
'PYMODULE'),
('email.feedparser',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\feedparser.py',
'PYMODULE'),
('email.generator',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\generator.py',
'PYMODULE'),
('email.header',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\header.py',
'PYMODULE'),
('email.headerregistry',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\headerregistry.py',
'PYMODULE'),
('email.iterators',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\iterators.py',
'PYMODULE'),
('email.message',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\message.py',
'PYMODULE'),
('email.parser',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\parser.py',
'PYMODULE'),
('email.policy',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\policy.py',
'PYMODULE'),
('email.quoprimime',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\quoprimime.py',
'PYMODULE'),
('email.utils',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\email\\utils.py',
'PYMODULE'),
('fnmatch',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\fnmatch.py',
'PYMODULE'),
('fractions',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\fractions.py',
'PYMODULE'),
('getopt',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\getopt.py',
'PYMODULE'),
('gettext',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\gettext.py',
'PYMODULE'),
('gzip',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\gzip.py',
'PYMODULE'),
('hashlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\hashlib.py',
'PYMODULE'),
('importlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\__init__.py',
'PYMODULE'),
('importlib._abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\_abc.py',
'PYMODULE'),
('importlib._bootstrap',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('importlib._bootstrap_external',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\abc.py',
'PYMODULE'),
('importlib.machinery',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\machinery.py',
'PYMODULE'),
('importlib.metadata',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\__init__.py',
'PYMODULE'),
('importlib.metadata._adapters',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_adapters.py',
'PYMODULE'),
('importlib.metadata._collections',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_collections.py',
'PYMODULE'),
('importlib.metadata._functools',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_functools.py',
'PYMODULE'),
('importlib.metadata._itertools',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_itertools.py',
'PYMODULE'),
('importlib.metadata._meta',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_meta.py',
'PYMODULE'),
('importlib.metadata._text',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\metadata\\_text.py',
'PYMODULE'),
('importlib.readers',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\readers.py',
'PYMODULE'),
('importlib.resources',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\__init__.py',
'PYMODULE'),
('importlib.resources._adapters',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_adapters.py',
'PYMODULE'),
('importlib.resources._common',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_common.py',
'PYMODULE'),
('importlib.resources._itertools',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_itertools.py',
'PYMODULE'),
('importlib.resources._legacy',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\_legacy.py',
'PYMODULE'),
('importlib.resources.abc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\abc.py',
'PYMODULE'),
('importlib.resources.readers',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\resources\\readers.py',
'PYMODULE'),
('importlib.util',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\importlib\\util.py',
'PYMODULE'),
('inspect',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\inspect.py',
'PYMODULE'),
('ipaddress',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\ipaddress.py',
'PYMODULE'),
('logging',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\logging\\__init__.py',
'PYMODULE'),
('lzma',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\lzma.py',
'PYMODULE'),
('numbers',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\numbers.py',
'PYMODULE'),
('opcode',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\opcode.py',
'PYMODULE'),
('pathlib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\pathlib.py',
'PYMODULE'),
('pickle',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\pickle.py',
'PYMODULE'),
('pprint',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\pprint.py',
'PYMODULE'),
('psutil',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\__init__.py',
'PYMODULE'),
('psutil._common',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\_common.py',
'PYMODULE'),
('psutil._pswindows',
'C:\\Users\\vlado\\PycharmProjects\\medicus\\.venv\\Lib\\site-packages\\psutil\\_pswindows.py',
'PYMODULE'),
('py_compile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\py_compile.py',
'PYMODULE'),
('quopri',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\quopri.py',
'PYMODULE'),
('random',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\random.py',
'PYMODULE'),
('selectors',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\selectors.py',
'PYMODULE'),
('shutil',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\shutil.py',
'PYMODULE'),
('signal',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\signal.py',
'PYMODULE'),
('socket',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\socket.py',
'PYMODULE'),
('statistics',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\statistics.py',
'PYMODULE'),
('string',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\string.py',
'PYMODULE'),
('stringprep',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\stringprep.py',
'PYMODULE'),
('subprocess',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\subprocess.py',
'PYMODULE'),
('tarfile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tarfile.py',
'PYMODULE'),
('tempfile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tempfile.py',
'PYMODULE'),
('textwrap',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\textwrap.py',
'PYMODULE'),
('threading',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\threading.py',
'PYMODULE'),
('token',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\token.py',
'PYMODULE'),
('tokenize',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tokenize.py',
'PYMODULE'),
('tracemalloc',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\tracemalloc.py',
'PYMODULE'),
('typing',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\typing.py',
'PYMODULE'),
('urllib',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\urllib\\__init__.py',
'PYMODULE'),
('urllib.parse',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\urllib\\parse.py',
'PYMODULE'),
('zipfile',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\__init__.py',
'PYMODULE'),
('zipfile.__main__',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\__main__.py',
'PYMODULE'),
('zipfile._path',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\_path\\__init__.py',
'PYMODULE'),
('zipfile._path.glob',
'C:\\Users\\vlado\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\zipfile\\_path\\glob.py',
'PYMODULE')])
@@ -0,0 +1,25 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.
Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named _sha512 - imported by random (optional)
missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional), posixpath (optional)
missing module named resource - imported by posix (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional)
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional), psutil (optional)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional), subprocess (delayed, conditional, optional)
missing module named _posixsubprocess - imported by subprocess (conditional)
missing module named fcntl - imported by subprocess (optional)
Binary file not shown.
@@ -0,0 +1,168 @@
"""
001_pruzkum_PN_tabulek.py
=========================
Průzkum Medicus DB hledáme tabulky a strukturu relacionou s PN
(pracovní neschopnost).
Spustit na Windows:
python "001_pruzkum_PN_tabulek.py"
Výstup: přehled tabulek s PN v názvu + jejich sloupce + ukázka dat.
"""
import fdb
import datetime
# ── Připojení ──────────────────────────────────────────────────────────────────
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
# ── 1. Všechny tabulky s "PN" v názvu ─────────────────────────────────────────
print("=" * 60)
print("TABULKY obsahující 'PN' v názvu")
print("=" * 60)
cur.execute("""
SELECT TRIM(rdb$relation_name)
FROM rdb$relations
WHERE rdb$view_blr IS NULL
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
AND UPPER(rdb$relation_name) CONTAINING 'PN'
ORDER BY rdb$relation_name
""")
pn_tables = [row[0] for row in cur.fetchall()]
if pn_tables:
for t in pn_tables:
print(f" {t}")
else:
print(" (žádná)")
# ── 2. Sloupce každé PN tabulky + ukázka dat ──────────────────────────────────
for table in pn_tables:
print(f"\n{'' * 60}")
print(f"TABULKA: {table}")
print(f"{'' * 60}")
# Sloupce
cur.execute("""
SELECT TRIM(rf.rdb$field_name),
f.rdb$field_type,
f.rdb$field_length,
rf.rdb$null_flag
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE TRIM(rf.rdb$relation_name) = ?
ORDER BY rf.rdb$field_position
""", (table,))
cols = cur.fetchall()
# field_type kódy Firebirdu (nejběžnější)
type_map = {
7: 'SMALLINT', 8: 'INTEGER', 10: 'FLOAT', 12: 'DATE',
13: 'TIME', 14: 'CHAR', 16: 'BIGINT', 27: 'DOUBLE',
35: 'TIMESTAMP', 37: 'VARCHAR', 261: 'BLOB',
}
col_names = []
for c in cols:
name, ftype, flen, nullable = c
type_str = type_map.get(ftype, f'type#{ftype}')
if flen and ftype in (14, 37):
type_str += f'({flen})'
null_str = '' if nullable else ' NOT NULL'
print(f" {name:<25} {type_str}{null_str}")
col_names.append(name)
# Počet řádků
try:
cur.execute(f'SELECT COUNT(*) FROM {table}')
pocet = cur.fetchone()[0]
print(f"\n --> Celkem řádků: {pocet}")
except Exception as e:
print(f"\n --> COUNT selhalo: {e}")
continue
# Ukázka prvních 5 řádků
if pocet > 0:
try:
cur.execute(f'SELECT FIRST 5 * FROM {table}')
rows = cur.fetchall()
print(f"\n Ukázka (max 5 řádků):")
header = " | ".join(f"{n[:12]:>12}" for n in col_names)
print(f" {header}")
print(f" {'-' * len(header)}")
for row in rows:
vals = []
for v in row:
if isinstance(v, (datetime.date, datetime.datetime)):
vals.append(v.isoformat()[:12])
elif isinstance(v, bytes):
vals.append(f'<blob {len(v)}B>')
elif v is None:
vals.append('NULL')
else:
vals.append(str(v)[:12])
print(f" {' | '.join(f'{v:>12}' for v in vals)}")
except Exception as e:
print(f" Ukázka selhala: {e}")
# ── 3. Hledáme i tabulky s "NESCHOP" nebo "NESCHOPENK" ────────────────────────
print(f"\n{'=' * 60}")
print("TABULKY s 'NESCHOP' nebo 'PRACOV' nebo 'SICK' v názvu")
print("=" * 60)
cur.execute("""
SELECT TRIM(rdb$relation_name)
FROM rdb$relations
WHERE rdb$view_blr IS NULL
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
AND (
UPPER(rdb$relation_name) CONTAINING 'NESCHOP'
OR UPPER(rdb$relation_name) CONTAINING 'PRACOV'
OR UPPER(rdb$relation_name) CONTAINING 'SICK'
)
ORDER BY rdb$relation_name
""")
extra = [row[0] for row in cur.fetchall()]
if extra:
for t in extra:
print(f" {t}")
else:
print(" (žádná)")
# ── 4. Rychlý přehled tabulky s datem "OD/DO" nebo "ZACATEK/KONEC" ──────────
print(f"\n{'=' * 60}")
print("SLOUPCE: hledáme 'DATPN', 'DATDO', 'DATUM_DO' v celé DB")
print("=" * 60)
cur.execute("""
SELECT TRIM(rf.rdb$relation_name), TRIM(rf.rdb$field_name)
FROM rdb$relation_fields rf
JOIN rdb$relations r ON rf.rdb$relation_name = r.rdb$relation_name
WHERE (r.rdb$system_flag IS NULL OR r.rdb$system_flag = 0)
AND r.rdb$view_blr IS NULL
AND (
UPPER(rf.rdb$field_name) CONTAINING 'DATPN'
OR UPPER(rf.rdb$field_name) = 'DATDO'
OR UPPER(rf.rdb$field_name) CONTAINING 'DATUM_DO'
OR UPPER(rf.rdb$field_name) CONTAINING 'DAT_DO'
OR UPPER(rf.rdb$field_name) CONTAINING 'PN'
)
ORDER BY rf.rdb$relation_name, rf.rdb$field_name
""")
hits = cur.fetchall()
if hits:
for tab, col in hits:
print(f" {tab:<30} {col}")
else:
print(" (nic nenalezeno)")
cur.close()
conn.close()
print(f"\n{'=' * 60}")
print("Průzkum dokončen.")
@@ -0,0 +1,184 @@
"""
002_detail_NES_NP_tabulky.py
============================
Průzkum klíčových tabulek neschopenky:
- NES → hlavní tabulka pracovní neschopnosti
- NP → pravděpodobně neschopenka podání
- SOC_NEPRITOMNOST → sociální nepřítomnost
Spustit na Windows:
python "002_detail_NES_NP_tabulky.py"
"""
import fdb
import datetime
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA',
password='masterkey',
charset='win1250'
)
cur = conn.cursor()
def serialize(v):
if isinstance(v, (datetime.date, datetime.datetime)):
return v.isoformat()
if isinstance(v, datetime.time):
return v.isoformat()
if isinstance(v, bytes):
# Pro BLOB zkusíme dekódovat jako text
try:
return v.decode('cp1250')[:80]
except Exception:
return f'<blob {len(v)}B>'
if v is None:
return 'NULL'
return str(v)
def inspect_table(table):
print(f"\n{'=' * 70}")
print(f" TABULKA: {table}")
print(f"{'=' * 70}")
# Sloupce
cur.execute("""
SELECT TRIM(rf.rdb$field_name),
f.rdb$field_type,
f.rdb$field_length
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE TRIM(rf.rdb$relation_name) = ?
ORDER BY rf.rdb$field_position
""", (table,))
cols = cur.fetchall()
type_map = {
7: 'SMALLINT', 8: 'INTEGER', 10: 'FLOAT', 12: 'DATE',
13: 'TIME', 14: 'CHAR', 16: 'BIGINT', 27: 'DOUBLE',
35: 'TIMESTAMP', 37: 'VARCHAR', 261: 'BLOB',
}
col_names = [c[0] for c in cols]
print("\nSloupce:")
for c in cols:
name, ftype, flen = c
type_str = type_map.get(ftype, f'type#{ftype}')
if flen and ftype in (14, 37):
type_str += f'({flen})'
print(f" {name:<30} {type_str}")
# Počet řádků
cur.execute(f'SELECT COUNT(*) FROM {table}')
pocet = cur.fetchone()[0]
print(f"\nCelkem řádků: {pocet}")
if pocet == 0:
return col_names
# Ukázka 5 nejnovějších (pokud má datum sloupec)
date_cols = [c[0] for c in cols if c[1] in (12, 35)] # DATE nebo TIMESTAMP
order = f'ORDER BY {date_cols[0]} DESC' if date_cols else ''
try:
cur.execute(f'SELECT FIRST 5 * FROM {table} {order}')
rows = cur.fetchall()
print(f"\nUkázka (5 {'nejnovějších' if order else 'prvních'}):")
for row in rows:
print()
for name, val in zip(col_names, row):
s = serialize(val)
if s != 'NULL':
print(f" {name:<30} {s}")
except Exception as e:
print(f" Ukázka selhala: {e}")
return col_names
# ── Prozkoumáme klíčové tabulky ───────────────────────────────────────────────
for tbl in ['NES', 'NP', 'SOC_NEPRITOMNOST']:
inspect_table(tbl)
# ── Speciálně: aktivní PN ─────────────────────────────────────────────────────
print(f"\n\n{'=' * 70}")
print(" POKUS: aktivní záznamy v NES (konec NULL nebo v budoucnosti)")
print(f"{'=' * 70}")
dnes = datetime.date.today()
# Nejdřív zjistíme sloupce NES
cur.execute("""
SELECT TRIM(rf.rdb$field_name), f.rdb$field_type
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE TRIM(rf.rdb$relation_name) = 'NES'
ORDER BY rf.rdb$field_position
""")
nes_cols = [(r[0], r[1]) for r in cur.fetchall()]
nes_col_names = [c[0] for c in nes_cols]
print(f"\nVšechny sloupce NES: {', '.join(nes_col_names)}")
# Hledáme sloupce s datumem konce PN
date_cols_nes = [c[0] for c in nes_cols if c[1] in (12, 35)]
print(f"Datumové sloupce: {date_cols_nes}")
# Zkusíme různé kandidátní sloupce pro "datum do"
candidates_do = [c for c in nes_col_names if 'DO' in c or 'KONEC' in c or 'END' in c.upper()]
candidates_od = [c for c in nes_col_names if 'OD' in c or 'ZACATEK' in c or 'VZNIK' in c or 'START' in c.upper()]
candidates_pac = [c for c in nes_col_names if 'PAC' in c or 'IDPAC' in c]
print(f"Kandidáti DATUM_DO: {candidates_do}")
print(f"Kandidáti DATUM_OD: {candidates_od}")
print(f"Kandidáti IDPAC: {candidates_pac}")
# Ukázka posledních 10 záznamů NES seřazená dle prvního datumového sloupce
if date_cols_nes:
try:
cur.execute(f'SELECT FIRST 10 * FROM NES ORDER BY {date_cols_nes[0]} DESC')
rows = cur.fetchall()
print(f"\nPosledních 10 záznamů NES (dle {date_cols_nes[0]} DESC):")
for row in rows:
print()
for name, val in zip(nes_col_names, row):
s = serialize(val)
if s != 'NULL':
print(f" {name:<30} {s}")
except Exception as e:
print(f"Selhalo: {e}")
# ── NP tabulka stejný rozbor ────────────────────────────────────────────────
print(f"\n\n{'=' * 70}")
print(" POKUS: aktivní záznamy v NP")
print(f"{'=' * 70}")
cur.execute("""
SELECT TRIM(rf.rdb$field_name), f.rdb$field_type
FROM rdb$relation_fields rf
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
WHERE TRIM(rf.rdb$relation_name) = 'NP'
ORDER BY rf.rdb$field_position
""")
np_cols = [(r[0], r[1]) for r in cur.fetchall()]
np_col_names = [c[0] for c in np_cols]
print(f"\nVšechny sloupce NP: {', '.join(np_col_names)}")
date_cols_np = [c[0] for c in np_cols if c[1] in (12, 35)]
print(f"Datumové sloupce: {date_cols_np}")
if date_cols_np:
try:
cur.execute(f'SELECT FIRST 10 * FROM NP ORDER BY {date_cols_np[0]} DESC')
rows = cur.fetchall()
print(f"\nPosledních 10 záznamů NP (dle {date_cols_np[0]} DESC):")
for row in rows:
print()
for name, val in zip(np_col_names, row):
s = serialize(val)
if s != 'NULL':
print(f" {name:<30} {s}")
except Exception as e:
print(f"Selhalo: {e}")
cur.close()
conn.close()
print(f"\n{'=' * 70}")
print("Hotovo.")
@@ -0,0 +1,208 @@
"""
003_aktivni_PN_seznam.py
========================
Vypíše seznam pacientů s aktivní pracovní neschopností (PN) k dnešnímu datu.
SQL logika převzata přímo z Medicusu (report "Přehled práce neschopných pacientů"):
- ZACNES <= dnes
- (KONNES >= dnes) OR (KONNES IS NULL)
- PRACNE = 'A'
- STORNO <> 'T'
Spouštění: tlačítkem z Medicusu nebo přímo pythonu.
"""
import sys, os, traceback
_LOG = r"C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude\003_error.log"
def _log_exception(exc_type, exc_value, exc_tb):
with open(_LOG, "w", encoding="utf-8") as f:
traceback.print_exception(exc_type, exc_value, exc_tb, file=f)
sys.excepthook = _log_exception
import fdb
import datetime
import tkinter as tk
from tkinter import ttk
# ── Připojení ─────────────────────────────────────────────────────────────────
DSN = r'localhost:c:\medicus 3\data\medicus.fdb'
USER = 'SYSDBA'
PASSWORD = 'masterkey'
CHARSET = 'win1250'
# ── SQL ───────────────────────────────────────────────────────────────────────
SQL = """
SELECT
kar.PRIJMENI,
kar.JMENO,
kar.RODCIS,
nes.ZACNES,
nes.KONNES,
nes.DIAGNO,
COALESCE(nes.ECN, nes.CISNES) AS CISNES
FROM NES nes
JOIN KAR kar ON kar.IDPAC = nes.IDPAC
WHERE
nes.ZACNES <= ?
AND (nes.KONNES >= ? OR nes.KONNES IS NULL)
AND nes.PRACNE = 'A'
AND nes.STORNO <> 'T'
ORDER BY kar.PRIJMENI, kar.JMENO
"""
def nacti_data():
dnes = datetime.date.today()
conn = fdb.connect(dsn=DSN, user=USER, password=PASSWORD, charset=CHARSET)
try:
cur = conn.cursor()
cur.execute(SQL, (dnes, dnes))
rows = cur.fetchall()
return dnes, rows
finally:
conn.close()
def delka_PN(zacnes, konnes, dnes):
"""Počet dní PN (od začátku do dnes nebo do konce)."""
do = konnes if konnes else dnes
return (do - zacnes).days + 1
def zobraz_okno(dnes, rows):
root = tk.Tk()
root.title(f"Aktivní pracovní neschopnost {dnes.strftime('%d.%m.%Y')}")
w, h = 900, 550
root.update_idletasks()
sw = root.winfo_screenwidth()
sh = root.winfo_screenheight()
x = (sw - w) // 2
y = (sh - h) // 2
root.geometry(f"{w}x{h}+{x}+{y}")
# ── Záhlaví ───────────────────────────────────────────────────────────────
hlavicka = tk.Frame(root, bg="#2c5f8a", pady=8)
hlavicka.pack(fill=tk.X)
tk.Label(
hlavicka,
text=f"Pracovní neschopnost aktivní k {dnes.strftime('%d.%m.%Y')} ({len(rows)} pacientů)",
font=("Segoe UI", 13, "bold"),
fg="white", bg="#2c5f8a"
).pack()
# ── Tabulka ───────────────────────────────────────────────────────────────
cols = ("Příjmení", "Jméno", "Rodné číslo", "Začátek", "Konec", "Dní", "Diagnóza", "Č. neschopenky")
frame = tk.Frame(root)
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
scroll_y = tk.Scrollbar(frame, orient=tk.VERTICAL)
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
scroll_x = tk.Scrollbar(frame, orient=tk.HORIZONTAL)
scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
tree = ttk.Treeview(
frame,
columns=cols,
show="headings",
yscrollcommand=scroll_y.set,
xscrollcommand=scroll_x.set
)
scroll_y.config(command=tree.yview)
scroll_x.config(command=tree.xview)
# Šířky sloupců
widths = [120, 100, 110, 85, 85, 50, 80, 150]
for col, w in zip(cols, widths):
tree.column(col, width=w, anchor=tk.W)
# ── Řazení kliknutím na záhlaví ───────────────────────────────────────────
_sort_state = {} # col -> ascending True/False
def sort_by(col):
ascending = not _sort_state.get(col, False)
_sort_state[col] = ascending
# Index sloupce pro čtení hodnoty
col_idx = cols.index(col)
data = [(tree.set(k, col), k) for k in tree.get_children("")]
# Číselné řazení pro "Dní"
if col == "Dní":
data.sort(key=lambda t: int(t[0]) if t[0].isdigit() else 0, reverse=not ascending)
else:
data.sort(key=lambda t: t[0].lower(), reverse=not ascending)
for idx, (_, k) in enumerate(data):
tree.move(k, "", idx)
# Šipka v záhlaví
for c in cols:
arrow = ("" if ascending else "") if c == col else ""
tree.heading(c, text=c + arrow, command=lambda c=c: sort_by(c))
# Obarvi znovu střídavě
for idx, (_, k) in enumerate(data):
current_tags = list(tree.item(k, "tags"))
dlouha = "dlouha" in current_tags
if dlouha:
tree.item(k, tags=("dlouha",))
else:
tree.item(k, tags=("even" if idx % 2 == 0 else "odd",))
for col in cols:
tree.heading(col, text=col, command=lambda c=col: sort_by(c))
# Střídavé barvy řádků
tree.tag_configure("even", background="#f0f4f8")
tree.tag_configure("odd", background="white")
tree.tag_configure("dlouha", foreground="#c0392b") # červeně > 90 dní
for i, row in enumerate(rows):
prijmeni, jmeno, rodcis, zacnes, konnes, diagno, cisnes = row
dni = delka_PN(zacnes, konnes, dnes)
konec_str = konnes.strftime("%d.%m.%Y") if konnes else "trvá"
diagno_str = (diagno or "").strip()
cisnes_str = (cisnes or "").strip()
tag = "dlouha" if dni > 90 else ("even" if i % 2 == 0 else "odd")
tree.insert("", tk.END, values=(
prijmeni,
jmeno,
rodcis,
zacnes.strftime("%d.%m.%Y"),
konec_str,
dni,
diagno_str,
cisnes_str,
), tags=(tag,))
tree.pack(fill=tk.BOTH, expand=True)
# ── Výchozí řazení: Dní vzestupně ─────────────────────────────────────────
sort_by("Dní")
# ── Legenda + zavřít ──────────────────────────────────────────────────────
spodek = tk.Frame(root, pady=6)
spodek.pack(fill=tk.X)
tk.Label(
spodek,
text="Červeně = PN delší než 90 dní",
font=("Segoe UI", 9),
fg="#c0392b"
).pack(side=tk.LEFT, padx=15)
tk.Button(
spodek,
text="Zavřít",
command=root.destroy,
width=12,
font=("Segoe UI", 10)
).pack(side=tk.RIGHT, padx=15)
root.mainloop()
# ── Hlavní tok ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
try:
dnes, rows = nacti_data()
zobraz_okno(dnes, rows)
except Exception:
with open(_LOG, "w", encoding="utf-8") as f:
traceback.print_exc(file=f)
raise

Some files were not shown because too many files have changed in this diff Show More