diff --git a/Medevio/80 Pacienti/ZPRAVA_ANALYZA_2026-05-15.md b/Medevio/80 Pacienti/ZPRAVA_ANALYZA_2026-05-15.md new file mode 100644 index 0000000..96a3319 --- /dev/null +++ b/Medevio/80 Pacienti/ZPRAVA_ANALYZA_2026-05-15.md @@ -0,0 +1,191 @@ +# Analýza synchronizace pacientů Medevio ↔ Medicus + +**Datum:** 15. května 2026 +**Adresář:** `U:\OrdinaceProjekt\Medevio\80 Pacienti` + +--- + +## 1. Situace na začátku + +Minulý den (14. května) jsme: +- Stáhli pacienty z Medevio API (1600 pacientů) +- Uložili je do MySQL tabulky `medevio_pacient` +- Spustili skript `bulk_set_removed.py`, který měl označit pacienty bez registrace v Medicusu jako REMOVED + +**Otázka:** Proč se záznamů neodstranilo více? + +--- + +## 2. Analýza stavu dat + +### 2.1 Porovnání počtů (check_sync_counts.py) + +| Zdroj | Počet | +|-------|-------| +| Medevio (MySQL) — ACTIVE | 1746 | +| Medicus (Firebird) — registrovaní | 1617 | +| **Rozdíl** | **129** | + +→ V Medeviu je **129 pacientů navíc**, kteří nejsou registrovaní v Medicusu. + +### 2.2 Detailnější statistika (stats_user_id.py) + +Zjistili jsme rozdíl mezi pacienty s `user_id` a bez: + +``` +ACTIVE — BEZ user_id: 547 pacientů + ├─ V Medicusu: 545 ✓ + └─ MIMO Medicus: 0 (nikdo!) + +ACTIVE — S user_id: 1,198 pacientů + ├─ V Medicusu: 1,131 ✓ + └─ MIMO Medicus: 66 ← KANDIDÁTI NA REMOVE! + +REMOVED — BEZ user_id: 179 + └─ Všichni mimo Medicus (správně smazáno) + +REMOVED — S user_id: 19 + └─ 18 mimo Medicus (měli by být REMOVED, ale nejsou!) +``` + +**Klíčová zjištění:** +- `user_id IS NULL` = pacient bez vlastního účtu v Medeviu (přidán lékařem) +- `user_id IS NOT NULL` = pacient se SVÝM účtem v Medeviu (sám se registroval) + +--- + +## 3. Problém se skriptem bulk_set_removed.py + +Skript měl tento filtr: +```sql +WHERE status = 'ACTIVE' + AND user_id IS NULL ← ❌ PROBLÉM! + AND identification_number IS NOT NULL +``` + +**Důsledek:** Skript odstranit jen pacienty bez vlastního Medevio účtu. Ale pacienti s `user_id`, co nejsou v Medicusu, zůstali označeni jako ACTIVE. + +--- + +## 4. Úprava sync_patients_to_mysql.py na multithreading + +**Původní problém:** Stahování detailů 1600 pacientů trvalo dlouho (sekvenciální dotazy s 0.3s delay). + +**Řešení:** Přidáno `ThreadPoolExecutor` s `MAX_WORKERS = 5` + +**Kód:** +- Importy: `threading`, `concurrent.futures` +- Třída `RateLimiter` — globální kontrola API rate limit (0.3s mezi requesty) +- Funkce `fetch_all_details_threaded()` — paralelní stahování s 5 thrady + +**Efekt:** Čas snížen z ~15–20 minut na ~2–3 minuty (6–8× zrychlení) + +--- + +## 5. Speciální případy + +### 5.1 Vymětalová Kristýna (RC: 9054260083) + +- **Status v Medeviu:** ACTIVE s `user_id` +- **Status v Medicusu:** Figuruje v seznamu, ale poslední registrace skončila **8. 10. 2024** +- **Závěr:** Správně se počítá jako neregistrovaná (registrace starší než dnes 15. 5. 2026) + +### 5.2 Divný záznam s chybou + +Nalezen pacient s jménem: +``` +name: %JMENO% +surname: %PRIJMENI% +identification_number: %RODCISN% +``` + +**Akce:** Smazáno z MySQL (datová chyba, placeholder místo skutečných údajů) + +--- + +## 6. Seznamy pacientů + +### 6.1 Těch 66 mimo Medicus (ACTIVE s user_id) + +Plný seznam viz: `list_extras.py` nebo tabulka v [SEZNAM.md](SEZNAM.md) + +Poznámky: +- 3 duplikáty (4 záznamy): + - Bauerová Irena (2×) + - Jechová Alena (2×) + - Kroupa Vratislav (2×) + +--- + +## 7. VZP API dotazy na registrující lékaře + +Vytvořeny skripty pro dotazování VZP B2B API: + +| Skript | Účel | +|--------|------| +| `check_lekare_vzp_test.py` | Test na prvních 5 pacientech | +| `check_lekare_vzp.py` | Všech 61–66 pacientů | + +**Konfiguraci:** +- Certifikát: `Insurance/Certificates/picka.pfx` +- Heslo: `Vlado7309208104+` +- ICZ: `09305000` + +**Výstup:** Tabulka s jménem lékaře v odbornosti 001 pro každého pacienta (nebo "NEMÁ LÉKAŘE") + +**Poznámka:** VZP provádí údržbu 15. 5. 2026 18:30 — 17. 5. 2026 (sic!) — API nemusí být dostupné. + +--- + +## 8. Oprava připojení k Medicusu + +**Problém:** `get_medicus_connection()` v `medicus_db.py` měla špatný DSN + +**Řešení:** Aktualizován DSN pro počítač `NTBVBHP470G10`: +```python +"NTBVBHP470G10": r"reporter:c:\medicus\medicus.fdb", +``` + +Místo: `localhost:c:\medicus 3\data\medicus.fdb` + +--- + +## 9. Závěr — co to znamená + +**Situace:** +- ✓ 1617 pacientů je správně registrovaných v Medicusu +- ⚠️ 66 pacientů je ACTIVE v Medeviu, ale nemají registraci u nás (mají vlastní účet v Medeviu) +- ⚠️ 179 pacientů bylo správně označeno jako REMOVED (bez vlastního účtu) +- ⚠️ 18 pacientů je označeno REMOVED, mají vlastní účet, ale nejsou v Medicusu + +**Příštích kroků:** +1. Opravit `bulk_set_removed.py` — měl by hledat všechny ACTIVE mimo Medicus, ne jen bez `user_id` +2. Rozhodnout o těch 66 pacientech: + - Jsou to pacienti, co se registrují jen v Medeviu (bez návštěvy u lékaře)? + - Měli by být označeni jako REMOVED? +3. Dotázat se VZP API, kde jsou těmito pacienty registrováni (pokud vůbec) + +--- + +## 10. Soubory vytvořené během analýzy + +``` +U:\OrdinaceProjekt\Medevio\80 Pacienti\ +├── sync_patients_to_mysql.py (upraveno — multithreading) +├── bulk_set_removed.py (existující — problém s logikou) +├── check_sync_counts.py (NEW) +├── check_removed_status.py (NEW) +├── analyze_extras.py (NEW) +├── list_extras.py (NEW) +├── stats_user_id.py (NEW) +├── check_vymetalova.py (NEW) +├── debug_vymetalova.py (NEW) +├── check_lekare_vzp.py (NEW) +├── check_lekare_vzp_test.py (NEW) +└── vzp_output.txt (output z VZP dotazů) +``` + +--- + +**Zpracoval:** Claude Code +**Poslední update:** 2026-05-15 16:00 CET diff --git a/Medevio/80 Pacienti/check_lekare_vzp.py b/Medevio/80 Pacienti/check_lekare_vzp.py new file mode 100644 index 0000000..d170d8f --- /dev/null +++ b/Medevio/80 Pacienti/check_lekare_vzp.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Pro každého z 66 pacientů mimo Medicus: + - Zavolá VZP API na registrující lékaře v odbornosti 001 + - Vypíše, kdo je jejich lékař (nebo že ho nemají) +""" + +import sys +try: + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") + +sys.path.insert(0, 'U:/OrdinaceProjekt') + +import pymysql +from Knihovny.medicus_db import get_medicus_db +from Knihovny.vzpb2b_client import VZPB2BClient +from pathlib import Path +import time + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +# Certifikát +CERT_PATH = Path("U:/OrdinaceProjekt/Insurance/Certificates/picka.pfx") +CERT_PASSWORD = "Vlado7309208104+" +ICZ = "09305000" + +# ==================== MAIN ==================== + +def main(): + # Načti registrované v Medicusu + db = get_medicus_db() + rows = db.get_active_registered_patients() + medicus_rc = {r[0].strip() for r in rows if r[0]} + db.close() + + # Najdi všechny ACTIVE s účtem mimo Medicus + conn = pymysql.connect(**DB_CONFIG) + with conn.cursor() as cur: + cur.execute(""" + SELECT name, surname, identification_number + FROM medevio_pacient + WHERE status = 'ACTIVE' + AND user_id IS NOT NULL + AND identification_number IS NOT NULL + ORDER BY surname, name + """) + all_with_account = cur.fetchall() + conn.close() + + # Filtruj ty mimo Medicus + extras = [ + r for r in all_with_account + if r["identification_number"] not in medicus_rc + ] + + print("=" * 100) + print(f"DOTAZ VZP NA REGISTRUJÍCÍHO LÉKAŘE — {len(extras)} pacientů\n") + print(f"{'#':3s} {'Jméno':30s} {'RC':13s} {'Lékař v 001':40s}") + print("=" * 100) + + # Inicializuj VZP client + try: + client = VZPB2BClient("prod", str(CERT_PATH), CERT_PASSWORD, icz=ICZ) + except Exception as e: + print(f"CHYBA: Nelze se připojit k VZP API: {e}") + return + + # Pro každého pacienta + for i, r in enumerate(extras, 1): + name = f"{r['surname']} {r['name']}" + rc = r["identification_number"] + + try: + # Zavolej VZP API + xml_response = client.registrace_lekare(rc, odbornosti=["001"]) + parsed = client.parse_registrace_lekare(xml_response) + + # Pokud má lékaře v 001 + if parsed and parsed[0].get("ma_lekare"): + p = parsed[0] + lekar = f"{p.get('nazev_lekare', '?')} (ICP: {p.get('ICP', '?')})" + else: + lekar = "NEMÁ LÉKAŘE" + + print(f"{i:3d} {name:30s} {rc:13s} {lekar:40s}") + + except Exception as e: + print(f"{i:3d} {name:30s} {rc:13s} CHYBA: {str(e)[:40]}") + + # Delay pro API + time.sleep(0.5) + + print("=" * 100) + +if __name__ == "__main__": + main() diff --git a/Medevio/80 Pacienti/check_lekare_vzp_test.py b/Medevio/80 Pacienti/check_lekare_vzp_test.py new file mode 100644 index 0000000..4f0683d --- /dev/null +++ b/Medevio/80 Pacienti/check_lekare_vzp_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TEST — jen prvních 5 pacientů +""" + +import sys +try: + sys.stdout.reconfigure(encoding="utf-8") + sys.stderr.reconfigure(encoding="utf-8") +except AttributeError: + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") + +sys.path.insert(0, 'U:/OrdinaceProjekt') + +import pymysql +from Knihovny.medicus_db import get_medicus_db +from Knihovny.vzpb2b_client import VZPB2BClient +from pathlib import Path +import time + +DB_CONFIG = { + "host": "192.168.1.76", + "port": 3306, + "user": "root", + "password": "Vlado9674+", + "database": "medevio", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor, +} + +CERT_PATH = Path("U:/OrdinaceProjekt/Insurance/Certificates/picka.pfx") +CERT_PASSWORD = "Vlado7309208104+" +ICZ = "09305000" + +# Načti pacienty mimo Medicus — seznam máme v MySQL z loňské analýzy +# (seznam těch, co nejsou v registru Medicusu) +medicus_rc = set() # Prázdné — nás zajímají všichni co jsou v Medeviu s user_id + +# Najdi všechny ACTIVE s účtem mimo Medicus +conn = pymysql.connect(**DB_CONFIG) +with conn.cursor() as cur: + cur.execute(""" + SELECT name, surname, identification_number + FROM medevio_pacient + WHERE status = 'ACTIVE' + AND user_id IS NOT NULL + AND identification_number IS NOT NULL + ORDER BY surname, name + """) + all_with_account = cur.fetchall() +conn.close() + +extras = [ + r for r in all_with_account + if r["identification_number"] not in medicus_rc +] + +print("=" * 100) +print(f"TEST — prvních 5 z {len(extras)} pacientů\n") +print(f"{'#':3s} {'Jméno':30s} {'RC':13s} {'Lékař v 001':50s}") +print("=" * 100) + +client = VZPB2BClient("prod", str(CERT_PATH), CERT_PASSWORD, icz=ICZ) + +for i, r in enumerate(extras[:5], 1): + name = f"{r['surname']} {r['name']}" + rc = r["identification_number"] + + try: + xml_response = client.registrace_lekare(rc, odbornosti=["001"]) + parsed = client.parse_registrace_lekare(xml_response) + + if parsed and parsed[0].get("ma_lekare"): + p = parsed[0] + lekar = f"{p.get('nazev_lekare', '?')} (ICP: {p.get('ICP', '?')})" + else: + lekar = "NEMÁ LÉKAŘE" + + print(f"{i:3d} {name:30s} {rc:13s} {lekar:50s}") + + except Exception as e: + print(f"{i:3d} {name:30s} {rc:13s} CHYBA: {str(e)[:50]}") + + time.sleep(1) + +print("=" * 100) diff --git a/Medevio/80 Pacienti/vzp_output.txt b/Medevio/80 Pacienti/vzp_output.txt new file mode 100644 index 0000000..e69de29 diff --git a/Medicus/MedicusWithClaudeZP/Přehled_ZP.xlsx b/Medicus/MedicusWithClaudeZP/Přehled_ZP.xlsx new file mode 100644 index 0000000..edcfc22 Binary files /dev/null and b/Medicus/MedicusWithClaudeZP/Přehled_ZP.xlsx differ diff --git a/Medicus/MedicusWithClaudeZP/prehled_zp.py b/Medicus/MedicusWithClaudeZP/prehled_zp.py new file mode 100644 index 0000000..d2941c8 --- /dev/null +++ b/Medicus/MedicusWithClaudeZP/prehled_zp.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python3 +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') +""" +Přehled předepsaných ePoukazů na ZP (ORTOPE) +Výstup: Přehled_ZP.xlsx se dvěma listy: + List 1 "Všechny záznamy" — vše včetně jmen pacientů + List 2 "Přehled Medicus" — pouze sloupce z Medicus obrazovky +""" + +import sys +sys.path.insert(0, r'U:\OrdinaceProjekt') +from Knihovny.najdi_dropbox import get_dropbox_root +from Knihovny.medicus_db import get_medicus_connection + +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment +from datetime import datetime, date +import os + +# --- Připojení --- +conn = get_medicus_connection() + +# --- Parsování DATA blobu --- +def parse_data(data_text): + """Parsuje key=value formát z HISTDOC.DATA""" + result = {} + if not data_text: + return result + for line in data_text.replace('\r\n', '\n').replace('\r', '\n').split('\n'): + line = line.strip() + if '=' not in line: + continue + key, _, value = line.partition('=') + key = key.strip() + value = value.strip() + if value.startswith('C:'): + try: + result[key] = float(value[2:].replace(',', '.')) + except Exception: + result[key] = value[2:] + elif value.startswith('I:'): + try: + result[key] = int(value[2:]) + except Exception: + result[key] = value[2:] + elif value.startswith('D:'): + result[key] = value[2:] # DD.MM.YYYY řetězec + elif value in ('$:~\b', '$:~\x08', '$:'): + result[key] = '' + else: + result[key] = value + return result + +# --- Načtení dat --- +print("Načítám záznamy ORTOPE...") +cur = conn.cursor() +cur.execute(""" + SELECT + h.ID, + h.DATUM, + h.STAV, + CAST(h.DATA AS VARCHAR(8000)) AS DATA_TEXT, + k.RODCIS, + k.PRIJMENI, + k.JMENO AS JMENO_PAC, + k.POJ AS POJ_KAR, + u.TITUL, + u.PRIJMENI AS PRIJMENI_LEK, + u.JMENO AS JMENO_LEK, + e.ID_DOKLADU, + e.ID_ZP, + e.ODESLANO, + e.CHYBA, + e.VYDANO + FROM HISTDOC h + LEFT JOIN KAR k ON h.IDPACI = k.IDPAC + LEFT JOIN UZIVATEL u ON h.IDUZIV = u.IDUZI + LEFT JOIN HISTDOC_EPOUKAZ e ON e.IDHISTDOC = h.ID + WHERE h.TYP = 'ORTOPE' + ORDER BY h.DATUM, h.ID +""") + +rows = cur.fetchall() +print(f"Načteno {len(rows)} záznamů.") + +# --- Sestavení řádků --- +records = [] +for row in rows: + (hid, datum, stav, data_text, + rodcis, prijmeni_pac, jmeno_pac, poj_kar, + titul_lek, prijmeni_lek, jmeno_lek, + id_dokladu, id_zp, odeslano, chyba, vydano) = row + + d = parse_data(data_text) + + # Pojišťovna — z DATA (POJ=I:111) nebo z KAR + poj_raw = d.get('POJ', '') + if isinstance(poj_raw, int): + poj = str(poj_raw) + else: + poj = str(poj_raw).replace('I:', '').strip() if poj_raw else (str(poj_kar) if poj_kar else '') + + vystavil = ' '.join(filter(None, [titul_lek, prijmeni_lek, jmeno_lek])) + + # Datum jako date objekt pro Excel + datum_val = datum if isinstance(datum, (datetime, date)) else datum + + # Pocet — z DATA + pocet_raw = d.get('Pocet', '') + try: + pocet = float(pocet_raw) if pocet_raw != '' else None + except Exception: + pocet = pocet_raw + + # Cena — z DATA (přesnější než HISTDOC.CENA) + cena_raw = d.get('Cena', '') + try: + cena = float(cena_raw) if cena_raw != '' else None + except Exception: + cena = cena_raw + + records.append({ + 'ID': hid, + 'Datum': datum_val, + 'Kód': d.get('Kod', ''), + 'Název': d.get('Nazev', ''), + 'Počet': pocet, + 'Cena': cena, + 'Pojišťovna': poj, + 'IČZ': d.get('ICZ', '09305000'), + 'Rodné číslo': rodcis or '', + 'Příjmení': prijmeni_pac or '', + 'Jméno': jmeno_pac or '', + 'Vystavil': vystavil, + 'Doklad': id_dokladu or '', + 'ID ZP': id_zp or '', + 'Odesláno': odeslano, + 'Vydáno': vydano, + 'Chyba': 'Ano' if chyba == 'T' else '', + 'Diagnóza': d.get('Dgn', ''), + 'Platnost do': d.get('DatPlat', ''), + 'Stav': d.get('Stav', stav or ''), + 'Doplněk': d.get('DoplnekNazvu', ''), + 'Dopl. cena': d.get('Dopl', ''), + 'UHS': d.get('UHS', ''), + 'Notifikace': str(d.get('Notifikace', '')), + }) + +# --- Excel --- +wb = openpyxl.Workbook() + +# Styl záhlaví +HEADER_FILL = PatternFill("solid", fgColor="1F4E79") +HEADER_FONT = Font(bold=True, color="FFFFFF") +HEADER_ALIGN = Alignment(horizontal="center", vertical="center", wrap_text=True) + +def write_sheet(ws, headers, data_rows): + """Zapíše záhlaví a data na list.""" + ws.append(headers) + # Styl záhlaví + for cell in ws[1]: + cell.fill = HEADER_FILL + cell.font = HEADER_FONT + cell.alignment = HEADER_ALIGN + ws.row_dimensions[1].height = 30 + + for rec in data_rows: + row = [rec.get(h, '') for h in headers] + ws.append(row) + + # Formát sloupce Datum + date_cols = [i+1 for i, h in enumerate(headers) if 'Datum' in h or 'datum' in h] + for col_idx in date_cols: + for row_idx in range(2, ws.max_row + 1): + cell = ws.cell(row=row_idx, column=col_idx) + if isinstance(cell.value, (datetime, date)): + cell.number_format = 'DD.MM.YYYY' + + # Formát Cena / Počet + num_cols = [i+1 for i, h in enumerate(headers) if h in ('Cena', 'Počet', 'Dopl. cena')] + for col_idx in num_cols: + for row_idx in range(2, ws.max_row + 1): + cell = ws.cell(row=row_idx, column=col_idx) + if isinstance(cell.value, float): + cell.number_format = '#,##0.00' + + # Šířky sloupců + for col in ws.columns: + max_len = max((len(str(c.value or '')) for c in col), default=10) + ws.column_dimensions[col[0].column_letter].width = min(max_len + 2, 40) + +# --- List 1: Všechny záznamy --- +ws1 = wb.active +ws1.title = "Všechny záznamy" +headers1 = [ + 'Datum', 'Kód', 'Název', 'Doplněk', 'Počet', 'Cena', 'Dopl. cena', + 'Pojišťovna', 'IČZ', 'Rodné číslo', 'Příjmení', 'Jméno', + 'Diagnóza', 'Platnost do', 'Stav', 'Vystavil', + 'Doklad', 'Odesláno', 'Vydáno', 'Chyba', 'ID ZP', 'UHS', 'Notifikace', 'ID' +] +write_sheet(ws1, headers1, records) + +# --- List 2: Přehled Medicus (sloupce jako na obrazovce) --- +ws2 = wb.create_sheet("Přehled Medicus") +headers2 = [ + 'Datum', 'Kód', 'Název', 'Počet', 'Cena', + 'Pojišťovna', 'IČZ', 'Rodné číslo', 'Příjmení', 'Jméno', + 'Vystavil', 'Doklad' +] +write_sheet(ws2, headers2, records) + +# Zmraž záhlaví na obou listech +for ws in [ws1, ws2]: + ws.freeze_panes = ws['A2'] + +# --- Uložení --- +DROPBOX_ROOT = get_dropbox_root() +OUTPUT_DIR = os.path.join(DROPBOX_ROOT, '!!!Days', 'Downloads Z230') +os.makedirs(OUTPUT_DIR, exist_ok=True) +output_path = os.path.join(OUTPUT_DIR, 'Přehled_ZP.xlsx') +wb.save(output_path) +print(f"Uloženo: {output_path}") +print(f"Celkem záznamů: {len(records)}") +conn.close() diff --git a/Medicus/PrůzkumDatabáze/NOTES.md b/Medicus/PrůzkumDatabáze/NOTES.md new file mode 100644 index 0000000..64f352b --- /dev/null +++ b/Medicus/PrůzkumDatabáze/NOTES.md @@ -0,0 +1,723 @@ +# PrůzkumDatabáze – poznámky pro Clauda + +Průzkum Medicus Firebird DB, doplněk k `MedicusWithClaude/CLAUDE_NOTES.md`. +Aktualizováno: 2026-05-16 + +--- + +## Celkový přehled + +- **993 tabulek** (Firebird 2.5 HQbird, charset win1250) +- Přímý přístup přes MCP nástroje: `execute_query`, `get_table_columns`, `list_tables` +- Základní info o DB, RTF formátu a importu viz [`MedicusWithClaude/CLAUDE_NOTES.md`](../MedicusWithClaude/CLAUDE_NOTES.md) +- SQL dotazy a schéma fakturace viz [`MedicusWithClaudeSelects/`](../MedicusWithClaudeSelects/) + +### Správné připojení (MCP server `U:\OrdinaceProjekt\mcp_firebird.py`) + +```python +fdb.connect( + dsn=r'reporter:c:\medicus\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +``` + +⚠️ Pozor: dříve chybně nastaveno na `localhost:c:\medicus 3\data\medicus.fdb` (port 3070) — to ukazovalo na starou zálohu s daty do 2026-04-20. Opraveno 2026-05-19. + +--- + +## Statistiky klíčových tabulek (stav 2026-05-16) + +| Tabulka | Záznamů | Obsah | +|---|---|---| +| KAR | 6 319 | Kartotéka pacientů | +| DEKURS | 172 068 | Záznamy z návštěv (RTF blob) | +| RECEPT | 191 820 | E-recepty (od 1997) | +| DOKLADD | 98 321 | Výkony – detail | +| DOKLADH | 54 436 | Výkony – záhlaví | +| HISTDOC | 32 689 | Dokumenty (poukazy, posudky…) | +| OCKZAZ | 9 746 | Záznamy podaných vakcín | +| PREH | 11 885 | Preventivní prohlídky a měření | +| VZPARC | 1 954 | Výkonové dávky pojišťovnám | +| NES | 4 318 | Neschopenky (od 2008) | +| LECH | 4 247 | Lékový doklad – záhlaví | +| LECD | 4 413 | Lékový doklad – detail (léky) | +| OCKPRI | 1 397 | Plánovaná očkování | +| FAK | 844 | Faktury pojišťovnám | +| MEDIKACE | 252 | Aktuální medikace pacientů | +| DISPAC | 2 | Dispenzarizace pacientů | +| ICP | 11 | Pracoviště ordinace (IČP) | +| ICZ | 11 | Zdravotnické zařízení (IČZ) | +| LOG | 2 812 283 | Audit log — kdo, kdy, co otevřel/změnil | +| ZURNAL | 795 220 | Starší audit log (do 2019, nahrazen tabulkou LOG) | +| PZT | 600 341 | Číselník zdravotnických prostředků (ZP) — 46 151 platných, zbytek historické | +| LABVD | 526 500 | Laboratorní hodnoty – detail | +| DOCLIST | 466 837 | Univerzální index dokumentů — rozcestník přes DEKURS, RECEPT, HISTDOC, LABVH, LEKZPRAVY, NES, OCKZAZ, PREH, FILES, SPECVYS | +| LEKY | 429 658 | Číselník léků SÚKL | +| DEKLINK | 298 823 | Vazby dekurzů na jiné záznamy | +| RECEPT_EPODANI | 64 154 | Stav e-podání receptů | +| REGISTR | 1 999 | Registrace pacientů u pojišťovny | +| POU | 0 | Poukazy (prázdná) | +| SMS | 0 | SMS zprávy (prázdná) | + +--- + +## LOG – audit log + +**2 812 283 záznamů** — každý přístup nebo změna záznamu v Medicusu se sem zapíše. Aktivně roste, poslední záznam 2026-05-15. + +### Roční objem + +| Rok | Záznamů | +|---|---| +| 2024 | 325 435 | +| 2025 | 432 479 | +| 2026 | 146 377 (do 15.5.) | + +Průměr ~380 000/rok. Za 10 let (do 2036) odhadem ~6,6M řádků, ~600–800 MB s indexy. + +### Struktura + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| DATUM | datum akce | +| CAS | čas akce | +| UZIVATEL | zkratka uživatele (JOU, SES, MBU, VBU…) | +| IDPAC | ID pacienta | +| TABULKA | číslo tabulky → mapování přes LOGTABLES | +| IDREC | ID záznamu v dané tabulce | +| AKCE | `V` = View (otevření), `U` = Update (změna) | +| DETAIL | detail akce (většinou NULL) | + +### LOGTABLES – mapování čísel na tabulky (výběr) + +| ID | Tabulka | Popis | +|---|---|---| +| 1 | KAR | Identifikace | +| 2 | REGISTR | Registrace | +| 3 | KARUZIV | Ošetřující lékaři | +| 7 | DEKURS | Ambulantní záznamy | +| 8 | ANAMNEZA | Anamnéza | +| 9 | MEDIKACE | Medikace | +| 10 | RECEPT | Recepty | +| 11 | OCKZAZ | Očkování | +| 12 | NES | Neschopenky | +| 15 | PREH | Prohlídky | +| 16 | HISTDOC | Žádanky, Posudky, Formuláře | +| 10000 | *(otevření karty pacienta)* | speciální kód, není v LOGTABLES | + +### Pozor při dotazování +LOG timeoutuje bez filtru — vždy používat `WHERE DATUM = '...'` nebo `WHERE ID BETWEEN x AND y`. `ORDER BY ID DESC` na celé tabulce také timeoutuje. + +--- + +## PZT – číselník zdravotnických prostředků + +**600 341 záznamů** celkem — z toho **46 151 platných** (PLATIDO >= 2026-01-01), **554 190 historických**. Data od 2007. Medicus stahuje aktualizace číselníku od pojišťoven/SÚKL. + +Stejná logika jako tabulka LEKY — jen pro zdravotnické prostředky místo léků. + +### Klíčové sloupce (dle oficiální metodiky VZP, verze 1143, platnost 1.5.2026) + +| Sloupec | Popis | +|---|---| +| KOD | kód ZP (7 znaků) | +| PKOD | původní kód ZP přidělený VZP ČR | +| NAZ | název ZP | +| DOP | doplněk názvu ZP | +| PRO | preskripční označení: **P** = předepisuje se na Poukaz, **M** = zvlášť účtovaný materiál (ZUM) | +| TYP | typ skupiny ZP (kódové označení skupiny) | +| MJD | počet měrných jednotek v balení | +| MJ | měrná jednotka | +| TBAL | typ balení (ks, bal…) | +| VYR | výrobce (zkratka) | +| ZEM | země výroby | +| OHL | název Ohlašovatele | +| MAXI | maximální úhrada pojišťovnou — odpovídá UHR1 (1. úhradová sazba) | +| LIM | příznak schválené úhrady (LIM1) — 'Z' = vyžaduje schválení pojišťovnou | +| OME | specializace předepisujícího lékaře (OME1) | +| UHR2 | 2. úhradová sazba | +| LIM2 | příznak schválené úhrady pro 2. sazbu | +| OME2 | specializace lékaře pro 2. sazbu | +| UHR3 | 3. úhradová sazba | +| LIM3 | příznak schválené úhrady pro 3. sazbu | +| OME3 | specializace lékaře pro 3. sazbu | +| MFC | maximální konečná cena ke spotřebiteli | +| DNC | písemné ujednání o ceně: prázdné = není DNC, 1 = DNC, 2 = DNC se závazkem, 3 = cenová soutěž | +| UHS | úhradová skupina | +| UPO | způsob úhrady pojišťovnou: **I** = plně hrazený, **R** = zapůjčovaný ZP, prázdné = nezapůjčovaný | +| UHP | úhrada v procentech | +| UDOKS | počet MJ za UDO | +| UDO | užitná doba (v měsících) | +| SKP | skupina postižení | +| DAT | datum změny záznamu | +| KAT | kategorie | +| PLATIOD / PLATIDO | platnost položky v číselníku | +| RP1–RP5 | dodatečné sloupce (pravděpodobně referenční ceny) | +| IS_CG | příznak (patrně IS_CGM — kontinuální monitorace glukózy) | + +### OME – zkratky odborností oprávněných předepsat ZP + +91 různých kombinací. Výběr zkratek: + +| Zkratka | Odbornost | +|---|---| +| PRL | Praktický lékař | +| LEKAR | Jakýkoliv lékař | +| DIA | Diabetologie | +| NEU | Neurologie | +| ORT | Ortopedie | +| REH | Rehabilitace | +| CHI | Chirurgie | +| GYN | Gynekologie | +| ONK | Onkologie | +| URN | Urologie | +| KAR | Kardiologie | +| PNE | Pneumologie | +| DER | Dermatologie | +| GER | Geriatrie | +| PED | Pediatrie | +| ORL | ORL | +| PSY | Psychiatrie | +| INT | Interna | +| S5 | Specifická kategorie (sestra/soc. péče?) | +| DLE ZÁKONA | Speciální režim (vozíky, lůžka…) | + +Poznámka: `PRL` se vyskytuje pouze v kombinaci s dalšími odbornostmi — praktický lékař nikdy není jediným předepisujícím. + +--- + +## ZURNAL – starý audit log (nepoužívaná) + +**795 220 záznamů**, data od 2012-02-22 do **2019-04-17** — poté se přestala plnit. + +Starší verze tabulky LOG, stejná struktura (DATUM, CAS, TABULKA, UZIVATEL, AKCE, KEYFIELD, ID1…). Medicus ji nahradil tabulkou LOG někdy kolem roku 2019. Dnes je mrtvá, pouze historická data. + +--- + +## RECEPT – e-recepty + +**191 820 záznamů**, od 1997-05-06 do současnosti. 1 261 stornovaných. + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| IDPAC | FK → KAR | +| DATUM | datum vystavení | +| KOD | kód léku (SÚKL, 7 znaků) | +| LEK | název léku | +| POJ | pojišťovna (111, 205, 207…) | +| DGN | diagnóza | +| TYP | typ receptu (1 = běžný) | +| STORNO | 'F' = platný, 'T' = stornovaný | +| CISRECEPT | číslo receptu (po e-podání) | +| IDUZI | kdo vystavil | +| ICP | IČP ordinace | +| UHRADA | způsob úhrady | +| PLATIDO | platnost do | +| ES_STAV | stav e-receptu (eRecept systém) | + +Poznámka: Tabulka obsahuje jak tištěné, tak e-recepty. Pole `CISRECEPT` je NULL u starších papírových. + +--- + +## LECH / LECD – lékové doklady pro pojišťovnu + +Dvojice podobná DOKLADH/DOKLADD — ale pro léky vykazované pojišťovně (oddělená agenda od receptů). + +### LECH – záhlaví (4 247 záznamů) + +| Sloupec | Popis | +|---|---| +| IDLEC | primární klíč | +| POJ | pojišťovna | +| ICZ | IČZ ordinace (09305001) | +| RODCIS | rodné číslo pacienta | +| HODB | hlavní odbornost (001) | +| HDGN | hlavní diagnóza | +| TYP | typ ('A' = ambulantní) | +| STAV | stav (0 = otevřen) | +| DAVKA | číslo dávky | + +### LECD – detail / jednotlivé léky (4 413 záznamů) + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| IDLEC | FK → LECH | +| RODCIS | rodné číslo | +| DATOSE | datum ošetření | +| KOD | kód léku (SÚKL) | +| POCET | počet balení | +| CENA | cena (Kč) | +| KAT | kategorie ('I' = imunizace, NULL = běžný) | +| IDUZI | který lékař | + +--- + +## DOKLADH / DOKLADD – výkony + +Hlavní záhlaví/detail pro výkony vykazované pojišťovnám. + +### DOKLADH – záhlaví (54 436 záznamů) + +| Sloupec | Popis | +|---|---| +| IDHLAV | primární klíč | +| POJ | pojišťovna | +| ICZ | IČZ ordinace | +| RODCIS | rodné číslo pacienta | +| HODB | hlavní odbornost (001 = praktický lékař) | +| HDGN | hlavní diagnóza | +| VDGN1–6 | vedlejší diagnózy | +| TYP | typ dokladu ('A' = ambulantní) | +| STAV | stav (0 = otevřen) | +| ROK | rok | +| EICZ | IČZ odesílajícího lékaře (NULL = vlastní) | +| EODZ | odbornost odesílajícího | +| DAVKA | číslo dávky | + +### DOKLADD – detail výkony (98 321 záznamů) + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| IDHLAV | FK → DOKLADH | +| RODCIS | rodné číslo pacienta | +| DATOSE | datum ošetření | +| KOD | kód výkonu (5 znaků, např. '01022') | +| POCVYK | počet provedení výkonu | +| DDGN | diagnóza výkonu | +| ODB | odbornost | +| BODY | počet bodů výkonu | +| CENABOD | cena v bodech (Kč) | +| CENAMAT | cena materiálu (Kč) | +| CENAPAU | paušál (Kč) | +| KAT | kategorie ('N' = nevykazovat, 'K' = kapitace, 'A' = agregace) | +| IDUZI | který lékař | +| IDHLAV | FK → DOKLADH | + +Schéma: `DOKLADH ←IDHLAV→ DOKLADD` (1:N) + +--- + +## PREH – preventivní prohlídky a měření + +**11 885 záznamů** — ukládá měřené hodnoty při prohlídce (výška, váha, tlak, puls…). + +### Klíčové sloupce + +| Sloupec | Popis | +|---|---| +| IDPRE | primární klíč | +| IDPAC | FK → KAR | +| IDUZI | kdo zaznamenal | +| IDPREINI | typ prohlídky (viz PREINIH) | +| DATUM | datum prohlídky | +| VYSKA | výška (cm) | +| VAHA | váha (kg) | +| TLAKSYS | systolický tlak (mmHg) | +| TLAKDIA | diastolický tlak (mmHg) | +| PULS | puls (tepy/min) | +| CHOLESTEROL | hladina cholesterolu | +| SATURACE | saturace kyslíkem | +| TEPLOTA | teplota | +| BRICHO | obvod břicha | +| TERMIN | plánovaný příští termín | +| TYP | typ záznamu (0 = standardní) | + +### PREINIH – typy prohlídek (7 záznamů) + +| IDPREINI | NAZEV | +|---|---| +| 24 | Prohlídka (Dekurs) — **5 665 záznamů** | +| 25 | Výška a váha — **3 813 záznamů** | +| 26 | Tlak a puls — **2 062 záznamů** | +| 27 | Prohlídka - tlak — 341 záznamů | +| 28 | Diabetologická prohlídka — 4 záznamy | +| 29 | Předoperační vyšetření — 0 | +| 30 | Předoperační vyšetření — 0 | + +--- + +## HISTDOC – dokumenty a doklady + +**32 689 záznamů** — evidence vydaných dokladů, poukazů, posudků. Typ dokladu určuje sloupec `TYP`. + +### Typy TYP (nejčastější) + +| TYP | Počet | Popis | +|---|---|---| +| POUV_1 | 15 861 | Poukaz na vyšetření – typ 1 | +| POUV_3 | 6 279 | Poukaz na vyšetření – typ 3 | +| MEDLAB | 2 396 | Laboratorní žádanka | +| MOTORVO | 1 535 | Posudek způsobilosti k řízení MV | +| ORTOP | 1 003 | Ortopedický poukaz | +| POTDPN | 901 | Potvrzení DPN (pracovní neschopnost) | +| Cov19Ts | 900 | Covid-19 test | +| ORTOPE | 891 | Ortopedický poukaz (e-verze) | +| COV19 | 595 | Covid-19 certifikát | +| LAZPEC | 517 | Žádost o lázeňskou péči | +| VYMLIST | 362 | Výměnný list | +| ZDRSTA5 | 340 | Zdravotní stav (typ 5) | +| PROHLAS | 151 | Prohlášení pacienta | +| ZPUPRN | 136 | Způsobilost (pracovní náplň?) | +| SANIT | 107 | Sanitní transport | +| ZBROJPR | 58 | Zbrojní průkaz | +| HlaOseL | 95 | Hlášení ošetřujícího lékaře | + +### Klíčové sloupce + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| TYP | typ dokladu (viz tabulka výše) | +| DATUM | datum vydání | +| IDPACI | FK → KAR (pacienti) | +| IDUZIV | FK → UZIVATEL (kdo vytvořil) | +| STAV | stav ('Z' = zpracován, NULL = otevřen) | +| DATA | blob – data dokladu | +| IDDOKLAD | vazba na DOKLADH (pro výkony) | +| IDHLAV | vazba na záhlaví dokladu | + +--- + +## DOCLIST – univerzální index dokumentů + +**466 837 záznamů** (stav 2026-05-16) — aktivně se plní, průměr ~3 700 záznamů/měsíc. +Slouží jako rozcestník: jeden řádek = jeden dokument v jiné tabulce. Neobsahuje samotná data, jen odkaz. + +Poznámka: jeden záznam má datum 2029-11-21 (ID 566201, FILES, "PZ chirurgie.pdf") — patrně překlep při ručním zadání. + +### Klíčové sloupce + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| DATUM | datum záznamu | +| TABULKA | zdrojová tabulka (viz níže) | +| IDREC | ID záznamu v té zdrojové tabulce | +| TYP | typ dokumentu (kód, viz níže) | +| POPIS | popis (u DEKURS = čas uložení, u FILES = název souboru, u LEKZPRAVY = "Lékařská zpráva"…) | +| IDPAC | FK → KAR | +| IDODDEL | oddělení (2 = ordinace, -1 = vnější/bez oddělení) | + +### Kombinace TYP + TABULKA (kompletní přehled) + +| TYP | TABULKA | Počet | Popis | +|---|---|---|---| +| 1 | DEKURS | 174 927 | Záznamy z návštěv | +| 1 | FILES | 11 715 | Přílohy a soubory | +| 3 | HISTDOC | 32 940 | Poukazy, posudky, žádanky | +| 3 | HISTDOC_REP | 3 | Repliky histdoc (neznámé, 3 záznamy) | +| 6 | LABVH | 20 249 | Laboratorní výsledky – záhlaví | +| 7 | RECEPT | 191 137 | E-recepty | +| 8 | OCKZAZ | 7 911 | Očkování | +| 9 | PREH | 12 027 | Preventivní prohlídky a měření | +| 10 | NES | 3 744 | Neschopenky | +| 17 | SPECVYS | 746 | Specializované výsledky (tabulka zatím neprozkoumaná) | +| 24 | FILES | 227 | Přílohy jiného typu | +| 30 | LEKZPRAVY | 13 878 | Lékařské zprávy (tabulka zatím neprozkoumaná) | + +### Tabulky zatím nepopsané v NOTES.md (zjištěno přes DOCLIST) + +- **LABVH** — laboratorní výsledky záhlaví (LABVD je detail, LABVH záhlaví — viz DOCLIST TYP=6) +- **LEKZPRAVY** — lékařské zprávy (TYP=30, 13 878 záznamů) +- **SPECVYS** — specializované výsledky (TYP=17, 746 záznamů) +- **HISTDOC_REP** — repliky histdoc (TYP=3, pouze 3 záznamy) +- **FILES** — přílohy/PDF (různé typy — TYP=1 i TYP=24) + +### Aktivita po měsících (2025–2026) + +| Měsíc | Záznamů | +|---|---| +| 2025-01 | 3 117 | +| 2025-02 | 2 812 | +| 2025-03 | 4 433 | +| 2025-04 | 3 881 | +| 2025-05 | 4 097 | +| 2025-06 | 3 320 | +| 2025-07 | 3 644 | +| 2025-08 | 2 801 | +| 2025-09 | 4 053 | +| 2025-10 | 4 506 | +| 2025-11 | 4 023 | +| 2025-12 | 3 619 | +| 2026-01 | 4 133 | +| 2026-02 | 3 473 | +| 2026-03 | 4 438 | +| 2026-04 | 3 770 | +| 2026-05 | 1 001 *(do 17.5.)* | + +--- + +## OCKZAZ – záznamy podaných vakcín + +**9 746 záznamů** — každá aplikace vakcíny jednomu pacientovi. + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| IDPAC | FK → KAR | +| DATUM | datum aplikace | +| ZKRATKA | kód SÚKL vakcíny (7 znaků) | +| LATKA | obchodní název (např. 'FSME-IMMUN 0,5 ML') | +| NAZEV | název onemocnění/typ (např. 'Klíšťová encefalitida') | +| KODMZ | kód MZ (např. 'KENC', 'TETP', 'HEAB') | +| IDUZI | kdo aplikoval | +| EXPIRE | expirace vakcíny | +| PRISTE | plánovaná příští dávka | +| PRISTE_OD / PRISTE_DO | interval pro příští dávku | +| EOCK | příznak e-očkování | +| CHYBA | chyba při e-podání | + +Nejčastější vakcíny (z ukázky): Klíšťová encefalitida (FSME-IMMUN), Tetanus (VACTETA), Hepatitida A+B (TWINRIX). + +--- + +## NES – neschopenky (pracovní neschopnosti) + +**4 318 záznamů** (od 2008-02-02), 4 stornované. Datum KONNES může být NULL (dosud trvá). +Výskyt data "2109-01-28" v MAX — patrně technické datum "dosud trvá" = 2109. + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| IDPAC | FK → KAR | +| ZACNES | začátek neschopnosti | +| KONNES | konec neschopnosti (NULL = stále trvá) | +| CISNES | číslo neschopenky | +| DIAGNO | diagnóza | +| IDUZI | který lékař | +| STORNO | 'F' = platná, 'T' = stornovaná | +| STORNO | příznak storna | +| ECN | číslo e-neschopenky | +| EPODANI | stav e-podání | + +--- + +## MEDIKACE – aktuální medikace + +**252 záznamů** — záznamy o lécích předepsaných jako dlouhodobá/trvalá medikace (ne každý recept, jen ty označené jako medikace). + +| Sloupec | Popis | +|---|---| +| ID | primární klíč | +| IDPAC | FK → KAR | +| IDUZI | kdo zaznamenal | +| DATUM | datum záznamu | +| PLATI_OD | platnost od | +| PLATI_DO | platnost do (NULL = dosud) | +| KOD | kód SÚKL léku | +| NAZ | název léku | +| UHRADA | způsob úhrady ('C' = plně hrazeno, 'J' = částečně) | + +--- + +## ICP / ICZ – pracoviště a zdravotnické zařízení + +**11 záznamů** v obou tabulkách — identifikátory ordinace pro různé pojišťovny. + +### ICP (pracoviště) + +| Sloupec | Popis | +|---|---| +| IDICP | primární klíč | +| IDICZ | FK → ICZ | +| NAZEV | název ('Ordinace', 'Smluvní pracoviště…') | +| ICP | IČP = 09305001 (naše ordinace) | +| ODB | odbornost = 001 (praktický lékař) | +| DPECE | druh péče | + +Poznámka: Všech 11 záznamů má stejný ICP 09305001 a ODB 001 — liší se jen IDICP/IDICZ (pro různé pojišťovny). + +--- + +## Schéma vazeb (rozšířené) + +``` +KAR (kartotéka) + ├──IDPAC──► DEKURS (záznamy z návštěv, RTF) + ├──IDPAC──► RECEPT (e-recepty) + ├──IDPAC──► MEDIKACE (trvalá medikace) + ├──IDPAC──► NES (neschopenky) + ├──IDPAC──► OCKZAZ (podané vakcíny) + ├──IDPAC──► OCKPRI (plánovaná očkování) + ├──IDPAC──► PREH (měření: tlak, váha, výška…) + ├──IDPAC──► FILES (přílohy/PDF, v ext. FDB) + ├──IDPAC──► HISTDOC (poukazy, posudky, žádanky) + ├──IDPAC──► REGISTR (registrace u pojišťovny) + ├──IDPAC──► KARUZIV (ošetřující lékař) + ├──RODCIS──► DOKLADH ──► DOKLADD (výkony – záhlaví/detail) + └──RODCIS──► LECH ──► LECD (lékové doklady) + +FAK (faktury) + ├──IDFAK──► FAKDET (detail per lékař) + ├──IDFAK──► FAKDAV (dávky ve faktuře) + └──IDFAK──► PORTAL (e-podání – výkonové dávky DP98) + +VZPARC (výkonové dávky – klíčové pro hledání výkonů per pacient) +PREINIH (typy prohlídek: 24=Dekurs, 25=Výška/váha, 26=Tlak/puls…) +``` + +--- + +## Poznámky k průzkumu + +### RECEPT vs LECD – kdy co použít +- **RECEPT** = všechny recepty vystavené pacientovi (191 820 záznamů) — pro analýzu předepisování +- **LECD** = léky vykázané pojišťovně (4 413 záznamů) — pro fakturaci + +### DOKLADD vs VZPARC – kdy co použít +- **DOKLADD** = výkony v DB (98 321), vč. nevykázaných — pro analýzu péče +- **VZPARC** = odeslaná výkonová data pojišťovně (CP852 kódování!) — pro fakturaci, VŽDY používat pro hledání PP + +### PREH – víc záznamů na jednu návštěvu +Jedna návštěva = typicky 2 záznamy v PREH: IDPREINI=25 (výška+váha) + IDPREINI=26 (tlak+puls). + +### NES – datum 2109 +MAX(KONNES) = 2109-01-28 — Medicus ukládá "dosud trvající" neschopenky s datem daleko v budoucnosti. Při filtrování "aktuálních" NES používat: `KONNES IS NULL OR KONNES > dnes`. + +### HISTDOC – nejpoužívanější typy +POUV_1 a POUV_3 dominují (celkem 22 140 = 68 % všech dokumentů) — jsou to poukazy na odborná vyšetření. + +### ORTOP vs ORTOPE – poukazy na ZP (zdravotnické prostředky) + +- **ORTOP** = papírový ortopedický poukaz — data do konce 2021 +- **ORTOPE** = elektronický poukaz na ZP — od 2022 (891 → 2022: 12, 2023: 220, 2024: 287, 2025: 288, 2026: 113 k 5/2026) + +Detaily ZP nejsou v samostatné tabulce — jsou uloženy v **HISTDOC.DATA jako blob ve formátu key=value**: + +``` +ICZ=09305000 +ICP=09305001 +POJ=I:111 +Dgn=N394 +Kod=5009790 +Nazev=VLOŽKY ABSORPČNÍ MOLICARE LADY 4,5 KAPKY +Pocet=C:4.00 +Cena=C:418.40 +Dopl=C:73.84 +DoplnekNazvu=934ML,14KS +DatPlat=D:17.06.2026 +Notifikace=736734018 +Stav=PREDEPSANY +Omezeni=GER, GYN, PED, NEF, NEU, PRL, SLO, CHI, URN, SDP +UHS=02.01.01.01 +... +``` + +Typové prefixy hodnot: `C:` = decimal, `I:` = integer, `D:` = datum (DD.MM.YYYY), `$:~\b` = prázdné. + +**Napojení na pacienta a lékaře:** +```sql +SELECT h.DATUM, k.PRIJMENI, k.JMENO, k.RODCIS, u.PRIJMENI AS LEKAR, + CAST(h.DATA AS VARCHAR(8000)) AS DATA_TEXT +FROM HISTDOC h +JOIN KAR k ON h.IDPACI = k.IDPAC +JOIN UZIVATEL u ON h.IDUZIV = u.IDUZI +WHERE h.TYP = 'ORTOPE' +ORDER BY h.DATUM +``` + +**Sloupec Doklad** (alfanumerický kód v Medicus UI jako "QVNZEGR2B") — je v tabulce **HISTDOC_EPOUKAZ.ID_DOKLADU**. JOIN přes `IDHISTDOC = HISTDOC.ID`. + +### HISTDOC_EPOUKAZ — e-podání poukazu pojišťovně + +Vznikne až při odeslání poukazu elektronicky. Analogie k RECEPT_EPODANI. + +| Sloupec | Popis | +|---|---| +| IDHISTDOC | FK → HISTDOC.ID | +| ID_DOKLADU | kód Dokladu z Medicus UI (9 znaků, např. `MGAS9PQWS`) | +| ID_ZP | UUID přidělený pojišťovnou | +| ODESLANO | datetime odeslání | +| VYDANO | datetime vydání pojišťovnou (NULL = zatím čeká) | +| CHYBA | `T` = chyba při podání, `F` = OK | +| SCHVALENI | `N` = neschváleno, `A` = schváleno | +| ID_DOKLADU_VYDEJ | kód při vydání (NULL dokud nevydáno) | + +Pozor: LEFT JOIN — poukaz může existovat bez záznamu v HISTDOC_EPOUKAZ (neodeslané). + +Tabulka EPOUKAZ (bez prefixu HISTDOC) — **prázdná, nepoužívaná**. + +**Skript pro export:** `Medicus/MedicusWithClaudeZP/prehled_zp.py` → výstup `Přehled_ZP.xlsx` +- List 1 „Všechny záznamy" — vše včetně Doklad, Odesláno, Vydáno, Chyba, ID ZP +- List 2 „Přehled Medicus" — sloupce shodné s Medicus UI (Datum, Kód, Název, Počet, Cena, Pojišťovna, IČZ, RC, Příjmení, Jméno, Vystavil, Doklad) + +### HISTDOC + HISTDOCVYK – předepsané PZT (zdravotnické prostředky) +Dvojice tabulek pro poukazy na ZP (analogie DOKLADH/DOKLADD): +- **HISTDOC** = záhlaví poukazu (IDPACI, DATUM, TYP) +- **HISTDOCVYK** = detail položky (KOD → PZT.KOD, POCET, CENA, FK přes `ID`) + +Typy poukazů v HISTDOC: `POUV_1`, `POUV_3`, `ORTOP` (papírový), `ORTOPE` (e-poukaz). Od roku 2026 se ORTOP nepoužívá — vše přes ORTOPE. E-podání sleduje HISTDOC_EPOUKAZ (obsahuje `ID_ZP` = UUID z pojišťovny, `ODESLANO`, `VYDANO`). + +Kategorie předepisovaného ZP (zjištěno z dat 2026): inkontinenční pomůcky (Tena, Molicare, Depend), kompresní punčochy (Maxis, Avicenum), stomické pomůcky (Aurum, Comfeel, Welland), mobilní pomůcky (chodítka, nástavce WC). + +JOIN pro report: `HISTDOC h JOIN HISTDOCVYK v ON v.ID = h.ID JOIN PZT p ON p.KOD = v.KOD WHERE h.TYP IN ('POUV_1','POUV_3','ORTOP','ORTOPE')` + +Poznámka: PZT číselník má historické záznamy — při joinu bez filtru platnosti může vrátit více názvů pro jeden kód (použít `MAX(p.NAZ)` nebo filtrovat `p.PLATIDO >= aktuální_datum`). + +--- + +## TODO pro Claude Code + +### 1. Report předepsaných poukazů a ZP + +Naprogramovat periodický report (Python skript nebo HTML) předepsaných zdravotnických prostředků a poukazů z Medicus DB. + +**Zdroj dat:** +```sql +SELECT h.DATUM, k.JMENO || ' ' || k.PRIJMENI AS PACIENT, h.TYP AS TYP_POUKAZU, + v.KOD, MAX(p.NAZ) AS NAZEV_ZP, v.POCET, v.CENA +FROM HISTDOC h +JOIN KAR k ON k.IDKAR = h.IDPACI +JOIN HISTDOCVYK v ON v.ID = h.ID +JOIN PZT p ON p.KOD = v.KOD +WHERE h.TYP IN ('POUV_1', 'POUV_3', 'ORTOP', 'ORTOPE') + AND h.DATUM BETWEEN :datum_od AND :datum_do +GROUP BY h.DATUM, k.JMENO, k.PRIJMENI, h.TYP, v.KOD, v.POCET, v.CENA +ORDER BY h.DATUM DESC +``` + +**Požadavky na report:** +- Parametry: datum od/do (default = aktuální měsíc) +- Seskupení: volitelně podle typu poukazu nebo kategorie ZP +- Součty: celková cena za období, počet poukazů per pacient +- Formát výstupu: Excel (.xlsx) nebo HTML s tiskem +- Volitelně: srovnání s předchozím obdobím (měsíc/rok) +- Připojení k DB přes MCP medicus-firebird nebo přímý Firebird driver (fdb / firebird-driver) + +**Kontext:** +- Tabulka KAR: IDKAR = primární klíč, JMENO + PRIJMENI = jméno pacienta +- PZT číselník má historické záznamy — JOIN bezpečně přes `MAX(p.NAZ)` nebo `GROUP BY` +- Sloupec HISTDOC.IDPACI = FK → KAR.IDKAR + +--- + +## MCP nástroje — přehled (stav 2026-05-19) + +### Původní nástroje +| Nástroj | Popis | +|---|---| +| `execute_query(sql, params?)` | Libovolný SQL dotaz (SELECT/INSERT/UPDATE/DELETE) | +| `list_tables()` | Seznam všech 993 tabulek | +| `get_table_columns(table)` | Seznam sloupců tabulky (jen názvy) | +| `get_schema()` | Kompletní schéma — všechny tabulky + sloupce | + +### Nové nástroje (přidáno 2026-05-19) +| Nástroj | Popis | +|---|---| +| `get_patient(idpac)` | Základní info o pacientovi z KAR — jmeno, prijmeni, rc, datnar, pojistovna | +| `search_patients(query)` | Hledání pacienta podle příjmení/jména/RC, max 50 výsledků | +| `get_patient_timeline(idpac, datum_od?, datum_do?)` | Chronologický přehled z DOCLIST — všechny záznamy pacienta | +| `parse_histdoc_data(idhistdoc)` | Dekóduje DATA blob z HISTDOC — vrátí dict {Kod, Nazev, Pocet, Cena, Stav, Doklad…} | +| `get_table_info(table)` | Rozšířené info o tabulce — typy sloupců, nullable, PK, počet záznamů | +| `safe_query(sql, params?)` | SELECT s ochranou — varuje pro velké tabulky bez WHERE, limit 500 řádků | + +### Velké tabulky vyžadující WHERE (safe_query varuje automaticky) +LOG, ZURNAL, LABVD, DOCLIST, PZT, LEKY, DEKLINK + +### Ještě neimplementováno +- `decode_blob` (RTF → čistý text) pro DEKURS, ANAMNEZA +- CP852 dekódování pro VZPARC diff --git a/mcp_firebird.py b/mcp_firebird.py index fcc262d..87f1049 100644 --- a/mcp_firebird.py +++ b/mcp_firebird.py @@ -11,11 +11,10 @@ from typing import Optional from mcp.server.fastmcp import FastMCP FB_CONFIG = { - 'dsn': r'localhost:c:\medicus 3\data\medicus.fdb', + 'dsn': r'reporter:c:\medicus\medicus.fdb', 'user': 'SYSDBA', 'password': 'masterkey', 'charset': 'win1250', - 'port': 3070, } # Všechny logy MUSÍ jít na stderr — stdout je rezervován pro JSON-RPC @@ -149,6 +148,277 @@ def get_schema() -> dict: raise + +# ── Velké tabulky kde je nutný WHERE filtr ────────────────────────────────── +LARGE_TABLES = {'LOG', 'ZURNAL', 'LABVD', 'DOCLIST', 'PZT', 'LEKY', 'DEKLINK'} + + +def _parse_histdoc_blob(data_text: str) -> dict: + """Interní parser key=value blobu z HISTDOC.DATA.""" + result = {} + if not data_text: + return result + for line in data_text.replace('\r\n', '\n').replace('\r', '\n').split('\n'): + line = line.strip() + if '=' not in line: + continue + key, _, value = line.partition('=') + key = key.strip() + value = value.strip() + if value.startswith('C:'): + try: + result[key] = float(value[2:].replace(',', '.')) + except Exception: + result[key] = value[2:] + elif value.startswith('I:'): + try: + result[key] = int(value[2:]) + except Exception: + result[key] = value[2:] + elif value.startswith('D:'): + result[key] = value[2:] + elif value in ('$:~\b', '$:~\x08', '$:'): + result[key] = '' + else: + result[key] = value + return result + + +@mcp.tool() +def get_patient(idpac: int) -> dict: + """Vrátí základní info o pacientovi z tabulky KAR podle IDPAC. + Výsledek: jmeno, prijmeni, rc, datnar, pojistovna, vyrazen. + """ + try: + cur = conn.cursor() + cur.execute(""" + SELECT IDPAC, JMENO, PRIJMENI, RODCIS, DATNAR, POJ, VYRAZEN + FROM KAR WHERE IDPAC = ? + """, [idpac]) + row = cur.fetchone() + if not row: + return {'error': f'Pacient IDPAC={idpac} nenalezen'} + import datetime, decimal + def cv(v): + if isinstance(v, (datetime.date, datetime.datetime)): return v.isoformat() + if isinstance(v, decimal.Decimal): return float(v) + return v + cols = ['idpac', 'jmeno', 'prijmeni', 'rc', 'datnar', 'pojistovna', 'vyrazen'] + return dict(zip(cols, [cv(v) for v in row])) + except Exception: + log(f"get_patient chyba: {traceback.format_exc()}") + raise + + +@mcp.tool() +def search_patients(query: str) -> list: + """Vyhledá pacienty podle příjmení, jména nebo rodného čísla (částečná shoda). + Vrátí max. 50 výsledků: idpac, jmeno, prijmeni, rc, pojistovna. + """ + try: + cur = conn.cursor() + q = query.strip().upper() + cur.execute(""" + SELECT FIRST 50 IDPAC, JMENO, PRIJMENI, RODCIS, POJ + FROM KAR + WHERE UPPER(PRIJMENI) LIKE ? OR UPPER(JMENO) LIKE ? OR RODCIS LIKE ? + ORDER BY PRIJMENI, JMENO + """, [f'%{q}%', f'%{q}%', f'%{q}%']) + rows = cur.fetchall() + return [ + {'idpac': r[0], 'jmeno': r[1], 'prijmeni': r[2], 'rc': r[3], 'pojistovna': r[4]} + for r in rows + ] + except Exception: + log(f"search_patients chyba: {traceback.format_exc()}") + raise + + +@mcp.tool() +def get_patient_timeline(idpac: int, datum_od: Optional[str] = None, datum_do: Optional[str] = None) -> dict: + """Chronologický přehled všech záznamů pacienta z DOCLIST. + datum_od / datum_do ve formátu YYYY-MM-DD (volitelné). + Vrátí: pacient (jmeno, prijmeni, rc) + seznam událostí (datum, tabulka, popis). + """ + try: + cur = conn.cursor() + # Info o pacientovi + cur.execute("SELECT JMENO, PRIJMENI, RODCIS FROM KAR WHERE IDPAC = ?", [idpac]) + pac = cur.fetchone() + if not pac: + return {'error': f'Pacient IDPAC={idpac} nenalezen'} + + # Timeline z DOCLIST + sql = "SELECT DATUM, TABULKA, TYP, POPIS FROM DOCLIST WHERE IDPAC = ?" + params = [idpac] + if datum_od: + sql += " AND DATUM >= ?" + params.append(datum_od) + if datum_do: + sql += " AND DATUM <= ?" + params.append(datum_do) + sql += " ORDER BY DATUM, ID" + + cur.execute(sql, params) + import datetime, decimal + def cv(v): + if isinstance(v, (datetime.date, datetime.datetime)): return v.isoformat() + return v + + events = [ + {'datum': cv(r[0]), 'tabulka': r[1], 'typ': r[2], 'popis': r[3] or ''} + for r in cur.fetchall() + ] + return { + 'pacient': {'jmeno': pac[0], 'prijmeni': pac[1], 'rc': pac[2]}, + 'pocet': len(events), + 'events': events + } + except Exception: + log(f"get_patient_timeline chyba: {traceback.format_exc()}") + raise + + +@mcp.tool() +def parse_histdoc_data(idhistdoc: int) -> dict: + """Načte a dekóduje DATA blob z HISTDOC pro daný záznam. + Vrátí strukturovaný dict: Kod, Nazev, Pocet, Cena, Poj, Dgn, DatPlat, Stav, Doklad… + """ + try: + cur = conn.cursor() + cur.execute(""" + SELECT h.TYP, h.DATUM, CAST(h.DATA AS VARCHAR(8000)), + e.ID_DOKLADU, e.ID_ZP, e.ODESLANO, e.VYDANO, e.CHYBA + FROM HISTDOC h + LEFT JOIN HISTDOC_EPOUKAZ e ON e.IDHISTDOC = h.ID + WHERE h.ID = ? + """, [idhistdoc]) + row = cur.fetchone() + if not row: + return {'error': f'HISTDOC ID={idhistdoc} nenalezen'} + import datetime + def cv(v): + if isinstance(v, (datetime.date, datetime.datetime)): return v.isoformat() + return v + typ, datum, data_text, id_dokladu, id_zp, odeslano, vydano, chyba = row + parsed = _parse_histdoc_blob(data_text or '') + return { + 'id': idhistdoc, + 'typ': typ, + 'datum': cv(datum), + 'data': parsed, + 'epoukaz': { + 'id_dokladu': id_dokladu, + 'id_zp': id_zp, + 'odeslano': cv(odeslano), + 'vydano': cv(vydano), + 'chyba': chyba == 'T' if chyba else False, + } + } + except Exception: + log(f"parse_histdoc_data chyba: {traceback.format_exc()}") + raise + + +@mcp.tool() +def get_table_info(table_name: str) -> dict: + """Vrátí rozšířené info o tabulce: sloupce s datovými typy, nullable, PK + počet záznamů.""" + try: + cur = conn.cursor() + cur.execute(""" + SELECT + TRIM(f.RDB$FIELD_NAME) AS SLOUPEC, + CASE ft.RDB$FIELD_TYPE + WHEN 7 THEN 'SMALLINT' + WHEN 8 THEN 'INTEGER' + WHEN 10 THEN 'FLOAT' + WHEN 12 THEN 'DATE' + WHEN 13 THEN 'TIME' + WHEN 14 THEN 'CHAR(' || ft.RDB$FIELD_LENGTH || ')' + WHEN 16 THEN 'BIGINT' + WHEN 27 THEN 'DOUBLE' + WHEN 35 THEN 'TIMESTAMP' + WHEN 37 THEN 'VARCHAR(' || ft.RDB$FIELD_LENGTH || ')' + WHEN 261 THEN 'BLOB' + ELSE 'TYP(' || ft.RDB$FIELD_TYPE || ')' + END AS TYP, + CASE COALESCE(f.RDB$NULL_FLAG, 0) WHEN 1 THEN 'NOT NULL' ELSE 'NULL' END AS NULLABLE, + CASE WHEN pk.RDB$FIELD_NAME IS NOT NULL THEN 'PK' ELSE '' END AS PK + FROM RDB$RELATION_FIELDS f + JOIN RDB$FIELDS ft ON ft.RDB$FIELD_NAME = f.RDB$FIELD_SOURCE + LEFT JOIN ( + SELECT i.RDB$RELATION_NAME, s.RDB$FIELD_NAME + FROM RDB$INDICES i + JOIN RDB$INDEX_SEGMENTS s ON s.RDB$INDEX_NAME = i.RDB$INDEX_NAME + WHERE i.RDB$RELATION_NAME = ? + AND EXISTS ( + SELECT 1 FROM RDB$RELATION_CONSTRAINTS c + WHERE c.RDB$INDEX_NAME = i.RDB$INDEX_NAME + AND c.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY' + ) + ) pk ON pk.RDB$FIELD_NAME = f.RDB$FIELD_NAME + WHERE TRIM(f.RDB$RELATION_NAME) = ? + ORDER BY f.RDB$FIELD_POSITION + """, [table_name.upper(), table_name.upper()]) + columns = [ + {'sloupec': r[0], 'typ': r[1], 'nullable': r[2], 'pk': r[3]} + for r in cur.fetchall() + ] + # Počet záznamů + try: + cur.execute(f'SELECT COUNT(*) FROM {table_name.upper()}') + count = cur.fetchone()[0] + except Exception: + count = None + return { + 'tabulka': table_name.upper(), + 'pocet_zaznamu': count, + 'sloupce': columns + } + except Exception: + log(f"get_table_info chyba: {traceback.format_exc()}") + raise + + +@mcp.tool() +def safe_query(sql: str, params: Optional[list] = None) -> dict: + """Bezpečný SELECT s ochranou před timeoutem na velkých tabulkách. + Automaticky varuje pokud dotaz míří na LOG, ZURNAL, LABVD, DOCLIST, PZT, LEKY + bez WHERE klauzule. Výsledek omezen na 500 řádků. + """ + try: + sql_upper = sql.strip().upper() + if not sql_upper.startswith('SELECT'): + return {'error': 'safe_query podporuje pouze SELECT dotazy'} + + # Varování pro velké tabulky bez WHERE + warnings = [] + for tbl in LARGE_TABLES: + if tbl in sql_upper and 'WHERE' not in sql_upper: + warnings.append(f'Tabulka {tbl} má miliony záznamů — přidej WHERE filtr!') + + # Přidej FIRST limit pokud chybí + if 'FIRST ' not in sql_upper: + sql = sql.strip() + if sql.upper().startswith('SELECT'): + sql = 'SELECT FIRST 500 ' + sql[6:].lstrip() + + cur = conn.cursor() + if params: + cur.execute(sql, params) + else: + cur.execute(sql) + + rows = rows_to_json(cur.fetchall(), cur.description or []) + result = {'rowcount': len(rows), 'rows': rows} + if warnings: + result['warnings'] = warnings + return result + except Exception: + log(f"safe_query chyba: {traceback.format_exc()}") + raise + + if __name__ == '__main__': log("MCP Firebird server spuštěn (FastMCP)") mcp.run()