notebookvb
This commit is contained in:
@@ -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,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