notebookvb

This commit is contained in:
Vladimir Buzalka
2026-05-19 06:15:10 +02:00
parent 0d1b89c117
commit 452969ab4a
8 changed files with 1614 additions and 2 deletions
@@ -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 ~1520 minut na ~23 minuty (68× 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 6166 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
+111
View File
@@ -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()
@@ -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)
View File
Binary file not shown.
+227
View File
@@ -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()
+723
View File
@@ -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ů, ~600800 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 |
| RP1RP5 | 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 |
| VDGN16 | 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 (20252026)
| 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
+272 -2
View File
@@ -11,11 +11,10 @@ from typing import Optional
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
FB_CONFIG = { FB_CONFIG = {
'dsn': r'localhost:c:\medicus 3\data\medicus.fdb', 'dsn': r'reporter:c:\medicus\medicus.fdb',
'user': 'SYSDBA', 'user': 'SYSDBA',
'password': 'masterkey', 'password': 'masterkey',
'charset': 'win1250', 'charset': 'win1250',
'port': 3070,
} }
# Všechny logy MUSÍ jít na stderr — stdout je rezervován pro JSON-RPC # Všechny logy MUSÍ jít na stderr — stdout je rezervován pro JSON-RPC
@@ -149,6 +148,277 @@ def get_schema() -> dict:
raise 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__': if __name__ == '__main__':
log("MCP Firebird server spuštěn (FastMCP)") log("MCP Firebird server spuštěn (FastMCP)")
mcp.run() mcp.run()