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_PORT = "3050"
|
||||
|
||||
SRC_DIR = Path(r"c:\medicusext")
|
||||
BACKUP_DIR = Path(r"U:\medicusbackup")
|
||||
SRC_DIR = Path(r"U:\externi")
|
||||
BACKUP_DIR = Path(r"U:\externabackup")
|
||||
|
||||
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'
|
||||
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í ---
|
||||
for f in os.listdir(output_dir):
|
||||
if f.endswith('_faktury.xlsx'):
|
||||
@@ -33,6 +40,7 @@ wb = openpyxl.Workbook()
|
||||
HEADER_FILL = PatternFill('solid', fgColor='2F5496')
|
||||
HEADER_FONT = Font(bold=True, color='FFFFFF')
|
||||
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')
|
||||
|
||||
def style_header(ws):
|
||||
@@ -262,6 +270,263 @@ for i, row in enumerate(fak_rows, start=2):
|
||||
else:
|
||||
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í
|
||||
# =====================
|
||||
@@ -269,4 +534,4 @@ for i, row in enumerate(fak_rows, start=2):
|
||||
conn.close()
|
||||
wb.save(output_path)
|
||||
sys.stdout.buffer.write(f'Ulozeno: {output_path}\n'.encode('utf-8'))
|
||||
sys.stdout.buffer.write(f'FAK: {len(fak_rows)} radku, FAKDET: {len(det_rows)} radku, PORTAL: {len(portal_rows)} radku\n'.encode('utf-8'))
|
||||
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
|
||||
- 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é!
|
||||
|
||||
Dávkové soubory (KDAVKA, FDAVKA) jsou uloženy v **CP852** (DOS Latin-2, prahistorické kódování).
|
||||
|
||||
Reference in New Issue
Block a user