notebookvb
This commit is contained in:
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
01_zakoupeno.py — workflow status 0: Zakoupeno (datum 31.12.2024)
|
||||||
|
=================================================================
|
||||||
|
První stav reconciliačního workflow. Všem pacientům ze zakoupeného souboru
|
||||||
|
(CSV příloha smlouvy, 1712 RČ) nastaví status 0 "Zakoupeno" k 31.12.2024.
|
||||||
|
|
||||||
|
- Pacienti ze smlouvy, kteří už jsou v Mongo (registrovaní k 1.1.2025) → status 0.
|
||||||
|
- Pacienti ze smlouvy, kteří v Mongo nejsou (odhlášeni u Buzalkové před předáním)
|
||||||
|
→ doplněni z Medicus kar s markerem `mimo_vzp_populaci` + status 0.
|
||||||
|
- Pacienti v Mongo mimo smlouvu → označeni `ve_smlouve=False` (status 0 nedostanou).
|
||||||
|
|
||||||
|
Workflow stav drží:
|
||||||
|
status, status_popis, status_datum (aktuální stav)
|
||||||
|
status_historie[] (postup stavů — pro další kroky)
|
||||||
|
|
||||||
|
Idempotentní — opakované spuštění status 0 nezduplikuje.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
import pymongo
|
||||||
|
from Knihovny.medicus_db import get_medicus_connection
|
||||||
|
|
||||||
|
# ── Konfigurace ────────────────────────────────────────────────────────────────
|
||||||
|
MONGO_URI = "mongodb://192.168.1.76:27017"
|
||||||
|
MONGO_DB = "ordinace"
|
||||||
|
MONGO_COLL = "registrovani_tracking"
|
||||||
|
CSV_PATH = Path(__file__).resolve().parent / "Inputs" / "2025-01-01 seznam_pacientu_jmeno_rc.csv"
|
||||||
|
|
||||||
|
STATUS = 0
|
||||||
|
STATUS_POPIS = "Zakoupeno"
|
||||||
|
STATUS_DATUM = "2024-12-31"
|
||||||
|
|
||||||
|
POJ_ZKR = {"111": "VZP", "201": "VoZP", "205": "ČPZP", "207": "OZP",
|
||||||
|
"209": "ZPŠ", "211": "ZPMV ČR", "213": "RBP"}
|
||||||
|
norm = lambda s: re.sub(r"\D", "", s or "")
|
||||||
|
|
||||||
|
|
||||||
|
def status_entry(now):
|
||||||
|
return {"status": STATUS, "status_popis": STATUS_POPIS,
|
||||||
|
"status_datum": STATUS_DATUM, "zapsano": now}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# ── CSV (zakoupený soubor) ──────────────────────────────────────────────────
|
||||||
|
csv_rc = {}
|
||||||
|
with CSV_PATH.open(encoding="utf-8-sig") as f:
|
||||||
|
for row in csv.DictReader(f, delimiter=";"):
|
||||||
|
csv_rc[norm(row["Rodné číslo"])] = row["Příjmení a jméno"]
|
||||||
|
contract = set(csv_rc)
|
||||||
|
print(f"Zakoupený soubor (CSV): {len(contract)} RČ")
|
||||||
|
|
||||||
|
cli = pymongo.MongoClient(MONGO_URI, serverSelectionTimeoutMS=3000)
|
||||||
|
coll = cli[MONGO_DB][MONGO_COLL]
|
||||||
|
coll.create_index("status")
|
||||||
|
coll.create_index("ve_smlouve")
|
||||||
|
|
||||||
|
pop = {norm(d["_id"]): d["_id"] for d in coll.find({}, {"_id": 1})}
|
||||||
|
popset = set(pop)
|
||||||
|
|
||||||
|
present = contract & popset
|
||||||
|
absent = contract - popset
|
||||||
|
extra = popset - contract
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# ── 1) Pacienti ze smlouvy už v Mongo → status 0 (idempotentně) ─────────────
|
||||||
|
n_pres = 0
|
||||||
|
for rc in present:
|
||||||
|
_id = pop[rc]
|
||||||
|
d = coll.find_one({"_id": _id}, {"status_historie": 1})
|
||||||
|
sh = d.get("status_historie", [])
|
||||||
|
if not any(e.get("status") == STATUS for e in sh):
|
||||||
|
sh = sh + [status_entry(now)]
|
||||||
|
coll.update_one({"_id": _id}, {"$set": {
|
||||||
|
"status": STATUS, "status_popis": STATUS_POPIS, "status_datum": STATUS_DATUM,
|
||||||
|
"ve_smlouve": True, "status_historie": sh, "updated_at": now}})
|
||||||
|
n_pres += 1
|
||||||
|
|
||||||
|
# ── 2) Pacienti v Mongo mimo smlouvu → ve_smlouve = False ───────────────────
|
||||||
|
coll.update_many({"_id": {"$in": [pop[rc] for rc in extra]}},
|
||||||
|
{"$set": {"ve_smlouve": False, "updated_at": now}})
|
||||||
|
|
||||||
|
# ── 3) Pacienti ze smlouvy chybějící v Mongo → doplnit z kar + status 0 ─────
|
||||||
|
conn = get_medicus_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
kar = {}
|
||||||
|
abslist = list(absent)
|
||||||
|
for i in range(0, len(abslist), 500):
|
||||||
|
b = abslist[i:i + 500]
|
||||||
|
ph = ",".join("?" for _ in b)
|
||||||
|
cur.execute(f"""
|
||||||
|
SELECT TRIM(k.rodcis), TRIM(k.prijmeni), TRIM(k.jmeno), TRIM(k.poj),
|
||||||
|
(SELECT MAX(r.datum_zruseni) FROM registr r JOIN icp i ON r.idicp=i.idicp
|
||||||
|
WHERE r.idpac=k.idpac AND i.icp='09305001' AND i.odb='001')
|
||||||
|
FROM kar k WHERE k.rodcis IN ({ph})""", b)
|
||||||
|
for rc, p, j, poj, zrus in cur.fetchall():
|
||||||
|
kar[(rc or "").strip()] = {"prijmeni": p, "jmeno": j,
|
||||||
|
"poj": (poj or "").strip(), "zruseni": zrus}
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
n_ins = 0
|
||||||
|
for rc in absent:
|
||||||
|
if coll.find_one({"_id": rc}):
|
||||||
|
continue
|
||||||
|
k = kar.get(rc, {})
|
||||||
|
poj = k.get("poj", "")
|
||||||
|
zrus = k.get("zruseni")
|
||||||
|
zrus_s = zrus.strftime("%Y-%m-%d") if zrus else None
|
||||||
|
snap = {
|
||||||
|
"k_datu": "2025-01-01", "kategorie": "ODHLASEN_PRED_PREDANIM",
|
||||||
|
"kategorie_popis": "Registrace u Buzalkové zrušena před předáním (1.1.2025)",
|
||||||
|
"v_zakoupenem_souboru": False,
|
||||||
|
"flag": "NEBYL V ZAKOUPENÉM SOUBORU PACIENTŮ",
|
||||||
|
"flag_duvod": f"registrace u Buzalkové zrušena {zrus_s} (před předáním)",
|
||||||
|
"praktik_nazev": None, "praktik_icz": None, "praktik_icp": None,
|
||||||
|
"praktik_od": None, "datum_zahajeni": None, "datum_ukonceni": None,
|
||||||
|
"medicus_zruseni": zrus_s,
|
||||||
|
}
|
||||||
|
coll.insert_one({
|
||||||
|
"_id": rc, "rc": rc,
|
||||||
|
"prijmeni": k.get("prijmeni"), "jmeno": k.get("jmeno"),
|
||||||
|
"pojistovna": {"kod": poj, "zkratka": POJ_ZKR.get(poj, poj)},
|
||||||
|
"medicus_poj": poj,
|
||||||
|
"status": STATUS, "status_popis": STATUS_POPIS, "status_datum": STATUS_DATUM,
|
||||||
|
"ve_smlouve": True, "mimo_vzp_populaci": True,
|
||||||
|
"vychozi_datum": "2025-01-01", "aktualni": snap,
|
||||||
|
"historie": [{**snap, "zmena": "doplněn ze smlouvy (mimo VZP populaci)"}],
|
||||||
|
"status_historie": [status_entry(now)],
|
||||||
|
"created_at": now, "updated_at": now,
|
||||||
|
})
|
||||||
|
n_ins += 1
|
||||||
|
|
||||||
|
# ── Souhrn ──────────────────────────────────────────────────────────────────
|
||||||
|
print(f"present (status 0 nastaveno) : {n_pres}")
|
||||||
|
print(f"absent doplněno ze smlouvy (insert) : {n_ins}")
|
||||||
|
print(f"extra mimo smlouvu (ve_smlouve=False): {len(extra)}")
|
||||||
|
print()
|
||||||
|
print(f"status 0 (Zakoupeno) celkem : {coll.count_documents({'status': 0})}")
|
||||||
|
print(f"ve_smlouve = True : {coll.count_documents({'ve_smlouve': True})}")
|
||||||
|
print(f"kolekce celkem : {coll.count_documents({})}")
|
||||||
|
cli.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
|||||||
|
# FinalReconcilliation — sledování stavu registrovaných pacientů
|
||||||
|
|
||||||
|
## Cíl
|
||||||
|
|
||||||
|
Jednoznačně roztřídit pacienty **registrované v Medicusu** podle **skutečnosti ověřené u pojišťovny**:
|
||||||
|
kdo je k danému dni jejich registrující **praktik (odbornost 001)** dle VZP B2B.
|
||||||
|
|
||||||
|
- praktik = **Buzalková (IČP 09305001)** → pacient **je** v zakoupeném souboru pacientů (OK)
|
||||||
|
- praktik = kdokoli jiný / žádný → **„NEBYL V ZAKOUPENÉM SOUBORU PACIENTŮ"**
|
||||||
|
|
||||||
|
„Registrovaný v Medicusu" je jen stav v software; tohle ověřuje realitu u pojišťovny.
|
||||||
|
|
||||||
|
## Úložiště — MongoDB
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Server | `mongodb://192.168.1.76:27017` (stejný stroj jako MySQL `medevio`) |
|
||||||
|
| Databáze | `ordinace` |
|
||||||
|
| Kolekce | `registrovani_tracking` |
|
||||||
|
| Klíč | `_id` = rodné číslo (1 dokument na pacienta) |
|
||||||
|
|
||||||
|
### Schéma dokumentu
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"_id": "8202...", "rc": "8202...", "prijmeni": "...", "jmeno": "...",
|
||||||
|
"pojistovna": {"kod": "111", "zkratka": "VZP"},
|
||||||
|
"vychozi_datum": "2025-01-01",
|
||||||
|
"aktualni": { ...snímek... },
|
||||||
|
"historie": [ { ...snímek..., "zmena": "výchozí snímek" } ],
|
||||||
|
"created_at": ISODate, "updated_at": ISODate
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Snímek (`aktualni` i položky `historie[]`):
|
||||||
|
`k_datu, kategorie, kategorie_popis, v_zakoupenem_souboru (bool), flag, flag_duvod,
|
||||||
|
praktik_nazev, praktik_icz, praktik_icp, praktik_od, datum_zahajeni, datum_ukonceni`
|
||||||
|
|
||||||
|
- **`praktik_nazev` / `praktik_icz` / `praktik_icp`** = KDO je registrující praktik dle VZP
|
||||||
|
(u `OK_BUZALKOVA` Buzalková, u `JINY_PRAKTIK` cizí ZZZ).
|
||||||
|
- **`praktik_od`** (= `datum_zahajeni`) = OD KDY je u tohoto praktika registrován.
|
||||||
|
- **`flag_duvod`** = čitelný důvod mimo soubor, např. `"jiný praktik: MOJE AMBULANCE A.S.
|
||||||
|
(IČZ 91777000) od 2014-01-01"`.
|
||||||
|
|
||||||
|
### Kategorie (plné podkategorie)
|
||||||
|
|
||||||
|
| kategorie | význam | v souboru |
|
||||||
|
|---|---|---|
|
||||||
|
| `OK_BUZALKOVA` | praktik 001 = Buzalková (IČP 09305001) | ✅ ano |
|
||||||
|
| `JINY_PRAKTIK` | praktik 001 je jiné ZZZ | 🚩 ne |
|
||||||
|
| `BEZ_PRAKTIKA_VZP` | u VZP záznam (jiná odb.), ale praktik 001 ne | 🚩 ne |
|
||||||
|
| `BEZ_ZAZNAMU_VZP` | VZP nevrátila žádný záznam (jiná pojišťovna / neplatné RČ / zaniklé pojištění) | 🚩 ne |
|
||||||
|
|
||||||
|
## Stav k výchozímu snímku 1.1.2025
|
||||||
|
|
||||||
|
Populace = 1688 pacientů registrovaných v Medicusu k 1.1.2025 (= RČ v `vzp_registrace_raw` pro to datum).
|
||||||
|
|
||||||
|
| kategorie | počet |
|
||||||
|
|---|---:|
|
||||||
|
| OK_BUZALKOVA | 1537 |
|
||||||
|
| JINY_PRAKTIK | 53 |
|
||||||
|
| BEZ_ZAZNAMU_VZP | 50 |
|
||||||
|
| BEZ_PRAKTIKA_VZP | 48 |
|
||||||
|
| **v souboru / mimo** | **1537 / 151** |
|
||||||
|
|
||||||
|
## Skript `seed_tracking.py`
|
||||||
|
|
||||||
|
Zdroj klasifikace = MySQL `medevio` tabulky `vzp_registrace_raw` + `vzp_registrace_lekari`
|
||||||
|
(plní je skripty z `Insurance/KdoJeLekar/`).
|
||||||
|
|
||||||
|
```
|
||||||
|
python seed_tracking.py # výchozí snímek k 2025-01-01
|
||||||
|
python seed_tracking.py 2026-05-02 # aplikuje další snímek (appendne změny do historie)
|
||||||
|
```
|
||||||
|
|
||||||
|
Funkce `apply_snapshot(coll, mysql, k_datu)`:
|
||||||
|
- nový pacient → vloží dokument s historií `["výchozí snímek"]`
|
||||||
|
- existující pacient → při změně `kategorie` nebo `praktik_icp` appendne položku do `historie[]`
|
||||||
|
a aktualizuje `aktualni`; jinak jen `updated_at`
|
||||||
|
|
||||||
|
→ tím se **postupně trackují změny stavu** mezi jednotlivými běhy.
|
||||||
|
|
||||||
|
### Doplnění jmen (BEZ_ZAZNAMU_VZP)
|
||||||
|
|
||||||
|
50 pacientů bez žádného VZP záznamu nemá jméno v MySQL `vzp_registrace_lekari`.
|
||||||
|
Jména + pojišťovnu jim doplňujeme z Medicus Firebird (tabulka `kar`) — uloženo i pole
|
||||||
|
`medicus_poj`. Pozn.: kdo má `medicus_poj=111` (VZP), ale je `BEZ_ZAZNAMU_VZP`, je reálně
|
||||||
|
podezřelý (zaniklé pojištění/úmrtí); 201/205/207/211 jsou prostě jiné pojišťovny.
|
||||||
|
|
||||||
|
## Reconciliation workflow — statusy
|
||||||
|
|
||||||
|
Zakoupený soubor (příloha smlouvy) = `Inputs/2025-01-01 seznam_pacientu_jmeno_rc.csv`
|
||||||
|
(OCR ze skenu; `;`-CSV, UTF-8 BOM; sloupce *Příjmení a jméno; Rodné číslo; Strana; Řádek*).
|
||||||
|
**1712 RČ.** (Opraven 1 OCR překlep RČ: Slavíková Zuzana `8956534235`→`8956039037`.)
|
||||||
|
|
||||||
|
Každý dokument nese workflow stav:
|
||||||
|
|
||||||
|
| pole | význam |
|
||||||
|
|---|---|
|
||||||
|
| `status` (int) | aktuální stav workflow |
|
||||||
|
| `status_popis` | název stavu |
|
||||||
|
| `status_datum` | datum platnosti stavu |
|
||||||
|
| `status_historie[]` | postup stavů (`status, status_popis, status_datum, zapsano`) |
|
||||||
|
| `ve_smlouve` (bool) | je pacient v zakoupeném souboru 1712? |
|
||||||
|
| `mimo_vzp_populaci` | true = doplněn ze smlouvy, nebyl ve VZP populaci k 1.1.2025 |
|
||||||
|
|
||||||
|
### Stavy
|
||||||
|
|
||||||
|
| status | popis | datum | skript |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **0** | **Zakoupeno** | 31.12.2024 | `01_zakoupeno.py` |
|
||||||
|
|
||||||
|
`01_zakoupeno.py` (idempotentní): nastaví status 0 všem 1712 ze smlouvy.
|
||||||
|
- 1678 už v Mongo → status 0
|
||||||
|
- 34 chybělo (odhlášeni u Buzalkové před předáním) → doplněno z `kar`, `mimo_vzp_populaci=true`,
|
||||||
|
`aktualni.kategorie="ODHLASEN_PRED_PREDANIM"` + `medicus_zruseni`
|
||||||
|
- 10 v Mongo mimo smlouvu → `ve_smlouve=false` (status 0 nedostali)
|
||||||
|
|
||||||
|
Kolekce po kroku 0: **1722 dokumentů** (1712 ve smlouvě + 10 mimo).
|
||||||
|
|
||||||
|
### Reconciliation 1712 (k 1.1.2025)
|
||||||
|
|
||||||
|
```
|
||||||
|
1712 zakoupeno (status 0)
|
||||||
|
−34 registrace zrušena před 1.1.2025 (mimo_vzp_populaci)
|
||||||
|
─────
|
||||||
|
1678 registrovaní v Medicusu k 1.1.2025
|
||||||
|
├ 1531 OK Buzalková · 50 jiný praktik · 49 bez záznamu · 48 bez praktika
|
||||||
|
```
|
||||||
|
|
||||||
|
## Další kroky (workflow)
|
||||||
|
|
||||||
|
- Definovat status 1, 2, … (např. 1 = ověřeno u VZP / registrovaný u Buzalkové).
|
||||||
|
- Aplikovat snímky z dalších běhů (29.4. a 2.5.2026 v MySQL) → naplní `historie[]`.
|
||||||
|
- Doplnit ověření **stavu pojištění** (`vzp_stav_pojisteni`).
|
||||||
|
- Finální reconciliation Excel + MCP nástroj nad kolekcí.
|
||||||
Binary file not shown.
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
report_tracking.py
|
||||||
|
==================
|
||||||
|
Excel report nad MongoDB `ordinace.registrovani_tracking`.
|
||||||
|
|
||||||
|
Pro každého pacienta zobrazí:
|
||||||
|
jméno, datum narození, rodné číslo, pojišťovnu, stav a důvod (kdo + od kdy).
|
||||||
|
|
||||||
|
Identifikační údaje (jméno, datum narození, pojišťovna) se berou AUTORITATIVNĚ
|
||||||
|
z Medicus Firebird tabulky `kar` (přes Knihovny.medicus_db.get_medicus_connection).
|
||||||
|
Stav a důvod (kategorie, flag, flag_duvod, praktik kdo/od kdy) z Mongo trackingu.
|
||||||
|
|
||||||
|
Výstup: report_registrovani_<vychozi_datum>.xlsx v tomto adresáři.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import date
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
import pymongo
|
||||||
|
from Knihovny.medicus_db import get_medicus_connection
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
|
# ── Konfigurace ────────────────────────────────────────────────────────────────
|
||||||
|
MONGO_URI = "mongodb://192.168.1.76:27017"
|
||||||
|
MONGO_DB = "ordinace"
|
||||||
|
MONGO_COLL = "registrovani_tracking"
|
||||||
|
|
||||||
|
POJ_NAZVY = {
|
||||||
|
"111": "VZP", "201": "VoZP", "205": "ČPZP", "207": "OZP",
|
||||||
|
"209": "ZPŠ", "211": "ZPMV ČR", "213": "RBP",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Barvy podle kategorie
|
||||||
|
BLUE_HEADER = "1F497D"
|
||||||
|
WHITE = "FFFFFF"
|
||||||
|
BARVA_KAT = {
|
||||||
|
"OK_BUZALKOVA": "EBF1DE", # zelená
|
||||||
|
"JINY_PRAKTIK": "FCE4D6", # červená
|
||||||
|
"BEZ_PRAKTIKA_VZP": "FFF2CC", # žlutá
|
||||||
|
"BEZ_ZAZNAMU_VZP": "DCE6F1", # modrá
|
||||||
|
}
|
||||||
|
STAV_TEXT = {
|
||||||
|
"OK_BUZALKOVA": "V souboru",
|
||||||
|
"JINY_PRAKTIK": "NEBYL v souboru",
|
||||||
|
"BEZ_PRAKTIKA_VZP": "NEBYL v souboru",
|
||||||
|
"BEZ_ZAZNAMU_VZP": "NEBYL v souboru",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def chunked(seq, n):
|
||||||
|
for i in range(0, len(seq), n):
|
||||||
|
yield seq[i:i + n]
|
||||||
|
|
||||||
|
|
||||||
|
def nacti_kar(conn, rcs):
|
||||||
|
"""Vrátí {rc: {prijmeni, jmeno, datnar, poj}} z Medicus kar."""
|
||||||
|
out = {}
|
||||||
|
cur = conn.cursor()
|
||||||
|
for batch in chunked(rcs, 500): # Firebird IN má limit 1500 prvků
|
||||||
|
ph = ",".join("?" for _ in batch)
|
||||||
|
cur.execute(
|
||||||
|
f"SELECT TRIM(rodcis), TRIM(prijmeni), TRIM(jmeno), datnar, TRIM(poj) "
|
||||||
|
f"FROM kar WHERE rodcis IN ({ph})", batch)
|
||||||
|
for rc, prij, jm, datnar, poj in cur.fetchall():
|
||||||
|
out[(rc or "").strip()] = {
|
||||||
|
"prijmeni": prij, "jmeno": jm,
|
||||||
|
"datnar": datnar, "poj": (poj or "").strip(),
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
client = pymongo.MongoClient(MONGO_URI, serverSelectionTimeoutMS=3000)
|
||||||
|
coll = client[MONGO_DB][MONGO_COLL]
|
||||||
|
|
||||||
|
docs = list(coll.find({}))
|
||||||
|
vychozi = docs[0].get("vychozi_datum", "snimek") if docs else "snimek"
|
||||||
|
rcs = [d["_id"] for d in docs]
|
||||||
|
|
||||||
|
print(f"Pacientů v trackingu: {len(rcs)}")
|
||||||
|
print("Načítám kar z Medicusu ...")
|
||||||
|
conn = get_medicus_connection()
|
||||||
|
kar = nacti_kar(conn, rcs)
|
||||||
|
conn.close()
|
||||||
|
print(f"Dohledáno v kar: {len(kar)}")
|
||||||
|
|
||||||
|
# ── Sestavení řádků ────────────────────────────────────────────────────────
|
||||||
|
rows = []
|
||||||
|
for d in docs:
|
||||||
|
rc = d["_id"]
|
||||||
|
a = d.get("aktualni", {})
|
||||||
|
k = kar.get(rc, {})
|
||||||
|
|
||||||
|
prijmeni = k.get("prijmeni") or d.get("prijmeni") or ""
|
||||||
|
jmeno = k.get("jmeno") or d.get("jmeno") or ""
|
||||||
|
datnar = k.get("datnar")
|
||||||
|
poj_kod = k.get("poj") or (d.get("pojistovna") or {}).get("kod") or ""
|
||||||
|
kat = a.get("kategorie", "")
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
"prijmeni": prijmeni,
|
||||||
|
"jmeno": jmeno,
|
||||||
|
"datnar": datnar.strftime("%d.%m.%Y") if datnar else "",
|
||||||
|
"rc": rc,
|
||||||
|
"poj": f"{poj_kod} {POJ_NAZVY.get(poj_kod, '')}".strip(),
|
||||||
|
"stav": STAV_TEXT.get(kat, kat),
|
||||||
|
"kategorie": a.get("kategorie_popis", ""),
|
||||||
|
"duvod": a.get("flag_duvod", ""),
|
||||||
|
"kat_kod": kat,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Řazení: nejdřív flagnutí (mimo soubor), pak podle příjmení
|
||||||
|
rows.sort(key=lambda r: (r["kat_kod"] == "OK_BUZALKOVA", r["prijmeni"], r["jmeno"]))
|
||||||
|
|
||||||
|
# ── Excel ──────────────────────────────────────────────────────────────────
|
||||||
|
wb = Workbook()
|
||||||
|
|
||||||
|
# List 1: Přehled
|
||||||
|
ws_p = wb.active
|
||||||
|
ws_p.title = "Přehled"
|
||||||
|
ws_p.column_dimensions["A"].width = 34
|
||||||
|
ws_p.column_dimensions["B"].width = 14
|
||||||
|
ws_p.merge_cells("A1:B1")
|
||||||
|
t = ws_p["A1"]
|
||||||
|
t.value = f"Registrovaní pacienti k {vychozi} — ověření praktika u VZP"
|
||||||
|
t.font = Font(name="Arial", bold=True, size=13, color=WHITE)
|
||||||
|
t.fill = PatternFill("solid", fgColor=BLUE_HEADER)
|
||||||
|
t.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||||
|
ws_p.row_dimensions[1].height = 34
|
||||||
|
ws_p["A2"] = f"Vygenerováno: {date.today().strftime('%d.%m.%Y')}"
|
||||||
|
ws_p["A2"].font = Font(name="Arial", italic=True, size=9, color="595959")
|
||||||
|
|
||||||
|
counts = defaultdict(int)
|
||||||
|
for r in rows:
|
||||||
|
counts[r["kat_kod"]] += 1
|
||||||
|
|
||||||
|
ws_p.cell(row=4, column=1, value="Kategorie / stav").font = Font(bold=True)
|
||||||
|
ws_p.cell(row=4, column=2, value="Počet").font = Font(bold=True)
|
||||||
|
poradi = ["OK_BUZALKOVA", "JINY_PRAKTIK", "BEZ_PRAKTIKA_VZP", "BEZ_ZAZNAMU_VZP"]
|
||||||
|
KAT_POPIS = {
|
||||||
|
"OK_BUZALKOVA": "V souboru (praktik Buzalková)",
|
||||||
|
"JINY_PRAKTIK": "Mimo soubor — jiný praktik",
|
||||||
|
"BEZ_PRAKTIKA_VZP": "Mimo soubor — bez praktika u VZP",
|
||||||
|
"BEZ_ZAZNAMU_VZP": "Mimo soubor — bez záznamu u VZP",
|
||||||
|
}
|
||||||
|
for i, kat in enumerate(poradi):
|
||||||
|
r = 5 + i
|
||||||
|
c1 = ws_p.cell(row=r, column=1, value=KAT_POPIS[kat])
|
||||||
|
c2 = ws_p.cell(row=r, column=2, value=counts[kat])
|
||||||
|
fill = PatternFill("solid", fgColor=BARVA_KAT[kat])
|
||||||
|
c1.fill = fill; c2.fill = fill
|
||||||
|
c1.font = Font(name="Arial", size=10)
|
||||||
|
ws_p.cell(row=9, column=1, value="CELKEM").font = Font(bold=True)
|
||||||
|
ws_p.cell(row=9, column=2, value=len(rows)).font = Font(bold=True)
|
||||||
|
mimo = len(rows) - counts["OK_BUZALKOVA"]
|
||||||
|
ws_p.cell(row=10, column=1, value="z toho NEBYL v zakoupeném souboru").font = Font(bold=True, color="C00000")
|
||||||
|
ws_p.cell(row=10, column=2, value=mimo).font = Font(bold=True, color="C00000")
|
||||||
|
|
||||||
|
# List 2: Pacienti
|
||||||
|
ws = wb.create_sheet("Pacienti")
|
||||||
|
COLS = [
|
||||||
|
("Příjmení", 20), ("Jméno", 14), ("Datum narození", 14),
|
||||||
|
("Rodné číslo", 14), ("Pojišťovna", 14), ("Stav", 16),
|
||||||
|
("Kategorie", 30), ("Důvod (kdo / od kdy)", 52),
|
||||||
|
]
|
||||||
|
for ci, (h, w) in enumerate(COLS, 1):
|
||||||
|
c = ws.cell(row=1, column=ci, value=h)
|
||||||
|
c.font = Font(name="Arial", bold=True, color=WHITE, size=10)
|
||||||
|
c.fill = PatternFill("solid", fgColor=BLUE_HEADER)
|
||||||
|
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||||
|
ws.column_dimensions[get_column_letter(ci)].width = w
|
||||||
|
ws.row_dimensions[1].height = 30
|
||||||
|
ws.freeze_panes = "A2"
|
||||||
|
|
||||||
|
thin = Side(style="thin", color="D9D9D9")
|
||||||
|
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||||
|
|
||||||
|
for ri, r in enumerate(rows, 2):
|
||||||
|
bg = BARVA_KAT.get(r["kat_kod"], "FFFFFF")
|
||||||
|
data = [r["prijmeni"], r["jmeno"], r["datnar"], r["rc"], r["poj"],
|
||||||
|
r["stav"], r["kategorie"], r["duvod"]]
|
||||||
|
for ci, val in enumerate(data, 1):
|
||||||
|
c = ws.cell(row=ri, column=ci, value=val)
|
||||||
|
c.font = Font(name="Arial", size=9)
|
||||||
|
c.fill = PatternFill("solid", fgColor=bg)
|
||||||
|
c.border = border
|
||||||
|
c.alignment = Alignment(vertical="center", wrap_text=(ci == 8))
|
||||||
|
if ci == 6 and r["kat_kod"] != "OK_BUZALKOVA":
|
||||||
|
c.font = Font(name="Arial", size=9, bold=True, color="C00000")
|
||||||
|
|
||||||
|
ws.auto_filter.ref = f"A1:{get_column_letter(len(COLS))}{len(rows) + 1}"
|
||||||
|
|
||||||
|
out = Path(__file__).resolve().parent / f"report_registrovani_{vychozi}.xlsx"
|
||||||
|
wb.save(out)
|
||||||
|
print(f"\nUloženo: {out}")
|
||||||
|
print(f"Řádků: {len(rows)} | v souboru: {counts['OK_BUZALKOVA']} | mimo: {mimo}")
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
seed_tracking.py
|
||||||
|
================
|
||||||
|
Naplní MongoDB databázi `ordinace`, kolekci `registrovani_tracking`, výchozím
|
||||||
|
snímkem registrovaných pacientů a jejich OVĚŘENÝM stavem u VZP.
|
||||||
|
|
||||||
|
Logika "v zakoupeném souboru pacientů":
|
||||||
|
- "Registrovaný v Medicusu" je jen stav v software.
|
||||||
|
- Skutečnost ověřujeme u pojišťovny: kdo je k danému dni registrující praktik
|
||||||
|
(odbornost 001) daného pacienta.
|
||||||
|
* praktik = Buzalková (IČP 09305001) -> v pořádku, v zakoupeném souboru
|
||||||
|
* praktik = někdo jiný / žádný -> NEBYL V ZAKOUPENÉM SOUBORU PACIENTŮ
|
||||||
|
|
||||||
|
Kategorie (plné podkategorie):
|
||||||
|
OK_BUZALKOVA praktik 001 je Buzalková (IČP 09305001)
|
||||||
|
JINY_PRAKTIK praktik 001 je jiné ZZZ
|
||||||
|
BEZ_PRAKTIKA_VZP pacient má u VZP záznam (jiná odbornost), ale praktika 001 ne
|
||||||
|
BEZ_ZAZNAMU_VZP VZP nevrátila žádný záznam (typicky jiná pojišťovna / neplatné RČ)
|
||||||
|
|
||||||
|
Schéma dokumentu (1 dokument na pacienta, _id = rodné číslo):
|
||||||
|
{
|
||||||
|
"_id": "8202...", "rc": "...", "prijmeni": "...", "jmeno": "...",
|
||||||
|
"pojistovna": {"kod": "111", "zkratka": "VZP"},
|
||||||
|
"vychozi_datum": "2025-01-01",
|
||||||
|
"aktualni": { ...snímek... },
|
||||||
|
"historie": [ { ...snímek..., "zmena": "výchozí snímek" }, ... ],
|
||||||
|
"created_at": ..., "updated_at": ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Snímek (aktualni i položka historie):
|
||||||
|
{ "k_datu", "kategorie", "kategorie_popis", "v_zakoupenem_souboru" (bool),
|
||||||
|
"flag", "praktik_nazev", "praktik_icz", "praktik_icp",
|
||||||
|
"datum_zahajeni", "datum_ukonceni" }
|
||||||
|
|
||||||
|
Spuštění:
|
||||||
|
python seed_tracking.py # seed k 2025-01-01
|
||||||
|
python seed_tracking.py 2026-05-02 # aplikuje další snímek (appendne změny do historie)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, date, timezone
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
import pymongo
|
||||||
|
from Knihovny.mysql_db import connect_mysql
|
||||||
|
|
||||||
|
# ── KONFIGURACE ───────────────────────────────────────────────────────────────
|
||||||
|
MONGO_URI = "mongodb://192.168.1.76:27017"
|
||||||
|
MONGO_DB = "ordinace"
|
||||||
|
MONGO_COLL = "registrovani_tracking"
|
||||||
|
ICP_BUZALKOVA = "09305001"
|
||||||
|
|
||||||
|
KATEGORIE_POPIS = {
|
||||||
|
"OK_BUZALKOVA": "OK – praktik je Buzalková (IČP 09305001)",
|
||||||
|
"JINY_PRAKTIK": "Registrován u jiného praktika",
|
||||||
|
"BEZ_PRAKTIKA_VZP": "U VZP bez praktika (odb. 001)",
|
||||||
|
"BEZ_ZAZNAMU_VZP": "VZP nevrátila žádný záznam (jiná pojišťovna / neplatné RČ)",
|
||||||
|
}
|
||||||
|
FLAG_MIMO_SOUBOR = "NEBYL V ZAKOUPENÉM SOUBORU PACIENTŮ"
|
||||||
|
|
||||||
|
|
||||||
|
def klasifikuj(praktik_001: dict | None, ma_nejaky_zaznam: bool) -> dict:
|
||||||
|
"""Vrátí snímek stavu (bez k_datu) na základě 001 záznamu z VZP."""
|
||||||
|
if praktik_001 and praktik_001.get("ICP") == ICP_BUZALKOVA:
|
||||||
|
kat = "OK_BUZALKOVA"
|
||||||
|
elif praktik_001:
|
||||||
|
kat = "JINY_PRAKTIK"
|
||||||
|
elif ma_nejaky_zaznam:
|
||||||
|
kat = "BEZ_PRAKTIKA_VZP"
|
||||||
|
else:
|
||||||
|
kat = "BEZ_ZAZNAMU_VZP"
|
||||||
|
|
||||||
|
v_souboru = (kat == "OK_BUZALKOVA")
|
||||||
|
nazev = (praktik_001 or {}).get("nazev_zzz")
|
||||||
|
icz = (praktik_001 or {}).get("ICZ")
|
||||||
|
od = (praktik_001 or {}).get("datum_zahajeni")
|
||||||
|
|
||||||
|
# Čitelný důvod, proč pacient NENÍ v zakoupeném souboru (kdo + od kdy)
|
||||||
|
if kat == "JINY_PRAKTIK":
|
||||||
|
flag_duvod = f"jiný praktik: {nazev} (IČZ {icz}) od {od}"
|
||||||
|
elif kat == "BEZ_PRAKTIKA_VZP":
|
||||||
|
flag_duvod = "u VZP bez registrujícího praktika (odb. 001)"
|
||||||
|
elif kat == "BEZ_ZAZNAMU_VZP":
|
||||||
|
flag_duvod = "VZP nevrátila žádný záznam (jiná pojišťovna / neplatné RČ / zaniklé pojištění)"
|
||||||
|
else:
|
||||||
|
flag_duvod = ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"kategorie": kat,
|
||||||
|
"kategorie_popis": KATEGORIE_POPIS[kat],
|
||||||
|
"v_zakoupenem_souboru": v_souboru,
|
||||||
|
"flag": "" if v_souboru else FLAG_MIMO_SOUBOR,
|
||||||
|
"flag_duvod": flag_duvod,
|
||||||
|
# "kdo" a "od kdy" registrujícího praktika dle VZP
|
||||||
|
"praktik_nazev": nazev,
|
||||||
|
"praktik_icz": icz,
|
||||||
|
"praktik_icp": (praktik_001 or {}).get("ICP"),
|
||||||
|
"praktik_od": od,
|
||||||
|
"datum_zahajeni": od,
|
||||||
|
"datum_ukonceni": (praktik_001 or {}).get("datum_ukonceni"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def nacti_snimek_z_mysql(mysql, k_datu: str) -> dict:
|
||||||
|
"""
|
||||||
|
Vrátí {rc: {prijmeni, jmeno, pojistovna{}, praktik_001 | None, ma_zaznam}}
|
||||||
|
pro populaci registrovaných dotázaných k danému datu.
|
||||||
|
"""
|
||||||
|
cur = mysql.cursor()
|
||||||
|
|
||||||
|
# Populace = všechna dotázaná RČ (raw) k tomuto datu
|
||||||
|
cur.execute("SELECT rc FROM vzp_registrace_raw WHERE k_datu = %s", (k_datu,))
|
||||||
|
populace = [r[0] for r in cur.fetchall()]
|
||||||
|
|
||||||
|
# Parsované záznamy lékařů k tomuto datu
|
||||||
|
cur.execute("""
|
||||||
|
SELECT rc, prijmeni, jmeno, kod_odbornosti, ICP, ICZ, nazev_zzz,
|
||||||
|
poj_kod, poj_zkratka, datum_zahajeni, datum_ukonceni
|
||||||
|
FROM vzp_registrace_lekari
|
||||||
|
WHERE k_datu = %s
|
||||||
|
""", (k_datu,))
|
||||||
|
|
||||||
|
data: dict[str, dict] = {rc: {"prijmeni": None, "jmeno": None,
|
||||||
|
"pojistovna": {"kod": None, "zkratka": None},
|
||||||
|
"praktik_001": None, "ma_zaznam": False}
|
||||||
|
for rc in populace}
|
||||||
|
|
||||||
|
for (rc, prijmeni, jmeno, odb, icp, icz, nazev_zzz,
|
||||||
|
poj_kod, poj_zkr, dat_zah, dat_uk) in cur.fetchall():
|
||||||
|
d = data.setdefault(rc, {"prijmeni": None, "jmeno": None,
|
||||||
|
"pojistovna": {"kod": None, "zkratka": None},
|
||||||
|
"praktik_001": None, "ma_zaznam": False})
|
||||||
|
d["ma_zaznam"] = True
|
||||||
|
if prijmeni and not d["prijmeni"]:
|
||||||
|
d["prijmeni"] = prijmeni
|
||||||
|
if jmeno and not d["jmeno"]:
|
||||||
|
d["jmeno"] = jmeno
|
||||||
|
# Pojišťovnu vezmi z jakéhokoli záznamu (preferuj 001 níže)
|
||||||
|
if poj_kod and not d["pojistovna"]["kod"]:
|
||||||
|
d["pojistovna"] = {"kod": poj_kod, "zkratka": poj_zkr}
|
||||||
|
if odb == "001":
|
||||||
|
d["praktik_001"] = {
|
||||||
|
"ICP": icp, "ICZ": icz, "nazev_zzz": nazev_zzz,
|
||||||
|
"poj_kod": poj_kod, "poj_zkratka": poj_zkr,
|
||||||
|
"datum_zahajeni": str(dat_zah) if dat_zah else None,
|
||||||
|
"datum_ukonceni": str(dat_uk) if dat_uk else None,
|
||||||
|
}
|
||||||
|
# Pojišťovna z 001 má přednost
|
||||||
|
if poj_kod:
|
||||||
|
d["pojistovna"] = {"kod": poj_kod, "zkratka": poj_zkr}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def apply_snapshot(coll, mysql, k_datu: str) -> dict:
|
||||||
|
"""
|
||||||
|
Klasifikuje populaci k danému datu a upsertne do Mongo.
|
||||||
|
Při změně kategorie/praktika oproti `aktualni` appendne do `historie`.
|
||||||
|
Vrátí statistiku.
|
||||||
|
"""
|
||||||
|
data = nacti_snimek_z_mysql(mysql, k_datu)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
stats = {"novych": 0, "zmen": 0, "beze_zmeny": 0, "kategorie": {}}
|
||||||
|
|
||||||
|
for rc, d in data.items():
|
||||||
|
snimek = klasifikuj(d["praktik_001"], d["ma_zaznam"])
|
||||||
|
snimek_s_datem = {"k_datu": k_datu, **snimek}
|
||||||
|
stats["kategorie"][snimek["kategorie"]] = stats["kategorie"].get(snimek["kategorie"], 0) + 1
|
||||||
|
|
||||||
|
existing = coll.find_one({"_id": rc})
|
||||||
|
if existing is None:
|
||||||
|
doc = {
|
||||||
|
"_id": rc, "rc": rc,
|
||||||
|
"prijmeni": d["prijmeni"], "jmeno": d["jmeno"],
|
||||||
|
"pojistovna": d["pojistovna"],
|
||||||
|
"vychozi_datum": k_datu,
|
||||||
|
"aktualni": snimek_s_datem,
|
||||||
|
"historie": [{**snimek_s_datem, "zmena": "výchozí snímek"}],
|
||||||
|
"created_at": now, "updated_at": now,
|
||||||
|
}
|
||||||
|
coll.insert_one(doc)
|
||||||
|
stats["novych"] += 1
|
||||||
|
else:
|
||||||
|
akt = existing.get("aktualni", {})
|
||||||
|
zmena = (akt.get("kategorie") != snimek["kategorie"]
|
||||||
|
or akt.get("praktik_icp") != snimek["praktik_icp"])
|
||||||
|
update = {"aktualni": snimek_s_datem, "updated_at": now}
|
||||||
|
if d["prijmeni"]:
|
||||||
|
update["prijmeni"] = d["prijmeni"]
|
||||||
|
if d["jmeno"]:
|
||||||
|
update["jmeno"] = d["jmeno"]
|
||||||
|
ops = {"$set": update}
|
||||||
|
if zmena:
|
||||||
|
popis = (f"{akt.get('kategorie')} → {snimek['kategorie']}")
|
||||||
|
ops["$push"] = {"historie": {**snimek_s_datem, "zmena": popis}}
|
||||||
|
stats["zmen"] += 1
|
||||||
|
else:
|
||||||
|
stats["beze_zmeny"] += 1
|
||||||
|
coll.update_one({"_id": rc}, ops)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
k_datu = sys.argv[1] if len(sys.argv) > 1 else "2025-01-01"
|
||||||
|
|
||||||
|
mysql = connect_mysql()
|
||||||
|
client = pymongo.MongoClient(MONGO_URI, serverSelectionTimeoutMS=3000)
|
||||||
|
client.admin.command("ping")
|
||||||
|
coll = client[MONGO_DB][MONGO_COLL]
|
||||||
|
|
||||||
|
# Indexy pro běžné dotazy
|
||||||
|
coll.create_index("aktualni.kategorie")
|
||||||
|
coll.create_index("aktualni.v_zakoupenem_souboru")
|
||||||
|
coll.create_index("prijmeni")
|
||||||
|
|
||||||
|
print(f"Aplikuji snímek k {k_datu} do {MONGO_DB}.{MONGO_COLL} ...")
|
||||||
|
stats = apply_snapshot(coll, mysql, k_datu)
|
||||||
|
|
||||||
|
print(f"\nNových pacientů : {stats['novych']}")
|
||||||
|
print(f"Změn stavu : {stats['zmen']}")
|
||||||
|
print(f"Beze změny : {stats['beze_zmeny']}")
|
||||||
|
print("\nRozpad podle kategorií:")
|
||||||
|
for kat, n in sorted(stats["kategorie"].items(), key=lambda x: -x[1]):
|
||||||
|
print(f" {kat:18s} {n:5d} {KATEGORIE_POPIS[kat]}")
|
||||||
|
|
||||||
|
celkem = sum(stats["kategorie"].values())
|
||||||
|
mimo = celkem - stats["kategorie"].get("OK_BUZALKOVA", 0)
|
||||||
|
print(f"\nCelkem v populaci: {celkem}")
|
||||||
|
print(f" v zakoupeném souboru (Buzalková): {stats['kategorie'].get('OK_BUZALKOVA', 0)}")
|
||||||
|
print(f" NEBYL v zakoupeném souboru : {mimo}")
|
||||||
|
|
||||||
|
mysql.close()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
MCP server pro pojišťovny (insurance) — používá oficiální MCP SDK (FastMCP)
|
||||||
|
Spustit: python mcp_insurance.py
|
||||||
|
|
||||||
|
Dotazy na VZP B2B portál pomocí klientského certifikátu (mTLS).
|
||||||
|
Vychází z knihovny Knihovny/vzpb2b_client.py a skriptů v Insurance/.
|
||||||
|
|
||||||
|
Nástroje:
|
||||||
|
- registrovani_lekari(rodne_cislo, k_datu) — kdo jsou registrující lékaři
|
||||||
|
pacienta (praktik 001, gynekolog 002, stomatolog 014, ...) ke dni k_datu.
|
||||||
|
- stav_pojisteni(rodne_cislo, k_datu, prijmeni) — jestli je pacient ke dni
|
||||||
|
k_datu platně pojištěný a u které pojišťovny.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import date
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from Knihovny.vzpb2b_client import VZPB2BClient
|
||||||
|
|
||||||
|
|
||||||
|
# Všechny logy MUSÍ jít na stderr — stdout je rezervován pro JSON-RPC
|
||||||
|
def log(msg: str):
|
||||||
|
print(msg, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Certifikát ──────────────────────────────────────────────────────────────
|
||||||
|
PFX_PATH = PROJECT_ROOT / "Insurance" / "Certificates" / "picka.pfx"
|
||||||
|
PFX_PASS = "Vlado7309208104+"
|
||||||
|
|
||||||
|
if not PFX_PATH.exists():
|
||||||
|
log(f"Chyba: certifikát nenalezen: {PFX_PATH}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
vzp = VZPB2BClient("prod", str(PFX_PATH), PFX_PASS)
|
||||||
|
log("VZP B2B klient inicializován (prod)")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Chyba inicializace VZP B2B klienta: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_rc(rodne_cislo: str) -> str:
|
||||||
|
"""Rodné číslo bez lomítka a nečíselných znaků."""
|
||||||
|
return re.sub(r"\D", "", rodne_cislo or "")
|
||||||
|
|
||||||
|
|
||||||
|
# MCP server
|
||||||
|
mcp = FastMCP("insurance")
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def registrovani_lekari(rodne_cislo: str, k_datu: Optional[str] = None) -> dict:
|
||||||
|
"""Zjisti registrující lékaře pacienta u VZP ke dni k_datu.
|
||||||
|
|
||||||
|
Dotáže se VZP B2B služby RegistracePojistencePZSB2B a vrátí, kdo je
|
||||||
|
registrující praktický lékař (odbornost 001), gynekolog (002),
|
||||||
|
stomatolog (014) a případně další, pokud je VZP eviduje.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rodne_cislo: Rodné číslo pacienta (lomítko nevadí).
|
||||||
|
k_datu: Datum ve formátu YYYY-MM-DD. Pokud chybí, použije se dnešek.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict s rodným číslem, datem dotazu, počtem nalezených registrací
|
||||||
|
a seznamem lékařů (každý: kód a název odbornosti, IČZ, IČP, jméno
|
||||||
|
lékaře, název ZZZ, pojišťovna, data registrace/zahájení/ukončení).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rc = _norm_rc(rodne_cislo)
|
||||||
|
if not rc:
|
||||||
|
return {"error": "Neplatné rodné číslo."}
|
||||||
|
|
||||||
|
if k_datu:
|
||||||
|
k_datu = k_datu.strip()
|
||||||
|
else:
|
||||||
|
k_datu = date.today().isoformat()
|
||||||
|
|
||||||
|
xml = vzp.registrace_lekare(rc=rc, k_datu=k_datu, odbornosti=None)
|
||||||
|
zaznamy = vzp.parse_registrace_lekare(xml)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"rodne_cislo": rc,
|
||||||
|
"k_datu": k_datu,
|
||||||
|
"pocet": len(zaznamy),
|
||||||
|
"lekari": zaznamy,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
log(f"registrovani_lekari chyba: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# Význam pole "stav" v odpovědi stavPojisteniB2B
|
||||||
|
_STAV_POPIS = {
|
||||||
|
"1": "pojištěn u uvedené pojišťovny",
|
||||||
|
"4": "cizinec (smluvní/EU pojištění) — považováno za pojištěného",
|
||||||
|
}
|
||||||
|
_STAV_POJISTEN = ("1", "4") # tyto kódy znamenají platné pojištění
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def stav_pojisteni(rodne_cislo: str, k_datu: Optional[str] = None,
|
||||||
|
prijmeni: Optional[str] = None) -> dict:
|
||||||
|
"""Zjisti, jestli je pacient ke dni k_datu platně pojištěný a u které pojišťovny.
|
||||||
|
|
||||||
|
Dotáže se VZP B2B služby stavPojisteniB2B. VZP centrálně eviduje pojištěnce
|
||||||
|
všech pojišťoven, takže odpověď vrátí i pojišťovnu jinou než VZP.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rodne_cislo: Rodné číslo pacienta (lomítko nevadí).
|
||||||
|
k_datu: Datum ve formátu YYYY-MM-DD. Pokud chybí, použije se dnešek.
|
||||||
|
prijmeni: Příjmení (volitelné) — VZP umožňuje křížovou kontrolu se jménem.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict s rodným číslem, datem dotazu, příznakem pojisteny (True/False),
|
||||||
|
kódem a popisem stavu, kódem zpracování požadavku a údaji o pojišťovně
|
||||||
|
(kód, název, kód pojistného vztahu).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rc = _norm_rc(rodne_cislo)
|
||||||
|
if not rc:
|
||||||
|
return {"error": "Neplatné rodné číslo."}
|
||||||
|
|
||||||
|
k_datu = k_datu.strip() if k_datu else date.today().isoformat()
|
||||||
|
prijmeni = prijmeni.strip() if prijmeni else None
|
||||||
|
|
||||||
|
xml = vzp.stav_pojisteni(rc=rc, k_datu=k_datu, prijmeni=prijmeni)
|
||||||
|
parsed = vzp.parse_stav_pojisteni(xml)
|
||||||
|
|
||||||
|
stav = parsed.get("stav")
|
||||||
|
pojisteny = stav in _STAV_POJISTEN
|
||||||
|
|
||||||
|
return {
|
||||||
|
"rodne_cislo": rc,
|
||||||
|
"k_datu": k_datu,
|
||||||
|
"pojisteny": pojisteny,
|
||||||
|
"stav": stav,
|
||||||
|
"stav_popis": _STAV_POPIS.get(stav, "nepojištěn u této pojišťovny / nenalezen"),
|
||||||
|
"stav_vyrizeni": parsed.get("stavVyrizeni"),
|
||||||
|
"kod_pojistovny": parsed.get("kodPojistovny"),
|
||||||
|
"nazev_pojistovny": parsed.get("nazevPojistovny"),
|
||||||
|
"pojisteni_kod": parsed.get("pojisteniKod"),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
log(f"stav_pojisteni chyba: {traceback.format_exc()}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
log("MCP Insurance server spuštěn (FastMCP)")
|
||||||
|
mcp.run()
|
||||||
Reference in New Issue
Block a user