Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f61cf04da | |||
| 4f586f4b57 |
@@ -1,185 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from datetime import datetime
|
|
||||||
import zipfile
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from EmailMessagingGraph import send_mail
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# CONFIG
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
|
|
||||||
FB_USER = "SYSDBA"
|
|
||||||
FB_PASS = "masterkey"
|
|
||||||
FB_PORT = "3050"
|
|
||||||
|
|
||||||
MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB"
|
|
||||||
EXT_DIR = Path(r"c:\medicusext")
|
|
||||||
BACKUP_DIR = Path(r"U:\medicusbackup")
|
|
||||||
|
|
||||||
MAIL_TO = "vladimir.buzalka@buzalka.cz"
|
|
||||||
|
|
||||||
CHUNK = 8 * 1024 * 1024 # 8 MB
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# HELPERS
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def run_gbak(label: str, db_conn: str, fbk: Path, log: Path) -> dict:
|
|
||||||
"""Run gbak, return result dict (without zip info)."""
|
|
||||||
result = {
|
|
||||||
"label": label,
|
|
||||||
"ok": False,
|
|
||||||
"fbk": fbk,
|
|
||||||
"fbk_size": 0,
|
|
||||||
"zip_size": 0,
|
|
||||||
"t_gbak": 0,
|
|
||||||
"t_zip": 0,
|
|
||||||
"error": None,
|
|
||||||
}
|
|
||||||
print(f"GBAK: {label} ... ", end="", flush=True)
|
|
||||||
t0 = time.time()
|
|
||||||
cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"]
|
|
||||||
with open(log, "w", encoding="utf-8") as f:
|
|
||||||
subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True)
|
|
||||||
result["t_gbak"] = time.time() - t0
|
|
||||||
result["fbk_size"] = fbk.stat().st_size
|
|
||||||
print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)")
|
|
||||||
result["ok"] = True
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def zip_single(label: str, fbk: Path, zipf: Path) -> tuple[int, float]:
|
|
||||||
"""ZIP one FBK into its own ZIP. Returns (zip_size, t_zip)."""
|
|
||||||
t1 = time.time()
|
|
||||||
processed = 0
|
|
||||||
fbk_size = fbk.stat().st_size
|
|
||||||
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
|
||||||
zi = zipfile.ZipInfo(fbk.name)
|
|
||||||
zi.compress_type = zipfile.ZIP_DEFLATED
|
|
||||||
with zf.open(zi, "w", force_zip64=True) as z:
|
|
||||||
with open(fbk, "rb") as src:
|
|
||||||
while buf := src.read(CHUNK):
|
|
||||||
z.write(buf)
|
|
||||||
processed += len(buf)
|
|
||||||
pct = processed * 100 / fbk_size
|
|
||||||
print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True)
|
|
||||||
print()
|
|
||||||
return zipf.stat().st_size, time.time() - t1
|
|
||||||
|
|
||||||
|
|
||||||
def zip_multiple(fbk_results: list[dict], zipf: Path) -> tuple[int, float]:
|
|
||||||
"""ZIP multiple FBK files into one ZIP. Returns (zip_size, t_zip)."""
|
|
||||||
t1 = time.time()
|
|
||||||
total_fbk_size = sum(r["fbk_size"] for r in fbk_results)
|
|
||||||
total_processed = 0
|
|
||||||
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
|
||||||
for r in fbk_results:
|
|
||||||
fbk = r["fbk"]
|
|
||||||
zi = zipfile.ZipInfo(fbk.name)
|
|
||||||
zi.compress_type = zipfile.ZIP_DEFLATED
|
|
||||||
with zf.open(zi, "w", force_zip64=True) as z:
|
|
||||||
with open(fbk, "rb") as src:
|
|
||||||
while buf := src.read(CHUNK):
|
|
||||||
z.write(buf)
|
|
||||||
total_processed += len(buf)
|
|
||||||
pct = total_processed * 100 / total_fbk_size
|
|
||||||
print(f"\r ZIP {fbk.name}: {pct:6.2f}%", end="", flush=True)
|
|
||||||
print()
|
|
||||||
return zipf.stat().st_size, time.time() - t1
|
|
||||||
|
|
||||||
|
|
||||||
def format_result(r: dict) -> str:
|
|
||||||
ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0
|
|
||||||
return (
|
|
||||||
f" {r['label']}: "
|
|
||||||
f"FBK {r['fbk_size']/1024/1024:.1f} MB → "
|
|
||||||
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
|
|
||||||
f"({ratio:.0f}% komprese, "
|
|
||||||
f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# MAIN
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def main():
|
|
||||||
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
now = datetime.now()
|
|
||||||
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
|
|
||||||
|
|
||||||
backed_up = []
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
# 1) Hlavní DB – MEDICUS.FDB → vlastní ZIP
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
|
|
||||||
zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip"
|
|
||||||
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
|
|
||||||
try:
|
|
||||||
r = run_gbak("MEDICUS", MAIN_DB, fbk, log)
|
|
||||||
log.unlink()
|
|
||||||
zip_size, t_zip = zip_single("MEDICUS", fbk, zipf)
|
|
||||||
fbk.unlink()
|
|
||||||
r["zip_size"] = zip_size
|
|
||||||
r["t_zip"] = t_zip
|
|
||||||
backed_up.append(r)
|
|
||||||
except Exception:
|
|
||||||
errors.append({"label": "MEDICUS", "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
|
|
||||||
for f in (fbk, log):
|
|
||||||
if f.exists():
|
|
||||||
f.unlink()
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
# 2) Externí DB – MEDICUS_FILES_*.fdb → všechny do jednoho ZIP
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
fdb_all = sorted(
|
|
||||||
set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")),
|
|
||||||
key=lambda p: p.name.lower(),
|
|
||||||
)
|
|
||||||
|
|
||||||
ext_results = []
|
|
||||||
for fdb in fdb_all:
|
|
||||||
name = fdb.stem
|
|
||||||
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
|
|
||||||
log = BACKUP_DIR / f"{name}_{ts}.log"
|
|
||||||
db_conn = f"localhost/{FB_PORT}:{fdb}"
|
|
||||||
try:
|
|
||||||
r = run_gbak(name, db_conn, fbk, log)
|
|
||||||
log.unlink()
|
|
||||||
ext_results.append(r)
|
|
||||||
except Exception:
|
|
||||||
errors.append({"label": name, "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
|
|
||||||
for f in (fbk, log):
|
|
||||||
if f.exists():
|
|
||||||
f.unlink()
|
|
||||||
|
|
||||||
# ZIP všechny externí FBK do jednoho souboru
|
|
||||||
if ext_results:
|
|
||||||
ext_zip = BACKUP_DIR / f"MEDICUS_FILES_{ts}.zip"
|
|
||||||
print(f"\nZIP externích DB → {ext_zip.name}")
|
|
||||||
try:
|
|
||||||
zip_size, t_zip = zip_multiple(ext_results, ext_zip)
|
|
||||||
for r in ext_results:
|
|
||||||
r["zip_size"] = zip_size # sdílená velikost výsledného ZIPu
|
|
||||||
r["t_zip"] = t_zip
|
|
||||||
r["fbk"].unlink()
|
|
||||||
backed_up.append(r)
|
|
||||||
except Exception:
|
|
||||||
errors.append({"label": "MEDICUS_FILES (zip)", "fbk_size": 0, "zip_size": 0, "t_gbak": 0, "t_zip": 0, "error": traceback.format_exc()})
|
|
||||||
for r in ext_results:
|
|
||||||
if r["fbk"].exists():
|
|
||||||
r["fbk"].unlink()
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
# Report
|
|
||||||
# ----------------------------------------------------------
|
|
||||||
total = 1 + len(fdb_all)
|
|
||||||
report = [
|
|
||||||
f"Backup Medicus – {now.strftime('%d.%m.%Y %H:%M')}",
|
|
||||||
f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}",
|
|
||||||
f"Výstupní adresář: {BACKUP_DIR}",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
if backed_up:
|
|
||||||
report.append("--- Zálohováno ---")
|
|
||||||
# Hlavní DB
|
|
||||||
main_results = [r for r in backed_up if r["label"] == "MEDICUS"]
|
|
||||||
ext_backed = [r for r in backed_up if r["label"] != "MEDICUS"]
|
|
||||||
for r in main_results:
|
|
||||||
report.append(format_result(r))
|
|
||||||
if ext_backed:
|
|
||||||
total_ext_fbk = sum(r["fbk_size"] for r in ext_backed)
|
|
||||||
ext_zip_size = ext_backed[0]["zip_size"] if ext_backed else 0
|
|
||||||
ratio = 100 * (1 - ext_zip_size / total_ext_fbk) if total_ext_fbk else 0
|
|
||||||
report.append(f" Externí DB ({len(ext_backed)} souborů):")
|
|
||||||
for r in ext_backed:
|
|
||||||
report.append(f" {r['label']}: FBK {r['fbk_size']/1024/1024:.1f} MB (gbak {r['t_gbak']:.0f}s)")
|
|
||||||
report.append(
|
|
||||||
f" → společný ZIP: {ext_zip_size/1024/1024:.1f} MB "
|
|
||||||
f"({ratio:.0f}% komprese, zip {ext_backed[0]['t_zip']:.0f}s)"
|
|
||||||
)
|
|
||||||
total_zip = sum(r["zip_size"] for r in main_results) + (ext_backed[0]["zip_size"] if ext_backed else 0)
|
|
||||||
report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB")
|
|
||||||
report.append("")
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
report.append("--- CHYBY ---")
|
|
||||||
for e in errors:
|
|
||||||
report.append(f" {e['label']}:\n{e['error']}")
|
|
||||||
report.append("")
|
|
||||||
|
|
||||||
has_errors = bool(errors)
|
|
||||||
subject = (
|
|
||||||
f"{'X' if has_errors else 'OK'} MEDICUS backup "
|
|
||||||
f"{len(backed_up)}/{total}"
|
|
||||||
+ (f" – {len(errors)} chyb" if has_errors else "")
|
|
||||||
)
|
|
||||||
|
|
||||||
send_mail(MAIL_TO, subject, "\n".join(report))
|
|
||||||
print("\n" + "\n".join(report))
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
raise RuntimeError(f"{len(errors)} backup(s) failed")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -18,8 +18,8 @@ FB_USER = "SYSDBA"
|
|||||||
FB_PASS = "masterkey"
|
FB_PASS = "masterkey"
|
||||||
FB_PORT = "3050"
|
FB_PORT = "3050"
|
||||||
|
|
||||||
SRC_DIR = Path(r"c:\medicusext")
|
SRC_DIR = Path(r"U:\externi")
|
||||||
BACKUP_DIR = Path(r"U:\medicusbackup")
|
BACKUP_DIR = Path(r"U:\externabackup")
|
||||||
|
|
||||||
MAIL_TO = "vladimir.buzalka@buzalka.cz"
|
MAIL_TO = "vladimir.buzalka@buzalka.cz"
|
||||||
|
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
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'u:\Dropbox\Ordinace\Reporty'
|
|
||||||
NAZEV_REPORTU = 'Dekurzy'
|
|
||||||
DATUM_OD = '2025-01-01'
|
|
||||||
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 = ['Rec', 'VykA', 'Files', 'MEDLAB', 'Lab', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac']
|
|
||||||
|
|
||||||
# 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
|
|
||||||
rows.append((datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni))
|
|
||||||
|
|
||||||
# ── 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, _ in rows for rid in ids_by_typ[typ]})
|
|
||||||
|
|
||||||
rec_det = fetch_details(cur, 'RECEPT', 'ID', 'ID', ['LEK','DSIG'], get_ids(rows,'Rec'))
|
|
||||||
vyka_det = fetch_details(cur, 'DOKLADD', 'ID', 'ID', ['KOD','DDGN'], get_ids(rows,'VykA'))
|
|
||||||
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'),
|
|
||||||
'MedLab': ('4A235A', 'F5EEF8'),
|
|
||||||
'Lab': ('145A32', 'D5F5E3'),
|
|
||||||
'Očkování': ('7E5109', 'FDEBD0'),
|
|
||||||
'Neschop.': ('922B21', 'FADBD8'),
|
|
||||||
'Léčiva': ('1A5276', 'D6EAF8'),
|
|
||||||
'SpecVys': ('0B5345', 'D1F2EB'),
|
|
||||||
'Platby': ('4D5656', 'EAECEE'),
|
|
||||||
'Ostatní': ('2C3E50', 'EBF5FB'),
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
cell.hyperlink = f'#{cil_list}!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()
|
|
||||||
|
|
||||||
# Pořadí listů a jejich konfigurace: (název, typ_bookmarku, detail_dict, sloupce, šířky, pk_label)
|
|
||||||
LISTY = [
|
|
||||||
('Recepty', 'Rec', rec_det, ['Datum','Jméno','Recept','Dávkování'], [12,25,25,12], None),
|
|
||||||
('Výkony', 'VykA', vyka_det, ['Datum','Jméno','Kód výkonu','Diagnóza'], [12,25,14,10], None),
|
|
||||||
('Soubory', 'Files', files_det, ['Datum','Jméno','Soubor','Datum souboru'], [12,25,35,14], None),
|
|
||||||
('MedLab', 'MEDLAB', medlab_det, ['Datum','Jméno','Typ'], [12,25,15], None),
|
|
||||||
('Lab', 'Lab', lab_det, ['Datum','Jméno','Číslo'], [12,25,20], None),
|
|
||||||
('Očkování', 'Ock', ock_det, ['Datum','Jméno','Datum očkování','Vakcína'], [12,25,14,30], None),
|
|
||||||
('Neschop.', 'Nes', nes_det, ['Datum','Jméno','Od','Do'], [12,25,12,12], None),
|
|
||||||
('Léčiva', 'Lec', lec_det, ['Datum','Jméno','Kód','Datum výkonu'], [12,25,12,14], None),
|
|
||||||
('SpecVys', 'SpecVys', spec_det, ['Datum','Jméno','Typ vyšetření','Datum vyšetření'], [12,25,25,14], None),
|
|
||||||
('Platby', 'PlaPac', pla_det, ['Datum','Jméno','Datum platby','Částka','Doklad'], [12,25,14,12,15], None),
|
|
||||||
('Ostatní', None, None, ['Datum','Jméno','Typ','ID','Název'], [12,25,12,10,30], None),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Vytvoříme listy
|
|
||||||
ws_d = wb.active
|
|
||||||
ws_d.title = "Dekurz"
|
|
||||||
ws_listy = {}
|
|
||||||
for nazev, *_ in LISTY:
|
|
||||||
ws_listy[nazev] = wb.create_sheet(nazev)
|
|
||||||
|
|
||||||
# Záhlaví listů
|
|
||||||
for nazev, typ, det, sloupce, sirky, _ in LISTY:
|
|
||||||
barva_hl, _ = BARVY_LISTU[nazev]
|
|
||||||
zapis_hlavicku(ws_listy[nazev], sloupce, sirky, barva_hl)
|
|
||||||
ws_listy[nazev].freeze_panes = 'A2'
|
|
||||||
|
|
||||||
# Záhlaví Dekurz
|
|
||||||
nazvy_d = ['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Pojišťovna'] + TOP_TYPY + ['Ostatní']
|
|
||||||
sirky_d = [12, 8, 8, 25, 14, 12 ] + [8]*10 + [8]
|
|
||||||
zapis_hlavicku(ws_d, nazvy_d, sirky_d, '2E75B6')
|
|
||||||
ws_d.freeze_panes = 'A2'
|
|
||||||
ws_d.auto_filter.ref = f"A1:Q{len(rows)+1}"
|
|
||||||
|
|
||||||
# Aktuální řádek pro každý list
|
|
||||||
row_ptr = {nazev: 2 for nazev, *_ in LISTY}
|
|
||||||
|
|
||||||
# ── Plnění dat ─────────────────────────────────────────────────────────────
|
|
||||||
def get_det_hodnoty(typ, rid, datum, jmeno_cel):
|
|
||||||
"""Vrátí seznam hodnot pro řádek detailního listu."""
|
|
||||||
if typ == 'Rec':
|
|
||||||
d = rec_det.get(rid, ('', ''))
|
|
||||||
return [datum, jmeno_cel, d[0] or '', d[1] or '']
|
|
||||||
elif typ == 'VykA':
|
|
||||||
d = vyka_det.get(rid, ('', ''))
|
|
||||||
return [datum, jmeno_cel, d[0] or '', (d[1] or '').strip()]
|
|
||||||
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'],
|
|
||||||
'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) in enumerate(rows, start=2):
|
|
||||||
# Ohraničení řádku Dekurz
|
|
||||||
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)
|
|
||||||
ws_d.cell(row=row_i, column=5, value=rodcis or '')
|
|
||||||
ws_d.cell(row=row_i, column=6, value=poj or '').alignment = Alignment(horizontal='center')
|
|
||||||
|
|
||||||
# Sloupce bookmarků
|
|
||||||
for col_off, (typ, pocet) in enumerate(zip(TOP_TYPY, top)):
|
|
||||||
col_i = 7 + col_off
|
|
||||||
if pocet == 0:
|
|
||||||
continue
|
|
||||||
# Najdi název listu pro tento typ
|
|
||||||
nazev_listu = next((n for n, t, *_ in LISTY if t == typ), None)
|
|
||||||
if nazev_listu and ids_by_typ[typ]:
|
|
||||||
_, barva_ll = BARVY_LISTU[nazev_listu]
|
|
||||||
hyperlink_cell(ws_d, row_i, col_i, nazev_listu, row_ptr[nazev_listu], pocet, barva_ll[1:] if len(barva_ll) > 6 else 'DCE6F1')
|
|
||||||
# Zapiš řádky na detailní list
|
|
||||||
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')
|
|
||||||
|
|
||||||
# Ostatní
|
|
||||||
if ostatni:
|
|
||||||
ws_det = ws_listy['Ostatní']
|
|
||||||
barva_hl, barva_r = BARVY_LISTU['Ostatní']
|
|
||||||
hyperlink_cell(ws_d, row_i, 17, 'Ostatní', row_ptr['Ostatní'], ostatni, barva_r)
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys, io, base64, re
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Nacti poslednich 5 PORTAL zaznamu s DATA
|
||||||
|
cur.execute("""
|
||||||
|
SELECT FIRST 5 ID, ODESLANO, IDFAK, ID_PODANI, DATA
|
||||||
|
FROM PORTAL
|
||||||
|
WHERE DATA IS NOT NULL
|
||||||
|
ORDER BY ID DESC
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
pid, odeslano, idfak, id_podani, data = row
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
print(f"PORTAL.ID={pid} ODESLANO={odeslano} IDFAK={idfak} ID_PODANI={id_podani}")
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
print("DATA: NULL")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# data je string (win1250)
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = data.decode('win1250', errors='replace')
|
||||||
|
|
||||||
|
# Parsuj XML obalku
|
||||||
|
print(f"DATA (prvnich 300 zn): {data[:300]}")
|
||||||
|
|
||||||
|
# Najdi BASE64 obsah uvnitr <Soubor ...>...</Soubor>
|
||||||
|
m = re.search(r'<Soubor[^>]*Format="BASE64"[^>]*>(.*?)</Soubor>', data, re.DOTALL)
|
||||||
|
if m:
|
||||||
|
b64_raw = m.group(1).strip()
|
||||||
|
# Dekoduj
|
||||||
|
try:
|
||||||
|
b64_clean = re.sub(r'\s+', '', b64_raw)
|
||||||
|
# Padding
|
||||||
|
b64_clean += '=' * (4 - len(b64_clean) % 4)
|
||||||
|
html_bytes = base64.b64decode(b64_clean)
|
||||||
|
html = html_bytes.decode('iso-8859-2', errors='replace')
|
||||||
|
|
||||||
|
# Extrahuj text
|
||||||
|
text = re.sub(r'<!--.*?-->', '', html, flags=re.DOTALL)
|
||||||
|
text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL)
|
||||||
|
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
|
||||||
|
text = re.sub(r'<[^>]+>', ' ', text)
|
||||||
|
text = re.sub(r' ', ' ', text)
|
||||||
|
text = re.sub(r'&', '&', text)
|
||||||
|
text = re.sub(r'\s+', ' ', text).strip()
|
||||||
|
|
||||||
|
print(f"\nDEKÓDOVANÝ HTML protokol:")
|
||||||
|
print(text[:2000])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Chyba dekódování: {e}")
|
||||||
|
else:
|
||||||
|
# Zkus plain text odpoved (ne BASE64)
|
||||||
|
m2 = re.search(r'<Soubor[^>]*>(.*?)</Soubor>', data, re.DOTALL)
|
||||||
|
if m2:
|
||||||
|
print(f"\nObsah Soubor (plain): {m2.group(1)[:500]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys, io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
for table in ['ED_BOOKOFSUBMISSIONS', 'ED_BOOKOFSUBMISSIONATTACH', 'ED_MAILBOXMESSAGE', 'ED_STORAGE']:
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
print(f"TABULKA: {table}")
|
||||||
|
print('='*70)
|
||||||
|
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = '{table}'
|
||||||
|
ORDER BY rf.RDB$FIELD_POSITION
|
||||||
|
""")
|
||||||
|
cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
print(f"Sloupce: {cols}")
|
||||||
|
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM {table}")
|
||||||
|
print(f"Počet: {cur.fetchone()[0]}")
|
||||||
|
|
||||||
|
# BLOB sloupce vynech
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE
|
||||||
|
WHERE rf.RDB$RELATION_NAME = '{table}'
|
||||||
|
AND f.RDB$FIELD_TYPE = 261
|
||||||
|
""")
|
||||||
|
blob_cols = {r[0].strip() for r in cur.fetchall()}
|
||||||
|
safe = [c for c in cols if c not in blob_cols]
|
||||||
|
|
||||||
|
# Najdi razeni
|
||||||
|
order_col = next((c for c in ['CREATEDATE','SENTDATE','CREATED','ODESLANO','DATUM','ID'] if c in cols), cols[0])
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT FIRST 10 {', '.join(safe)} FROM {table} ORDER BY {order_col} DESC")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f"\nPosledních 10 (ORDER BY {order_col} DESC):")
|
||||||
|
for row in rows:
|
||||||
|
print(f" {dict(zip(safe, row))}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Chyba SELECT: {e}")
|
||||||
|
# Zkus bez order
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT FIRST 5 {', '.join(safe)} FROM {table}")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(f" {dict(zip(safe, row))}")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"Chyba i bez ORDER: {e2}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\nHotovo.")
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys, io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("1. Sloupce HPN")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = 'HPN'
|
||||||
|
ORDER BY rf.RDB$FIELD_POSITION
|
||||||
|
""")
|
||||||
|
col_names = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
print(f" Sloupce: {col_names}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("2. Počet záznamů a max datum")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("SELECT COUNT(*) FROM HPN")
|
||||||
|
print(f" Celkem: {cur.fetchone()[0]} záznamů")
|
||||||
|
|
||||||
|
# Najdi datumove sloupce
|
||||||
|
date_candidates = [c for c in col_names if any(x in c for x in ['DAT', 'ODE', 'VYT', 'CAS'])]
|
||||||
|
print(f" Datumové sloupce: {date_candidates}")
|
||||||
|
for dc in date_candidates[:3]:
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT MIN({dc}), MAX({dc}) FROM HPN")
|
||||||
|
mn, mx = cur.fetchone()
|
||||||
|
print(f" {dc}: {mn} .. {mx}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {dc}: {e}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("3. Posledních 20 záznamů (bez BLOBů)")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Zjisti BLOB sloupce
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE
|
||||||
|
WHERE rf.RDB$RELATION_NAME = 'HPN'
|
||||||
|
AND f.RDB$FIELD_TYPE = 261
|
||||||
|
""")
|
||||||
|
blob_cols = {r[0].strip() for r in cur.fetchall()}
|
||||||
|
print(f" BLOB sloupce: {blob_cols}")
|
||||||
|
|
||||||
|
safe_cols = [c for c in col_names if c not in blob_cols]
|
||||||
|
|
||||||
|
# Zjisti razeni - zkus VYTVORENO nebo ODESLANO nebo ID
|
||||||
|
order_col = next((c for c in ['VYTVORENO', 'ODESLANO', 'DATUM', 'ID'] if c in col_names), col_names[0])
|
||||||
|
|
||||||
|
cur.execute(f"SELECT FIRST 20 {', '.join(safe_cols)} FROM HPN ORDER BY {order_col} DESC")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
d = dict(zip(safe_cols, row))
|
||||||
|
print(f" {d}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("4. Záznamy od 2026-02-01")
|
||||||
|
print("=" * 60)
|
||||||
|
for dc in date_candidates[:2]:
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM HPN WHERE {dc} >= '2026-02-01'")
|
||||||
|
cnt = cur.fetchone()[0]
|
||||||
|
if cnt > 0:
|
||||||
|
print(f" {dc} >= 2026-02-01: {cnt} záznamů")
|
||||||
|
cur.execute(f"SELECT FIRST 5 {', '.join(safe_cols)} FROM HPN WHERE {dc} >= '2026-02-01' ORDER BY {dc} DESC")
|
||||||
|
for row in cur.fetchall():
|
||||||
|
print(f" {dict(zip(safe_cols, row))}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {dc}: {e}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("5. HPN_NOTIFIKACE_DETAIL – sloupce a ukazka")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = 'HPN_NOTIFIKACE_DETAIL'
|
||||||
|
ORDER BY rf.RDB$FIELD_POSITION
|
||||||
|
""")
|
||||||
|
hnd_cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
print(f" Sloupce: {hnd_cols}")
|
||||||
|
cur.execute("SELECT COUNT(*) FROM HPN_NOTIFIKACE_DETAIL")
|
||||||
|
print(f" Záznamy: {cur.fetchone()[0]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\nHotovo.")
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys, io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("1. Sloupce HPN_PODANI")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rf.RDB$FIELD_NAME, rf.RDB$FIELD_POSITION
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = 'HPN_PODANI'
|
||||||
|
ORDER BY rf.RDB$FIELD_POSITION
|
||||||
|
""")
|
||||||
|
col_names = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
print(f" Sloupce: {col_names}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("2. Ukazka HPN_PODANI (bez BLOB ODPOVED)")
|
||||||
|
print("=" * 60)
|
||||||
|
safe_cols = [c for c in col_names if c != 'ODPOVED']
|
||||||
|
cur.execute(f"SELECT FIRST 10 {', '.join(safe_cols)} FROM HPN_PODANI ORDER BY ODESLANO DESC")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(dict(zip(safe_cols, row)))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("3. Hledej tabulky s POJ nebo ZP sloupcem + data od 2026-02")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Najdi vsechny tabulky ktere maji sloupec POJ nebo ZP
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT rf.RDB$RELATION_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME
|
||||||
|
WHERE r.RDB$SYSTEM_FLAG = 0
|
||||||
|
AND (TRIM(rf.RDB$FIELD_NAME) = 'POJ' OR TRIM(rf.RDB$FIELD_NAME) = 'ZP')
|
||||||
|
ORDER BY rf.RDB$RELATION_NAME
|
||||||
|
""")
|
||||||
|
poj_tables = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
print(f" Tabulky s POJ/ZP: {len(poj_tables)}")
|
||||||
|
|
||||||
|
# Z techto tabulek vyber ty ktere maji ODESLANO nebo DATUM a data od 2026-02
|
||||||
|
hits = []
|
||||||
|
for table in poj_tables:
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = '{table}'
|
||||||
|
AND (TRIM(rf.RDB$FIELD_NAME) LIKE '%ODESLAN%'
|
||||||
|
OR TRIM(rf.RDB$FIELD_NAME) LIKE '%VYTVOR%'
|
||||||
|
OR TRIM(rf.RDB$FIELD_NAME) = 'DATUM')
|
||||||
|
""")
|
||||||
|
date_cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
for dc in date_cols:
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-02-01'")
|
||||||
|
cnt = cur.fetchone()[0]
|
||||||
|
if cnt > 0:
|
||||||
|
hits.append((table, dc, cnt))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
hits.sort(key=lambda x: -x[2])
|
||||||
|
for table, dc, cnt in hits:
|
||||||
|
print(f" {table:<40} {dc}: {cnt} od 2026-02")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("4. Hledej tabulky s PODACI_CISLO nebo PODAC nebo ID_PODANI")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT rf.RDB$RELATION_NAME, rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME
|
||||||
|
WHERE r.RDB$SYSTEM_FLAG = 0
|
||||||
|
AND (TRIM(rf.RDB$FIELD_NAME) LIKE '%PODACI%'
|
||||||
|
OR TRIM(rf.RDB$FIELD_NAME) = 'ID_PODANI'
|
||||||
|
OR TRIM(rf.RDB$FIELD_NAME) LIKE '%PODANI%')
|
||||||
|
ORDER BY rf.RDB$RELATION_NAME
|
||||||
|
""")
|
||||||
|
for r in cur.fetchall():
|
||||||
|
print(f" {r[0].strip():<40} sloupec: {r[1].strip()}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("5. PORTAL - co tam chybi od 2026-02?")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT FIRST 5 ID, IDFAK, ODESLANO, STAV, ID_PODANI, DAVKA_ROK, BB_DAVKA, BB_FAKTURA
|
||||||
|
FROM PORTAL
|
||||||
|
ORDER BY ODESLANO DESC
|
||||||
|
""")
|
||||||
|
for row in cur.fetchall():
|
||||||
|
print(f" {row}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\nHotovo.")
|
||||||
@@ -19,6 +19,13 @@ now = datetime.now()
|
|||||||
filename = now.strftime('%Y-%m-%d_%H-%M-%S') + '_faktury.xlsx'
|
filename = now.strftime('%Y-%m-%d_%H-%M-%S') + '_faktury.xlsx'
|
||||||
output_path = os.path.join(output_dir, filename)
|
output_path = os.path.join(output_dir, filename)
|
||||||
|
|
||||||
|
# --- Složka pro soubory (KDAVKA, REQUEST atd.) ---
|
||||||
|
files_dir = os.path.join(output_dir, 'SOUBORY_PRO_FAKTURY_REPORT')
|
||||||
|
import shutil
|
||||||
|
if os.path.exists(files_dir):
|
||||||
|
shutil.rmtree(files_dir)
|
||||||
|
os.makedirs(files_dir)
|
||||||
|
|
||||||
# --- Smazání předchozích verzí ---
|
# --- Smazání předchozích verzí ---
|
||||||
for f in os.listdir(output_dir):
|
for f in os.listdir(output_dir):
|
||||||
if f.endswith('_faktury.xlsx'):
|
if f.endswith('_faktury.xlsx'):
|
||||||
@@ -33,6 +40,7 @@ wb = openpyxl.Workbook()
|
|||||||
HEADER_FILL = PatternFill('solid', fgColor='2F5496')
|
HEADER_FILL = PatternFill('solid', fgColor='2F5496')
|
||||||
HEADER_FONT = Font(bold=True, color='FFFFFF')
|
HEADER_FONT = Font(bold=True, color='FFFFFF')
|
||||||
LINK_FONT = Font(color='0563C1', underline='single')
|
LINK_FONT = Font(color='0563C1', underline='single')
|
||||||
|
PLAIN_FONT = Font(color='000000') # normální font bez modrého podtržení
|
||||||
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
|
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
|
||||||
|
|
||||||
def style_header(ws):
|
def style_header(ws):
|
||||||
@@ -262,6 +270,263 @@ for i, row in enumerate(fak_rows, start=2):
|
|||||||
else:
|
else:
|
||||||
cell.value = ''
|
cell.value = ''
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# List 5 – ED_PODANI (ED_BOOKOFSUBMISSIONS)
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
ws5 = wb.create_sheet('ED_PODANI')
|
||||||
|
|
||||||
|
REQUESTTYPE_MAP = {0: 'Registrační', 1: 'Výkonová'}
|
||||||
|
HICCODE_MAP = {
|
||||||
|
'111': 'VZP', '201': 'VoZP', '205': 'ČPZP',
|
||||||
|
'207': 'OZP', '209': 'ZPŠ', '211': 'ZPMV',
|
||||||
|
'212': '212', '213': '213', '217': '217', '228': '228', '333': '333',
|
||||||
|
}
|
||||||
|
|
||||||
|
cur.execute('''
|
||||||
|
SELECT
|
||||||
|
ID, CREATED, SENTDATE, HICCODE, REQUESTTYPE,
|
||||||
|
SUBMISSIONID, INVOICENUMBER,
|
||||||
|
PERIODFROM, PERIODTO, TOTALSUM,
|
||||||
|
STATE, CREATOR, HCPPERSONNAME, HCPCODE, UNIQUEID
|
||||||
|
FROM ED_BOOKOFSUBMISSIONS
|
||||||
|
ORDER BY ID DESC
|
||||||
|
''')
|
||||||
|
ed_cols_raw = [d[0] for d in cur.description]
|
||||||
|
ed_rows = cur.fetchall()
|
||||||
|
|
||||||
|
# Lidštější záhlaví
|
||||||
|
ed_headers = [
|
||||||
|
'ID', 'Vytvořeno', 'Odesláno', 'ZP', 'Typ podání',
|
||||||
|
'Podací č.', 'Faktura',
|
||||||
|
'Období od', 'Období do', 'Částka (Kč)',
|
||||||
|
'Stav', 'Autor', 'Lékař', 'IČZ', 'UUID',
|
||||||
|
]
|
||||||
|
ws5.append(ed_headers)
|
||||||
|
|
||||||
|
# Mapa CISFAK → řádek na listu FAK (pro hyperlink)
|
||||||
|
cisfak_to_fak_row = {}
|
||||||
|
for i, row in enumerate(fak_rows, start=2):
|
||||||
|
cisfak = row[1] # CISFAK je druhý sloupec z DB (index 1)
|
||||||
|
if cisfak and cisfak not in cisfak_to_fak_row:
|
||||||
|
cisfak_to_fak_row[cisfak] = i
|
||||||
|
|
||||||
|
for i, row in enumerate(ed_rows, start=2):
|
||||||
|
(eid, created, sentdate, hiccode, reqtype,
|
||||||
|
submid, invoicenum,
|
||||||
|
perfrom, perto, totalsum,
|
||||||
|
state, creator, hcpperson, hcpcode, uniqueid) = row
|
||||||
|
|
||||||
|
out = [
|
||||||
|
eid,
|
||||||
|
fmt(created),
|
||||||
|
fmt(sentdate),
|
||||||
|
f"{hiccode} {HICCODE_MAP.get(str(hiccode), '')}" if hiccode else '',
|
||||||
|
REQUESTTYPE_MAP.get(reqtype, str(reqtype)) if reqtype is not None else '',
|
||||||
|
fmt(submid),
|
||||||
|
fmt(invoicenum),
|
||||||
|
fmt(perfrom),
|
||||||
|
fmt(perto),
|
||||||
|
float(totalsum) if totalsum is not None else '',
|
||||||
|
fmt(state),
|
||||||
|
fmt(creator),
|
||||||
|
fmt(hcpperson),
|
||||||
|
fmt(hcpcode),
|
||||||
|
fmt(uniqueid),
|
||||||
|
]
|
||||||
|
ws5.append(out)
|
||||||
|
if i % 2 == 0:
|
||||||
|
for cell in ws5[i]:
|
||||||
|
cell.fill = ZEBRA_FILL
|
||||||
|
|
||||||
|
style_header(ws5)
|
||||||
|
ws5.freeze_panes = 'A2'
|
||||||
|
autofit(ws5)
|
||||||
|
|
||||||
|
# Hyperlink: Faktura (sloupec 7) → list FAK
|
||||||
|
for i, row in enumerate(ed_rows, start=2):
|
||||||
|
invoicenum = row[6] # INVOICENUMBER
|
||||||
|
if invoicenum and invoicenum in cisfak_to_fak_row:
|
||||||
|
cell = ws5.cell(row=i, column=7)
|
||||||
|
# +1 kvůli vloženému sloupci FAKDET na listu FAK
|
||||||
|
cell.hyperlink = f'#FAK!B{cisfak_to_fak_row[invoicenum]}'
|
||||||
|
cell.font = LINK_FONT
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# List 6 – ED_PODANI_DATA (BLOBy z ED_BOOKOFSUBMISSIONS)
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
ws6 = wb.create_sheet('ED_PODANI_DATA')
|
||||||
|
|
||||||
|
cur.execute('''
|
||||||
|
SELECT ID, HICCODE, SENTDATE, SUBMISSIONID, INVOICENUMBER,
|
||||||
|
KDAVKACONTENT, REQUEST, SERVERRESPONSE, PROTOCOL
|
||||||
|
FROM ED_BOOKOFSUBMISSIONS
|
||||||
|
ORDER BY ID DESC
|
||||||
|
''')
|
||||||
|
ed_data_rows = cur.fetchall()
|
||||||
|
|
||||||
|
ed_data_headers = ['ID', 'ZP', 'Odesláno', 'Podací č.', 'Faktura',
|
||||||
|
'KDAVKA', 'REQUEST (XML)', 'SERVERRESPONSE', 'PROTOCOL']
|
||||||
|
ws6.append(ed_data_headers)
|
||||||
|
|
||||||
|
def decode_ed_blob(v, enc):
|
||||||
|
"""Dekóduj BLOB z ED_BOOKOFSUBMISSIONS."""
|
||||||
|
if v is None:
|
||||||
|
return ''
|
||||||
|
if hasattr(v, 'read'):
|
||||||
|
raw = v.read()
|
||||||
|
else:
|
||||||
|
raw = v
|
||||||
|
if not raw:
|
||||||
|
return ''
|
||||||
|
if isinstance(raw, str):
|
||||||
|
# fdb vrátil jako win1250 string – zrekonstruuj bytes
|
||||||
|
raw = raw.encode('cp1250', errors='replace')
|
||||||
|
try:
|
||||||
|
text = raw.decode(enc, errors='replace')
|
||||||
|
except Exception:
|
||||||
|
return repr(raw[:200])
|
||||||
|
# Odstraň prázdné řádky a sjednoť odřádkování
|
||||||
|
lines = [l for l in text.splitlines() if l.strip()]
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
for i, row in enumerate(ed_data_rows, start=2):
|
||||||
|
eid, hiccode, sentdate, submid, invoicenum, kdavka, request, serverresp, protocol = row
|
||||||
|
|
||||||
|
# KDAVKACONTENT – fdb vrací str přes win1250 spojení, použij přímo
|
||||||
|
if kdavka is None:
|
||||||
|
kdavka_txt = ''
|
||||||
|
else:
|
||||||
|
raw = kdavka.read() if hasattr(kdavka, 'read') else kdavka
|
||||||
|
if isinstance(raw, bytes):
|
||||||
|
raw = raw.encode('latin-1', errors='replace').decode('cp852', errors='replace')
|
||||||
|
lines = [l for l in raw.splitlines() if l.strip()]
|
||||||
|
kdavka_txt = '\n'.join(lines)
|
||||||
|
|
||||||
|
# REQUEST – XML; může být uložen jako UTF-16 binary nebo jako ASCII/UTF-8 string
|
||||||
|
request_txt = ''
|
||||||
|
if request is not None:
|
||||||
|
raw = request.read() if hasattr(request, 'read') else request
|
||||||
|
if isinstance(raw, str):
|
||||||
|
raw_b = raw.encode('latin-1', errors='replace')
|
||||||
|
else:
|
||||||
|
raw_b = raw
|
||||||
|
if raw_b:
|
||||||
|
# Detekce podle BOM – jedině tehdy jde o skutečné UTF-16 binární data
|
||||||
|
if raw_b[:2] in (b'\xff\xfe', b'\xfe\xff'):
|
||||||
|
request_txt = raw_b.decode('utf-16', errors='replace')
|
||||||
|
elif raw_b[:1] == b'<':
|
||||||
|
# ASCII/UTF-8 XML – fdb ho vrátil jako string, použij přímo
|
||||||
|
request_txt = raw_b.decode('utf-8', errors='replace')
|
||||||
|
else:
|
||||||
|
request_txt = raw_b.decode('cp1250', errors='replace')
|
||||||
|
lines = [l for l in request_txt.splitlines() if l.strip()]
|
||||||
|
request_txt = '\n'.join(lines)
|
||||||
|
|
||||||
|
# SERVERRESPONSE a PROTOCOL – latin-1 re-encoding, pak iso-8859-2
|
||||||
|
def decode_latin_blob(v, enc):
|
||||||
|
if v is None:
|
||||||
|
return ''
|
||||||
|
raw = v.read() if hasattr(v, 'read') else v
|
||||||
|
if not raw:
|
||||||
|
return ''
|
||||||
|
if isinstance(raw, str):
|
||||||
|
raw = raw.encode('latin-1', errors='replace')
|
||||||
|
text = raw.decode(enc, errors='replace')
|
||||||
|
lines = [l for l in text.splitlines() if l.strip()]
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
serverresp_txt = decode_latin_blob(serverresp, 'iso-8859-2')
|
||||||
|
protocol_txt = decode_latin_blob(protocol, 'iso-8859-2')
|
||||||
|
|
||||||
|
# Ulož soubory pro klikací hyperlinky
|
||||||
|
rel_prefix = 'SOUBORY_PRO_FAKTURY_REPORT'
|
||||||
|
kdavka_link = ''
|
||||||
|
request_link = ''
|
||||||
|
serverresp_link = ''
|
||||||
|
protocol_link = ''
|
||||||
|
|
||||||
|
if kdavka_txt:
|
||||||
|
fname = f'KDAVKA_{eid}.txt'
|
||||||
|
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(kdavka_txt)
|
||||||
|
kdavka_link = f'{rel_prefix}/{fname}'
|
||||||
|
|
||||||
|
if request_txt:
|
||||||
|
fname = f'REQUEST_{eid}.xml'
|
||||||
|
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(request_txt)
|
||||||
|
request_link = f'{rel_prefix}/{fname}'
|
||||||
|
|
||||||
|
if serverresp_txt:
|
||||||
|
fname = f'SERVERRESPONSE_{eid}.xml'
|
||||||
|
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(serverresp_txt)
|
||||||
|
serverresp_link = f'{rel_prefix}/{fname}'
|
||||||
|
|
||||||
|
if protocol_txt:
|
||||||
|
fname = f'PROTOCOL_{eid}.html'
|
||||||
|
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(protocol_txt)
|
||||||
|
protocol_link = f'{rel_prefix}/{fname}'
|
||||||
|
|
||||||
|
out = [
|
||||||
|
eid,
|
||||||
|
f"{hiccode} {HICCODE_MAP.get(str(hiccode), '')}" if hiccode else '',
|
||||||
|
fmt(sentdate),
|
||||||
|
fmt(submid),
|
||||||
|
fmt(invoicenum),
|
||||||
|
kdavka_txt,
|
||||||
|
request_txt,
|
||||||
|
serverresp_txt,
|
||||||
|
protocol_txt,
|
||||||
|
]
|
||||||
|
ws6.append(out)
|
||||||
|
if i % 2 == 0:
|
||||||
|
for cell in ws6[i]:
|
||||||
|
cell.fill = ZEBRA_FILL
|
||||||
|
for cell in ws6[i]:
|
||||||
|
cell.alignment = WRAP
|
||||||
|
ws6.row_dimensions[i].height = 80
|
||||||
|
|
||||||
|
# Hyperlinky na soubory – F=KDAVKA, G=REQUEST, H=SERVERRESPONSE, I=PROTOCOL
|
||||||
|
for col_idx, link in [(6, kdavka_link), (7, request_link),
|
||||||
|
(8, serverresp_link), (9, protocol_link)]:
|
||||||
|
if link:
|
||||||
|
cell = ws6.cell(row=i, column=col_idx)
|
||||||
|
cell.hyperlink = link
|
||||||
|
cell.font = PLAIN_FONT # bez modrého podtržení
|
||||||
|
|
||||||
|
style_header(ws6)
|
||||||
|
ws6.freeze_panes = 'A2'
|
||||||
|
|
||||||
|
# Šířky sloupců ED_PODANI_DATA
|
||||||
|
for col, width in zip(['A','B','C','D','E','F','G','H','I'],
|
||||||
|
[6, 10, 12, 16, 14, 80, 60, 60, 40]):
|
||||||
|
ws6.column_dimensions[col].width = width
|
||||||
|
|
||||||
|
# Hyperlinky ED_PODANI ↔ ED_PODANI_DATA (přes ID)
|
||||||
|
ed_id_to_ws5_row = {row[0]: i for i, row in enumerate(ed_rows, start=2)}
|
||||||
|
ed_id_to_ws6_row = {row[0]: i for i, row in enumerate(ed_data_rows, start=2)}
|
||||||
|
|
||||||
|
# ED_PODANI sloupec 1 (ID) → ED_PODANI_DATA
|
||||||
|
for i, row in enumerate(ed_rows, start=2):
|
||||||
|
eid = row[0]
|
||||||
|
if eid in ed_id_to_ws6_row:
|
||||||
|
cell = ws5.cell(row=i, column=1)
|
||||||
|
cell.hyperlink = f'#ED_PODANI_DATA!A{ed_id_to_ws6_row[eid]}'
|
||||||
|
cell.font = LINK_FONT
|
||||||
|
|
||||||
|
# ED_PODANI_DATA sloupec 1 (ID) → ED_PODANI
|
||||||
|
for i, row in enumerate(ed_data_rows, start=2):
|
||||||
|
eid = row[0]
|
||||||
|
if eid in ed_id_to_ws5_row:
|
||||||
|
cell = ws6.cell(row=i, column=1)
|
||||||
|
cell.hyperlink = f'#ED_PODANI!A{ed_id_to_ws5_row[eid]}'
|
||||||
|
cell.font = LINK_FONT
|
||||||
|
cell.alignment = WRAP
|
||||||
|
|
||||||
# =====================
|
# =====================
|
||||||
# Uložení
|
# Uložení
|
||||||
# =====================
|
# =====================
|
||||||
@@ -269,4 +534,4 @@ for i, row in enumerate(fak_rows, start=2):
|
|||||||
conn.close()
|
conn.close()
|
||||||
wb.save(output_path)
|
wb.save(output_path)
|
||||||
sys.stdout.buffer.write(f'Ulozeno: {output_path}\n'.encode('utf-8'))
|
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'))
|
sys.stdout.buffer.write(f'FAK: {len(fak_rows)} radku, FAKDET: {len(det_rows)} radku, PORTAL: {len(portal_rows)} radku, ED_PODANI: {len(ed_rows)} radku\n'.encode('utf-8'))
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys, io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Ze screenshotu vime:
|
||||||
|
# - sloupce: Vytvoreno, Odeslano, ZP, Stav, Zpravy, Podaci c., Faktura
|
||||||
|
# - ZP: 111, 201, 205, 207, 209, 211
|
||||||
|
# - Podaci c.: 58933293, 174804160, 26082877, D01F260218...
|
||||||
|
# - datum: 01.03.2026, 05.03.2026, 12.03.2026, 23.03.2026, 24.03.2026
|
||||||
|
# - typ: "Reg. listy" (bez faktury), vykonove davky (s fakturou)
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("1. EOCK_DAVKA - sloupce a ukazka")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = 'EOCK_DAVKA'
|
||||||
|
ORDER BY rf.RDB$FIELD_POSITION
|
||||||
|
""")
|
||||||
|
eock_cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
print(f" Sloupce: {eock_cols}")
|
||||||
|
cur.execute("SELECT COUNT(*) FROM EOCK_DAVKA")
|
||||||
|
print(f" Pocet: {cur.fetchone()[0]}")
|
||||||
|
safe = [c for c in eock_cols if c not in ('DATA', 'DAVKA', 'ODPOVED')]
|
||||||
|
if safe:
|
||||||
|
cur.execute(f"SELECT FIRST 3 {', '.join(safe)} FROM EOCK_DAVKA ORDER BY ID DESC")
|
||||||
|
for r in cur.fetchall():
|
||||||
|
print(f" {dict(zip(safe, r))}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("2. FAK - zaznamy z brezna 2026")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT ID, CISFAK, POJ, DATVYS, DATODE, OBDOB, CENA, DRUH, STAV
|
||||||
|
FROM FAK
|
||||||
|
WHERE DATVYS >= '2026-03-01' OR DATODE >= '2026-03-01'
|
||||||
|
ORDER BY ID DESC
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f" Celkem: {len(rows)}")
|
||||||
|
for r in rows:
|
||||||
|
print(f" ID={r[0]} CISFAK={r[1]} POJ={r[2]} DATVYS={r[3]} DATODE={r[4]} OBDOB={r[5]} CENA={r[6]} DRUH={r[7]} STAV={r[8]}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("3. Hledej tabulky ktere maji sloupec VYTVORENO nebo PODACI")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT rf.RDB$RELATION_NAME, rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME
|
||||||
|
WHERE r.RDB$SYSTEM_FLAG = 0
|
||||||
|
AND (TRIM(rf.RDB$FIELD_NAME) LIKE '%VYTVOR%'
|
||||||
|
OR TRIM(rf.RDB$FIELD_NAME) LIKE '%PODACI%')
|
||||||
|
ORDER BY rf.RDB$RELATION_NAME
|
||||||
|
""")
|
||||||
|
for r in cur.fetchall():
|
||||||
|
print(f" {r[0].strip():<40} {r[1].strip()}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("4. Hledej tabulky ktere maji POJ sloupec a zaznamy z brezna 2026")
|
||||||
|
print("=" * 60)
|
||||||
|
# Vsechny tabulky s POJ sloupcem
|
||||||
|
cur.execute("""
|
||||||
|
SELECT DISTINCT rf.RDB$RELATION_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME
|
||||||
|
WHERE r.RDB$SYSTEM_FLAG = 0
|
||||||
|
AND TRIM(rf.RDB$FIELD_NAME) = 'POJ'
|
||||||
|
ORDER BY rf.RDB$RELATION_NAME
|
||||||
|
""")
|
||||||
|
poj_tables = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
|
||||||
|
for table in poj_tables:
|
||||||
|
# Najdi vsechny datumove sloupce
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = '{table}'
|
||||||
|
AND TRIM(rf.RDB$FIELD_NAME) LIKE '%DAT%'
|
||||||
|
""")
|
||||||
|
date_cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
for dc in date_cols:
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-03-01'")
|
||||||
|
cnt = cur.fetchone()[0]
|
||||||
|
if cnt > 0:
|
||||||
|
print(f" {table:<40} {dc}: {cnt} od 2026-03-01")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("5. Prohledej vsechny tabulky - ktere maji ~15 zaznamu od 2026-03-01")
|
||||||
|
print(" (v eDavky screenshotu je ~15 radku z brezna)")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT RDB$RELATION_NAME FROM RDB$RELATIONS
|
||||||
|
WHERE RDB$SYSTEM_FLAG = 0
|
||||||
|
ORDER BY RDB$RELATION_NAME
|
||||||
|
""")
|
||||||
|
all_tables = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Hledej tabulky s datumovym sloupcem a 10-50 zaznamy od 2026-03
|
||||||
|
hits = []
|
||||||
|
for table in all_tables:
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = '{table}'
|
||||||
|
AND (TRIM(rf.RDB$FIELD_NAME) IN ('ODESLANO', 'VYTVORENO', 'DATODE', 'DATVYS', 'DATUM_ODESLANI'))
|
||||||
|
""")
|
||||||
|
date_cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
for dc in date_cols:
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-03-01'")
|
||||||
|
cnt = cur.fetchone()[0]
|
||||||
|
if 5 <= cnt <= 100:
|
||||||
|
hits.append((table, dc, cnt))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
hits.sort(key=lambda x: x[2])
|
||||||
|
for table, dc, cnt in hits:
|
||||||
|
# Vypis sloupce teto tabulky
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT rf.RDB$FIELD_NAME
|
||||||
|
FROM RDB$RELATION_FIELDS rf
|
||||||
|
WHERE rf.RDB$RELATION_NAME = '{table}'
|
||||||
|
ORDER BY rf.RDB$FIELD_POSITION
|
||||||
|
""")
|
||||||
|
cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
print(f" {table:<40} {dc}: {cnt} | sloupce: {cols[:10]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print("\nHotovo.")
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Hledani tabulky pro eDavky / Kniha podani v Medicus DB.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
import fdb
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA', password='masterkey', charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("1. Tabulky s relevantními názvy")
|
||||||
|
print("=" * 60)
|
||||||
|
cur.execute("""
|
||||||
|
SELECT RDB$RELATION_NAME
|
||||||
|
FROM RDB$RELATIONS
|
||||||
|
WHERE RDB$SYSTEM_FLAG = 0
|
||||||
|
AND (
|
||||||
|
TRIM(RDB$RELATION_NAME) LIKE '%DAV%'
|
||||||
|
OR TRIM(RDB$RELATION_NAME) LIKE '%PORTAL%'
|
||||||
|
OR TRIM(RDB$RELATION_NAME) LIKE '%PODANI%'
|
||||||
|
OR TRIM(RDB$RELATION_NAME) LIKE '%EDAVK%'
|
||||||
|
OR TRIM(RDB$RELATION_NAME) LIKE '%KNIHA%'
|
||||||
|
OR TRIM(RDB$RELATION_NAME) LIKE '%PODAC%'
|
||||||
|
OR TRIM(RDB$RELATION_NAME) LIKE '%ELEK%'
|
||||||
|
)
|
||||||
|
ORDER BY RDB$RELATION_NAME
|
||||||
|
""")
|
||||||
|
tables = [row[0].strip() for row in cur.fetchall()]
|
||||||
|
for t in tables:
|
||||||
|
print(f" {t}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("2. Počty záznamů a max datum v relevantních tabulkách")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for table in tables:
|
||||||
|
try:
|
||||||
|
# Zkus najít datumový sloupec
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT RDB$FIELD_NAME FROM RDB$RELATION_FIELDS
|
||||||
|
WHERE RDB$RELATION_NAME = '{table}'
|
||||||
|
ORDER BY RDB$FIELD_POSITION
|
||||||
|
""")
|
||||||
|
cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM {table}")
|
||||||
|
count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
# Najdi datum sloupec
|
||||||
|
date_col = None
|
||||||
|
for c in cols:
|
||||||
|
if any(x in c for x in ['DAT', 'ODE', 'VYT', 'CAS', 'TIME']):
|
||||||
|
date_col = c
|
||||||
|
break
|
||||||
|
|
||||||
|
if date_col:
|
||||||
|
try:
|
||||||
|
cur.execute(f"SELECT MAX({date_col}) FROM {table}")
|
||||||
|
max_date = cur.fetchone()[0]
|
||||||
|
print(f" {table:<30} {count:>6} záznamů max {date_col}={max_date}")
|
||||||
|
except Exception:
|
||||||
|
print(f" {table:<30} {count:>6} záznamů cols: {cols[:5]}")
|
||||||
|
else:
|
||||||
|
print(f" {table:<30} {count:>6} záznamů cols: {cols[:5]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" {table:<30} chyba: {e}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("3. Aktuální stav PORTAL")
|
||||||
|
print("=" * 60)
|
||||||
|
try:
|
||||||
|
cur.execute("SELECT COUNT(*), MAX(ODESLANO) FROM PORTAL")
|
||||||
|
cnt, mx = cur.fetchone()
|
||||||
|
print(f" PORTAL: {cnt} záznamů, max ODESLANO={mx}")
|
||||||
|
cur.execute("SELECT FIRST 5 ID, IDFAK, ODESLANO, STAV, ID_PODANI, DAVKA_ROK FROM PORTAL ORDER BY ID DESC")
|
||||||
|
for row in cur.fetchall():
|
||||||
|
print(f" {row}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" PORTAL chyba: {e}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print("4. Hledání tabulek s datem >= 2026-02-01 (čerstvá data)")
|
||||||
|
print("=" * 60)
|
||||||
|
# Projdi všechny tabulky a hledej ty které mají záznamy z 2026
|
||||||
|
suspicious = []
|
||||||
|
cur.execute("""
|
||||||
|
SELECT RDB$RELATION_NAME FROM RDB$RELATIONS
|
||||||
|
WHERE RDB$SYSTEM_FLAG = 0
|
||||||
|
ORDER BY RDB$RELATION_NAME
|
||||||
|
""")
|
||||||
|
all_tables = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
|
||||||
|
for table in all_tables:
|
||||||
|
try:
|
||||||
|
# Najdi datum sloupce
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT RDB$FIELD_NAME FROM RDB$RELATION_FIELDS
|
||||||
|
WHERE RDB$RELATION_NAME = '{table}'
|
||||||
|
AND (
|
||||||
|
TRIM(RDB$FIELD_NAME) LIKE '%ODESLANO%'
|
||||||
|
OR TRIM(RDB$FIELD_NAME) LIKE '%VYTVORENO%'
|
||||||
|
OR TRIM(RDB$FIELD_NAME) LIKE '%DATUM_ODE%'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
date_cols = [r[0].strip() for r in cur.fetchall()]
|
||||||
|
for dc in date_cols:
|
||||||
|
cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-02-01'")
|
||||||
|
cnt = cur.fetchone()[0]
|
||||||
|
if cnt > 0:
|
||||||
|
suspicious.append((table, dc, cnt))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for table, dc, cnt in suspicious:
|
||||||
|
print(f" {table:<35} {dc}: {cnt} záznamů od 2026-02-01")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
print()
|
||||||
|
print("Hotovo.")
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys, io, re
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
|
||||||
|
TRACE_FILE = r'c:\Program Files\Firebird\Firebird_2_5_CGM\default_trace.log'
|
||||||
|
TIME_FROM = '2026-03-29T13:20:00'
|
||||||
|
TIME_TO = '2026-03-29T13:20:15'
|
||||||
|
|
||||||
|
# Hledame dotazy ktere pravdepodobne patri k eDavky oknu:
|
||||||
|
# - zminka o tabulkach s davkami/podanim na pojistovnu
|
||||||
|
# - sloupce jako ZP, PODACI, ODESLANO, STAV v kontextu davek
|
||||||
|
KEYWORDS = [
|
||||||
|
'EDAVKY', 'EDAVKA', 'DAVKY_POJ', 'REGISTR_POJ',
|
||||||
|
'PODACI_CISLO', 'PODACI',
|
||||||
|
'FAKDAV', 'BB_DAVKA', 'BB_FAKTURA',
|
||||||
|
'STAV_PODANI', 'ID_PODANI',
|
||||||
|
'DAVKA_ROK', 'DAVKA_DISK', 'DAVKA_CASTKA',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Tabulky ktere NECHCEME (HPN = neschopenky, RECEPT = recepty, atd.)
|
||||||
|
EXCLUDE_TABLES = ['FROM HPN', 'FROM NES', 'FROM RECEPT', 'FROM CLICKDOC',
|
||||||
|
'FROM UZIVATEL', 'FROM ZARIZENI', 'FROM ODDEL', 'FROM PRACOVISTE']
|
||||||
|
|
||||||
|
print(f"Čtu trace: {TRACE_FILE}")
|
||||||
|
print(f"Časové okno: {TIME_FROM} .. {TIME_TO}")
|
||||||
|
print(f"Hledám klíčová slova: {KEYWORDS}")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
ts_re = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}')
|
||||||
|
|
||||||
|
in_window = False
|
||||||
|
current_ts = ''
|
||||||
|
current_block = []
|
||||||
|
hits = []
|
||||||
|
all_selects_tables = {} # tabulka -> pocet vyskytu
|
||||||
|
|
||||||
|
def get_from_tables(text):
|
||||||
|
"""Extrahuj tabulky z FROM klauzule."""
|
||||||
|
tables = re.findall(r'\bFROM\s+([A-Z_][A-Z0-9_]*)', text, re.IGNORECASE)
|
||||||
|
tables += re.findall(r'\bJOIN\s+([A-Z_][A-Z0-9_]*)', text, re.IGNORECASE)
|
||||||
|
return [t.upper() for t in tables]
|
||||||
|
|
||||||
|
def process_block(ts, lines):
|
||||||
|
text = '\n'.join(lines)
|
||||||
|
if not re.search(r'\bSELECT\b', text, re.IGNORECASE):
|
||||||
|
return
|
||||||
|
|
||||||
|
text_up = text.upper()
|
||||||
|
|
||||||
|
# Spocitej tabulky
|
||||||
|
tables = get_from_tables(text_up)
|
||||||
|
for t in tables:
|
||||||
|
all_selects_tables[t] = all_selects_tables.get(t, 0) + 1
|
||||||
|
|
||||||
|
# Filtr - hledej klicova slova
|
||||||
|
for kw in KEYWORDS:
|
||||||
|
if kw in text_up:
|
||||||
|
hits.append((ts, kw, text))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Alternativne: dotazy na PORTAL nebo FAK s datumem
|
||||||
|
if ('PORTAL' in text_up or 'FAKDAV' in text_up) and 'SELECT' in text_up:
|
||||||
|
# Vynech jednoduche dotazy
|
||||||
|
if len(text) > 100:
|
||||||
|
hits.append((ts, 'PORTAL/FAKDAV', text))
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(TRACE_FILE, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.rstrip('\n')
|
||||||
|
m = ts_re.match(line)
|
||||||
|
if m:
|
||||||
|
if current_block and in_window:
|
||||||
|
process_block(current_ts, current_block)
|
||||||
|
current_ts = line[:19]
|
||||||
|
current_block = [line]
|
||||||
|
in_window = (TIME_FROM <= current_ts <= TIME_TO)
|
||||||
|
else:
|
||||||
|
if in_window:
|
||||||
|
current_block.append(line)
|
||||||
|
if current_block and in_window:
|
||||||
|
process_block(current_ts, current_block)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"CHYBA: soubor nenalezen: {TRACE_FILE}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"\nNejčastější tabulky v SELECT dotazech v okně (top 40):")
|
||||||
|
sorted_tables = sorted(all_selects_tables.items(), key=lambda x: -x[1])
|
||||||
|
for t, cnt in sorted_tables[:40]:
|
||||||
|
print(f" {t:<40} {cnt}x")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"{'='*70}")
|
||||||
|
print(f"Nalezeno relevantních bloků (klíčová slova): {len(hits)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for i, (ts, kw, text) in enumerate(hits):
|
||||||
|
print(f"{'='*70}")
|
||||||
|
print(f"[{i+1}] {ts} klíčové slovo: {kw}")
|
||||||
|
print(text[:4000])
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not hits:
|
||||||
|
print("Žádné přesné shody. Zkusíme PREPARE_STATEMENT bloky s FROM tabulkami:")
|
||||||
|
print("(viz seznam tabulek nahoře - neobvyklé názvy mohou být klíč)")
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
# 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 1130–1134 |
|
|
||||||
| `TOKS` | dokladd + vykony | Kódy 15118–15121 |
|
|
||||||
| `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`)
|
|
||||||
@@ -1,410 +0,0 @@
|
|||||||
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.")
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
"""
|
|
||||||
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()
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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()
|
|
||||||
Binary file not shown.
@@ -1,117 +0,0 @@
|
|||||||
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}")
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# 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í
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
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'))
|
|
||||||
@@ -235,6 +235,246 @@ P ... – preventivní prohlídka
|
|||||||
- Kapitace se v FAK.KAPITACE neukazuje (je 0), ale v FAKDET.CENAKAP ano – nutno ověřit
|
- 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
|
- PORTAL = registrační dávky, nesouvisí s fakturací, IDFAK bývá NULL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## eDávky – elektronické odesílání dávek pojišťovnám (zjištěno 2026-03-29)
|
||||||
|
|
||||||
|
### Přehled tabulek
|
||||||
|
|
||||||
|
Modul eDávky v Medicusu používá tabulky s prefixem `ED_`:
|
||||||
|
|
||||||
|
| Tabulka | Záznamy | Popis |
|
||||||
|
|---|---|---|
|
||||||
|
| `ED_BOOKOFSUBMISSIONS` | 998 | Hlavní tabulka – Kniha podání (od 2016) |
|
||||||
|
| `ED_BOOKOFSUBMISSIONATTACH` | 0 | Přílohy k podáním (zatím nevyužito) |
|
||||||
|
| `ED_MAILBOXMESSAGE` | 0 | Schránka zpráv od pojišťoven (zatím prázdná) |
|
||||||
|
| `ED_STORAGE` | 5 | Konfigurace – certifikáty a přihlašovací údaje |
|
||||||
|
|
||||||
|
### Jak jsme tabulku našli
|
||||||
|
Přes Firebird trace log – při otevření okna eDávky v Medicusu se nejčastěji dotazuje
|
||||||
|
na `ED_BOOKOFSUBMISSIONS` (45x) a `ED_MAILBOXMESSAGE` (4990x).
|
||||||
|
|
||||||
|
### PORTAL vs ED_BOOKOFSUBMISSIONS
|
||||||
|
- **PORTAL** (180 záznamů, max 2026-01-27) = starý systém podávání dávek
|
||||||
|
- **ED_BOOKOFSUBMISSIONS** (998 záznamů, od 2016) = nový systém (přechod ~únor 2026)
|
||||||
|
- Nepřekrývají se – žádný záznam není v obou (různá ID_PODANI/SUBMISSIONID)
|
||||||
|
- PORTAL má data od 2014, ED_BOOKOFSUBMISSIONS od 2016 (oba systémy běžely paralelně)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ED_BOOKOFSUBMISSIONS – hlavní tabulka Knihy podání
|
||||||
|
|
||||||
|
**Sloupce:**
|
||||||
|
|
||||||
|
| Sloupec | Popis |
|
||||||
|
|---|---|
|
||||||
|
| `ID` | primární klíč |
|
||||||
|
| `CREATED` | datum vytvoření |
|
||||||
|
| `SENTDATE` | datum odeslání pojišťovně |
|
||||||
|
| `CREATOR` | jméno autora (např. "Buzalková Michaela MUDr.") |
|
||||||
|
| `HCPCODE` | IČZ ordinace (09305000) |
|
||||||
|
| `HCPPERSONNAME` | jméno lékaře |
|
||||||
|
| `HICCODE` | kód pojišťovny (111, 201, 205, 207, 209, 211...) |
|
||||||
|
| `INVOICENUMBER` | číslo faktury (např. 0000260020) – NULL pro reg. dávky |
|
||||||
|
| `PERIODFROM` / `PERIODTO` | období dávky |
|
||||||
|
| `REQUESTTYPE` | typ: **0** = registrační (Reg. listy), **1** = výkonová s fakturou |
|
||||||
|
| `STATE` | stav podání (0 = odesláno OK) |
|
||||||
|
| `SUBMISSIONID` | podací číslo přidělené pojišťovnou (např. 59135047) |
|
||||||
|
| `TOTALSUM` | celková částka (0 pro reg. dávky) |
|
||||||
|
| `UNIQUEID` | UUID záznamu |
|
||||||
|
| `USERDESCRIPTION` | popis (zobrazuje se ve sloupci "Zprávy") |
|
||||||
|
| `REQUEST` | XML žádosti odeslané pojišťovně (BLOB) |
|
||||||
|
| `SERVERRESPONSE` | odpověď pojišťovny (BLOB, bytes, kódování iso-8859-2) |
|
||||||
|
| `FDAVKACONTENT` | obsah FDAVKA (BLOB) |
|
||||||
|
| `KDAVKACONTENT` | obsah KDAVKA (BLOB) |
|
||||||
|
| `PROTOCOL` | protokol (BLOB) |
|
||||||
|
|
||||||
|
**Pojišťovny v datech:** 111 (VZP), 201 (VoZP), 205 (ČPZP), 207 (OZP), 209 (ZPŠ), 211 (ZPMV)
|
||||||
|
|
||||||
|
### Kódování pojišťoven (správné!)
|
||||||
|
- **111** = VZP (Všeobecná zdravotní pojišťovna)
|
||||||
|
- **201** = VoZP (Vojenská zdravotní pojišťovna)
|
||||||
|
- **205** = ČPZP (Česká průmyslová zdravotní pojišťovna)
|
||||||
|
- **207** = OZP (Oborová zdravotní pojišťovna)
|
||||||
|
- **209** = ZPŠ (Zdravotní pojišťovna Škoda)
|
||||||
|
- **211** = ZPMV (Zdravotní pojišťovna ministerstva vnitra)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ED_STORAGE – konfigurace certifikátů a přihlašovacích údajů
|
||||||
|
|
||||||
|
Sloupce: `ID`, `NAME`, `VALUEB` (BLOB XML), `IDUZI`
|
||||||
|
|
||||||
|
**Záznamy:**
|
||||||
|
- `ServerSettingsXml` (per uživatel, IDUZI=None/2/4/6) – XML s certifikáty pro každou pojišťovnu
|
||||||
|
- `LastMessagesDownloadTime` (IDUZI=None) – datum posledního stažení zpráv
|
||||||
|
|
||||||
|
**Struktura ServerSettingsXml:**
|
||||||
|
```xml
|
||||||
|
<Settings>
|
||||||
|
<PortalZP>
|
||||||
|
<Portal Code="201">
|
||||||
|
<SigningCertificate Issuer="..." SerialNumber="..." />
|
||||||
|
</Portal>
|
||||||
|
<!-- Code: 201, 205, 207, 209, 212, 213, 217, 228, 333 -->
|
||||||
|
</PortalZP>
|
||||||
|
<PortalZPMV>
|
||||||
|
<!-- Pro ZPMV (211): PIN, Password, Email uloženy v plaintextu! -->
|
||||||
|
<Portal Code="211" PIN="..." Password="..." Email="ordinace@buzalkova.cz" />
|
||||||
|
</PortalZPMV>
|
||||||
|
<PortalVZP>
|
||||||
|
<!-- Pro VZP (111): SigningCertificate + AuthenticationCertificate -->
|
||||||
|
<Portal Code="111" UseAlternativePortal="true">
|
||||||
|
<SigningCertificate Issuer="..." SerialNumber="..." />
|
||||||
|
<AuthenticationCertificate Issuer="..." SerialNumber="..." />
|
||||||
|
</Portal>
|
||||||
|
</PortalVZP>
|
||||||
|
</Settings>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Certifikát Buzalky Vladimíra (IDUZI=6):**
|
||||||
|
- Vydavatel: I.CA EU Qualified CA2/RSA 06/2022 (První certifikační autorita, a.s.)
|
||||||
|
- SerialNumber: `0247068517B0049E2E`
|
||||||
|
- Platí pro pojišťovny: 201, 205, 207, 209, 213, 217, 228
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Formát REQUEST XML – registrační dávka (REQUESTTYPE=0)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="utf-16"?>
|
||||||
|
<SubmissionRequest>
|
||||||
|
<SubmissionType>RegistrationCards</SubmissionType>
|
||||||
|
<InvoiceType>None</InvoiceType>
|
||||||
|
<PeriodFrom>2026-03-01T00:00:00.000</PeriodFrom>
|
||||||
|
<PeriodTo>2026-03-31T00:00:00.000</PeriodTo>
|
||||||
|
<TotalSum>0</TotalSum>
|
||||||
|
<HcpCode>09305000</HcpCode> <!-- IČZ ordinace -->
|
||||||
|
<FICZ>09305000</FICZ>
|
||||||
|
<HcpOrgNum>68366370</HcpOrgNum> <!-- IČO -->
|
||||||
|
<HcpPersonName>MUDr. Buzalka Vladimír</HcpPersonName>
|
||||||
|
<HicCode>111</HicCode> <!-- kód pojišťovny -->
|
||||||
|
<HicName>Všeobecná zdravotní pojišťovna ČR</HicName>
|
||||||
|
<HicCity>0900</HicCity>
|
||||||
|
<FPOB>0900</FPOB>
|
||||||
|
<VDPOJ>1</VDPOJ>
|
||||||
|
<InvoiceItems>
|
||||||
|
<InvoiceItem>
|
||||||
|
<BatchNumber>8</BatchNumber>
|
||||||
|
<Type>DP80</Type> <!-- DP80 = registrační dávka -->
|
||||||
|
<Price>0.00</Price>
|
||||||
|
<Year>2026</Year>
|
||||||
|
<Month>3</Month>
|
||||||
|
<DBODY>0</DBODY>
|
||||||
|
<DUHR>2</DUHR>
|
||||||
|
</InvoiceItem>
|
||||||
|
</InvoiceItems>
|
||||||
|
</SubmissionRequest>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Formát REQUEST XML – výkonová dávka s fakturou (REQUESTTYPE=1)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="utf-16"?>
|
||||||
|
<SubmissionRequest>
|
||||||
|
<SubmissionType>HealthCareBilling</SubmissionType>
|
||||||
|
<InvoiceNumber>0000260026</InvoiceNumber>
|
||||||
|
<InvoiceType>HealthCareInvoice</InvoiceType>
|
||||||
|
<DateOfIssue>2026-03-01T00:00:00.000</DateOfIssue>
|
||||||
|
<DateOfDispatch>2026-03-01T00:00:00.000</DateOfDispatch>
|
||||||
|
<DateOfMaturity>2026-03-31T00:00:00.000</DateOfMaturity>
|
||||||
|
<PeriodFrom>2025-12-01T00:00:00.000</PeriodFrom>
|
||||||
|
<PeriodTo>2025-12-31T00:00:00.000</PeriodTo>
|
||||||
|
<TotalSum>85</TotalSum>
|
||||||
|
<HcpCode>09305000</HcpCode>
|
||||||
|
<HcpOrgNum>68366370</HcpOrgNum>
|
||||||
|
<HcpAccountNumber>2800046620</HcpAccountNumber>
|
||||||
|
<HcpAccountPrefix>000000</HcpAccountPrefix>
|
||||||
|
<HcpAccountBankCode>2010</HcpAccountBankCode>
|
||||||
|
<HcpName>Praktický lékař pro dospělé</HcpName>
|
||||||
|
<HcpStreet>Lovosická 440/40</HcpStreet>
|
||||||
|
<HcpCity>Praha 9-Prosek</HcpCity>
|
||||||
|
<HcpZipCode>19000</HcpZipCode>
|
||||||
|
<HcpPersonName>MUDr. Buzalková Michaela</HcpPersonName>
|
||||||
|
<HicCode>207</HicCode>
|
||||||
|
<HicStreet>Ročkotova 1225/1</HicStreet>
|
||||||
|
<HicCity>Praha 4</HicCity>
|
||||||
|
<HicZipCode>140 21</HicZipCode>
|
||||||
|
<FPOB>0900</FPOB>
|
||||||
|
<VDPECE>51</VDPECE>
|
||||||
|
<InvoiceItems>
|
||||||
|
<InvoiceItem>
|
||||||
|
<BatchNumber>34</BatchNumber>
|
||||||
|
<Type>DP05</Type> <!-- DP05 = výkonová dávka -->
|
||||||
|
<Year>2025</Year>
|
||||||
|
<Month>12</Month>
|
||||||
|
<DBODY>85</DBODY>
|
||||||
|
<DUHR>1</DUHR>
|
||||||
|
</InvoiceItem>
|
||||||
|
</InvoiceItems>
|
||||||
|
</SubmissionRequest>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Poznámka k diakritice v REQUEST:** Medicus ukládá XML v UTF-16, ale česká diakritika v polích jako HcpPersonName, HicName apod. bývá uložena bez háčků/čárek (bug Medicusu při tvorbě XML). Toto není chyba dekódování.
|
||||||
|
|
||||||
|
### Formát SERVERRESPONSE (odpověď pojišťovny)
|
||||||
|
- Kódování: **iso-8859-2** (bytes)
|
||||||
|
- Struktura: XML `<Komunikace><Data ...><Soubor ...>text</Soubor></Data><Podpis>PKCS7</Podpis></Komunikace>`
|
||||||
|
- `PZP_IdPodani` = přidělené podací číslo
|
||||||
|
- `PZP_Chyba="0"` = bez chyby
|
||||||
|
- Odpověď je podepsána pojišťovnou (PKCS7)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Plán: skript pro automatické odeslání žádosti o seznam registrovaných
|
||||||
|
|
||||||
|
### Portály pojišťoven – 3 skupiny
|
||||||
|
|
||||||
|
#### Skupina 1 – Společný portál (201, 205, 207, 209, 213, 217, 228)
|
||||||
|
- Jeden společný portál pro všechny tyto pojišťovny
|
||||||
|
- Autentizace a podepisování: **kvalifikovaný certifikát I.CA EU Qualified CA2/RSA 06/2022**
|
||||||
|
- SerialNumber (Buzalka): `0247068517B0049E2E`
|
||||||
|
- Vydavatel: První certifikační autorita, a.s.
|
||||||
|
|
||||||
|
#### Skupina 2 – VZP (111) – vlastní portál
|
||||||
|
- VZP má **samostatný portál** (`UseAlternativePortal="true"` v konfiguraci)
|
||||||
|
- Podepisování: **komerční certifikát I.CA Public CA/RSA 06/2022** (nižší stupeň než kvalifikovaný)
|
||||||
|
- SerialNumber: `01DE0F46B713505F1F`
|
||||||
|
- Autentizace vůči portálu: **certifikát Komerční banky (DCS CA KB)**
|
||||||
|
- SerialNumber: `46E67A`
|
||||||
|
|
||||||
|
#### Skupina 3 – ZPMV (211) – samostatní exoti
|
||||||
|
- Žádný certifikát, přihlašování **heslem**
|
||||||
|
- Konfigurace: PIN, Password, Email (uloženo v plaintextu v ED_STORAGE)
|
||||||
|
- Email: `ordinace@buzalkova.cz`
|
||||||
|
|
||||||
|
### Formáty odpovědí v PORTAL.DATA (historické)
|
||||||
|
Různé pojišťovny vracely různé formáty:
|
||||||
|
- **ČPZP/ZPMV** (D01 portál): XML `<ekomunikace><statuscode>100</statuscode><idpodani>D01F...</idpodani>`
|
||||||
|
- **OZP a ostatní** (starý portál): XML `<Komunikace><Data ...><Soubor Format="BASE64">HTML protokol</Soubor></Data></Komunikace>`
|
||||||
|
- HTML protokol je v iso-8859-2, obsahuje tabulku s detaily dávky
|
||||||
|
|
||||||
|
### Kódování BLOBů v ED_BOOKOFSUBMISSIONS
|
||||||
|
|
||||||
|
| Sloupec | Kódování | Poznámka |
|
||||||
|
|---|---|---|
|
||||||
|
| `KDAVKACONTENT` | CP1250 | fdb vrací str přes win1250 spojení – použít přímo |
|
||||||
|
| `REQUEST` | ASCII/UTF-8 nebo UTF-16 | Detekovat podle BOM: `FF FE`/`FE FF` → utf-16, začíná `<` → utf-8 |
|
||||||
|
| `SERVERRESPONSE` | iso-8859-2 | latin-1 re-encoding → decode iso-8859-2 |
|
||||||
|
| `PROTOCOL` | iso-8859-2 | latin-1 re-encoding → decode iso-8859-2 |
|
||||||
|
|
||||||
|
**Důležité:** Pro re-encoding str→bytes vždy používat `latin-1` (ne `cp1250`), protože latin-1 zachová všechny bajty 0–255 beze změny včetně null bajtů. Pro utf-16 nikdy nezkoušet decode bez ověření BOM – bez BOM Python tiše vrátí čínské znaky místo chyby.
|
||||||
|
|
||||||
|
**Co potřebujeme ještě zjistit:**
|
||||||
|
- URL endpointů jednotlivých portálů (zachytit přes síťový trace)
|
||||||
|
- Jak přesně se REQUEST podepisuje certifikátem (PKCS12 / Windows certificate store)
|
||||||
|
|
||||||
|
**Co už máme:**
|
||||||
|
- Formát REQUEST XML (viz výše)
|
||||||
|
- Certifikáty a přihlašovací údaje z ED_STORAGE
|
||||||
|
- Strukturu odpovědi pojišťovny
|
||||||
|
- Rozdělení pojišťoven do 3 skupin podle způsobu přístupu
|
||||||
|
|
||||||
## Kódování KDAVKA/FDAVKA – důležité!
|
## 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í).
|
Dávkové soubory (KDAVKA, FDAVKA) jsou uloženy v **CP852** (DOS Latin-2, prahistorické kódování).
|
||||||
|
|||||||
Reference in New Issue
Block a user