notebookvb
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "report-lekovy-zaznam",
|
||||||
|
"runtimeExecutable": "C:/Users/vlado/PycharmProjects/Recepty/.venv/Scripts/python",
|
||||||
|
"runtimeArgs": ["C:/Users/vlado/PycharmProjects/Recepty/report_server.py"],
|
||||||
|
"port": 8765
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+51983
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ JMENA = "Vladimír"
|
|||||||
DATUM_NAROZENI = "1973-09-20"
|
DATUM_NAROZENI = "1973-09-20"
|
||||||
|
|
||||||
POCET_ZNAKU_ATC = 7
|
POCET_ZNAKU_ATC = 7
|
||||||
POCET_MESICU = 60
|
POCET_MESICU =60
|
||||||
|
|
||||||
VYSTUP = Path(__file__).parent / "odpoved_lekovy_zaznam.xml"
|
VYSTUP = Path(__file__).parent / "odpoved_lekovy_zaznam.xml"
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ DDL_TABULKY = [
|
|||||||
"DROP TABLE IF EXISTS vydej",
|
"DROP TABLE IF EXISTS vydej",
|
||||||
"DROP TABLE IF EXISTS predpis",
|
"DROP TABLE IF EXISTS predpis",
|
||||||
"DROP TABLE IF EXISTS zprava",
|
"DROP TABLE IF EXISTS zprava",
|
||||||
|
"DROP TABLE IF EXISTS predepisujici",
|
||||||
|
"DROP TABLE IF EXISTS vydavajici",
|
||||||
|
|
||||||
# ── zprava ────────────────────────────────────────────────────────────────
|
# ── zprava ────────────────────────────────────────────────────────────────
|
||||||
# zprava_odpoved_type + zprava_type:
|
# zprava_odpoved_type + zprava_type:
|
||||||
@@ -186,6 +188,50 @@ DDL_TABULKY = [
|
|||||||
INDEX idx_nazev (nazev)
|
INDEX idx_nazev (nazev)
|
||||||
) ENGINE=InnoDB
|
) ENGINE=InnoDB
|
||||||
""",
|
""",
|
||||||
|
|
||||||
|
# ── predepisujici ─────────────────────────────────────────────────────────
|
||||||
|
# PredepisujiciSeznam > Predepisujici:
|
||||||
|
# Lekar: Kod CHAR(36), Jmeno: Prijmeni(35), Jmena(24)
|
||||||
|
# ICZ CHAR(8)?, ICP CHAR(8)?
|
||||||
|
# PZS: Nazev(200), Adresa: NazevUlice, CisloPopisne, CisloOrientacni, NazevObce, PSC
|
||||||
|
# Telefon(20)?
|
||||||
|
# lekar_kod = predpis.kod_predepisujiciho
|
||||||
|
"""
|
||||||
|
CREATE TABLE predepisujici (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
lekar_kod CHAR(36) NOT NULL UNIQUE,
|
||||||
|
prijmeni VARCHAR(35),
|
||||||
|
jmena VARCHAR(24),
|
||||||
|
icz CHAR(8),
|
||||||
|
icp CHAR(8),
|
||||||
|
pzs_nazev VARCHAR(200),
|
||||||
|
ulice VARCHAR(150),
|
||||||
|
mesto VARCHAR(100),
|
||||||
|
psc CHAR(5),
|
||||||
|
telefon VARCHAR(20),
|
||||||
|
INDEX idx_icp (icp),
|
||||||
|
INDEX idx_icz (icz)
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
""",
|
||||||
|
|
||||||
|
# ── vydavajici ────────────────────────────────────────────────────────────
|
||||||
|
# VydavajiciSeznam > Vydavajici:
|
||||||
|
# Lekarnik: Kod CHAR(36), Jmeno: Prijmeni(35), Jmena(24)
|
||||||
|
# PZS: Nazev(200), Telefon(20)?, Adresa: NazevUlice, CisloPopisne, CisloOrientacni, NazevObce, PSC
|
||||||
|
# lekarnik_kod = vydej.kod_vydavajiciho
|
||||||
|
"""
|
||||||
|
CREATE TABLE vydavajici (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
lekarnik_kod CHAR(36) NOT NULL UNIQUE,
|
||||||
|
prijmeni VARCHAR(35),
|
||||||
|
jmena VARCHAR(24),
|
||||||
|
pzs_nazev VARCHAR(200),
|
||||||
|
ulice VARCHAR(150),
|
||||||
|
mesto VARCHAR(100),
|
||||||
|
psc CHAR(5),
|
||||||
|
telefon VARCHAR(20)
|
||||||
|
) ENGINE=InnoDB
|
||||||
|
""",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -199,6 +245,79 @@ def vytvor_schema(conn):
|
|||||||
print("Schema OK (5 tabulek smazano a vytvoreno znovu)")
|
print("Schema OK (5 tabulek smazano a vytvoreno znovu)")
|
||||||
|
|
||||||
|
|
||||||
|
# ── parsovani predepisujicich a vydavajicich ─────────────────────────────────
|
||||||
|
|
||||||
|
def _parsuj_adresu(pzs_el):
|
||||||
|
"""Ze elementu PZS vraci (ulice, mesto, psc)."""
|
||||||
|
adr = pzs_el.find(f"{{{NS}}}Adresa") if pzs_el is not None else None
|
||||||
|
if adr is None:
|
||||||
|
return None, None, None
|
||||||
|
ulice_parts = [
|
||||||
|
t(adr, "NazevUlice") or "",
|
||||||
|
t(adr, "CisloPopisne") or "",
|
||||||
|
t(adr, "CisloOrientacni") or "",
|
||||||
|
]
|
||||||
|
ulice = " ".join(p for p in ulice_parts if p).strip() or None
|
||||||
|
psc = t(adr, "PSC")
|
||||||
|
if psc and len(psc) > 5:
|
||||||
|
psc = psc[:5]
|
||||||
|
return ulice, t(adr, "NazevObce"), psc
|
||||||
|
|
||||||
|
|
||||||
|
def parsuj_predepisujici(doklad):
|
||||||
|
"""Vraci seznam slovniku pro tabulku predepisujici."""
|
||||||
|
seznam = []
|
||||||
|
sez_el = doklad.find(f"{{{NS}}}PredepisujiciSeznam")
|
||||||
|
if sez_el is None:
|
||||||
|
return seznam
|
||||||
|
for el in sez_el.findall(f"{{{NS}}}Predepisujici"):
|
||||||
|
lekar = el.find(f"{{{NS}}}Lekar")
|
||||||
|
if lekar is None:
|
||||||
|
continue
|
||||||
|
jmeno = lekar.find(f"{{{NS}}}Jmeno")
|
||||||
|
pzs = el.find(f"{{{NS}}}PZS")
|
||||||
|
ulice, mesto, psc = _parsuj_adresu(pzs)
|
||||||
|
seznam.append(dict(
|
||||||
|
lekar_kod = t(lekar, "Kod"),
|
||||||
|
prijmeni = t(jmeno, "Prijmeni") if jmeno is not None else None,
|
||||||
|
jmena = t(jmeno, "Jmena") if jmeno is not None else None,
|
||||||
|
icz = t(el, "ICZ"),
|
||||||
|
icp = t(el, "ICP"),
|
||||||
|
pzs_nazev = t(pzs, "Nazev") if pzs is not None else None,
|
||||||
|
ulice = ulice,
|
||||||
|
mesto = mesto,
|
||||||
|
psc = psc,
|
||||||
|
telefon = t(el, "Telefon"),
|
||||||
|
))
|
||||||
|
return seznam
|
||||||
|
|
||||||
|
|
||||||
|
def parsuj_vydavajici(doklad):
|
||||||
|
"""Vraci seznam slovniku pro tabulku vydavajici."""
|
||||||
|
seznam = []
|
||||||
|
sez_el = doklad.find(f"{{{NS}}}VydavajiciSeznam")
|
||||||
|
if sez_el is None:
|
||||||
|
return seznam
|
||||||
|
for el in sez_el.findall(f"{{{NS}}}Vydavajici"):
|
||||||
|
lekarnik = el.find(f"{{{NS}}}Lekarnik")
|
||||||
|
if lekarnik is None:
|
||||||
|
continue
|
||||||
|
jmeno = lekarnik.find(f"{{{NS}}}Jmeno")
|
||||||
|
pzs = el.find(f"{{{NS}}}PZS")
|
||||||
|
ulice, mesto, psc = _parsuj_adresu(pzs)
|
||||||
|
seznam.append(dict(
|
||||||
|
lekarnik_kod = t(lekarnik, "Kod"),
|
||||||
|
prijmeni = t(jmeno, "Prijmeni") if jmeno is not None else None,
|
||||||
|
jmena = t(jmeno, "Jmena") if jmeno is not None else None,
|
||||||
|
pzs_nazev = t(pzs, "Nazev") if pzs is not None else None,
|
||||||
|
ulice = ulice,
|
||||||
|
mesto = mesto,
|
||||||
|
psc = psc,
|
||||||
|
telefon = t(pzs, "Telefon") if pzs is not None else None,
|
||||||
|
))
|
||||||
|
return seznam
|
||||||
|
|
||||||
|
|
||||||
# ── parsovani leku ────────────────────────────────────────────────────────────
|
# ── parsovani leku ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def parsuj_slozky_predpis(lek_el):
|
def parsuj_slozky_predpis(lek_el):
|
||||||
@@ -385,7 +504,10 @@ def parsuj_xml(xml_soubor):
|
|||||||
row.update(lek_fields)
|
row.update(lek_fields)
|
||||||
vydeji.append((row, slozky))
|
vydeji.append((row, slozky))
|
||||||
|
|
||||||
return zprava, predpisy, vydeji
|
predepisujici = parsuj_predepisujici(doklad)
|
||||||
|
vydavajici = parsuj_vydavajici(doklad)
|
||||||
|
|
||||||
|
return zprava, predpisy, vydeji, predepisujici, vydavajici
|
||||||
|
|
||||||
|
|
||||||
# ── ulozeni do DB ─────────────────────────────────────────────────────────────
|
# ── ulozeni do DB ─────────────────────────────────────────────────────────────
|
||||||
@@ -397,7 +519,7 @@ def _najdi_id(cur, tabulka, sloupec, hodnota):
|
|||||||
return row["id"] if row else None
|
return row["id"] if row else None
|
||||||
|
|
||||||
|
|
||||||
def uloz(conn, zprava, predpisy, vydeji):
|
def uloz(conn, zprava, predpisy, vydeji, predepisujici, vydavajici):
|
||||||
iplp_predpisu = 0
|
iplp_predpisu = 0
|
||||||
iplp_vydejuu = 0
|
iplp_vydejuu = 0
|
||||||
|
|
||||||
@@ -495,6 +617,56 @@ def uloz(conn, zprava, predpisy, vydeji):
|
|||||||
print(f" vydeji: {vlozeno_v} novych (celkem {len(vydeji)})")
|
print(f" vydeji: {vlozeno_v} novych (celkem {len(vydeji)})")
|
||||||
print(f" vydej_slozka: {vlozeno_vs} slozek z {iplp_vydejuu} IPLP vydejuu")
|
print(f" vydej_slozka: {vlozeno_vs} slozek z {iplp_vydejuu} IPLP vydejuu")
|
||||||
|
|
||||||
|
# ── predepisujici ─────────────────────────────────────────────────────
|
||||||
|
vlozeno_pre = 0
|
||||||
|
for row in predepisujici:
|
||||||
|
if not row.get("lekar_kod"):
|
||||||
|
continue
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO predepisujici
|
||||||
|
(lekar_kod, prijmeni, jmena, icz, icp,
|
||||||
|
pzs_nazev, ulice, mesto, psc, telefon)
|
||||||
|
VALUES
|
||||||
|
(%(lekar_kod)s, %(prijmeni)s, %(jmena)s, %(icz)s, %(icp)s,
|
||||||
|
%(pzs_nazev)s, %(ulice)s, %(mesto)s, %(psc)s, %(telefon)s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
prijmeni = VALUES(prijmeni),
|
||||||
|
jmena = VALUES(jmena),
|
||||||
|
icz = VALUES(icz),
|
||||||
|
icp = VALUES(icp),
|
||||||
|
pzs_nazev = VALUES(pzs_nazev),
|
||||||
|
ulice = VALUES(ulice),
|
||||||
|
mesto = VALUES(mesto),
|
||||||
|
psc = VALUES(psc),
|
||||||
|
telefon = VALUES(telefon)
|
||||||
|
""", row)
|
||||||
|
vlozeno_pre += cur.rowcount
|
||||||
|
print(f" predepisujici: {vlozeno_pre} radku (celkem {len(predepisujici)})")
|
||||||
|
|
||||||
|
# ── vydavajici ────────────────────────────────────────────────────────
|
||||||
|
vlozeno_vyd = 0
|
||||||
|
for row in vydavajici:
|
||||||
|
if not row.get("lekarnik_kod"):
|
||||||
|
continue
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO vydavajici
|
||||||
|
(lekarnik_kod, prijmeni, jmena,
|
||||||
|
pzs_nazev, ulice, mesto, psc, telefon)
|
||||||
|
VALUES
|
||||||
|
(%(lekarnik_kod)s, %(prijmeni)s, %(jmena)s,
|
||||||
|
%(pzs_nazev)s, %(ulice)s, %(mesto)s, %(psc)s, %(telefon)s)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
prijmeni = VALUES(prijmeni),
|
||||||
|
jmena = VALUES(jmena),
|
||||||
|
pzs_nazev = VALUES(pzs_nazev),
|
||||||
|
ulice = VALUES(ulice),
|
||||||
|
mesto = VALUES(mesto),
|
||||||
|
psc = VALUES(psc),
|
||||||
|
telefon = VALUES(telefon)
|
||||||
|
""", row)
|
||||||
|
vlozeno_vyd += cur.rowcount
|
||||||
|
print(f" vydavajici: {vlozeno_vyd} radku (celkem {len(vydavajici)})")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -505,15 +677,16 @@ def main():
|
|||||||
print(f"XML: {xml} ({xml.stat().st_size // 1024} KB)")
|
print(f"XML: {xml} ({xml.stat().st_size // 1024} KB)")
|
||||||
|
|
||||||
print("Parsovani XML ...")
|
print("Parsovani XML ...")
|
||||||
zprava, predpisy, vydeji = parsuj_xml(xml)
|
zprava, predpisy, vydeji, predepisujici, vydavajici = parsuj_xml(xml)
|
||||||
print(f" -> {len(predpisy)} predpisu, {len(vydeji)} vydejuu")
|
print(f" -> {len(predpisy)} predpisu, {len(vydeji)} vydejuu, "
|
||||||
|
f"{len(predepisujici)} predepisujicich, {len(vydavajici)} vydavajicich")
|
||||||
|
|
||||||
print("Pripojeni k MySQL ...")
|
print("Pripojeni k MySQL ...")
|
||||||
conn = pymysql.connect(**DB)
|
conn = pymysql.connect(**DB)
|
||||||
try:
|
try:
|
||||||
vytvor_schema(conn)
|
vytvor_schema(conn)
|
||||||
print("Ukladani ...")
|
print("Ukladani ...")
|
||||||
uloz(conn, zprava, predpisy, vydeji)
|
uloz(conn, zprava, predpisy, vydeji, predepisujici, vydavajici)
|
||||||
print("Hotovo OK")
|
print("Hotovo OK")
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+103
@@ -0,0 +1,103 @@
|
|||||||
|
# Report: Pravidelná medikace z lékového záznamu
|
||||||
|
|
||||||
|
## Soubor
|
||||||
|
`report_server.py` — HTTP server na portu 8765, generuje HTML report z MySQL (`medicus`).
|
||||||
|
|
||||||
|
Spuštění: `.venv/Scripts/python report_server.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zdroj dat
|
||||||
|
|
||||||
|
Tabulky v MySQL (db `medicus`):
|
||||||
|
|
||||||
|
| Tabulka | Obsah |
|
||||||
|
|---------|-------|
|
||||||
|
| `predpis` | předepsané léky, ATC, datum, množství, návod |
|
||||||
|
| `vydej` | vydání léku lékárnou |
|
||||||
|
| `predepisujici` | lékař — UUID, jméno, IČZ, IČP |
|
||||||
|
| `vydavajici` | lékárník — UUID, jméno, PZS |
|
||||||
|
| `vzp_pracoviste` | VZP číselník IČP → odbornost (import týdně) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Algoritmus pravidelnosti
|
||||||
|
|
||||||
|
### Krok 1 — Základní metriky (SQL)
|
||||||
|
```sql
|
||||||
|
GROUP BY LEFT(atc, 5) -- ATC na 5. místě (4. úroveň)
|
||||||
|
|
||||||
|
pocet_predpisu = COUNT(DISTINCT predpis.id)
|
||||||
|
celkove_mnozstvi = SUM(mnozstvi)
|
||||||
|
avg_mnozstvi = AVG(mnozstvi)
|
||||||
|
rozpeti_dni = DATEDIFF(MAX(datum), MIN(datum))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Krok 2 — Výpočet normalizovaného intervalu (Python)
|
||||||
|
```
|
||||||
|
avg_interval = rozpeti_dni / (pocet_predpisu - 1) # průměr dní mezi předpisy
|
||||||
|
norm_interval = avg_interval / avg_mnozstvi # normalizováno na 1 balení
|
||||||
|
```
|
||||||
|
|
||||||
|
`norm_interval` = odhad, na kolik dní jedno balení vystačí.
|
||||||
|
|
||||||
|
### Krok 3 — Klasifikace
|
||||||
|
| Kategorie | Podmínka |
|
||||||
|
|-----------|----------|
|
||||||
|
| **nepravidelný** | `pocet_predpisu < 2` nebo `rozpeti_dni = 0` |
|
||||||
|
| **dle potřeby** (PRN) | většina návodů obsahuje klíčová slova: *dle potřeby, p.p., D.p., při bolesti* |
|
||||||
|
| **pravidelný** | `norm_interval ≤ 100 dní` |
|
||||||
|
| **možná** | `norm_interval 100–185 dní` |
|
||||||
|
| **epizodický** | `norm_interval > 185 dní` |
|
||||||
|
|
||||||
|
> **Důležité:** `rozpeti_dni = 0` znamená, že všechny předpisy byly vystaveny ve stejný den
|
||||||
|
> (lékař dal více balení najednou). Není to opakované předepisování — musí být klasifikováno
|
||||||
|
> jako nepravidelné.
|
||||||
|
|
||||||
|
### Krok 4 — Řazení výstupu
|
||||||
|
1. Pravidelné
|
||||||
|
2. Možná pravidelné
|
||||||
|
3. Epizodické
|
||||||
|
4. PRN
|
||||||
|
|
||||||
|
V rámci kategorie sestupně dle počtu vydání.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Odbornost předepisujícího
|
||||||
|
|
||||||
|
JOIN: `predpis → predepisujici → vzp_pracoviste`
|
||||||
|
|
||||||
|
Číselník VZP je aktuální snapshot — neobsahuje historická data. Recepty starší než
|
||||||
|
`platnost_od` nejnovějšího záznamu pro dané IČP by JOIN na datum nesplnily.
|
||||||
|
|
||||||
|
**Řešení:** vždy vzít nejnovější záznam pro IČP bez ohledu na datum předpisu:
|
||||||
|
```sql
|
||||||
|
LEFT JOIN vzp_pracoviste vp
|
||||||
|
ON vp.icp = pre.icp
|
||||||
|
AND vp.id = (
|
||||||
|
SELECT id FROM vzp_pracoviste
|
||||||
|
WHERE icp = pre.icp
|
||||||
|
ORDER BY platnost_od DESC LIMIT 1
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Odbornost se prakticky nemění — IČP ordinace praktického lékaře zůstane `001` bez ohledu
|
||||||
|
na datum předpisu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Barevné kódování reportu
|
||||||
|
|
||||||
|
| Barva řádku | Kategorie |
|
||||||
|
|-------------|-----------|
|
||||||
|
| Zelená | pravidelný |
|
||||||
|
| Žlutá | možná pravidelný |
|
||||||
|
| Šedá | epizodický |
|
||||||
|
| Červená | dle potřeby (PRN) |
|
||||||
|
|
||||||
|
| Odznak odbornosti | Barva |
|
||||||
|
|-------------------|-------|
|
||||||
|
| Praktický lékař (001, 002) | zelená |
|
||||||
|
| Specialista | modrá |
|
||||||
|
| Neznámá | šedá |
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
"""
|
||||||
|
Import VZP číselníku pracovišť (soubory *.Lh7) do MySQL tabulky vzp_pracoviste.
|
||||||
|
Spouštět každý týden po stažení nového souboru do složky Import/.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
# Windows konzole - povol UTF-8 výstup
|
||||||
|
if sys.stdout.encoding != "utf-8":
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
import mysql.connector
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
"host": "192.168.1.76",
|
||||||
|
"user": "root",
|
||||||
|
"password": "Vlado9674+",
|
||||||
|
"database": "medicus",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPORT_DIR = os.path.join(os.path.dirname(__file__), "Import")
|
||||||
|
|
||||||
|
CREATE_TABLE_SQL = """
|
||||||
|
CREATE TABLE IF NOT EXISTS vzp_pracoviste (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
ico CHAR(8) NOT NULL,
|
||||||
|
icz CHAR(8) NOT NULL,
|
||||||
|
icp CHAR(8) NOT NULL,
|
||||||
|
odbornost VARCHAR(4) NOT NULL,
|
||||||
|
platnost_od DATE NOT NULL,
|
||||||
|
platnost_do DATE NOT NULL,
|
||||||
|
nazev_zarizeni VARCHAR(200),
|
||||||
|
nazev_pracoviste VARCHAR(200),
|
||||||
|
ulice VARCHAR(150),
|
||||||
|
mesto VARCHAR(100),
|
||||||
|
psc CHAR(5),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_icp (icp),
|
||||||
|
INDEX idx_icz (icz),
|
||||||
|
INDEX idx_odbornost (odbornost),
|
||||||
|
INDEX idx_platnost (platnost_od, platnost_do)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
"""
|
||||||
|
|
||||||
|
BATCH_SIZE = 1000
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(s: str) -> date | None:
|
||||||
|
"""Převede DDMMYYYY na date. Rok 3000 → 9999-12-31."""
|
||||||
|
s = s.strip()
|
||||||
|
if len(s) != 8:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
d, m, y = int(s[0:2]), int(s[2:4]), int(s[4:8])
|
||||||
|
if y >= 3000:
|
||||||
|
return date(9999, 12, 31)
|
||||||
|
return date(y, m, d)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_latest_file() -> str:
|
||||||
|
files = glob.glob(os.path.join(IMPORT_DIR, "*.Lh7"))
|
||||||
|
if not files:
|
||||||
|
raise FileNotFoundError(f"Žádný *.Lh7 soubor nenalezen v {IMPORT_DIR}")
|
||||||
|
return max(files, key=os.path.getmtime)
|
||||||
|
|
||||||
|
|
||||||
|
def import_file(filepath: str, conn: mysql.connector.MySQLConnection) -> int:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("DROP TABLE IF EXISTS vzp_pracoviste")
|
||||||
|
cursor.execute(CREATE_TABLE_SQL)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO vzp_pracoviste
|
||||||
|
(ico, icz, icp, odbornost, platnost_od, platnost_do,
|
||||||
|
nazev_zarizeni, nazev_pracoviste, ulice, mesto, psc)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
|
||||||
|
batch = []
|
||||||
|
total = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
with open(filepath, encoding="cp1250", errors="replace", newline="") as f:
|
||||||
|
reader = csv.reader(f, quotechar='"', skipinitialspace=True)
|
||||||
|
for row in reader:
|
||||||
|
if len(row) < 10:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
platnost_od = parse_date(row[4])
|
||||||
|
platnost_do = parse_date(row[5])
|
||||||
|
if platnost_od is None or platnost_do is None:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ulice = název ulice + číslo popisné + číslo orientační (sloupce 11,12,13)
|
||||||
|
ulice_parts = [row[11].strip(), row[12].strip(), row[13].strip()] if len(row) > 13 else []
|
||||||
|
ulice = " ".join(p for p in ulice_parts if p) or row[8].strip()
|
||||||
|
|
||||||
|
psc = row[14].strip() if len(row) > 14 else ""
|
||||||
|
if len(psc) > 5:
|
||||||
|
psc = psc[:5]
|
||||||
|
|
||||||
|
batch.append((
|
||||||
|
row[0].strip(), # ico
|
||||||
|
row[1].strip(), # icz
|
||||||
|
row[2].strip(), # icp
|
||||||
|
row[3].strip(), # odbornost
|
||||||
|
platnost_od,
|
||||||
|
platnost_do,
|
||||||
|
row[6].strip()[:200] if len(row) > 6 else "", # nazev_zarizeni
|
||||||
|
row[7].strip()[:200] if len(row) > 7 else "", # nazev_pracoviste
|
||||||
|
ulice[:150],
|
||||||
|
row[9].strip()[:100] if len(row) > 9 else "", # mesto
|
||||||
|
psc,
|
||||||
|
))
|
||||||
|
|
||||||
|
if len(batch) >= BATCH_SIZE:
|
||||||
|
cursor.executemany(insert_sql, batch)
|
||||||
|
conn.commit()
|
||||||
|
total += len(batch)
|
||||||
|
batch.clear()
|
||||||
|
|
||||||
|
if batch:
|
||||||
|
cursor.executemany(insert_sql, batch)
|
||||||
|
conn.commit()
|
||||||
|
total += len(batch)
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
return total, skipped
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
filepath = sys.argv[1] if len(sys.argv) > 1 else find_latest_file()
|
||||||
|
filename = os.path.basename(filepath)
|
||||||
|
|
||||||
|
print(f"Soubor: {filename}")
|
||||||
|
print(f"Databáze: {DB_CONFIG['host']}/{DB_CONFIG['database']}")
|
||||||
|
print(f"Začátek: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
conn = mysql.connector.connect(**DB_CONFIG)
|
||||||
|
try:
|
||||||
|
total, skipped = import_file(filepath, conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"Importováno: {total} záznamů")
|
||||||
|
if skipped:
|
||||||
|
print(f"Přeskočeno: {skipped} řádků (neúplná data)")
|
||||||
|
print(f"Hotovo: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,267 @@
|
|||||||
|
"""
|
||||||
|
Jednoduchý HTTP server — generuje HTML report léků z MySQL a servíruje ho na portu 8765.
|
||||||
|
"""
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
import pymysql, pymysql.cursors
|
||||||
|
|
||||||
|
DB = dict(host="192.168.1.76", user="root", password="Vlado9674+",
|
||||||
|
database="medicus", charset="utf8mb4",
|
||||||
|
cursorclass=pymysql.cursors.DictCursor)
|
||||||
|
|
||||||
|
ATC_POPIS = {
|
||||||
|
"N06AX": "Jiná antidepresiva",
|
||||||
|
"M04AA": "Léky proti dně (urikostatika)",
|
||||||
|
"C10BA": "Statiny v kombinaci",
|
||||||
|
"C10AA": "Statiny",
|
||||||
|
"N05BA": "Benzodiazepinová anxiolytika",
|
||||||
|
"R01BA": "Systémová dekongestiva",
|
||||||
|
"A02BC": "Inhibitory protonové pumpy",
|
||||||
|
"G04BE": "Léky na erektilní dysfunkci",
|
||||||
|
"C10AX": "Jiná hypolipidemika (ezetimib)",
|
||||||
|
"C09AA": "ACE inhibitory",
|
||||||
|
"N05BX": "Jiná anxiolytika",
|
||||||
|
"N02AJ": "Opioidy + neopioidy",
|
||||||
|
"R01AD": "Nosní kortikosteroidy",
|
||||||
|
"R06AX": "Antihistaminika",
|
||||||
|
"J01FA": "Makrolidová antibiotika",
|
||||||
|
"M03BX": "Centrální myorelaxancia",
|
||||||
|
"A10BX": "Inkretiny (GLP-1 agonisté)",
|
||||||
|
"R05CB": "Mukolytika",
|
||||||
|
"N05CF": "Z-hypnotika",
|
||||||
|
"J01CE": "Penicilinová antibiotika",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Klíčová slova v navodu naznačující PRN (dle potřeby)
|
||||||
|
PRN_SLOVA = ("dle potřeby", "dle potreby", "p.p.", "d.p.", "pp ", " pp",
|
||||||
|
"při bolesti", "pri bolesti", "dle potreb", "podle potřeby",
|
||||||
|
"podle potreby", "podle potreby")
|
||||||
|
|
||||||
|
|
||||||
|
def query():
|
||||||
|
conn = pymysql.connect(**DB)
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
LEFT(p.atc, 5) AS atc_skupina,
|
||||||
|
MIN(p.nazev) AS nazev_leku,
|
||||||
|
COUNT(DISTINCT p.id) AS pocet_predpisu,
|
||||||
|
COUNT(DISTINCT v.id) AS pocet_vydani,
|
||||||
|
SUM(p.mnozstvi) AS celkove_mnozstvi,
|
||||||
|
AVG(p.mnozstvi) AS avg_mnozstvi,
|
||||||
|
MIN(p.datum_vystaveni) AS prvni_predpis,
|
||||||
|
MAX(p.datum_vystaveni) AS posledni_predpis,
|
||||||
|
DATEDIFF(MAX(p.datum_vystaveni),
|
||||||
|
MIN(p.datum_vystaveni)) AS rozpeti_dni,
|
||||||
|
MAX(p.opakovani) AS opakovani,
|
||||||
|
GROUP_CONCAT(p.navod ORDER BY p.datum_vystaveni SEPARATOR '|') AS navody,
|
||||||
|
GROUP_CONCAT(DISTINCT vp.odbornost
|
||||||
|
ORDER BY vp.odbornost) AS odbornosti,
|
||||||
|
GROUP_CONCAT(DISTINCT vp.nazev_pracoviste
|
||||||
|
ORDER BY vp.odbornost) AS pracoviste
|
||||||
|
FROM predpis p
|
||||||
|
LEFT JOIN vydej v
|
||||||
|
ON v.id_lp_predpis = p.id_lp_predpis
|
||||||
|
LEFT JOIN predepisujici pre
|
||||||
|
ON pre.lekar_kod = p.kod_predepisujiciho
|
||||||
|
LEFT JOIN vzp_pracoviste vp
|
||||||
|
ON vp.icp = pre.icp
|
||||||
|
AND vp.id = (
|
||||||
|
SELECT id FROM vzp_pracoviste
|
||||||
|
WHERE icp = pre.icp
|
||||||
|
ORDER BY platnost_od DESC LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE p.atc IS NOT NULL
|
||||||
|
GROUP BY LEFT(p.atc, 5)
|
||||||
|
HAVING pocet_predpisu >= 2 OR MAX(p.opakovani) IS NOT NULL
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Vypočítej pravidelnost v Pythonu a seřaď
|
||||||
|
for r in rows:
|
||||||
|
r["pravidelnost"], r["norm_interval"] = vypocitej_pravidelnost(r)
|
||||||
|
|
||||||
|
rows.sort(key=lambda r: (
|
||||||
|
["pravidelna", "mozna", "nepravidelna", "prn"].index(r["pravidelnost"]),
|
||||||
|
-(r["pocet_vydani"] or 0)
|
||||||
|
))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def vypocitej_pravidelnost(r):
|
||||||
|
"""Vrací (kategorie, normalizovany_interval_dni)."""
|
||||||
|
pocet = r["pocet_predpisu"] or 1
|
||||||
|
rozpeti = r["rozpeti_dni"] or 0
|
||||||
|
avg_mnoz = float(r["avg_mnozstvi"] or 1)
|
||||||
|
|
||||||
|
# PRN detekce — pokud většina návodů obsahuje PRN klíčová slova
|
||||||
|
navody = (r["navody"] or "").lower()
|
||||||
|
navod_list = navody.split("|")
|
||||||
|
prn_pocet = sum(1 for n in navod_list if any(s in n for s in PRN_SLOVA))
|
||||||
|
if prn_pocet > len(navod_list) / 2:
|
||||||
|
return "prn", None
|
||||||
|
|
||||||
|
if pocet < 2 or rozpeti == 0:
|
||||||
|
return "nepravidelna", None
|
||||||
|
|
||||||
|
avg_interval = rozpeti / (pocet - 1) # dny mezi předpisy
|
||||||
|
norm_interval = avg_interval / avg_mnoz # normalizováno na 1 balení
|
||||||
|
|
||||||
|
if norm_interval <= 40:
|
||||||
|
return "pravidelna", round(norm_interval)
|
||||||
|
elif norm_interval <= 100:
|
||||||
|
return "pravidelna", round(norm_interval)
|
||||||
|
elif norm_interval <= 185:
|
||||||
|
return "mozna", round(norm_interval)
|
||||||
|
else:
|
||||||
|
return "nepravidelna", round(norm_interval)
|
||||||
|
|
||||||
|
|
||||||
|
def badge_pravidelnost(pravidelnost, norm_interval):
|
||||||
|
if pravidelnost == "prn":
|
||||||
|
return '<span class="prav prav-prn">dle potřeby</span>'
|
||||||
|
if norm_interval is None:
|
||||||
|
return '<span class="prav prav-ne">?</span>'
|
||||||
|
if pravidelnost == "pravidelna":
|
||||||
|
return f'<span class="prav prav-ano">pravidelný · ~{norm_interval} dní/bal.</span>'
|
||||||
|
if pravidelnost == "mozna":
|
||||||
|
return f'<span class="prav prav-mozna">možná · ~{norm_interval} dní/bal.</span>'
|
||||||
|
return f'<span class="prav prav-ne">epizodický · ~{norm_interval} dní/bal.</span>'
|
||||||
|
|
||||||
|
|
||||||
|
def badge_odbornost(odbornosti, pracoviste):
|
||||||
|
if not odbornosti:
|
||||||
|
return '<span class="badge unknown">neznámá</span>'
|
||||||
|
kody = odbornosti.split(",")
|
||||||
|
nazvy = pracoviste.split(",") if pracoviste else []
|
||||||
|
parts = []
|
||||||
|
for i, kod in enumerate(kody):
|
||||||
|
nazev = nazvy[i].strip() if i < len(nazvy) else kod
|
||||||
|
cls = "gp" if kod.strip() in ("001", "002") else "spec"
|
||||||
|
parts.append(f'<span class="badge {cls}" title="{nazev}">{kod.strip()}</span>')
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def bar(val, max_val, color):
|
||||||
|
pct = min(int(val / max_val * 100), 100)
|
||||||
|
return (f'<div class="bar-wrap">'
|
||||||
|
f'<div class="bar" style="width:{pct}%;background:{color}"></div>'
|
||||||
|
f'<span>{val}</span></div>')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_html(rows):
|
||||||
|
max_vydani = max((r["pocet_vydani"] or 0 for r in rows), default=1)
|
||||||
|
|
||||||
|
radky = []
|
||||||
|
for r in rows:
|
||||||
|
atc = r["atc_skupina"] or ""
|
||||||
|
popis = ATC_POPIS.get(atc, "")
|
||||||
|
pr = r["pravidelnost"]
|
||||||
|
row_cls = {"pravidelna": "row-ano", "mozna": "row-mozna",
|
||||||
|
"prn": "row-prn", "nepravidelna": "row-ne"}.get(pr, "")
|
||||||
|
|
||||||
|
radky.append(f"""
|
||||||
|
<tr class="{row_cls}">
|
||||||
|
<td><strong>{atc}</strong><br><small>{popis}</small></td>
|
||||||
|
<td>{r['nazev_leku'] or ''}</td>
|
||||||
|
<td>{badge_pravidelnost(r['pravidelnost'], r['norm_interval'])}</td>
|
||||||
|
<td>{bar(r['pocet_predpisu'], max_vydani, '#4f8ef7')}</td>
|
||||||
|
<td>{bar(r['pocet_vydani'], max_vydani, '#34c97a')}</td>
|
||||||
|
<td style="font-size:0.82em;white-space:nowrap">
|
||||||
|
{r['prvni_predpis']}<br>→ {r['posledni_predpis']}
|
||||||
|
</td>
|
||||||
|
<td>{badge_odbornost(r['odbornosti'], r['pracoviste'])}</td>
|
||||||
|
</tr>""")
|
||||||
|
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html lang="cs">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Lékový záznam — pravidelná medikace</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
||||||
|
body {{ font-family: system-ui, sans-serif; background: #f4f6fb; color: #222; padding: 24px; }}
|
||||||
|
h1 {{ font-size: 1.4rem; margin-bottom: 4px; color: #1a2a4a; }}
|
||||||
|
.subtitle {{ color: #666; font-size: 0.9rem; margin-bottom: 20px; }}
|
||||||
|
table {{ width: 100%; border-collapse: collapse; background: #fff;
|
||||||
|
border-radius: 10px; overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08); }}
|
||||||
|
th {{ background: #1a2a4a; color: #fff; padding: 10px 12px;
|
||||||
|
text-align: left; font-size: 0.82rem; white-space: nowrap; }}
|
||||||
|
td {{ padding: 9px 12px; font-size: 0.88rem; border-bottom: 1px solid #eef0f5;
|
||||||
|
vertical-align: middle; }}
|
||||||
|
tr:last-child td {{ border-bottom: none; }}
|
||||||
|
tr.row-ano td {{ background: #f0fdf4; }}
|
||||||
|
tr.row-mozna td {{ background: #fffbeb; }}
|
||||||
|
tr.row-ne td {{ background: #fafafa; }}
|
||||||
|
tr.row-prn td {{ background: #fef2f2; }}
|
||||||
|
tr:hover td {{ filter: brightness(0.96); }}
|
||||||
|
.bar-wrap {{ display: flex; align-items: center; gap: 6px; }}
|
||||||
|
.bar {{ height: 8px; border-radius: 4px; min-width: 2px; }}
|
||||||
|
.bar-wrap span {{ font-size: 0.82rem; color: #444; min-width: 20px; }}
|
||||||
|
.badge {{ display: inline-block; padding: 2px 7px; border-radius: 10px;
|
||||||
|
font-size: 0.75rem; font-weight: 600; cursor: default; }}
|
||||||
|
.badge.gp {{ background: #d1fae5; color: #065f46; }}
|
||||||
|
.badge.spec {{ background: #dbeafe; color: #1e40af; }}
|
||||||
|
.badge.unknown {{ background: #f3f4f6; color: #9ca3af; }}
|
||||||
|
.prav {{ display: inline-block; padding: 3px 9px; border-radius: 12px;
|
||||||
|
font-size: 0.78rem; font-weight: 600; white-space: nowrap; }}
|
||||||
|
.prav-ano {{ background: #dcfce7; color: #166534; }}
|
||||||
|
.prav-mozna {{ background: #fef9c3; color: #854d0e; }}
|
||||||
|
.prav-ne {{ background: #f3f4f6; color: #6b7280; }}
|
||||||
|
.prav-prn {{ background: #fee2e2; color: #991b1b; }}
|
||||||
|
.legend {{ margin-top: 16px; font-size: 0.8rem; color: #555;
|
||||||
|
display: flex; gap: 20px; flex-wrap: wrap; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Lékový záznam — pravidelná medikace</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
Skupiny ATC (5. místo) · seřazeno dle pravidelnosti ·
|
||||||
|
normalizovaný interval = průměrný počet dní na 1 balení
|
||||||
|
</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ATC skupina</th>
|
||||||
|
<th>Lék (příklad)</th>
|
||||||
|
<th>Pravidelnost</th>
|
||||||
|
<th>Předpisů</th>
|
||||||
|
<th>Vydání</th>
|
||||||
|
<th>Rozsah</th>
|
||||||
|
<th>Odbornost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{''.join(radky)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="legend">
|
||||||
|
<span><span class="prav prav-ano">pravidelný</span> norm. interval ≤ 100 dní/bal.</span>
|
||||||
|
<span><span class="prav prav-mozna">možná</span> 100–185 dní/bal.</span>
|
||||||
|
<span><span class="prav prav-ne">epizodický</span> >185 dní/bal.</span>
|
||||||
|
<span><span class="prav prav-prn">dle potřeby</span> navod obsahuje PRN</span>
|
||||||
|
<span><span class="badge gp">001</span> Praktický lékař
|
||||||
|
<span class="badge spec">xxx</span> Specialista</span>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
html = generate_html(query()).encode("utf-8")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", len(html))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(html)
|
||||||
|
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = HTTPServer(("localhost", 8765), Handler)
|
||||||
|
print("Report server bezi na http://localhost:8765")
|
||||||
|
server.serve_forever()
|
||||||
Reference in New Issue
Block a user