From 82719baba8e8a3fe4468d5041fffc12bef84b30a Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Wed, 25 Mar 2026 17:32:12 +0100 Subject: [PATCH] notebook vb --- Backup/BackupAll.py | 185 ++++++++++++++ PNWithClaude/001_pruzkum_PN_tabulek.py | 168 +++++++++++++ PNWithClaude/002_detail_NES_NP_tabulky.py | 184 ++++++++++++++ PNWithClaude/003_aktivni_PN_seznam.py | 208 ++++++++++++++++ PNWithClaude/README.md | 291 ++++++++++++++++++++++ PNWithClaude/test_spusteni.log | 8 + PNWithClaude/test_spusteni.py | 42 ++++ 7 files changed, 1086 insertions(+) create mode 100644 Backup/BackupAll.py create mode 100644 PNWithClaude/001_pruzkum_PN_tabulek.py create mode 100644 PNWithClaude/002_detail_NES_NP_tabulky.py create mode 100644 PNWithClaude/003_aktivni_PN_seznam.py create mode 100644 PNWithClaude/README.md create mode 100644 PNWithClaude/test_spusteni.log create mode 100644 PNWithClaude/test_spusteni.py diff --git a/Backup/BackupAll.py b/Backup/BackupAll.py new file mode 100644 index 0000000..f902e72 --- /dev/null +++ b/Backup/BackupAll.py @@ -0,0 +1,185 @@ +import subprocess +import os +from pathlib import Path +from datetime import datetime +import zipfile +import time +import traceback + +from EmailMessagingGraph import send_mail + +# ============================================================ +# CONFIG +# ============================================================ + +GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe" +FB_USER = "SYSDBA" +FB_PASS = "masterkey" +FB_PORT = "3050" + +MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB" +EXT_DIR = Path(r"U:\externi") +BACKUP_DIR = Path(r"U:\medicusbackup") + +MAIL_TO = "vladimir.buzalka@buzalka.cz" + +CHUNK = 8 * 1024 * 1024 # 8 MB + + +# ============================================================ +# HELPERS +# ============================================================ + +def gbak_and_zip(label: str, db_conn: str, fbk: Path, zipf: Path, log: Path) -> dict: + """ + Run gbak backup and ZIP the result. + Returns a result dict. + """ + result = { + "label": label, + "ok": False, + "fbk_size": 0, + "zip_size": 0, + "t_gbak": 0, + "t_zip": 0, + "error": None, + } + + # 1) GBAK + print(f"GBAK: {label} ... ", end="", flush=True) + t0 = time.time() + cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"] + with open(log, "w", encoding="utf-8") as f: + subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True) + result["t_gbak"] = time.time() - t0 + result["fbk_size"] = fbk.stat().st_size + print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)") + + # 2) ZIP + t1 = time.time() + processed = 0 + fbk_size = result["fbk_size"] + with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: + zi = zipfile.ZipInfo(fbk.name) + zi.compress_type = zipfile.ZIP_DEFLATED + with zf.open(zi, "w", force_zip64=True) as z: + with open(fbk, "rb") as src: + while buf := src.read(CHUNK): + z.write(buf) + processed += len(buf) + pct = processed * 100 / fbk_size + print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True) + print() + result["t_zip"] = time.time() - t1 + result["zip_size"] = zipf.stat().st_size + + # 3) Delete FBK + LOG + fbk.unlink() + log.unlink() + + result["ok"] = True + return result + + +def format_result(r: dict) -> str: + ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0 + return ( + f" {r['label']}: " + f"FBK {r['fbk_size']/1024/1024:.1f} MB → " + f"ZIP {r['zip_size']/1024/1024:.1f} MB " + f"({ratio:.0f}% komprese, " + f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)" + ) + + +# ============================================================ +# MAIN +# ============================================================ + +def main(): + BACKUP_DIR.mkdir(parents=True, exist_ok=True) + now = datetime.now() + ts = now.strftime("%Y-%m-%d_%H-%M-%S") + + backed_up = [] + errors = [] + + # ---------------------------------------------------------- + # 1) Hlavní DB – MEDICUS.FDB + # ---------------------------------------------------------- + fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk" + zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip" + log = BACKUP_DIR / f"MEDICUS_{ts}.log" + try: + r = gbak_and_zip("MEDICUS", MAIN_DB, fbk, zipf, log) + backed_up.append(r) + except Exception: + errors.append({"label": "MEDICUS", "error": traceback.format_exc()}) + for f in (fbk, log): + if f.exists(): + f.unlink() + + # ---------------------------------------------------------- + # 2) Externí DB – MEDICUS_FILES_*.fdb + # ---------------------------------------------------------- + fdb_all = sorted( + set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")), + key=lambda p: p.name.lower(), + ) + + for fdb in fdb_all: + name = fdb.stem + fbk = BACKUP_DIR / f"{name}_{ts}.fbk" + zipf = BACKUP_DIR / f"{name}_{ts}.zip" + log = BACKUP_DIR / f"{name}_{ts}.log" + db_conn = f"localhost/{FB_PORT}:{fdb}" + try: + r = gbak_and_zip(name, db_conn, fbk, zipf, log) + backed_up.append(r) + except Exception: + errors.append({"label": name, "error": traceback.format_exc()}) + for f in (fbk, log): + if f.exists(): + f.unlink() + + # ---------------------------------------------------------- + # Report + # ---------------------------------------------------------- + total = 1 + len(fdb_all) + report = [ + f"Backup Medicus – {now.strftime('%d.%m.%Y %H:%M')}", + f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}", + f"Výstupní adresář: {BACKUP_DIR}", + "", + ] + + if backed_up: + report.append("--- Zálohováno ---") + total_zip = sum(r["zip_size"] for r in backed_up) + for r in backed_up: + report.append(format_result(r)) + report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB") + report.append("") + + if errors: + report.append("--- CHYBY ---") + for e in errors: + report.append(f" {e['label']}:\n{e['error']}") + report.append("") + + has_errors = bool(errors) + subject = ( + f"{'X' if has_errors else 'OK'} MEDICUS backup " + f"{len(backed_up)}/{total}" + + (f" – {len(errors)} chyb" if has_errors else "") + ) + + send_mail(MAIL_TO, subject, "\n".join(report)) + print("\n" + "\n".join(report)) + + if errors: + raise RuntimeError(f"{len(errors)} backup(s) failed") + + +if __name__ == "__main__": + main() diff --git a/PNWithClaude/001_pruzkum_PN_tabulek.py b/PNWithClaude/001_pruzkum_PN_tabulek.py new file mode 100644 index 0000000..d5de1e3 --- /dev/null +++ b/PNWithClaude/001_pruzkum_PN_tabulek.py @@ -0,0 +1,168 @@ +""" +001_pruzkum_PN_tabulek.py +========================= +Průzkum Medicus DB – hledáme tabulky a strukturu relacionou s PN +(pracovní neschopnost). + +Spustit na Windows: + python "001_pruzkum_PN_tabulek.py" + +Výstup: přehled tabulek s PN v názvu + jejich sloupce + ukázka dat. +""" +import fdb +import datetime + +# ── Připojení ────────────────────────────────────────────────────────────────── +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', + password='masterkey', + charset='win1250' +) +cur = conn.cursor() + +# ── 1. Všechny tabulky s "PN" v názvu ───────────────────────────────────────── +print("=" * 60) +print("TABULKY obsahující 'PN' v názvu") +print("=" * 60) + +cur.execute(""" + SELECT TRIM(rdb$relation_name) + FROM rdb$relations + WHERE rdb$view_blr IS NULL + AND (rdb$system_flag IS NULL OR rdb$system_flag = 0) + AND UPPER(rdb$relation_name) CONTAINING 'PN' + ORDER BY rdb$relation_name +""") +pn_tables = [row[0] for row in cur.fetchall()] + +if pn_tables: + for t in pn_tables: + print(f" {t}") +else: + print(" (žádná)") + +# ── 2. Sloupce každé PN tabulky + ukázka dat ────────────────────────────────── +for table in pn_tables: + print(f"\n{'─' * 60}") + print(f"TABULKA: {table}") + print(f"{'─' * 60}") + + # Sloupce + cur.execute(""" + SELECT TRIM(rf.rdb$field_name), + f.rdb$field_type, + f.rdb$field_length, + rf.rdb$null_flag + FROM rdb$relation_fields rf + JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name + WHERE TRIM(rf.rdb$relation_name) = ? + ORDER BY rf.rdb$field_position + """, (table,)) + cols = cur.fetchall() + + # field_type kódy Firebirdu (nejběžnější) + type_map = { + 7: 'SMALLINT', 8: 'INTEGER', 10: 'FLOAT', 12: 'DATE', + 13: 'TIME', 14: 'CHAR', 16: 'BIGINT', 27: 'DOUBLE', + 35: 'TIMESTAMP', 37: 'VARCHAR', 261: 'BLOB', + } + + col_names = [] + for c in cols: + name, ftype, flen, nullable = c + type_str = type_map.get(ftype, f'type#{ftype}') + if flen and ftype in (14, 37): + type_str += f'({flen})' + null_str = '' if nullable else ' NOT NULL' + print(f" {name:<25} {type_str}{null_str}") + col_names.append(name) + + # Počet řádků + try: + cur.execute(f'SELECT COUNT(*) FROM {table}') + pocet = cur.fetchone()[0] + print(f"\n --> Celkem řádků: {pocet}") + except Exception as e: + print(f"\n --> COUNT selhalo: {e}") + continue + + # Ukázka prvních 5 řádků + if pocet > 0: + try: + cur.execute(f'SELECT FIRST 5 * FROM {table}') + rows = cur.fetchall() + print(f"\n Ukázka (max 5 řádků):") + header = " | ".join(f"{n[:12]:>12}" for n in col_names) + print(f" {header}") + print(f" {'-' * len(header)}") + for row in rows: + vals = [] + for v in row: + if isinstance(v, (datetime.date, datetime.datetime)): + vals.append(v.isoformat()[:12]) + elif isinstance(v, bytes): + vals.append(f'') + elif v is None: + vals.append('NULL') + else: + vals.append(str(v)[:12]) + print(f" {' | '.join(f'{v:>12}' for v in vals)}") + except Exception as e: + print(f" Ukázka selhala: {e}") + +# ── 3. Hledáme i tabulky s "NESCHOP" nebo "NESCHOPENK" ──────────────────────── +print(f"\n{'=' * 60}") +print("TABULKY s 'NESCHOP' nebo 'PRACOV' nebo 'SICK' v názvu") +print("=" * 60) + +cur.execute(""" + SELECT TRIM(rdb$relation_name) + FROM rdb$relations + WHERE rdb$view_blr IS NULL + AND (rdb$system_flag IS NULL OR rdb$system_flag = 0) + AND ( + UPPER(rdb$relation_name) CONTAINING 'NESCHOP' + OR UPPER(rdb$relation_name) CONTAINING 'PRACOV' + OR UPPER(rdb$relation_name) CONTAINING 'SICK' + ) + ORDER BY rdb$relation_name +""") +extra = [row[0] for row in cur.fetchall()] +if extra: + for t in extra: + print(f" {t}") +else: + print(" (žádná)") + +# ── 4. Rychlý přehled – tabulky s datem "OD/DO" nebo "ZACATEK/KONEC" ────────── +print(f"\n{'=' * 60}") +print("SLOUPCE: hledáme 'DATPN', 'DATDO', 'DATUM_DO' v celé DB") +print("=" * 60) + +cur.execute(""" + SELECT TRIM(rf.rdb$relation_name), TRIM(rf.rdb$field_name) + FROM rdb$relation_fields rf + JOIN rdb$relations r ON rf.rdb$relation_name = r.rdb$relation_name + WHERE (r.rdb$system_flag IS NULL OR r.rdb$system_flag = 0) + AND r.rdb$view_blr IS NULL + AND ( + UPPER(rf.rdb$field_name) CONTAINING 'DATPN' + OR UPPER(rf.rdb$field_name) = 'DATDO' + OR UPPER(rf.rdb$field_name) CONTAINING 'DATUM_DO' + OR UPPER(rf.rdb$field_name) CONTAINING 'DAT_DO' + OR UPPER(rf.rdb$field_name) CONTAINING 'PN' + ) + ORDER BY rf.rdb$relation_name, rf.rdb$field_name +""") +hits = cur.fetchall() +if hits: + for tab, col in hits: + print(f" {tab:<30} {col}") +else: + print(" (nic nenalezeno)") + +cur.close() +conn.close() +print(f"\n{'=' * 60}") +print("Průzkum dokončen.") diff --git a/PNWithClaude/002_detail_NES_NP_tabulky.py b/PNWithClaude/002_detail_NES_NP_tabulky.py new file mode 100644 index 0000000..5ad73bf --- /dev/null +++ b/PNWithClaude/002_detail_NES_NP_tabulky.py @@ -0,0 +1,184 @@ +""" +002_detail_NES_NP_tabulky.py +============================ +Průzkum klíčových tabulek neschopenky: + - NES → hlavní tabulka pracovní neschopnosti + - NP → pravděpodobně neschopenka podání + - SOC_NEPRITOMNOST → sociální nepřítomnost + +Spustit na Windows: + python "002_detail_NES_NP_tabulky.py" +""" +import fdb +import datetime + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', + password='masterkey', + charset='win1250' +) +cur = conn.cursor() + + +def serialize(v): + if isinstance(v, (datetime.date, datetime.datetime)): + return v.isoformat() + if isinstance(v, datetime.time): + return v.isoformat() + if isinstance(v, bytes): + # Pro BLOB zkusíme dekódovat jako text + try: + return v.decode('cp1250')[:80] + except Exception: + return f'' + if v is None: + return 'NULL' + return str(v) + + +def inspect_table(table): + print(f"\n{'=' * 70}") + print(f" TABULKA: {table}") + print(f"{'=' * 70}") + + # Sloupce + cur.execute(""" + SELECT TRIM(rf.rdb$field_name), + f.rdb$field_type, + f.rdb$field_length + FROM rdb$relation_fields rf + JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name + WHERE TRIM(rf.rdb$relation_name) = ? + ORDER BY rf.rdb$field_position + """, (table,)) + cols = cur.fetchall() + type_map = { + 7: 'SMALLINT', 8: 'INTEGER', 10: 'FLOAT', 12: 'DATE', + 13: 'TIME', 14: 'CHAR', 16: 'BIGINT', 27: 'DOUBLE', + 35: 'TIMESTAMP', 37: 'VARCHAR', 261: 'BLOB', + } + col_names = [c[0] for c in cols] + print("\nSloupce:") + for c in cols: + name, ftype, flen = c + type_str = type_map.get(ftype, f'type#{ftype}') + if flen and ftype in (14, 37): + type_str += f'({flen})' + print(f" {name:<30} {type_str}") + + # Počet řádků + cur.execute(f'SELECT COUNT(*) FROM {table}') + pocet = cur.fetchone()[0] + print(f"\nCelkem řádků: {pocet}") + + if pocet == 0: + return col_names + + # Ukázka 5 nejnovějších (pokud má datum sloupec) + date_cols = [c[0] for c in cols if c[1] in (12, 35)] # DATE nebo TIMESTAMP + order = f'ORDER BY {date_cols[0]} DESC' if date_cols else '' + + try: + cur.execute(f'SELECT FIRST 5 * FROM {table} {order}') + rows = cur.fetchall() + print(f"\nUkázka (5 {'nejnovějších' if order else 'prvních'}):") + for row in rows: + print() + for name, val in zip(col_names, row): + s = serialize(val) + if s != 'NULL': + print(f" {name:<30} {s}") + except Exception as e: + print(f" Ukázka selhala: {e}") + + return col_names + + +# ── Prozkoumáme klíčové tabulky ─────────────────────────────────────────────── +for tbl in ['NES', 'NP', 'SOC_NEPRITOMNOST']: + inspect_table(tbl) + +# ── Speciálně: aktivní PN ───────────────────────────────────────────────────── +print(f"\n\n{'=' * 70}") +print(" POKUS: aktivní záznamy v NES (konec NULL nebo v budoucnosti)") +print(f"{'=' * 70}") + +dnes = datetime.date.today() + +# Nejdřív zjistíme sloupce NES +cur.execute(""" + SELECT TRIM(rf.rdb$field_name), f.rdb$field_type + FROM rdb$relation_fields rf + JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name + WHERE TRIM(rf.rdb$relation_name) = 'NES' + ORDER BY rf.rdb$field_position +""") +nes_cols = [(r[0], r[1]) for r in cur.fetchall()] +nes_col_names = [c[0] for c in nes_cols] + +print(f"\nVšechny sloupce NES: {', '.join(nes_col_names)}") + +# Hledáme sloupce s datumem konce PN +date_cols_nes = [c[0] for c in nes_cols if c[1] in (12, 35)] +print(f"Datumové sloupce: {date_cols_nes}") + +# Zkusíme různé kandidátní sloupce pro "datum do" +candidates_do = [c for c in nes_col_names if 'DO' in c or 'KONEC' in c or 'END' in c.upper()] +candidates_od = [c for c in nes_col_names if 'OD' in c or 'ZACATEK' in c or 'VZNIK' in c or 'START' in c.upper()] +candidates_pac = [c for c in nes_col_names if 'PAC' in c or 'IDPAC' in c] +print(f"Kandidáti DATUM_DO: {candidates_do}") +print(f"Kandidáti DATUM_OD: {candidates_od}") +print(f"Kandidáti IDPAC: {candidates_pac}") + +# Ukázka posledních 10 záznamů NES seřazená dle prvního datumového sloupce +if date_cols_nes: + try: + cur.execute(f'SELECT FIRST 10 * FROM NES ORDER BY {date_cols_nes[0]} DESC') + rows = cur.fetchall() + print(f"\nPosledních 10 záznamů NES (dle {date_cols_nes[0]} DESC):") + for row in rows: + print() + for name, val in zip(nes_col_names, row): + s = serialize(val) + if s != 'NULL': + print(f" {name:<30} {s}") + except Exception as e: + print(f"Selhalo: {e}") + +# ── NP tabulka – stejný rozbor ──────────────────────────────────────────────── +print(f"\n\n{'=' * 70}") +print(" POKUS: aktivní záznamy v NP") +print(f"{'=' * 70}") + +cur.execute(""" + SELECT TRIM(rf.rdb$field_name), f.rdb$field_type + FROM rdb$relation_fields rf + JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name + WHERE TRIM(rf.rdb$relation_name) = 'NP' + ORDER BY rf.rdb$field_position +""") +np_cols = [(r[0], r[1]) for r in cur.fetchall()] +np_col_names = [c[0] for c in np_cols] +print(f"\nVšechny sloupce NP: {', '.join(np_col_names)}") +date_cols_np = [c[0] for c in np_cols if c[1] in (12, 35)] +print(f"Datumové sloupce: {date_cols_np}") + +if date_cols_np: + try: + cur.execute(f'SELECT FIRST 10 * FROM NP ORDER BY {date_cols_np[0]} DESC') + rows = cur.fetchall() + print(f"\nPosledních 10 záznamů NP (dle {date_cols_np[0]} DESC):") + for row in rows: + print() + for name, val in zip(np_col_names, row): + s = serialize(val) + if s != 'NULL': + print(f" {name:<30} {s}") + except Exception as e: + print(f"Selhalo: {e}") + +cur.close() +conn.close() +print(f"\n{'=' * 70}") +print("Hotovo.") diff --git a/PNWithClaude/003_aktivni_PN_seznam.py b/PNWithClaude/003_aktivni_PN_seznam.py new file mode 100644 index 0000000..8655dae --- /dev/null +++ b/PNWithClaude/003_aktivni_PN_seznam.py @@ -0,0 +1,208 @@ +""" +003_aktivni_PN_seznam.py +======================== +Vypíše seznam pacientů s aktivní pracovní neschopností (PN) k dnešnímu datu. + +SQL logika převzata přímo z Medicusu (report "Přehled práce neschopných pacientů"): + - ZACNES <= dnes + - (KONNES >= dnes) OR (KONNES IS NULL) + - PRACNE = 'A' + - STORNO <> 'T' + +Spouštění: tlačítkem z Medicusu nebo přímo pythonu. +""" +import sys, os, traceback + +_LOG = r"C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude\003_error.log" + +def _log_exception(exc_type, exc_value, exc_tb): + with open(_LOG, "w", encoding="utf-8") as f: + traceback.print_exception(exc_type, exc_value, exc_tb, file=f) +sys.excepthook = _log_exception + +import fdb +import datetime +import tkinter as tk +from tkinter import ttk + +# ── Připojení ───────────────────────────────────────────────────────────────── +DSN = r'localhost:c:\medicus 3\data\medicus.fdb' +USER = 'SYSDBA' +PASSWORD = 'masterkey' +CHARSET = 'win1250' + +# ── SQL ─────────────────────────────────────────────────────────────────────── +SQL = """ +SELECT + kar.PRIJMENI, + kar.JMENO, + kar.RODCIS, + nes.ZACNES, + nes.KONNES, + nes.DIAGNO, + COALESCE(nes.ECN, nes.CISNES) AS CISNES +FROM NES nes +JOIN KAR kar ON kar.IDPAC = nes.IDPAC +WHERE + nes.ZACNES <= ? +AND (nes.KONNES >= ? OR nes.KONNES IS NULL) +AND nes.PRACNE = 'A' +AND nes.STORNO <> 'T' +ORDER BY kar.PRIJMENI, kar.JMENO +""" + + +def nacti_data(): + dnes = datetime.date.today() + conn = fdb.connect(dsn=DSN, user=USER, password=PASSWORD, charset=CHARSET) + try: + cur = conn.cursor() + cur.execute(SQL, (dnes, dnes)) + rows = cur.fetchall() + return dnes, rows + finally: + conn.close() + + +def delka_PN(zacnes, konnes, dnes): + """Počet dní PN (od začátku do dnes nebo do konce).""" + do = konnes if konnes else dnes + return (do - zacnes).days + 1 + + +def zobraz_okno(dnes, rows): + root = tk.Tk() + root.title(f"Aktivní pracovní neschopnost – {dnes.strftime('%d.%m.%Y')}") + w, h = 900, 550 + root.update_idletasks() + sw = root.winfo_screenwidth() + sh = root.winfo_screenheight() + x = (sw - w) // 2 + y = (sh - h) // 2 + root.geometry(f"{w}x{h}+{x}+{y}") + + # ── Záhlaví ─────────────────────────────────────────────────────────────── + hlavicka = tk.Frame(root, bg="#2c5f8a", pady=8) + hlavicka.pack(fill=tk.X) + tk.Label( + hlavicka, + text=f"Pracovní neschopnost – aktivní k {dnes.strftime('%d.%m.%Y')} ({len(rows)} pacientů)", + font=("Segoe UI", 13, "bold"), + fg="white", bg="#2c5f8a" + ).pack() + + # ── Tabulka ─────────────────────────────────────────────────────────────── + cols = ("Příjmení", "Jméno", "Rodné číslo", "Začátek", "Konec", "Dní", "Diagnóza", "Č. neschopenky") + frame = tk.Frame(root) + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + scroll_y = tk.Scrollbar(frame, orient=tk.VERTICAL) + scroll_y.pack(side=tk.RIGHT, fill=tk.Y) + scroll_x = tk.Scrollbar(frame, orient=tk.HORIZONTAL) + scroll_x.pack(side=tk.BOTTOM, fill=tk.X) + + tree = ttk.Treeview( + frame, + columns=cols, + show="headings", + yscrollcommand=scroll_y.set, + xscrollcommand=scroll_x.set + ) + scroll_y.config(command=tree.yview) + scroll_x.config(command=tree.xview) + + # Šířky sloupců + widths = [120, 100, 110, 85, 85, 50, 80, 150] + for col, w in zip(cols, widths): + tree.column(col, width=w, anchor=tk.W) + + # ── Řazení kliknutím na záhlaví ─────────────────────────────────────────── + _sort_state = {} # col -> ascending True/False + + def sort_by(col): + ascending = not _sort_state.get(col, False) + _sort_state[col] = ascending + # Index sloupce pro čtení hodnoty + col_idx = cols.index(col) + data = [(tree.set(k, col), k) for k in tree.get_children("")] + # Číselné řazení pro "Dní" + if col == "Dní": + data.sort(key=lambda t: int(t[0]) if t[0].isdigit() else 0, reverse=not ascending) + else: + data.sort(key=lambda t: t[0].lower(), reverse=not ascending) + for idx, (_, k) in enumerate(data): + tree.move(k, "", idx) + # Šipka v záhlaví + for c in cols: + arrow = (" ▲" if ascending else " ▼") if c == col else "" + tree.heading(c, text=c + arrow, command=lambda c=c: sort_by(c)) + # Obarvi znovu střídavě + for idx, (_, k) in enumerate(data): + current_tags = list(tree.item(k, "tags")) + dlouha = "dlouha" in current_tags + if dlouha: + tree.item(k, tags=("dlouha",)) + else: + tree.item(k, tags=("even" if idx % 2 == 0 else "odd",)) + + for col in cols: + tree.heading(col, text=col, command=lambda c=col: sort_by(c)) + + # Střídavé barvy řádků + tree.tag_configure("even", background="#f0f4f8") + tree.tag_configure("odd", background="white") + tree.tag_configure("dlouha", foreground="#c0392b") # červeně > 90 dní + + for i, row in enumerate(rows): + prijmeni, jmeno, rodcis, zacnes, konnes, diagno, cisnes = row + dni = delka_PN(zacnes, konnes, dnes) + konec_str = konnes.strftime("%d.%m.%Y") if konnes else "trvá" + diagno_str = (diagno or "").strip() + cisnes_str = (cisnes or "").strip() + + tag = "dlouha" if dni > 90 else ("even" if i % 2 == 0 else "odd") + tree.insert("", tk.END, values=( + prijmeni, + jmeno, + rodcis, + zacnes.strftime("%d.%m.%Y"), + konec_str, + dni, + diagno_str, + cisnes_str, + ), tags=(tag,)) + + tree.pack(fill=tk.BOTH, expand=True) + + # ── Výchozí řazení: Dní vzestupně ───────────────────────────────────────── + sort_by("Dní") + + # ── Legenda + zavřít ────────────────────────────────────────────────────── + spodek = tk.Frame(root, pady=6) + spodek.pack(fill=tk.X) + tk.Label( + spodek, + text="Červeně = PN delší než 90 dní", + font=("Segoe UI", 9), + fg="#c0392b" + ).pack(side=tk.LEFT, padx=15) + tk.Button( + spodek, + text="Zavřít", + command=root.destroy, + width=12, + font=("Segoe UI", 10) + ).pack(side=tk.RIGHT, padx=15) + + root.mainloop() + + +# ── Hlavní tok ──────────────────────────────────────────────────────────────── +if __name__ == "__main__": + try: + dnes, rows = nacti_data() + zobraz_okno(dnes, rows) + except Exception: + with open(_LOG, "w", encoding="utf-8") as f: + traceback.print_exc(file=f) + raise diff --git a/PNWithClaude/README.md b/PNWithClaude/README.md new file mode 100644 index 0000000..364192c --- /dev/null +++ b/PNWithClaude/README.md @@ -0,0 +1,291 @@ +# PNWithClaude – Dokumentace projektu + +> **Účel:** Python skripty spouštěné tlačítkem z Medicusu, které rozšiřují možnosti +> vestavěných reportů. Vznikly díky SQL loggeru/debuggeru v Medicusu, který odhalil +> strukturu databáze. + +--- + +## Prostředí + +| Položka | Hodnota | +|---|---| +| Python | 3.12.9 (64-bit) | +| Python exe | `C:\Users\vlado\PycharmProjects\Medicus\.venv\Scripts\python.exe` | +| Pythonw exe | `C:\Users\vlado\PycharmProjects\Medicus\.venv\Scripts\pythonw.exe` | +| Projekt | `C:\Users\vlado\PycharmProjects\Medicus\` | +| Skripty | `C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude\` | +| Databáze | Firebird – `localhost:c:\medicus 3\data\medicus.fdb` | + +### Klíčové Python balíčky +- `fdb` – Firebird databázový driver +- `tkinter` – GUI (součást Pythonu, není třeba instalovat) + +--- + +## Připojení k databázi + +```python +import fdb + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', + password='masterkey', + charset='win1250' +) +``` + +> ⚠️ Charset `win1250` je kritický – bez něj jsou česká písmena rozbitá. + +--- + +## Konvence pojmenování skriptů + +``` +NNN_popisne_jmeno.py +``` +- `NNN` = třímístné číslo (001, 002, 003, ...) +- Příklad: `003_aktivni_PN_seznam.py` + +--- + +## Jak nastavit tlačítko v Medicusu + +Medicus → **Konfigurace → Externí programy** (nebo Nástroje → Nastavení) + +### Pole v dialogu „Externí program" + +| Pole | Hodnota | +|---|---| +| **Příkazový řádek** | `"C:\Users\vlado\PycharmProjects\Medicus\.venv\Scripts\pythonw.exe" "C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude\NNN_skript.py"` | +| **Program** | `C:\Users\vlado\PycharmProjects\Medicus\.venv\Scripts\pythonw.exe` | +| **Spustit v** | *(nechat prázdné)* | +| **Popis** | Název tlačítka v Medicusu | +| **Pacient** | ☐ nezaškrtávat (pokud skript nepotřebuje aktuálního pacienta) | + +> ⚠️ **`pythonw.exe`** místo `python.exe` = žádné černé konzolové okno! +> +> ⚠️ **„Spustit v" nechat prázdné** – Medicus hlásí chybu pokud složka +> „neexistuje" (i když existuje), ale hlavně to nepotřebujeme, +> protože používáme absolutní cesty všude. + +### Jak přidat tlačítko na lištu +Po uložení externího programu → pravým tlačítkem na lištu → přizpůsobit → +najít nový program → přetáhnout na lištu. + +### Předávání dat z Medicusu skriptu +Medicus umí předat proměnné přes příkazový řádek, např.: +``` +"pythonw.exe" "skript.py" "%RODCISN%" "%JMENO%" "%PRIJMENI%" +``` +Dostupné proměnné (ukázka ze stávající konfigurace laboratoře): +- `%JMENO%` – jméno pacienta +- `%PRIJMENI%` – příjmení +- `%RODCISN%` – rodné číslo (bez lomítka) +- `%POJ%` – pojišťovna +- `%DGN%` – diagnóza + +V Pythonu pak čteš: `sys.argv[1]`, `sys.argv[2]`, atd. + +--- + +## Jak zjistit SQL dotazy Medicusu – SQL Logger + +1. Medicus → **Nástroje → SQL Monitor** (nebo podobně v menu) +2. Zapnout logging +3. Provést akci v Medicusu (otevřít záložku, spustit report) +4. Zkopírovat SQL z logu + +Log se ukládá do: `C:\Medicus 3\Debug\Monitor_DDMMYY_HHMMSS.log` + +> 💡 **Zlatý důl!** SQL logger odhalí přesnou strukturu tabulek a podmínky +> filtrování, které Medicus interně používá. + +--- + +## Struktura databáze – klíčové tabulky + +### Pacienti +| Tabulka | Popis | +|---|---| +| `KAR` | Kartotéka pacientů – `IDPAC`, `PRIJMENI`, `JMENO`, `RODCIS` | + +### Pracovní neschopnost (PN) +| Tabulka | Popis | +|---|---| +| `NES` | **Hlavní tabulka neschopenek** | +| `HPN` | Elektronická podání na ČSSZ (eNeschopenka) – komunikační vrstva, 5693 záznamů | +| `HPN_NOTIFIKACE` | Notifikace z ČSSZ | +| `HPN_NOTIFIKACE_DETAIL` | Detail notifikací, stavy podání (`STAV_PODANI`) | +| `HPN_PODANI` | Podání na ČSSZ | +| `HOSPNES` | Hospitalizační neschopenky | + +### Klíčové sloupce tabulky `NES` +| Sloupec | Popis | +|---|---| +| `IDPAC` | ID pacienta (JOIN s KAR) | +| `ZACNES` | Začátek PN (DATE) | +| `KONNES` | Konec PN (DATE) – NULL = stále trvá | +| `DIAGNO` | Diagnóza (MKN-10 kód) | +| `CISNES` | Číslo neschopenky (starý formát) | +| `ECN` | eČíslo neschopenky (nový elektronický formát) | +| `PRACNE` | Pracovní neschopnost = `'A'` | +| `STORNO` | Storno záznamu = `'T'` (True) | +| `STATDPNKOD` | Stav DPN kód | +| `VERZE_DPN` | Verze DPN | + +--- + +## SQL – aktivní PN k dnešnímu datu + +```sql +SELECT + kar.PRIJMENI, + kar.JMENO, + kar.RODCIS, + nes.ZACNES, + nes.KONNES, + nes.DIAGNO, + COALESCE(nes.ECN, nes.CISNES) AS CISNES -- preferuj eČíslo, fallback na staré +FROM NES nes +JOIN KAR kar ON kar.IDPAC = nes.IDPAC +WHERE + nes.ZACNES <= CAST('TODAY' AS DATE) +AND (nes.KONNES >= CAST('TODAY' AS DATE) OR nes.KONNES IS NULL) +AND nes.PRACNE = 'A' +AND nes.STORNO <> 'T' +ORDER BY kar.PRIJMENI, kar.JMENO +``` + +> 💡 `COALESCE(nes.ECN, nes.CISNES)` – starší neschopenky mají jen `CISNES`, +> novější elektronické mají `ECN`. Takhle dostaneme vždy správné číslo. + +--- + +## Šablona nového skriptu + +```python +""" +NNN_nazev_skriptu.py +==================== +Popis co skript dělá. +""" +import sys, os, traceback + +_LOG = r"C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude\NNN_error.log" + +def _log_exception(exc_type, exc_value, exc_tb): + with open(_LOG, "w", encoding="utf-8") as f: + traceback.print_exception(exc_type, exc_value, exc_tb, file=f) +sys.excepthook = _log_exception + +import fdb +import datetime +import tkinter as tk +from tkinter import ttk + +DSN = r'localhost:c:\medicus 3\data\medicus.fdb' +USER = 'SYSDBA' +PASSWORD = 'masterkey' +CHARSET = 'win1250' + +SQL = """ + SELECT ... + FROM ... + WHERE ... +""" + +def nacti_data(): + conn = fdb.connect(dsn=DSN, user=USER, password=PASSWORD, charset=CHARSET) + try: + cur = conn.cursor() + cur.execute(SQL) + return cur.fetchall() + finally: + conn.close() + +def zobraz_okno(rows): + root = tk.Tk() + root.title("Název okna") + # Centrování na střed monitoru + w, h = 900, 550 + root.update_idletasks() + sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() + root.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}") + # ... GUI kód ... + root.mainloop() + +if __name__ == "__main__": + try: + rows = nacti_data() + zobraz_okno(rows) + except Exception: + with open(_LOG, "w", encoding="utf-8") as f: + traceback.print_exc(file=f) + raise +``` + +--- + +## Řazení v ttk.Treeview – vzorový kód + +```python +_sort_state = {} # uchovává směr řazení pro každý sloupec + +def sort_by(col): + ascending = not _sort_state.get(col, False) + _sort_state[col] = ascending + data = [(tree.set(k, col), k) for k in tree.get_children("")] + if col == "Dní": # číselné sloupce + data.sort(key=lambda t: int(t[0]) if t[0].isdigit() else 0, reverse=not ascending) + else: # textové sloupce + data.sort(key=lambda t: t[0].lower(), reverse=not ascending) + for idx, (_, k) in enumerate(data): + tree.move(k, "", idx) + # Šipka v záhlaví aktivního sloupce + for c in cols: + arrow = (" ▲" if ascending else " ▼") if c == col else "" + tree.heading(c, text=c + arrow, command=lambda c=c: sort_by(c)) + +# Připoj řazení na záhlaví +for col in cols: + tree.heading(col, text=col, command=lambda c=col: sort_by(c)) + +# Výchozí řazení při spuštění +sort_by("Dní") +``` + +--- + +## Ladění problémů + +### Skript nefunguje z Medicusu, ale z PyCharmu ano +1. Přepnout dočasně na `python.exe` (ne `pythonw.exe`) – uvidíš konzoli +2. Zkontrolovat `NNN_error.log` v adresáři skriptů +3. Nejčastější příčiny: + - `__file__` je prázdný → používej **absolutní cesty** pro log soubory + - Chybí `fbclient.dll` v PATH → testuj test skriptem + - Pole „Spustit v" v Medicusu → **nechat prázdné** + +### Test skript pro ověření prostředí +Viz `test_spusteni.py` – testuje Python, fdb import, tkinter a DB spojení. +Výstup: `Python: OK`, `fdb: OK`, `tkinter: OK`, `DB spojeni: OK` + +### Černé konzolové okno +Použít `pythonw.exe` místo `python.exe` v konfiguraci Medicusu. + +--- + +## Seznam skriptů + +| Číslo | Soubor | Popis | +|---|---|---| +| 001 | `001_pruzkum_PN_tabulek.py` | Průzkum struktury DB – tabulky a sloupce s PN | +| 003 | `003_aktivni_PN_seznam.py` | **Aktivní PN k dnešnímu datu** – tlačítko na liště | + +> Číslo 002 přeskočeno (bylo pracovní/testovací stadium). + +--- + +*Projekt vznikl: březen 2026 | Medicus 3 Komfort | Firebird DB | Python 3.12* diff --git a/PNWithClaude/test_spusteni.log b/PNWithClaude/test_spusteni.log new file mode 100644 index 0000000..84a296a --- /dev/null +++ b/PNWithClaude/test_spusteni.log @@ -0,0 +1,8 @@ +Python: C:\Users\vlado\PycharmProjects\Medicus\.venv\Scripts\python.exe +Verze: 3.12.9 (tags/v3.12.9:fdb8142, Feb 4 2025, 15:27:58) [MSC v.1942 64 bit (AMD64)] +Cwd: C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude +__file__: C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude\test_spusteni.py + +fdb: OK +tkinter: OK +DB spojeni: OK \ No newline at end of file diff --git a/PNWithClaude/test_spusteni.py b/PNWithClaude/test_spusteni.py new file mode 100644 index 0000000..658c805 --- /dev/null +++ b/PNWithClaude/test_spusteni.py @@ -0,0 +1,42 @@ +""" +Testovací skript - zjistí proč nejde 003. +Zapíše výsledek do test_spusteni.log +""" +import sys +import os +import traceback + +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_spusteni.log") + +lines = [] +lines.append(f"Python: {sys.executable}") +lines.append(f"Verze: {sys.version}") +lines.append(f"Cwd: {os.getcwd()}") +lines.append(f"__file__: {os.path.abspath(__file__)}") +lines.append("") + +try: + import fdb + lines.append("fdb: OK") +except Exception as e: + lines.append(f"fdb CHYBA: {e}") + lines.append(traceback.format_exc()) + +try: + import tkinter as tk + lines.append("tkinter: OK") +except Exception as e: + lines.append(f"tkinter CHYBA: {e}") + +try: + conn = __import__("fdb").connect( + dsn=r"localhost:c:\medicus 3\data\medicus.fdb", + user="SYSDBA", password="masterkey", charset="win1250" + ) + conn.close() + lines.append("DB spojeni: OK") +except Exception as e: + lines.append(f"DB CHYBA: {e}") + +with open(LOG, "w", encoding="utf-8") as f: + f.write("\n".join(lines))