notebook vb
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
import zipfile
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from EmailMessagingGraph import send_mail
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONFIG
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
GBAK = r"C:\Program Files\Firebird\Firebird_2_5_CGM\bin\gbak.exe"
|
||||||
|
FB_USER = "SYSDBA"
|
||||||
|
FB_PASS = "masterkey"
|
||||||
|
FB_PORT = "3050"
|
||||||
|
|
||||||
|
MAIN_DB = r"localhost/3050:C:\medicus 3\data\MEDICUS.FDB"
|
||||||
|
EXT_DIR = Path(r"U:\externi")
|
||||||
|
BACKUP_DIR = Path(r"U:\medicusbackup")
|
||||||
|
|
||||||
|
MAIL_TO = "vladimir.buzalka@buzalka.cz"
|
||||||
|
|
||||||
|
CHUNK = 8 * 1024 * 1024 # 8 MB
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# HELPERS
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def gbak_and_zip(label: str, db_conn: str, fbk: Path, zipf: Path, log: Path) -> dict:
|
||||||
|
"""
|
||||||
|
Run gbak backup and ZIP the result.
|
||||||
|
Returns a result dict.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
"label": label,
|
||||||
|
"ok": False,
|
||||||
|
"fbk_size": 0,
|
||||||
|
"zip_size": 0,
|
||||||
|
"t_gbak": 0,
|
||||||
|
"t_zip": 0,
|
||||||
|
"error": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1) GBAK
|
||||||
|
print(f"GBAK: {label} ... ", end="", flush=True)
|
||||||
|
t0 = time.time()
|
||||||
|
cmd = [GBAK, "-b", "-user", FB_USER, "-pas", FB_PASS, db_conn, str(fbk), "-v"]
|
||||||
|
with open(log, "w", encoding="utf-8") as f:
|
||||||
|
subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, check=True)
|
||||||
|
result["t_gbak"] = time.time() - t0
|
||||||
|
result["fbk_size"] = fbk.stat().st_size
|
||||||
|
print(f"OK ({result['t_gbak']:.0f}s, {result['fbk_size']/1024/1024:.1f} MB)")
|
||||||
|
|
||||||
|
# 2) ZIP
|
||||||
|
t1 = time.time()
|
||||||
|
processed = 0
|
||||||
|
fbk_size = result["fbk_size"]
|
||||||
|
with zipfile.ZipFile(zipf, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||||
|
zi = zipfile.ZipInfo(fbk.name)
|
||||||
|
zi.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
with zf.open(zi, "w", force_zip64=True) as z:
|
||||||
|
with open(fbk, "rb") as src:
|
||||||
|
while buf := src.read(CHUNK):
|
||||||
|
z.write(buf)
|
||||||
|
processed += len(buf)
|
||||||
|
pct = processed * 100 / fbk_size
|
||||||
|
print(f"\r ZIP {label}: {pct:6.2f}%", end="", flush=True)
|
||||||
|
print()
|
||||||
|
result["t_zip"] = time.time() - t1
|
||||||
|
result["zip_size"] = zipf.stat().st_size
|
||||||
|
|
||||||
|
# 3) Delete FBK + LOG
|
||||||
|
fbk.unlink()
|
||||||
|
log.unlink()
|
||||||
|
|
||||||
|
result["ok"] = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def format_result(r: dict) -> str:
|
||||||
|
ratio = 100 * (1 - r["zip_size"] / r["fbk_size"]) if r["fbk_size"] else 0
|
||||||
|
return (
|
||||||
|
f" {r['label']}: "
|
||||||
|
f"FBK {r['fbk_size']/1024/1024:.1f} MB → "
|
||||||
|
f"ZIP {r['zip_size']/1024/1024:.1f} MB "
|
||||||
|
f"({ratio:.0f}% komprese, "
|
||||||
|
f"gbak {r['t_gbak']:.0f}s, zip {r['t_zip']:.0f}s)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
now = datetime.now()
|
||||||
|
ts = now.strftime("%Y-%m-%d_%H-%M-%S")
|
||||||
|
|
||||||
|
backed_up = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 1) Hlavní DB – MEDICUS.FDB
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
fbk = BACKUP_DIR / f"MEDICUS_{ts}.fbk"
|
||||||
|
zipf = BACKUP_DIR / f"MEDICUS_{ts}.zip"
|
||||||
|
log = BACKUP_DIR / f"MEDICUS_{ts}.log"
|
||||||
|
try:
|
||||||
|
r = gbak_and_zip("MEDICUS", MAIN_DB, fbk, zipf, log)
|
||||||
|
backed_up.append(r)
|
||||||
|
except Exception:
|
||||||
|
errors.append({"label": "MEDICUS", "error": traceback.format_exc()})
|
||||||
|
for f in (fbk, log):
|
||||||
|
if f.exists():
|
||||||
|
f.unlink()
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 2) Externí DB – MEDICUS_FILES_*.fdb
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
fdb_all = sorted(
|
||||||
|
set(EXT_DIR.glob("MEDICUS_FILES_*.fdb")) | set(EXT_DIR.glob("MEDICUS_FILES_*.FDB")),
|
||||||
|
key=lambda p: p.name.lower(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for fdb in fdb_all:
|
||||||
|
name = fdb.stem
|
||||||
|
fbk = BACKUP_DIR / f"{name}_{ts}.fbk"
|
||||||
|
zipf = BACKUP_DIR / f"{name}_{ts}.zip"
|
||||||
|
log = BACKUP_DIR / f"{name}_{ts}.log"
|
||||||
|
db_conn = f"localhost/{FB_PORT}:{fdb}"
|
||||||
|
try:
|
||||||
|
r = gbak_and_zip(name, db_conn, fbk, zipf, log)
|
||||||
|
backed_up.append(r)
|
||||||
|
except Exception:
|
||||||
|
errors.append({"label": name, "error": traceback.format_exc()})
|
||||||
|
for f in (fbk, log):
|
||||||
|
if f.exists():
|
||||||
|
f.unlink()
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Report
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
total = 1 + len(fdb_all)
|
||||||
|
report = [
|
||||||
|
f"Backup Medicus – {now.strftime('%d.%m.%Y %H:%M')}",
|
||||||
|
f"Celkem DB: {total} | OK: {len(backed_up)} | Chyby: {len(errors)}",
|
||||||
|
f"Výstupní adresář: {BACKUP_DIR}",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
if backed_up:
|
||||||
|
report.append("--- Zálohováno ---")
|
||||||
|
total_zip = sum(r["zip_size"] for r in backed_up)
|
||||||
|
for r in backed_up:
|
||||||
|
report.append(format_result(r))
|
||||||
|
report.append(f" Celková velikost ZIP: {total_zip/1024/1024:.1f} MB")
|
||||||
|
report.append("")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
report.append("--- CHYBY ---")
|
||||||
|
for e in errors:
|
||||||
|
report.append(f" {e['label']}:\n{e['error']}")
|
||||||
|
report.append("")
|
||||||
|
|
||||||
|
has_errors = bool(errors)
|
||||||
|
subject = (
|
||||||
|
f"{'X' if has_errors else 'OK'} MEDICUS backup "
|
||||||
|
f"{len(backed_up)}/{total}"
|
||||||
|
+ (f" – {len(errors)} chyb" if has_errors else "")
|
||||||
|
)
|
||||||
|
|
||||||
|
send_mail(MAIL_TO, subject, "\n".join(report))
|
||||||
|
print("\n" + "\n".join(report))
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
raise RuntimeError(f"{len(errors)} backup(s) failed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"""
|
||||||
|
001_pruzkum_PN_tabulek.py
|
||||||
|
=========================
|
||||||
|
Průzkum Medicus DB – hledáme tabulky a strukturu relacionou s PN
|
||||||
|
(pracovní neschopnost).
|
||||||
|
|
||||||
|
Spustit na Windows:
|
||||||
|
python "001_pruzkum_PN_tabulek.py"
|
||||||
|
|
||||||
|
Výstup: přehled tabulek s PN v názvu + jejich sloupce + ukázka dat.
|
||||||
|
"""
|
||||||
|
import fdb
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# ── Připojení ──────────────────────────────────────────────────────────────────
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA',
|
||||||
|
password='masterkey',
|
||||||
|
charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# ── 1. Všechny tabulky s "PN" v názvu ─────────────────────────────────────────
|
||||||
|
print("=" * 60)
|
||||||
|
print("TABULKY obsahující 'PN' v názvu")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT TRIM(rdb$relation_name)
|
||||||
|
FROM rdb$relations
|
||||||
|
WHERE rdb$view_blr IS NULL
|
||||||
|
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
|
||||||
|
AND UPPER(rdb$relation_name) CONTAINING 'PN'
|
||||||
|
ORDER BY rdb$relation_name
|
||||||
|
""")
|
||||||
|
pn_tables = [row[0] for row in cur.fetchall()]
|
||||||
|
|
||||||
|
if pn_tables:
|
||||||
|
for t in pn_tables:
|
||||||
|
print(f" {t}")
|
||||||
|
else:
|
||||||
|
print(" (žádná)")
|
||||||
|
|
||||||
|
# ── 2. Sloupce každé PN tabulky + ukázka dat ──────────────────────────────────
|
||||||
|
for table in pn_tables:
|
||||||
|
print(f"\n{'─' * 60}")
|
||||||
|
print(f"TABULKA: {table}")
|
||||||
|
print(f"{'─' * 60}")
|
||||||
|
|
||||||
|
# Sloupce
|
||||||
|
cur.execute("""
|
||||||
|
SELECT TRIM(rf.rdb$field_name),
|
||||||
|
f.rdb$field_type,
|
||||||
|
f.rdb$field_length,
|
||||||
|
rf.rdb$null_flag
|
||||||
|
FROM rdb$relation_fields rf
|
||||||
|
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
|
||||||
|
WHERE TRIM(rf.rdb$relation_name) = ?
|
||||||
|
ORDER BY rf.rdb$field_position
|
||||||
|
""", (table,))
|
||||||
|
cols = cur.fetchall()
|
||||||
|
|
||||||
|
# field_type kódy Firebirdu (nejběžnější)
|
||||||
|
type_map = {
|
||||||
|
7: 'SMALLINT', 8: 'INTEGER', 10: 'FLOAT', 12: 'DATE',
|
||||||
|
13: 'TIME', 14: 'CHAR', 16: 'BIGINT', 27: 'DOUBLE',
|
||||||
|
35: 'TIMESTAMP', 37: 'VARCHAR', 261: 'BLOB',
|
||||||
|
}
|
||||||
|
|
||||||
|
col_names = []
|
||||||
|
for c in cols:
|
||||||
|
name, ftype, flen, nullable = c
|
||||||
|
type_str = type_map.get(ftype, f'type#{ftype}')
|
||||||
|
if flen and ftype in (14, 37):
|
||||||
|
type_str += f'({flen})'
|
||||||
|
null_str = '' if nullable else ' NOT NULL'
|
||||||
|
print(f" {name:<25} {type_str}{null_str}")
|
||||||
|
col_names.append(name)
|
||||||
|
|
||||||
|
# Počet řádků
|
||||||
|
try:
|
||||||
|
cur.execute(f'SELECT COUNT(*) FROM {table}')
|
||||||
|
pocet = cur.fetchone()[0]
|
||||||
|
print(f"\n --> Celkem řádků: {pocet}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n --> COUNT selhalo: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ukázka prvních 5 řádků
|
||||||
|
if pocet > 0:
|
||||||
|
try:
|
||||||
|
cur.execute(f'SELECT FIRST 5 * FROM {table}')
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f"\n Ukázka (max 5 řádků):")
|
||||||
|
header = " | ".join(f"{n[:12]:>12}" for n in col_names)
|
||||||
|
print(f" {header}")
|
||||||
|
print(f" {'-' * len(header)}")
|
||||||
|
for row in rows:
|
||||||
|
vals = []
|
||||||
|
for v in row:
|
||||||
|
if isinstance(v, (datetime.date, datetime.datetime)):
|
||||||
|
vals.append(v.isoformat()[:12])
|
||||||
|
elif isinstance(v, bytes):
|
||||||
|
vals.append(f'<blob {len(v)}B>')
|
||||||
|
elif v is None:
|
||||||
|
vals.append('NULL')
|
||||||
|
else:
|
||||||
|
vals.append(str(v)[:12])
|
||||||
|
print(f" {' | '.join(f'{v:>12}' for v in vals)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Ukázka selhala: {e}")
|
||||||
|
|
||||||
|
# ── 3. Hledáme i tabulky s "NESCHOP" nebo "NESCHOPENK" ────────────────────────
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print("TABULKY s 'NESCHOP' nebo 'PRACOV' nebo 'SICK' v názvu")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT TRIM(rdb$relation_name)
|
||||||
|
FROM rdb$relations
|
||||||
|
WHERE rdb$view_blr IS NULL
|
||||||
|
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
|
||||||
|
AND (
|
||||||
|
UPPER(rdb$relation_name) CONTAINING 'NESCHOP'
|
||||||
|
OR UPPER(rdb$relation_name) CONTAINING 'PRACOV'
|
||||||
|
OR UPPER(rdb$relation_name) CONTAINING 'SICK'
|
||||||
|
)
|
||||||
|
ORDER BY rdb$relation_name
|
||||||
|
""")
|
||||||
|
extra = [row[0] for row in cur.fetchall()]
|
||||||
|
if extra:
|
||||||
|
for t in extra:
|
||||||
|
print(f" {t}")
|
||||||
|
else:
|
||||||
|
print(" (žádná)")
|
||||||
|
|
||||||
|
# ── 4. Rychlý přehled – tabulky s datem "OD/DO" nebo "ZACATEK/KONEC" ──────────
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print("SLOUPCE: hledáme 'DATPN', 'DATDO', 'DATUM_DO' v celé DB")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT TRIM(rf.rdb$relation_name), TRIM(rf.rdb$field_name)
|
||||||
|
FROM rdb$relation_fields rf
|
||||||
|
JOIN rdb$relations r ON rf.rdb$relation_name = r.rdb$relation_name
|
||||||
|
WHERE (r.rdb$system_flag IS NULL OR r.rdb$system_flag = 0)
|
||||||
|
AND r.rdb$view_blr IS NULL
|
||||||
|
AND (
|
||||||
|
UPPER(rf.rdb$field_name) CONTAINING 'DATPN'
|
||||||
|
OR UPPER(rf.rdb$field_name) = 'DATDO'
|
||||||
|
OR UPPER(rf.rdb$field_name) CONTAINING 'DATUM_DO'
|
||||||
|
OR UPPER(rf.rdb$field_name) CONTAINING 'DAT_DO'
|
||||||
|
OR UPPER(rf.rdb$field_name) CONTAINING 'PN'
|
||||||
|
)
|
||||||
|
ORDER BY rf.rdb$relation_name, rf.rdb$field_name
|
||||||
|
""")
|
||||||
|
hits = cur.fetchall()
|
||||||
|
if hits:
|
||||||
|
for tab, col in hits:
|
||||||
|
print(f" {tab:<30} {col}")
|
||||||
|
else:
|
||||||
|
print(" (nic nenalezeno)")
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print("Průzkum dokončen.")
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""
|
||||||
|
002_detail_NES_NP_tabulky.py
|
||||||
|
============================
|
||||||
|
Průzkum klíčových tabulek neschopenky:
|
||||||
|
- NES → hlavní tabulka pracovní neschopnosti
|
||||||
|
- NP → pravděpodobně neschopenka podání
|
||||||
|
- SOC_NEPRITOMNOST → sociální nepřítomnost
|
||||||
|
|
||||||
|
Spustit na Windows:
|
||||||
|
python "002_detail_NES_NP_tabulky.py"
|
||||||
|
"""
|
||||||
|
import fdb
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
conn = fdb.connect(
|
||||||
|
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
|
||||||
|
user='SYSDBA',
|
||||||
|
password='masterkey',
|
||||||
|
charset='win1250'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(v):
|
||||||
|
if isinstance(v, (datetime.date, datetime.datetime)):
|
||||||
|
return v.isoformat()
|
||||||
|
if isinstance(v, datetime.time):
|
||||||
|
return v.isoformat()
|
||||||
|
if isinstance(v, bytes):
|
||||||
|
# Pro BLOB zkusíme dekódovat jako text
|
||||||
|
try:
|
||||||
|
return v.decode('cp1250')[:80]
|
||||||
|
except Exception:
|
||||||
|
return f'<blob {len(v)}B>'
|
||||||
|
if v is None:
|
||||||
|
return 'NULL'
|
||||||
|
return str(v)
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_table(table):
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print(f" TABULKA: {table}")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
# Sloupce
|
||||||
|
cur.execute("""
|
||||||
|
SELECT TRIM(rf.rdb$field_name),
|
||||||
|
f.rdb$field_type,
|
||||||
|
f.rdb$field_length
|
||||||
|
FROM rdb$relation_fields rf
|
||||||
|
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
|
||||||
|
WHERE TRIM(rf.rdb$relation_name) = ?
|
||||||
|
ORDER BY rf.rdb$field_position
|
||||||
|
""", (table,))
|
||||||
|
cols = cur.fetchall()
|
||||||
|
type_map = {
|
||||||
|
7: 'SMALLINT', 8: 'INTEGER', 10: 'FLOAT', 12: 'DATE',
|
||||||
|
13: 'TIME', 14: 'CHAR', 16: 'BIGINT', 27: 'DOUBLE',
|
||||||
|
35: 'TIMESTAMP', 37: 'VARCHAR', 261: 'BLOB',
|
||||||
|
}
|
||||||
|
col_names = [c[0] for c in cols]
|
||||||
|
print("\nSloupce:")
|
||||||
|
for c in cols:
|
||||||
|
name, ftype, flen = c
|
||||||
|
type_str = type_map.get(ftype, f'type#{ftype}')
|
||||||
|
if flen and ftype in (14, 37):
|
||||||
|
type_str += f'({flen})'
|
||||||
|
print(f" {name:<30} {type_str}")
|
||||||
|
|
||||||
|
# Počet řádků
|
||||||
|
cur.execute(f'SELECT COUNT(*) FROM {table}')
|
||||||
|
pocet = cur.fetchone()[0]
|
||||||
|
print(f"\nCelkem řádků: {pocet}")
|
||||||
|
|
||||||
|
if pocet == 0:
|
||||||
|
return col_names
|
||||||
|
|
||||||
|
# Ukázka 5 nejnovějších (pokud má datum sloupec)
|
||||||
|
date_cols = [c[0] for c in cols if c[1] in (12, 35)] # DATE nebo TIMESTAMP
|
||||||
|
order = f'ORDER BY {date_cols[0]} DESC' if date_cols else ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
cur.execute(f'SELECT FIRST 5 * FROM {table} {order}')
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f"\nUkázka (5 {'nejnovějších' if order else 'prvních'}):")
|
||||||
|
for row in rows:
|
||||||
|
print()
|
||||||
|
for name, val in zip(col_names, row):
|
||||||
|
s = serialize(val)
|
||||||
|
if s != 'NULL':
|
||||||
|
print(f" {name:<30} {s}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Ukázka selhala: {e}")
|
||||||
|
|
||||||
|
return col_names
|
||||||
|
|
||||||
|
|
||||||
|
# ── Prozkoumáme klíčové tabulky ───────────────────────────────────────────────
|
||||||
|
for tbl in ['NES', 'NP', 'SOC_NEPRITOMNOST']:
|
||||||
|
inspect_table(tbl)
|
||||||
|
|
||||||
|
# ── Speciálně: aktivní PN ─────────────────────────────────────────────────────
|
||||||
|
print(f"\n\n{'=' * 70}")
|
||||||
|
print(" POKUS: aktivní záznamy v NES (konec NULL nebo v budoucnosti)")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
dnes = datetime.date.today()
|
||||||
|
|
||||||
|
# Nejdřív zjistíme sloupce NES
|
||||||
|
cur.execute("""
|
||||||
|
SELECT TRIM(rf.rdb$field_name), f.rdb$field_type
|
||||||
|
FROM rdb$relation_fields rf
|
||||||
|
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
|
||||||
|
WHERE TRIM(rf.rdb$relation_name) = 'NES'
|
||||||
|
ORDER BY rf.rdb$field_position
|
||||||
|
""")
|
||||||
|
nes_cols = [(r[0], r[1]) for r in cur.fetchall()]
|
||||||
|
nes_col_names = [c[0] for c in nes_cols]
|
||||||
|
|
||||||
|
print(f"\nVšechny sloupce NES: {', '.join(nes_col_names)}")
|
||||||
|
|
||||||
|
# Hledáme sloupce s datumem konce PN
|
||||||
|
date_cols_nes = [c[0] for c in nes_cols if c[1] in (12, 35)]
|
||||||
|
print(f"Datumové sloupce: {date_cols_nes}")
|
||||||
|
|
||||||
|
# Zkusíme různé kandidátní sloupce pro "datum do"
|
||||||
|
candidates_do = [c for c in nes_col_names if 'DO' in c or 'KONEC' in c or 'END' in c.upper()]
|
||||||
|
candidates_od = [c for c in nes_col_names if 'OD' in c or 'ZACATEK' in c or 'VZNIK' in c or 'START' in c.upper()]
|
||||||
|
candidates_pac = [c for c in nes_col_names if 'PAC' in c or 'IDPAC' in c]
|
||||||
|
print(f"Kandidáti DATUM_DO: {candidates_do}")
|
||||||
|
print(f"Kandidáti DATUM_OD: {candidates_od}")
|
||||||
|
print(f"Kandidáti IDPAC: {candidates_pac}")
|
||||||
|
|
||||||
|
# Ukázka posledních 10 záznamů NES seřazená dle prvního datumového sloupce
|
||||||
|
if date_cols_nes:
|
||||||
|
try:
|
||||||
|
cur.execute(f'SELECT FIRST 10 * FROM NES ORDER BY {date_cols_nes[0]} DESC')
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f"\nPosledních 10 záznamů NES (dle {date_cols_nes[0]} DESC):")
|
||||||
|
for row in rows:
|
||||||
|
print()
|
||||||
|
for name, val in zip(nes_col_names, row):
|
||||||
|
s = serialize(val)
|
||||||
|
if s != 'NULL':
|
||||||
|
print(f" {name:<30} {s}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Selhalo: {e}")
|
||||||
|
|
||||||
|
# ── NP tabulka – stejný rozbor ────────────────────────────────────────────────
|
||||||
|
print(f"\n\n{'=' * 70}")
|
||||||
|
print(" POKUS: aktivní záznamy v NP")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT TRIM(rf.rdb$field_name), f.rdb$field_type
|
||||||
|
FROM rdb$relation_fields rf
|
||||||
|
JOIN rdb$fields f ON rf.rdb$field_source = f.rdb$field_name
|
||||||
|
WHERE TRIM(rf.rdb$relation_name) = 'NP'
|
||||||
|
ORDER BY rf.rdb$field_position
|
||||||
|
""")
|
||||||
|
np_cols = [(r[0], r[1]) for r in cur.fetchall()]
|
||||||
|
np_col_names = [c[0] for c in np_cols]
|
||||||
|
print(f"\nVšechny sloupce NP: {', '.join(np_col_names)}")
|
||||||
|
date_cols_np = [c[0] for c in np_cols if c[1] in (12, 35)]
|
||||||
|
print(f"Datumové sloupce: {date_cols_np}")
|
||||||
|
|
||||||
|
if date_cols_np:
|
||||||
|
try:
|
||||||
|
cur.execute(f'SELECT FIRST 10 * FROM NP ORDER BY {date_cols_np[0]} DESC')
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f"\nPosledních 10 záznamů NP (dle {date_cols_np[0]} DESC):")
|
||||||
|
for row in rows:
|
||||||
|
print()
|
||||||
|
for name, val in zip(np_col_names, row):
|
||||||
|
s = serialize(val)
|
||||||
|
if s != 'NULL':
|
||||||
|
print(f" {name:<30} {s}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Selhalo: {e}")
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("Hotovo.")
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
003_aktivni_PN_seznam.py
|
||||||
|
========================
|
||||||
|
Vypíše seznam pacientů s aktivní pracovní neschopností (PN) k dnešnímu datu.
|
||||||
|
|
||||||
|
SQL logika převzata přímo z Medicusu (report "Přehled práce neschopných pacientů"):
|
||||||
|
- ZACNES <= dnes
|
||||||
|
- (KONNES >= dnes) OR (KONNES IS NULL)
|
||||||
|
- PRACNE = 'A'
|
||||||
|
- STORNO <> 'T'
|
||||||
|
|
||||||
|
Spouštění: tlačítkem z Medicusu nebo přímo pythonu.
|
||||||
|
"""
|
||||||
|
import sys, os, traceback
|
||||||
|
|
||||||
|
_LOG = r"C:\Users\vlado\PycharmProjects\Medicus\PNWithClaude\003_error.log"
|
||||||
|
|
||||||
|
def _log_exception(exc_type, exc_value, exc_tb):
|
||||||
|
with open(_LOG, "w", encoding="utf-8") as f:
|
||||||
|
traceback.print_exception(exc_type, exc_value, exc_tb, file=f)
|
||||||
|
sys.excepthook = _log_exception
|
||||||
|
|
||||||
|
import fdb
|
||||||
|
import datetime
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
# ── Připojení ─────────────────────────────────────────────────────────────────
|
||||||
|
DSN = r'localhost:c:\medicus 3\data\medicus.fdb'
|
||||||
|
USER = 'SYSDBA'
|
||||||
|
PASSWORD = 'masterkey'
|
||||||
|
CHARSET = 'win1250'
|
||||||
|
|
||||||
|
# ── SQL ───────────────────────────────────────────────────────────────────────
|
||||||
|
SQL = """
|
||||||
|
SELECT
|
||||||
|
kar.PRIJMENI,
|
||||||
|
kar.JMENO,
|
||||||
|
kar.RODCIS,
|
||||||
|
nes.ZACNES,
|
||||||
|
nes.KONNES,
|
||||||
|
nes.DIAGNO,
|
||||||
|
COALESCE(nes.ECN, nes.CISNES) AS CISNES
|
||||||
|
FROM NES nes
|
||||||
|
JOIN KAR kar ON kar.IDPAC = nes.IDPAC
|
||||||
|
WHERE
|
||||||
|
nes.ZACNES <= ?
|
||||||
|
AND (nes.KONNES >= ? OR nes.KONNES IS NULL)
|
||||||
|
AND nes.PRACNE = 'A'
|
||||||
|
AND nes.STORNO <> 'T'
|
||||||
|
ORDER BY kar.PRIJMENI, kar.JMENO
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def nacti_data():
|
||||||
|
dnes = datetime.date.today()
|
||||||
|
conn = fdb.connect(dsn=DSN, user=USER, password=PASSWORD, charset=CHARSET)
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(SQL, (dnes, dnes))
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return dnes, rows
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def delka_PN(zacnes, konnes, dnes):
|
||||||
|
"""Počet dní PN (od začátku do dnes nebo do konce)."""
|
||||||
|
do = konnes if konnes else dnes
|
||||||
|
return (do - zacnes).days + 1
|
||||||
|
|
||||||
|
|
||||||
|
def zobraz_okno(dnes, rows):
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title(f"Aktivní pracovní neschopnost – {dnes.strftime('%d.%m.%Y')}")
|
||||||
|
w, h = 900, 550
|
||||||
|
root.update_idletasks()
|
||||||
|
sw = root.winfo_screenwidth()
|
||||||
|
sh = root.winfo_screenheight()
|
||||||
|
x = (sw - w) // 2
|
||||||
|
y = (sh - h) // 2
|
||||||
|
root.geometry(f"{w}x{h}+{x}+{y}")
|
||||||
|
|
||||||
|
# ── Záhlaví ───────────────────────────────────────────────────────────────
|
||||||
|
hlavicka = tk.Frame(root, bg="#2c5f8a", pady=8)
|
||||||
|
hlavicka.pack(fill=tk.X)
|
||||||
|
tk.Label(
|
||||||
|
hlavicka,
|
||||||
|
text=f"Pracovní neschopnost – aktivní k {dnes.strftime('%d.%m.%Y')} ({len(rows)} pacientů)",
|
||||||
|
font=("Segoe UI", 13, "bold"),
|
||||||
|
fg="white", bg="#2c5f8a"
|
||||||
|
).pack()
|
||||||
|
|
||||||
|
# ── Tabulka ───────────────────────────────────────────────────────────────
|
||||||
|
cols = ("Příjmení", "Jméno", "Rodné číslo", "Začátek", "Konec", "Dní", "Diagnóza", "Č. neschopenky")
|
||||||
|
frame = tk.Frame(root)
|
||||||
|
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
scroll_y = tk.Scrollbar(frame, orient=tk.VERTICAL)
|
||||||
|
scroll_y.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
scroll_x = tk.Scrollbar(frame, orient=tk.HORIZONTAL)
|
||||||
|
scroll_x.pack(side=tk.BOTTOM, fill=tk.X)
|
||||||
|
|
||||||
|
tree = ttk.Treeview(
|
||||||
|
frame,
|
||||||
|
columns=cols,
|
||||||
|
show="headings",
|
||||||
|
yscrollcommand=scroll_y.set,
|
||||||
|
xscrollcommand=scroll_x.set
|
||||||
|
)
|
||||||
|
scroll_y.config(command=tree.yview)
|
||||||
|
scroll_x.config(command=tree.xview)
|
||||||
|
|
||||||
|
# Šířky sloupců
|
||||||
|
widths = [120, 100, 110, 85, 85, 50, 80, 150]
|
||||||
|
for col, w in zip(cols, widths):
|
||||||
|
tree.column(col, width=w, anchor=tk.W)
|
||||||
|
|
||||||
|
# ── Řazení kliknutím na záhlaví ───────────────────────────────────────────
|
||||||
|
_sort_state = {} # col -> ascending True/False
|
||||||
|
|
||||||
|
def sort_by(col):
|
||||||
|
ascending = not _sort_state.get(col, False)
|
||||||
|
_sort_state[col] = ascending
|
||||||
|
# Index sloupce pro čtení hodnoty
|
||||||
|
col_idx = cols.index(col)
|
||||||
|
data = [(tree.set(k, col), k) for k in tree.get_children("")]
|
||||||
|
# Číselné řazení pro "Dní"
|
||||||
|
if col == "Dní":
|
||||||
|
data.sort(key=lambda t: int(t[0]) if t[0].isdigit() else 0, reverse=not ascending)
|
||||||
|
else:
|
||||||
|
data.sort(key=lambda t: t[0].lower(), reverse=not ascending)
|
||||||
|
for idx, (_, k) in enumerate(data):
|
||||||
|
tree.move(k, "", idx)
|
||||||
|
# Šipka v záhlaví
|
||||||
|
for c in cols:
|
||||||
|
arrow = (" ▲" if ascending else " ▼") if c == col else ""
|
||||||
|
tree.heading(c, text=c + arrow, command=lambda c=c: sort_by(c))
|
||||||
|
# Obarvi znovu střídavě
|
||||||
|
for idx, (_, k) in enumerate(data):
|
||||||
|
current_tags = list(tree.item(k, "tags"))
|
||||||
|
dlouha = "dlouha" in current_tags
|
||||||
|
if dlouha:
|
||||||
|
tree.item(k, tags=("dlouha",))
|
||||||
|
else:
|
||||||
|
tree.item(k, tags=("even" if idx % 2 == 0 else "odd",))
|
||||||
|
|
||||||
|
for col in cols:
|
||||||
|
tree.heading(col, text=col, command=lambda c=col: sort_by(c))
|
||||||
|
|
||||||
|
# Střídavé barvy řádků
|
||||||
|
tree.tag_configure("even", background="#f0f4f8")
|
||||||
|
tree.tag_configure("odd", background="white")
|
||||||
|
tree.tag_configure("dlouha", foreground="#c0392b") # červeně > 90 dní
|
||||||
|
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
prijmeni, jmeno, rodcis, zacnes, konnes, diagno, cisnes = row
|
||||||
|
dni = delka_PN(zacnes, konnes, dnes)
|
||||||
|
konec_str = konnes.strftime("%d.%m.%Y") if konnes else "trvá"
|
||||||
|
diagno_str = (diagno or "").strip()
|
||||||
|
cisnes_str = (cisnes or "").strip()
|
||||||
|
|
||||||
|
tag = "dlouha" if dni > 90 else ("even" if i % 2 == 0 else "odd")
|
||||||
|
tree.insert("", tk.END, values=(
|
||||||
|
prijmeni,
|
||||||
|
jmeno,
|
||||||
|
rodcis,
|
||||||
|
zacnes.strftime("%d.%m.%Y"),
|
||||||
|
konec_str,
|
||||||
|
dni,
|
||||||
|
diagno_str,
|
||||||
|
cisnes_str,
|
||||||
|
), tags=(tag,))
|
||||||
|
|
||||||
|
tree.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# ── Výchozí řazení: Dní vzestupně ─────────────────────────────────────────
|
||||||
|
sort_by("Dní")
|
||||||
|
|
||||||
|
# ── Legenda + zavřít ──────────────────────────────────────────────────────
|
||||||
|
spodek = tk.Frame(root, pady=6)
|
||||||
|
spodek.pack(fill=tk.X)
|
||||||
|
tk.Label(
|
||||||
|
spodek,
|
||||||
|
text="Červeně = PN delší než 90 dní",
|
||||||
|
font=("Segoe UI", 9),
|
||||||
|
fg="#c0392b"
|
||||||
|
).pack(side=tk.LEFT, padx=15)
|
||||||
|
tk.Button(
|
||||||
|
spodek,
|
||||||
|
text="Zavřít",
|
||||||
|
command=root.destroy,
|
||||||
|
width=12,
|
||||||
|
font=("Segoe UI", 10)
|
||||||
|
).pack(side=tk.RIGHT, padx=15)
|
||||||
|
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Hlavní tok ────────────────────────────────────────────────────────────────
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
dnes, rows = nacti_data()
|
||||||
|
zobraz_okno(dnes, rows)
|
||||||
|
except Exception:
|
||||||
|
with open(_LOG, "w", encoding="utf-8") as f:
|
||||||
|
traceback.print_exc(file=f)
|
||||||
|
raise
|
||||||
@@ -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*
|
||||||
@@ -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
|
||||||
@@ -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))
|
||||||
Reference in New Issue
Block a user