notebookvb
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
Binary file not shown.
@@ -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()
|
||||
@@ -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
|
||||
+272
-2
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user