From 3141875629c721751d28585d7d08ee7cd240857c Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Tue, 31 Mar 2026 07:47:17 +0200 Subject: [PATCH 1/4] notebook vb --- MedicusWithClaudePN/PN.md | 321 +++++++++++++++++++++++++++++++ MedicusWithClaudePN/pn_report.py | 320 ++++++++++++++++++++++++++++++ MedicusWithClaudePN/test_pn.py | 28 +++ 3 files changed, 669 insertions(+) create mode 100644 MedicusWithClaudePN/PN.md create mode 100644 MedicusWithClaudePN/pn_report.py create mode 100644 MedicusWithClaudePN/test_pn.py diff --git a/MedicusWithClaudePN/PN.md b/MedicusWithClaudePN/PN.md new file mode 100644 index 0000000..2b75ba2 --- /dev/null +++ b/MedicusWithClaudePN/PN.md @@ -0,0 +1,321 @@ +# MedicusWithClaudePN – Pracovní neschopnosti + +## Účel + +Report aktivních pracovních neschopností pro MUDr. Buzalkovou Michaelu. +Generuje PDF a odesílá na výchozí tiskárnu. + +## Spuštění + +```bash +# Test – otevře PDF v prohlížeči, netiskne: +python pn_report.py --no-print + +# Ostrý provoz – vytiskne rovnou na výchozí tiskárnu: +python pn_report.py +``` + +## Požadavky + +```bash +pip install reportlab pywin32 +``` + +## SQL dotaz – aktivní PN + +Zachycen přes Firebird trace přímo z Medicusu (přesná kopie logiky aplikace), +doplněn o podotaz na poslední 14denní potvrzení z tabulky HPN. + +```sql +SELECT + nes.id, + nes.idpac, + TRIM(kar.prijmeni) || ', ' || TRIM(kar.jmeno) AS jmeno, + kar.rodcis, + nes.zacnes, + nes.konnes, + nes.diagno, + COALESCE(nes.ecn, nes.cisnes) AS cisnes, + (SELECT MAX(h.datum) FROM hpn h + WHERE h.idnes = nes.id AND h.typ = '2' AND h.storno = 'F') AS posl_potvrzeni +FROM nes, kar +WHERE nes.zacnes <= current_date + AND nes.konnes IS NULL + AND nes.idpac = kar.idpac + AND nes.pracne = 'A' + AND nes.storno <> 'T' + AND ( + NOT EXISTS (SELECT id FROM nesd WHERE nesd.idnes = nes.id) + OR (SELECT FIRST 1 kam FROM nesd WHERE nesd.idnes = nes.id + ORDER BY nesd.datum DESC, nesd.id DESC) = 'N' + ) +ORDER BY kar.prijmeni ASC, kar.jmeno ASC +``` + +### Klíčové podmínky + +| Podmínka | Význam | +|---|---| +| `pracne = 'A'` | Pouze pracovní neschopnosti (ne jiné typy) | +| `storno <> 'T'` | Vyřazení stornovaných záznamů | +| `zacnes <= current_date` | PN již začala | +| `konnes IS NULL` | PN dosud neskončila – datum konce nemůže být v budoucnosti (pravidlo ČSSZ), aktivní PN má vždy `konnes = NULL` | +| `nesd` subquery | PN nebyla předána dál – poslední záznam `Kam = 'N'` = stále u pacienta | +| `COALESCE(ecn, cisnes)` | Použije ECN (elektronické), jinak starší CISNES | + +## Sloupce reportu + +| Sloupec | Zdroj | Poznámka | +|---|---|---| +| # | – | Pořadové číslo | +| Příjmení a jméno | KAR | | +| Rod. číslo | KAR | | +| Začátek PN | NES.ZACNES | | +| Dnů | výpočet | Počet dní od začátku PN do dnes | +| Diagnóza | NES.DIAGNO | | +| Posl. potvrzení | HPN (TYP='2') | Datum posledního 14denního potvrzení | +| Dní od potvr. | výpočet | Červeně pokud > 14 dní | + +## Tabulka HPN – typy podání + +| TYP | Význam | +|---|---| +| `H` | Hlášení neschopnosti (vznik PN) | +| `1` | První zpráva | +| `P` | **Průběžná zpráva = 14denní potvrzení trvání PN** | +| `2` | Neznámý typ (2033 záznamů v DB, ale ne pro průběžná potvrzení) | +| `C`, `Y`, `Z` | Vzácné typy (jednotky záznamů) | + +Vazba: `HPN.IDNES → NES.ID` + +--- + +## Jak Medicus zobrazuje PN daného pacienta (zachyceno z trace) + +### 1. Seznam všech PN pacienta + +```sql +SELECT + ID, IDPAC, DATNES, CISNES, PODNIK, ADRESA, PROFES, ZACNES, + KONNES, PRACNE, PRICINA, DIAGNO, KONDIA, PREDAN, IDUZI, + VYSTAVIL, DATUKONNES, IDODD, IDPRAC, STORNO, + DATVYCHOD, DATVYCHDO, VYCH1OD, VYCH1DO, VYCH2OD, VYCH2DO, VYCH3OD, VYCH3DO, + DATOSETRENI, DATPRINAV, OMLUVENKA, RODCISNES, + DatNastUstPece, DatUkonUstPece, ICPE, ECN, EPODANI, + ADR_OBEC, ADR_CP, ADR_CO, ADR_DOD, ADR_PSC, ADR_STAT, + ZAM_ADRESA, DATUKON_OSSZ, ZAMDRUH, STATDPNKOD, + SOUHLAS_SSZKOD, SOUHLAS_SSZNAZ, SOUHLAS_DATUM, + DGZMENA, CIZI, OSSZ, DUVOD_UKONCENI, UKON_OSSZ, PORUS_REZIMU, + UKON_OSSZNAZ, ADR_ZMENA, coalesce(ECN, CISNES) as CISLO, + ADR_ZMENA_DO, POTVRZENI_VYDANO, DATNAR, SPRAVCE_POJ, + ZAM_OBEC, ZAM_CO, ZAM_CP, ZAM_DOD, ZAM_PSC, ZAM_STAT, ZAM_CCSZ_ID, ZAM_CSSZ_VARSYM, + VYCHINDIVIDUAL, VERZE_DPN, LEKAR_VYSTAVIL, LEKAR_VYSTAVIL_ICPE, + USEDATNAR, KONTAKT_TEL, KONTAKT_EMAIL, + case when KONTAKT_TEL is not NULL then 'S' + when KONTAKT_EMAIL is not null then 'E' + else NULL end as NOTIFIKACE, + coalesce(KONTAKT_TEL, KONTAKT_EMAIL) as NOTIFIKACE_KONTAKT +FROM NES +WHERE IDPAC = ? +ORDER BY DATNES ASC, ID ASC +``` + +### 2. Formuláře eNeschopenky pro vybranou PN (záložka "Formuláře eNeschopenky") + +Zobrazuje pouze TYP `H`, `1`, `2` (ne `P` = propuštění). +Pouze záznamy s vazbou na HISTDOC (`IDHISTDOC IS NOT NULL`). + +```sql +SELECT + HD.ID AS HISTDOCID, + H.TYP AS HISTDOCTYP, -- H=hlášení, 1=první zpráva, 2=průběžná/potvrzení + HD.DATUM AS DATZAD, -- Datum vystavení (z HISTDOC) + H.DATUM AS DATPOD, -- Datum podání (z HPN) + H.STAV, + H.ODBAVENO, + HD.TYP +FROM HPN H +JOIN HISTDOC HD ON H.IDHISTDOC = HD.ID +WHERE H.IDNES = ? +ORDER BY HD.DATUM ASC +``` + +### 3. Rychlý přehled formulářů (bez HISTDOC) + +Používá se pro zjištění stavu – vrací všechny záznamy TYP `H`, `1`, `2`: + +```sql +SELECT * FROM HPN +WHERE IDNES = ? + AND STORNO = 'F' + AND TYP IN ('1', '2', 'H') +ORDER BY DATUM DESC, CAS DESC, ID DESC +``` + +### 4. TFHpnHistorie – formulář "Historie HPN" (acHpnHistorie) + +Kompletní přehled všech HPN záznamů pro vybranou PN. Spouští se akcí `acHpnHistorie`. + +```sql +select h.ID, h.IDNES, h.IDPODANI, h.TYP, h.DATA, h.DATUM, h.CAS, h.ODPOVED, + h.IDPRAC, h.IDUZI, h.UPRAVENO, h.OPRAVA_ID, h.STAV, h.STORNO, + h.POR_CISLO, h.ID_CHYBY, h.OSSZ, + n.CisNes, n.Ecn, coalesce(n.CisNes, n.Ecn) as Cislo, n.IdPac, + k.Prijmeni, k.Jmeno, k.Titul, coalesce(n.RODCISNES, k.RODCIS) as RODCIS, + u.Zkratka, hp.Odeslano, hp.CorelationId, + (select first 1 h2.ID from HPN h2 where h2.OPRAVA_ID = h.ID) as IdOpravy, + h.verze_dpn, + n.SPRAVCE_POJ, + n.ADRESA as ULICE, n.ADR_CP, n.ADR_CO, n.ADR_DOD, n.ADR_OBEC, n.ADR_PSC, n.ADR_STAT, + n.PODNIK, n.PROFES, n.ZAM_ADRESA, n.ZAM_CP, n.ZAM_CO, n.ZAM_OBEC, n.ZAM_PSC, n.ZAM_STAT, + n.ZACNES, n.DIAGNO, n.DATNES, n.PRICINA, + n.KONNES, n.KONDIA, n.DATUKONNES, + n.DATVYCHOD, n.VYCH1OD, n.VYCH1DO, n.VYCH2OD, n.VYCH2DO, + n.PRICINA, n.ICPE, n.VYCH3OD, n.VYCH3DO, n.KONTAKT_TEL, + h.IDHISTDOC, h.ODBAVENO, + IIF((H.IDHISTDOC is not null), + (select HD.TYP from HISTDOC HD where HD.ID = H.IDHISTDOC), null) as HISTDOCTYP +from HPN h + left join NES n on (n.id = h.idnes) + left join KAR k on (k.IdPac = n.IdPac) + left join UZIVATEL u on (u.IdUzi = h.IdUzi) + left join HPN_PODANI hp on (hp.ID = h.IdPodani) +where h.IdNes = ? +ORDER BY h.Datum ASC, h.Cas ASC, h.Id ASC +``` + +### 5. Kontrola čekajících podání (PN s neodeslanými HPN záznamy) + +```sql +select nes.id from nes +where nes.idpac = ? + and nes.storno = 'F' + and nes.epodani = 'T' + and nes.icpe = ? + and coalesce(nes.verze_dpn, '') not in ('', 'p', 'o') + and exists ( + select 1 from hpn + where hpn.idnes = nes.id + and hpn.storno = 'F' + and hpn.typ in ('1', '2', 'H') + and hpn.idpodani is null -- dosud neodesláno + and hpn.stav <> 99 + and hpn.stav <> 10 + ) +``` + +### 6. Kontrola potvrzení vydaného tento měsíc (POTVRZENI_VYDANO) + +Medicus kontroluje zda pro daného pacienta existuje PN aktivní alespoň 10 dní, +u které ještě nebylo vydáno potvrzení v aktuálním měsíci: + +```sql +select first 1 ZACNES, CISNES, ID, POTVRZENI_VYDANO +from NES +where (IDPAC = ?) + and (? >= ZACNES + 10) -- PN trvá alespoň 10 dní + and ((KONNES is NULL) or (KONNES > ?)) + and (STORNO = 'F') + and ( + extract(month from POTVRZENI_VYDANO) || extract(year from POTVRZENI_VYDANO) + = + extract(month from cast(? as date)) || extract(year from cast(? as date)) + ) +``` + +### 7. Vyhledání HPN záznamu podle ICPE v XML datech + +Číslo `11031812` (ICPE lékaře) se hledá přímo v obsahu XML blobu `HPN.DATA`. +Medicus takto identifikuje konkrétní HPN záznam při opravě nebo ověření stavu: + +```sql +-- Neodeslané nebo čekající záznamy: +select h.id from HPN h +left join HPN h2 on (h2.OPRAVA_ID = h.ID) +where (h.storno = 'F') + and ((h.stav in (0,1)) or (h.stav is NULL)) + and (h2.OPRAVA_ID is null) + and (H.DATA containing '11031812') -- hledá ICPE v XML obsahu + and (h.IDNES = ?) + +-- Úspěšně odeslané záznamy (stav=1): +select h.id from HPN h +left join HPN h2 on (h2.OPRAVA_ID = h.ID) +where (h.storno = 'F') + and (h.stav = 1) + and h2.OPRAVA_ID is null + and (H.DATA containing '11031812') + and h.IDNES = ? +``` + +### 8. Předání/Převzetí (záložka "Předání/Převzetí") + +```sql +SELECT ID, IDNES, KAMODKUD, DATUM, KAM, ICZ, ICPE, ICO, JMENO_LEKARE +FROM NESD +WHERE IDNES = ? +ORDER BY DATUM ASC, ID ASC +``` + +### Poznámky + +- **HISTDOC** – každé odeslání formuláře vytváří záznam v HISTDOC; HPN bez IDHISTDOC se v UI nezobrazí +- **HPN.STAV** – stav podání (1 = odesláno/přijato) +- **HPN.ODBAVENO** – příznak zpracování (`'F'` = ne, `'T'` = ano) +- HPN záznamy TYP='2' (průběžná potvrzení) **nemají IDHISTDOC** – JOIN s HISTDOC by je odfiltroval. Pro datum posledního potvrzení v reportu proto používáme prostý MAX bez JOINu. HISTDOC mají pouze TYP='P' (ukončení PN). + +--- + +## Vazby tabulky NES (zjištěno z DB) + +### Formální FK constrainty + +| Směr | Vazba | Popis | +|---|---|---| +| NES → KAR | `NES.IDPAC → KAR.IDPAC` | Každá neschopenka patří pacientovi v kartotéce | +| HPN → NES | `HPN.IDNES → NES.ID` | Formuláře/hlášení HPN odkazují na konkrétní neschopenku | + +### Logické vazby (bez FK constraintu) + +| Tabulka | Pole | Poznámka | +|---|---|---| +| NESD | `NESD.IDNES → NES.ID` | Předání/převzetí PN – vazba jen kódem, ne constraintem | +| HISTDOC | `HPN.IDHISTDOC → HISTDOC.ID` | Dokumenty k formulářům – vazba přes HPN | + +### Indexy na NES + +| Index | Unique | Pole | +|---|---|---| +| PK_NES | Ano | ID | +| FK_NES_KAR | Ne | IDPAC | +| NES_POTVRZENI_VYDANO | Ne | POTVRZENI_VYDANO | + +### Poznámka + +Medicus obecně používá minimum DB constraintů – většina vazeb je řešena aplikačním kódem. +`NESD` a `HISTDOC` nemají formální FK na `NES`, přesto jsou klíčové pro zobrazení PN v UI. + +--- + +## Červené zvýraznění + +Sloupce "Posl. potvrzení" a "Dní od potvr." jsou červeně zvýrazněny pokud: +- Od posledního potvrzení uplynulo více než 14 dní, nebo +- PN nemá žádné potvrzení a trvá déle než 14 dní (zobrazí se s `(!)`) + +## Tisk + +Skript používá `win32api.ShellExecute` s příkazem `'print'` – odešle PDF +na výchozí tiskárnu Windows. + +## Automatizace + +Plánované spouštění každé pondělí a pátek ráno přes Windows Task Scheduler +– zatím nenastaveno, připravit až bude skript stabilní. + +## Soubory + +| Soubor | Obsah | +|---|---| +| `pn_report.py` | Hlavní skript – DB dotaz, generování PDF, tisk | +| `PN.md` | Tento soubor – dokumentace | diff --git a/MedicusWithClaudePN/pn_report.py b/MedicusWithClaudePN/pn_report.py new file mode 100644 index 0000000..9e95aa0 --- /dev/null +++ b/MedicusWithClaudePN/pn_report.py @@ -0,0 +1,320 @@ +""" +pn_report.py – Report aktivních pracovních neschopností +Generuje PDF a posílá na výchozí tiskárnu. + +Spuštění: + python pn_report.py # vytvoří PDF a vytiskne + python pn_report.py --no-print # jen vytvoří PDF (pro testování) + +Požadavky: + pip install reportlab pywin32 +""" + +import fdb +import sys +import os +import tempfile +from datetime import date, datetime + +from reportlab.lib.pagesizes import A4 +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + +# --------------------------------------------------------------------------- +# Konfigurace +# --------------------------------------------------------------------------- + +DB_DSN = r'localhost:c:\medicus 3\data\medicus.fdb' +DB_USER = 'SYSDBA' +DB_PASS = 'masterkey' +DB_CHARSET = 'win1250' + +# Pokud chcete soubor uložit trvale, nastavte výstupní adresář: +OUTPUT_DIR = None # None = dočasný soubor, smaže se po tisku +#OUTPUT_DIR = r'u:\Dropbox\!!!Days\Downloads Z230' + +# --------------------------------------------------------------------------- +# Dotaz – aktivní PN (konnes IS NULL nebo v budoucnosti, storno='F') +# --------------------------------------------------------------------------- + +SQL = """ + SELECT + nes.id AS idnes, + nes.idpac, + TRIM(kar.prijmeni) || ', ' || TRIM(kar.jmeno) AS jmeno, + kar.rodcis, + nes.zacnes, + nes.konnes, + nes.diagno, + COALESCE(nes.ecn, nes.cisnes) AS cisnes, + (SELECT MAX(h.datum) FROM hpn h + WHERE h.idnes = nes.id AND h.typ = 'P' AND h.storno = 'F') AS posl_potvrzeni + FROM nes, kar + WHERE nes.zacnes <= current_date + AND nes.konnes IS NULL + AND nes.idpac = kar.idpac + AND nes.pracne = 'A' + AND nes.storno <> 'T' + AND ( + NOT EXISTS (SELECT id FROM nesd WHERE nesd.idnes = nes.id) + OR (SELECT FIRST 1 kam FROM nesd WHERE nesd.idnes = nes.id + ORDER BY nesd.datum DESC, nesd.id DESC) = 'N' + ) + ORDER BY kar.prijmeni ASC, kar.jmeno ASC +""" + +# --------------------------------------------------------------------------- +# Pomocné funkce +# --------------------------------------------------------------------------- + +def fmt_date(val): + """Datum → DD.MM.YYYY nebo prázdný řetězec.""" + if val is None: + return '' + if isinstance(val, (date, datetime)): + return val.strftime('%d.%m.%Y') + return str(val) + +def fmt_str(val): + if val is None: + return '' + return str(val).strip() + +def delka_pn(zacnes, konnes): + """Počet dnů PN (od začátku do dnes / do konce).""" + if zacnes is None: + return '' + end = konnes if konnes else date.today() + if isinstance(zacnes, datetime): + zacnes = zacnes.date() + if isinstance(end, datetime): + end = end.date() + if isinstance(zacnes, date) and isinstance(end, date): + days = (end - zacnes).days + 1 + return str(days) + return '' + +# --------------------------------------------------------------------------- +# Načtení fontu s českou diakritikou +# --------------------------------------------------------------------------- + +def register_font(): + """ + Zkusí zaregistrovat DejaVuSans (umí win1250 znaky). + Fallback: Helvetica (bez diakritiky – nouzové řešení). + """ + font_paths = [ + r'C:\Windows\Fonts\DejaVuSans.ttf', + r'C:\Windows\Fonts\arial.ttf', + r'C:\Windows\Fonts\segoeui.ttf', + ] + for path in font_paths: + if os.path.exists(path): + name = os.path.splitext(os.path.basename(path))[0] + try: + pdfmetrics.registerFont(TTFont(name, path)) + pdfmetrics.registerFont(TTFont(name + '-Bold', + path.replace('.ttf', 'bd.ttf') if 'arial' in path.lower() + else path.replace('.ttf', '-Bold.ttf') + if os.path.exists(path.replace('.ttf', '-Bold.ttf')) + else path + )) + return name, name + '-Bold' + except Exception: + continue + return 'Helvetica', 'Helvetica-Bold' + +# --------------------------------------------------------------------------- +# Generování PDF +# --------------------------------------------------------------------------- + +def build_pdf(rows, output_path, font_name, font_bold): + today_str = date.today().strftime('%d.%m.%Y') + weekday_cs = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle'] + weekday = weekday_cs[date.today().weekday()] + + doc = SimpleDocTemplate( + output_path, + pagesize=A4, + topMargin=1.5*cm, + bottomMargin=1.5*cm, + leftMargin=1.5*cm, + rightMargin=1.5*cm, + ) + + styles = getSampleStyleSheet() + + def para(text, size=10, bold=False, align='LEFT', color=colors.black): + fn = font_bold if bold else font_name + al = {'LEFT': 0, 'CENTER': 1, 'RIGHT': 2}.get(align, 0) + from reportlab.platypus import Paragraph as P + from reportlab.lib.styles import ParagraphStyle + st = ParagraphStyle('x', fontName=fn, fontSize=size, + textColor=color, alignment=al, leading=size*1.3) + return P(text, st) + + story = [] + + # Záhlaví + story.append(para('MUDr. Buzalková Michaela – ordinace praktického lékaře', + size=9, color=colors.grey)) + story.append(para(f'Aktivní pracovní neschopnosti', + size=16, bold=True)) + story.append(para(f'Vytištěno: {weekday} {today_str} | Počet záznamů: {len(rows)}', + size=9, color=colors.grey)) + story.append(Spacer(1, 0.4*cm)) + + if not rows: + story.append(para('Žádné aktivní pracovní neschopnosti.', size=12)) + doc.build(story) + return + + # Záhlaví tabulky + headers = ['#', 'Příjmení a jméno', 'Rod. číslo', 'Začátek PN', + 'Dnů', 'Diagnóza', 'Posl. potvrzení', 'Dní od potvr.'] + + col_widths = [0.7*cm, 5.5*cm, 2.8*cm, 2.4*cm, 1.4*cm, 2.2*cm, 3.0*cm, 2.2*cm] + + table_data = [headers] + overdue_rows = [] # indexy řádků kde je potvrzení po splatnosti + + for idx, row in enumerate(rows, start=1): + idnes, idpac, jmeno, rodcis, zacnes, konnes, diagno, cisnes, posl_potvrzeni = row + + # Počet dní od posledního potvrzení + if posl_potvrzeni is not None: + pp = posl_potvrzeni.date() if isinstance(posl_potvrzeni, datetime) else posl_potvrzeni + dni_od = (date.today() - pp).days + dni_od_str = str(dni_od) + if dni_od > 14: + overdue_rows.append(idx + 1) # +1 kvůli záhlaví + else: + # Žádné potvrzení – počítáme od začátku PN + zac = zacnes.date() if isinstance(zacnes, datetime) else zacnes + if zac: + dni_od = (date.today() - zac).days + dni_od_str = str(dni_od) + ' (!)' + if dni_od > 14: + overdue_rows.append(idx + 1) + else: + dni_od_str = '—' + + table_data.append([ + str(idx), + fmt_str(jmeno), + fmt_str(rodcis), + fmt_date(zacnes), + delka_pn(zacnes, konnes), + fmt_str(diagno), + fmt_date(posl_potvrzeni) if posl_potvrzeni else '—', + dni_od_str, + ]) + + tbl = Table(table_data, colWidths=col_widths, repeatRows=1) + + style = TableStyle([ + # Záhlaví + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2F5496')), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), + ('FONTNAME', (0, 0), (-1, 0), font_bold), + ('FONTSIZE', (0, 0), (-1, 0), 8), + ('ALIGN', (0, 0), (-1, 0), 'CENTER'), + ('BOTTOMPADDING',(0, 0), (-1, 0), 5), + ('TOPPADDING', (0, 0), (-1, 0), 5), + # Data + ('FONTNAME', (0, 1), (-1, -1), font_name), + ('FONTSIZE', (0, 1), (-1, -1), 8), + ('ALIGN', (0, 1), (0, -1), 'CENTER'), # # + ('ALIGN', (5, 1), (5, -1), 'RIGHT'), # Dnů + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), + ('TOPPADDING', (0, 1), (-1, -1), 3), + ('BOTTOMPADDING',(0, 1), (-1, -1), 3), + # Mřížka + ('GRID', (0, 0), (-1, -1), 0.3, colors.HexColor('#AAAAAA')), + ('LINEBELOW', (0, 0), (-1, 0), 1, colors.HexColor('#2F5496')), + # Zebra + *[('BACKGROUND', (0, i), (-1, i), colors.HexColor('#DCE6F1')) + for i in range(2, len(table_data), 2)], + # Červené zvýraznění – potvrzení po splatnosti (> 14 dní) + *[('BACKGROUND', (6, i), (7, i), colors.HexColor('#F4CCCC')) + for i in overdue_rows], + *[('TEXTCOLOR', (6, i), (7, i), colors.HexColor('#CC0000')) + for i in overdue_rows], + *[('FONTNAME', (6, i), (7, i), font_bold) + for i in overdue_rows], + ]) + tbl.setStyle(style) + story.append(tbl) + + # Patička + story.append(Spacer(1, 0.5*cm)) + story.append(para(f'--- konec reportu ({len(rows)} záznamů) ---', + size=8, color=colors.grey, align='CENTER')) + + doc.build(story) + +# --------------------------------------------------------------------------- +# Tisk +# --------------------------------------------------------------------------- + +def print_pdf(path): + """Pošle PDF na výchozí tiskárnu přes Windows ShellExecute.""" + try: + import win32api + win32api.ShellExecute(0, 'print', path, None, '.', 0) + print(f'Odesláno na tiskárnu: {path}') + except ImportError: + # Fallback – otevře soubor v PDF prohlížeči (ruční tisk) + print('pywin32 není nainstalován, otevírám PDF...') + os.startfile(path) + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + no_print = '--no-print' in sys.argv + + # Připojení k DB + print('Připojuji se k DB...') + conn = fdb.connect(dsn=DB_DSN, user=DB_USER, password=DB_PASS, charset=DB_CHARSET) + cur = conn.cursor() + + print('Načítám aktivní PN...') + cur.execute(SQL) + rows = cur.fetchall() + conn.close() + print(f'Nalezeno {len(rows)} aktivních PN.') + + # Font + font_name, font_bold = register_font() + print(f'Font: {font_name}') + + # Výstupní soubor + if OUTPUT_DIR: + os.makedirs(OUTPUT_DIR, exist_ok=True) + out_path = os.path.join(OUTPUT_DIR, + date.today().strftime('%Y-%m-%d') + '_pn_report.pdf') + else: + fd, out_path = tempfile.mkstemp(suffix='_pn_report.pdf') + os.close(fd) + + print(f'Generuji PDF: {out_path}') + build_pdf(rows, out_path, font_name, font_bold) + print('PDF hotovo.') + + if no_print: + print('(tisk přeskočen – --no-print)') + # Otevřeme pro náhled + os.startfile(out_path) + else: + print_pdf(out_path) + # Dočasný soubor necháme – tiskárna ho potřebuje přečíst + # Windows ho smaže sám po zpracování tisku (temp adresář) + +if __name__ == '__main__': + main() diff --git a/MedicusWithClaudePN/test_pn.py b/MedicusWithClaudePN/test_pn.py new file mode 100644 index 0000000..f4e6cfa --- /dev/null +++ b/MedicusWithClaudePN/test_pn.py @@ -0,0 +1,28 @@ +import fdb, sys +sys.stdout.reconfigure(encoding='utf-8') +conn = fdb.connect(dsn=r'localhost:c:\medicus 3\data\medicus.fdb', user='SYSDBA', password='masterkey', charset='win1250') +cur = conn.cursor() + +sql = """ + SELECT nes.id, TRIM(kar.prijmeni) || ', ' || TRIM(kar.jmeno) AS jmeno, + nes.zacnes, + (SELECT MAX(h.datum) FROM hpn h + WHERE h.idnes = nes.id AND h.typ = 'P' AND h.storno = 'F') AS posl_potvrzeni + FROM nes, kar + WHERE nes.zacnes <= current_date + AND nes.konnes IS NULL + AND nes.idpac = kar.idpac + AND nes.pracne = 'A' + AND nes.storno <> 'T' + AND ( + NOT EXISTS (SELECT id FROM nesd WHERE nesd.idnes = nes.id) + OR (SELECT FIRST 1 kam FROM nesd WHERE nesd.idnes = nes.id + ORDER BY nesd.datum DESC, nesd.id DESC) = 'N' + ) + ORDER BY kar.prijmeni ASC +""" + +cur.execute(sql) +for row in cur.fetchall(): + print(row) +conn.close() From 4950c00309d17d57b7b790df48edaaac298f37be Mon Sep 17 00:00:00 2001 From: "vladimir.buzalka" Date: Tue, 31 Mar 2026 14:08:53 +0200 Subject: [PATCH 2/4] z230 --- .../010 Můj skript | 379 ++++++++++++++++ .../CLAUDE_NOTES.md | 188 ++++++++ .../komplexni_report.py | 410 ++++++++++++++++++ MedicusWithClaudePosudek/CLAUDE_NOTES.md | 143 ++++++ MedicusWithClaudePosudek/posudky_report.py | 237 ++++++++++ 5 files changed, 1357 insertions(+) create mode 100644 MedicusWithClaudeKomplexniReport/010 Můj skript create mode 100644 MedicusWithClaudeKomplexniReport/CLAUDE_NOTES.md create mode 100644 MedicusWithClaudeKomplexniReport/komplexni_report.py create mode 100644 MedicusWithClaudePosudek/CLAUDE_NOTES.md create mode 100644 MedicusWithClaudePosudek/posudky_report.py diff --git a/MedicusWithClaudeKomplexniReport/010 Můj skript b/MedicusWithClaudeKomplexniReport/010 Můj skript new file mode 100644 index 0000000..e8dda84 --- /dev/null +++ b/MedicusWithClaudeKomplexniReport/010 Můj skript @@ -0,0 +1,379 @@ +import os +import fdb +import csv,time,pandas as pd +import openpyxl + + +PathToSaveCSV=r"z:\Dropbox\Ordinace\Reporty" +timestr = time.strftime("%Y-%m-%d %H-%M-%S ") +CSVname="Pacienti.xlsx" + +# ================= DELETE OLD REPORTS (KEEP TODAY) ================== +from datetime import datetime + +today = datetime.now().strftime("%Y-%m-%d") + +for fname in os.listdir(PathToSaveCSV): + if fname.endswith("Pacienti.xlsx"): + file_date = fname[:10] # first 10 chars = YYYY-MM-DD + if file_date != today: # delete only older files + try: + os.remove(os.path.join(PathToSaveCSV, fname)) + print(f"🗑️ Deleted old report: {fname}") + except Exception as e: + print(f"⚠️ Could not delete {fname}: {e}") + + +con = fdb.connect( + host='192.168.1.10', database=r'm:\MEDICUS\data\medicus.FDB', + user='sysdba', password='masterkey',charset='WIN1250') + +#Server=192.168.1.10 +#Path=M:\Medicus\Data\Medicus.fdb + +# Create a Cursor object that operates in the context of Connection con: +cur = con.cursor() + +# import openpyxl module +import openpyxl +import xlwings as xw +wb = openpyxl.Workbook() +sheet = wb.active +# wb.save("sample.xlsx") + + +#Načtení očkování registrovaných pacientů +cur.execute("select rodcis,prijmeni,jmeno,ockzaz.datum,kodmz,ockzaz.poznamka,latka,nazev,expire from registr join kar on registr.idpac=kar.idpac join ockzaz on registr.idpac=ockzaz.idpac where datum_zruseni is null and kar.vyrazen!='A' and kar.rodcis is not null and idicp!=0 order by ockzaz.datum desc") +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.title="Očkování" +sheet.append(["Rodne cislo","Prijmeni","Jmeno","Datum ockovani","Kod MZ","Sarze","Latka","Nazev","Expirace"]) +#nacteno jsou ockovani +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + + + + +#Načtení registrovaných pacientů +cur.execute("select rodcis,prijmeni,jmeno,datum_registrace,registr.idpac,poj from registr join kar on registr.idpac=kar.idpac where kar.vyrazen!='A' and kar.rodcis is not null and idicp!=0 and datum_zruseni is null") +nacteno=cur.fetchall() +print(len(nacteno)) + +wb.create_sheet('Registrovani',0) +sheet=wb['Registrovani'] + +sheet.append(["Rodne cislo","Prijmeni","Jmeno","Datum registrace","ID pacienta","Pojistovna"]) +#nacteno jsou registrovani +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + + +#Načtení receptů +cur.execute("""select +kar.rodcis, +TRIM(kar.prijmeni) ||' '|| substring(kar.jmeno from 1 for 1) ||'.' as jmeno, +recept.datum, +TRIM(recept.lek) ||' '|| trim(recept.dop) as lek, +recept.expori AS Poc, +CASE + WHEN recept.opakovani is null THEN 1 + ELSE recept.opakovani + END AS OP, +recept.uhrada, +recept.dsig, +recept.NOTIFIKACE_KONTAKT as notifikace, +recept_epodani.erp, +recept_epodani.vystavitel_jmeno, +recept.atc, +recept.CENAPOJ, +recept.cenapac +from recept LEFT Join RECEPT_EPODANI on recept.id_epodani=recept_epodani.id +LEFT join kar on recept.idpac=kar.idpac +order by datum desc,erp desc""" +) +nacteno=cur.fetchall() +print(len(nacteno)) + +wb.create_sheet('Recepty',0) +sheet=wb['Recepty'] + +sheet.title="Recepty" +sheet.append(["Rodné číslo","Jméno","Datum vystavení","Název leku","Poč.","Op.","Úhr.","Da signa","Notifikace","eRECEPT","Vystavil","ATC","Cena pojišťovna","Cena pacient"]) +#nacteno jsou ockovani +for row in nacteno: + try: + sheet.append(row) + except: + continue + +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Načtení vykony vsech +cur.execute("select dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,dokladd.pocvyk,dokladd.ddgn,dokladd.body,vykony.naz " + "from kar join dokladd on kar.rodcis=dokladd.rodcis join vykony on dokladd.kod=vykony.kod where (datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null) order by dokladd.datose desc,dokladd.rodcis") + +wb.create_sheet('Vykony',0) +sheet=wb['Vykony'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum vykonu","Kod","Pocet","Dg.","Body","Nazev"]) +#nacteno jsou ockovani +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Načtení neschopenek + +import datetime +def pocet_dni(zacnes,konnes,pracne): + dnes=datetime.date.today() + if pracne=='A': + return (dnes-zacnes).days + if pracne=='N' and zacnes is not None and konnes is not None and zacnes<=konnes: + return (konnes-zacnes).days + else: + return "NA" + +cur.execute("select nes.idpac, " + "kar.rodcis, " + "TRIM(prijmeni) ||', '|| TRIM(jmeno), " + "nes.datnes, " + "nes.ecn, " + "nes.zacnes, " + "nes.pracne, " + "nes.konnes, " + "nes.diagno, " + "nes.kondia, " + "nes.updated " + "from nes " + "left join kar on nes.idpac=kar.idpac where nes.datnes<=current_date " + "order by datnes desc") + + +tmpnacteno_vse=[] +nacteno_vse=cur.fetchall() + +cur.execute("select nes.idpac, " + "kar.rodcis, " + "TRIM(prijmeni) ||', '|| TRIM(jmeno), " + "nes.datnes, " + "nes.ecn, " + "nes.zacnes, " + "nes.pracne, " + "nes.konnes, " + "nes.diagno, " + "nes.kondia, " + "nes.updated " + "from nes " + "left join kar on nes.idpac=kar.idpac where nes.datnes<=current_date and pracne='A'" + "order by datnes desc") + +tmpnacteno_aktivni=[] +nacteno_aktivni=cur.fetchall() + +for row in nacteno_vse: + tmpnacteno_vse.append((row[0],row[1],row[2],row[3],row[4],row[5],row[6],row[7],pocet_dni(row[5],row[7],row[6]),row[8],row[9],row[10])) + +for row in nacteno_aktivni: + (tmpnacteno_aktivni.append((row[0],row[1],row[2],row[3],row[4],row[5],row[6],row[7],pocet_dni(row[5],row[7],row[6]),row[8],row[9],row[10]))) + +wb.create_sheet('Neschopenky všechny',0) +sheet=wb["Neschopenky všechny"] +sheet.append(["ID pac","Rodne cislo","Jmeno","Datum neschopenky","Číslo neschopenky","Zacatek","Aktivní?","Konec","Pocet dni","Diagnoza zacatel","Diagnoza konec","Aktualizovano"]) +for row in tmpnacteno_vse: + sheet.append(row) + +wb.create_sheet('Neschopenky aktivní',0) +sheet=wb["Neschopenky aktivní"] +sheet.append(["ID pac","Rodne cislo","Jmeno","Datum neschopenky","Číslo neschopenky","Zacatek","Aktivní?","Konec","Pocet dni","Diagnoza zacatel","Diagnoza konec","Aktualizovano"]) +for row in tmpnacteno_aktivni: + sheet.append(row) + +#Načtení preventivni prohlidky +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=1022 or dokladd.kod=1021) " +"order by datose desc") + +wb.create_sheet('Preventivni prohlidky',0) +sheet=wb['Preventivni prohlidky'] + + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Nacteni INR +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=01443) " +"order by datose desc") + +wb.create_sheet('INR',0) +sheet=wb['INR'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Nacteni CRP +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=02230 or dokladd.kod=09111) " +"order by datose desc,dokladd.rodcis,dokladd.kod") + +wb.create_sheet('CRP',0) +sheet=wb['CRP'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + + +#Nacteni Holter +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=17129) " +"order by datose desc,dokladd.rodcis,dokladd.kod") + +wb.create_sheet('Holter',0) +sheet=wb['Holter'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Nacteni prostata +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and (dokladd.kod=01130 or dokladd.kod=01131 or dokladd.kod=01132 or dokladd.kod=01133 or dokladd.kod=01134) " +"order by datose desc,dokladd.rodcis,dokladd.kod") + +wb.create_sheet('Prostata',0) +sheet=wb['Prostata'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Nacteni TOKS +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and " +"(dokladd.kod=15118 or dokladd.kod=15119 or dokladd.kod=15120 or dokladd.kod=15121) " +"order by datose desc,dokladd.rodcis,dokladd.kod") + +wb.create_sheet('TOKS',0) +sheet=wb['TOKS'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Nacteni COVID +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and " +"(dokladd.kod=01306) " +"order by datose desc,dokladd.rodcis,dokladd.kod") + +wb.create_sheet('COVID',0) +sheet=wb['COVID'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + +#Nacteni Streptest +cur.execute("select all dokladd.rodcis,TRIM(prijmeni) ||', '|| TRIM(jmeno),dokladd.datose,dokladd.kod,vykony.naz,dokladd.ddgn,dokladd.body " +"from dokladd left join kar on dokladd.rodcis=kar.rodcis join vykony on dokladd.kod=vykony.kod where " +"((datose>=vykony.platiod and datose<=vykony.platido) OR (datose>=vykony.platiod and vykony.platido is null)) and " +"(dokladd.kod=02220) " +"order by datose desc,dokladd.rodcis,dokladd.kod") + +wb.create_sheet('Streptest',0) +sheet=wb['Streptest'] + +nacteno=cur.fetchall() +print(len(nacteno)) + +sheet.append(["Rodne cislo","Jmeno","Datum","Kod","Název","Dg.","Body"]) + +for row in nacteno: + sheet.append(row) + + +# autofilter +for ws in wb.worksheets: + # Get the maximum number of rows and columns + max_row = ws.max_row + max_column = ws.max_column + ws.auto_filter.ref = f"A1:{openpyxl.utils.get_column_letter(max_column)}{max_row}" + # ws.auto_filter.ref = ws.dimensions + + + + + +wb.save(os.path.join(PathToSaveCSV ,timestr+CSVname)) + + +# Tento modul je pouze na autofit jednotlivych sloupcu na vsech listech workbooku +file = os.path.join(PathToSaveCSV ,timestr+CSVname) +with xw.App(visible=False) as app: + wb = xw.Book(file) + for sheet in range(len(wb.sheets)): + ws = wb.sheets[sheet] + ws.autofit() + + # centrování receptů + sheet = wb.sheets['Recepty'] + for sloupec in ["C:C", "E:E", "F:F", "G:G", "I:I", "M:M", "N:N"]: + sheet.range(sloupec).api.HorizontalAlignment = 3 # 3 = Center + + + wb.save() + wb.close() + + diff --git a/MedicusWithClaudeKomplexniReport/CLAUDE_NOTES.md b/MedicusWithClaudeKomplexniReport/CLAUDE_NOTES.md new file mode 100644 index 0000000..ef07410 --- /dev/null +++ b/MedicusWithClaudeKomplexniReport/CLAUDE_NOTES.md @@ -0,0 +1,188 @@ +# MedicusWithClaudeKomplexniReport – CLAUDE_NOTES + +## Co skript dělá + +`komplexni_report.py` generuje komplexní Excel přehled ordinace. +Soubor se ukládá do `u:\Dropbox\!!!Days\Downloads Z230\YYYY-MM-DD_HH-MM-SS_Pacienti.xlsx`. +Předchozí verze (`*Pacienti.xlsx`) se před zápisem automaticky smažou. + +## Spuštění + +``` +C:\Python\python.exe komplexni_report.py +``` + +Trvá cca **10 minut** (kvůli xlwings autofit přes celý Excel). +Spouští se automaticky v noci → nevadí. + +## Připojení k DB + +```python +fdb.connect( + host='localhost', + database=r'c:\MEDICUS 3\data\medicus.FDB', + user='sysdba', password='masterkey', charset='WIN1250' +) +``` + +## Závislosti + +``` +pip install fdb openpyxl xlwings extract-msg beautifulsoup4 python-dateutil +``` + +--- + +## Listy v Excelu (pořadí) + +| List | Zdroj | Popis | +|---|---|---| +| `Registrovani` | registr + kar | Aktivní registrovaní pacienti | +| `Očkování` | ockzaz + registr + kar | Záznamy o očkování registrovaných | +| `Recepty` | recept + recept_epodani + kar | Všechny recepty, eRECEPT čísla, ceny | +| `Vykony` | dokladd + kar + vykony | Všechny výkony (s platným číselníkem) | +| `Neschopenky všechny` | nes + kar | Všechny neschopenky | +| `Neschopenky aktivní` | nes + kar | Pouze aktivní (pracne='A') | +| `Preventivni prohlidky` | dokladd + vykony | Kódy 1021, 1022 | +| `INR` | dokladd + vykony | Kód 1443 | +| `CRP` | dokladd + vykony | Kódy 2230, 9111 | +| `Holter` | dokladd + vykony | Kód 17129 | +| `Prostata` | dokladd + vykony | Kódy 1130–1134 | +| `TOKS` | dokladd + vykony | Kódy 15118–15121 | +| `COVID` | dokladd + vykony | Kód 1306 | +| `Streptest` | dokladd + vykony | Kód 2220 | +| `Posudky řidičák` | HISTDOC (TYP=MOTORVO) + KAR | Ruční posudky k řízení MV | +| `ePosudky registr` | HISTDOC (TYP=EPOSMRO) + HISTDOC_EPOSUDEK + KAR | Elektronická podání do centrálního registru | + +--- + +## Pomocné funkce + +### `sanitize(val)` +Opraví znaky neplatné pro Excel: +- `µ` → `u` +- řídící znaky (ord < 32, kromě tab/LF/CR) → `_` +- náhradní znaky Unicode (0xFFFE, 0xFFFF, surrogáty) → `_` + +Použito ve všech listech kde hrozí problematická data z DB. + +### `fmt(val)` +Vrátí `''` pro None, jinak zavolá `sanitize()`. + +### `add_vykony_sheet(sheet_name, kody)` +Helper pro listy s výkony. Přijme název listu a seznam kódů výkonů. +SQL: `dokladd JOIN kar JOIN vykony WHERE kod IN (...) AND platnost kódu platí`. +Řazení: datum DESC, rodcis, kod. + +### `pocet_dni(zacnes, konnes, pracne)` +Výpočet délky neschopenky: +- `pracne='A'` (aktivní) → dny od začátku do dnes +- `pracne='N'` → dny od začátku do konce +- jinak → `"NA"` + +### `parse_data(data_str)` +Parsuje `key=value` text z pole `HISTDOC.DATA` do slovníku. +Každý řádek = jeden klíč/hodnota oddělené `=`. + +### `parse_date(val)` +Převede formát `D:DD.MM.YYYY` (jak ho ukládá Medicus) na `datetime.date`. + +### `style_header(ws)` / `autofit_ws(ws)` +Styl záhlaví (modrý fill, bílý tučný text, centrování) a šířky sloupců (max 50 znaků). +Používají se jen na listech s posudky (ostatní listy řeší xlwings). + +--- + +## Listy s posudky – detail + +### `Posudky řidičák` (MOTORVO) + +Data jsou uložena v `HISTDOC.DATA` jako `key=value` text. +Parsovaná pole: + +| Sloupec | Zdroj v DATA | +|---|---| +| PorCislo | `PorCislo` nebo `HISTDOC.PORCISLO` | +| DatumVyd | `DatumVyd` (formát `D:DD.MM.YYYY`) | +| DatKonec | `DatKonec` (formát `D:DD.MM.YYYY`) | +| DruhProh | `DruhProh` | +| Posouzeni | odvozeno z `Posouzeni`, `Posouzeni2`, `ZpusobPodminka` | +| ZpusobPodminka | `ZpusobPodminka` | +| SkupinaPodminka | `SkupinaPodminka` | +| Skupiny | `ZpusobJe` | + +**Logika Posouzeni:** +- `Posouzeni2 = T` → `nezpůsobilý` +- `ZpusobPodminka = B:1` → `způsobilý s podmínkou` +- `Posouzeni = T` → `způsobilý` + +**Sloupec ePosudek:** +`ANO` pokud existuje záznam v HISTDOC s `TYP='EPOSMRO'` pro stejného pacienta (IDPACI) a stejné datum. +Párování: `(IDPACI, DATUM)` – přímá FK vazba mezi MOTORVO a EPOSMRO neexistuje. +Buňka s ANO je zelená (fill + font). + +**Zebra pruhování:** liché řádky bílé, sudé světle modré (`DCE6F1`). + +### `ePosudky registr` (EPOSMRO) + +Elektronická podání do centrálního registru způsobilosti. +Stát tuto funkci zavedl přibližně od aktualizace Medicusu (03/2026). + +Data parsovaná z `HISTDOC.DATA`: + +| Sloupec | Zdroj v DATA | +|---|---| +| DatumVyd | `DatumVystaveni` | +| DatKonec | `PlatnostDo` | +| DruhProhlidky | `DruhProhlidkyNazev` | +| DruhPosudku | `DruhPosudkuNazev` | +| Vysledek | `VysledekNazev` | +| StavPosudku | `StavPosudkuNazev` | +| TypAkce | `TypAkceNazev` | + +Stavová pole z `HISTDOC_EPOSUDEK`: +- `ID_PODANI` – ID podání do registru +- `ODESLANO` – timestamp odeslání +- `STATUS_ODESL` – stav odpovědi z registru (`O` = odesláno) + +**Zneplatnění:** `StavPosudku = zneplatneny` = lékař aktivně odvolal způsobilost +(např. pacient prodělal mrtvici, epileptický záchvat atp.). +Zneplatnění je samostatný EPOSMRO záznam, ne modifikace původního. + +--- + +## xlwings – závěrečný krok + +Po `wb.save()` se soubor otevře přes xlwings (vyžaduje plný Excel): +1. `sheet.autofit()` na všech listech – správné šířky sloupců +2. Na listu `Recepty`: centrování sloupců C, E, F, G, I, M, N +3. `wb_xw.save()` + zavření + +xlwings je nutný pro spolehlivý autofit (openpyxl ho neumí přesně). +Trvá ~10 minut, spouští se v noci. + +--- + +## Pořadí zpracování (pro debugování) + +``` +DB connect +→ smazání starých souborů +→ SQL dotazy (Registrovani, Očkování, Recepty, Výkony, Neschopenky) +→ add_vykony_sheet × 8 +→ MOTORVO + EPOSMRO listy (s parsováním DATA) +→ autofilter na všech listech +→ con.close() + wb.save() +→ xlwings autofit + centrování +→ Hotovo. +``` + +Print výstup v konzoli ukazuje počty řádků každého listu – užitečné pro kontrolu. + +--- + +## Rozšíření v budoucnu + +- Přidat další typy posudků (pracovní, vstupní, sportovní...) ze `VS_POSUDKY` +- Případně sledovat stav podání EPOSMRO v čase (datum odeslání vs. datum posudku) +- Automatické spouštění přes Windows Task Scheduler (jako `faktury_report.py`) diff --git a/MedicusWithClaudeKomplexniReport/komplexni_report.py b/MedicusWithClaudeKomplexniReport/komplexni_report.py new file mode 100644 index 0000000..4a11b2a --- /dev/null +++ b/MedicusWithClaudeKomplexniReport/komplexni_report.py @@ -0,0 +1,410 @@ +import os +import time +import fdb +import openpyxl +import xlwings as xw +from datetime import datetime, date +from openpyxl.utils import get_column_letter +from openpyxl.styles import Font, PatternFill, Alignment + +# --- Konfigurace --- +PathToSaveCSV = r"u:\Dropbox\!!!Days\Downloads Z230" +timestr = time.strftime("%Y-%m-%d_%H-%M-%S_") +output_path = os.path.join(PathToSaveCSV, timestr + "Pacienti.xlsx") + +# --- Smazání předchozích verzí --- +for fname in os.listdir(PathToSaveCSV): + if fname.endswith("Pacienti.xlsx"): + try: + os.remove(os.path.join(PathToSaveCSV, fname)) + except Exception as e: + print(f"Nelze smazat {fname}: {e}") + +# --- Připojení k DB --- +con = fdb.connect( + host='localhost', database=r'c:\MEDICUS 3\data\medicus.FDB', + user='sysdba', password='masterkey', charset='WIN1250' +) +cur = con.cursor() + +wb = openpyxl.Workbook() + +# ===================== +# Pomocné funkce +# ===================== + +# Styly pro posudky +HEADER_FILL = PatternFill('solid', fgColor='2F5496') +HEADER_FONT = Font(bold=True, color='FFFFFF') +ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1') +GREEN_FILL = PatternFill('solid', fgColor='C6EFCE') +GREEN_FONT = Font(bold=True, color='276221') + +def style_header(ws): + for cell in ws[1]: + cell.fill = HEADER_FILL + cell.font = HEADER_FONT + cell.alignment = Alignment(horizontal='center') + +def autofit_ws(ws): + for col in ws.columns: + max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col) + ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 50) + +def sanitize(val): + """Nahradí znaky neplatné pro Excel: µ → u, ostatní → _""" + if not isinstance(val, str): + return val + result = [] + for ch in val: + if ch == 'µ': + result.append('u') + elif ord(ch) < 32 and ch not in '\t\n\r': + result.append('_') + elif ord(ch) in (0xFFFE, 0xFFFF) or 0xD800 <= ord(ch) <= 0xDFFF: + result.append('_') + else: + result.append(ch) + return ''.join(result) + +def fmt(val): + return '' if val is None else sanitize(val) + +def parse_data(data_str): + """Parsuje key=value text z HISTDOC.DATA do slovníku.""" + result = {} + if not data_str: + return result + for line in data_str.splitlines(): + if '=' in line: + key, _, val = line.partition('=') + result[key.strip()] = val.strip() + return result + +def parse_date(val): + """Převede 'D:DD.MM.YYYY' na datetime.date.""" + if val and val.startswith('D:'): + try: + return datetime.strptime(val[2:], '%d.%m.%Y').date() + except ValueError: + return val + return val + +VYKONY_CONDITION = """ + (datose >= vykony.platiod AND datose <= vykony.platido) + OR (datose >= vykony.platiod AND vykony.platido IS NULL) +""" +VYKONY_HEADERS = ["Rodne cislo", "Jmeno", "Datum vykonu", "Kod", "Název", "Dg.", "Body"] + +def add_vykony_sheet(sheet_name, kody): + """Přidá list s výkony filtrovanými podle seznamu kódů.""" + kod_list = ", ".join(str(k) for k in kody) + cur.execute(f""" + SELECT dokladd.rodcis, + TRIM(prijmeni) || ', ' || TRIM(jmeno), + dokladd.datose, dokladd.kod, vykony.naz, dokladd.ddgn, dokladd.body + FROM dokladd + LEFT JOIN kar ON dokladd.rodcis = kar.rodcis + JOIN vykony ON dokladd.kod = vykony.kod + WHERE ({VYKONY_CONDITION}) + AND dokladd.kod IN ({kod_list}) + ORDER BY datose DESC, dokladd.rodcis, dokladd.kod + """) + rows = cur.fetchall() + print(f"{sheet_name}: {len(rows)}") + ws = wb.create_sheet(sheet_name) + ws.append(VYKONY_HEADERS) + for row in rows: + ws.append(list(row)) + +# ===================== +# List: Registrovaní +# ===================== +cur.execute(""" + SELECT rodcis, prijmeni, jmeno, datum_registrace, registr.idpac, poj + FROM registr + JOIN kar ON registr.idpac = kar.idpac + WHERE kar.vyrazen != 'A' + AND kar.rodcis IS NOT NULL + AND idicp != 0 + AND datum_zruseni IS NULL +""") +rows = cur.fetchall() +print(f"Registrovaní: {len(rows)}") +ws = wb.active +ws.title = 'Registrovani' +ws.append(["Rodne cislo", "Prijmeni", "Jmeno", "Datum registrace", "ID pacienta", "Pojistovna"]) +for row in rows: + ws.append(list(row)) + +# ===================== +# List: Očkování +# ===================== +cur.execute(""" + SELECT rodcis, prijmeni, jmeno, ockzaz.datum, kodmz, ockzaz.poznamka, latka, nazev, expire + FROM registr + JOIN kar ON registr.idpac = kar.idpac + JOIN ockzaz ON registr.idpac = ockzaz.idpac + WHERE datum_zruseni IS NULL + AND kar.vyrazen != 'A' + AND kar.rodcis IS NOT NULL + AND idicp != 0 + ORDER BY ockzaz.datum DESC +""") +rows = cur.fetchall() +print(f"Očkování: {len(rows)}") +ws = wb.create_sheet("Očkování") +ws.append(["Rodne cislo", "Prijmeni", "Jmeno", "Datum ockovani", "Kod MZ", "Sarze", "Latka", "Nazev", "Expirace"]) +for row in rows: + ws.append(list(row)) + +# ===================== +# List: Recepty +# ===================== +cur.execute(""" + SELECT kar.rodcis, + TRIM(kar.prijmeni) || ' ' || SUBSTRING(kar.jmeno FROM 1 FOR 1) || '.' AS jmeno, + recept.datum, + TRIM(recept.lek) || ' ' || TRIM(recept.dop) AS lek, + recept.expori AS Poc, + CASE WHEN recept.opakovani IS NULL THEN 1 ELSE recept.opakovani END AS OP, + recept.uhrada, + recept.dsig, + recept.NOTIFIKACE_KONTAKT AS notifikace, + recept_epodani.erp, + recept_epodani.vystavitel_jmeno, + recept.atc, + recept.CENAPOJ, + recept.cenapac + FROM recept + LEFT JOIN RECEPT_EPODANI ON recept.id_epodani = recept_epodani.id + LEFT JOIN kar ON recept.idpac = kar.idpac + ORDER BY datum DESC, erp DESC +""") +rows = cur.fetchall() +print(f"Recepty: {len(rows)}") +ws = wb.create_sheet("Recepty") +ws.append(["Rodné číslo", "Jméno", "Datum vystavení", "Název leku", "Poč.", "Op.", "Úhr.", + "Da signa", "Notifikace", "eRECEPT", "Vystavil", "ATC", "Cena pojišťovna", "Cena pacient"]) +for row in rows: + ws.append([sanitize(v) if isinstance(v, str) else v for v in row]) + +# ===================== +# List: Výkony všechny +# ===================== +cur.execute(f""" + SELECT dokladd.rodcis, + TRIM(prijmeni) || ', ' || TRIM(jmeno), + dokladd.datose, dokladd.kod, dokladd.pocvyk, dokladd.ddgn, dokladd.body, vykony.naz + FROM kar + JOIN dokladd ON kar.rodcis = dokladd.rodcis + JOIN vykony ON dokladd.kod = vykony.kod + WHERE {VYKONY_CONDITION} + ORDER BY dokladd.datose DESC, dokladd.rodcis +""") +rows = cur.fetchall() +print(f"Výkony: {len(rows)}") +ws = wb.create_sheet("Vykony") +ws.append(["Rodne cislo", "Jmeno", "Datum vykonu", "Kod", "Pocet", "Dg.", "Body", "Nazev"]) +for row in rows: + ws.append(list(row)) + +# ===================== +# Listy: Neschopenky +# ===================== +def pocet_dni(zacnes, konnes, pracne): + dnes = date.today() + if pracne == 'A': + return (dnes - zacnes).days if zacnes else "NA" + if pracne == 'N' and zacnes and konnes and zacnes <= konnes: + return (konnes - zacnes).days + return "NA" + +def nes_row(r): + return (r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], + pocet_dni(r[5], r[7], r[6]), r[8], r[9], r[10]) + +NES_HEADERS = ["ID pac", "Rodne cislo", "Jmeno", "Datum neschopenky", "Číslo neschopenky", + "Zacatek", "Aktivní?", "Konec", "Pocet dni", "Diagnoza zacatel", "Diagnoza konec", "Aktualizovano"] + +cur.execute(""" + SELECT nes.idpac, kar.rodcis, + TRIM(prijmeni) || ', ' || TRIM(jmeno), + nes.datnes, nes.ecn, nes.zacnes, nes.pracne, nes.konnes, + nes.diagno, nes.kondia, nes.updated + FROM nes + LEFT JOIN kar ON nes.idpac = kar.idpac + WHERE nes.datnes <= CURRENT_DATE + ORDER BY datnes DESC +""") +vse = cur.fetchall() +aktivni = [r for r in vse if r[6] == 'A'] +print(f"Neschopenky: {len(vse)} celkem, {len(aktivni)} aktivních") + +ws = wb.create_sheet("Neschopenky všechny") +ws.append(NES_HEADERS) +for r in vse: + ws.append(list(nes_row(r))) + +ws = wb.create_sheet("Neschopenky aktivní") +ws.append(NES_HEADERS) +for r in aktivni: + ws.append(list(nes_row(r))) + +# ===================== +# Výkonové listy – jednotlivé typy výkonů +# ===================== +add_vykony_sheet('Preventivni prohlidky', [1022, 1021]) +add_vykony_sheet('INR', [1443]) +add_vykony_sheet('CRP', [2230, 9111]) +add_vykony_sheet('Holter', [17129]) +add_vykony_sheet('Prostata', [1130, 1131, 1132, 1133, 1134]) +add_vykony_sheet('TOKS', [15118, 15119, 15120, 15121]) +add_vykony_sheet('COVID', [1306]) +add_vykony_sheet('Streptest', [2220]) + +# ===================== +# List: Posudky řidičák – MOTORVO (ruční) +# ===================== +cur.execute("SELECT IDPACI, DATUM FROM HISTDOC WHERE TYP = 'EPOSMRO'") +eposmro_keys = set((r[0], r[1]) for r in cur.fetchall()) + +cur.execute(""" + SELECT h.ID, h.DATUM, h.IDPACI, + k.PRIJMENI, k.JMENO, k.RODCIS, + h.DATA, h.PORCISLO, h.STAV, h.PRINTED, h.IDUZIV, h.CREATED + FROM HISTDOC h + JOIN KAR k ON k.IDPAC = h.IDPACI + WHERE h.TYP = 'MOTORVO' + ORDER BY h.ID DESC +""") +motorvo_rows = cur.fetchall() +print(f"MOTORVO: {len(motorvo_rows)}") + +motorvo_headers = [ + 'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS', + 'PorCislo', 'DatumVyd', 'DatKonec', 'DruhProh', + 'Posouzeni', 'ZpusobPodminka', 'SkupinaPodminka', 'Skupiny', + 'ePosudek', 'STAV', 'PRINTED', 'IDUZIV', 'CREATED' +] +ws = wb.create_sheet("Posudky řidičák") +ws.append(motorvo_headers) + +epos_col_idx = motorvo_headers.index('ePosudek') + 1 + +for i, row in enumerate(motorvo_rows, start=2): + (hid, datum, idpac, prijmeni, jmeno, rodcis, + data_blob, porcislo, stav, printed, iduziv, created) = row + data = parse_data(data_blob) + + if data.get('Posouzeni2') == 'T': + posouzeni = 'nezpůsobilý' + elif data.get('ZpusobPodminka') == 'B:1': + posouzeni = 'způsobilý s podmínkou' + elif data.get('Posouzeni') == 'T': + posouzeni = 'způsobilý' + else: + posouzeni = '' + + ws.append([ + hid, fmt(datum), idpac, fmt(prijmeni), fmt(jmeno), fmt(rodcis), + fmt(porcislo or data.get('PorCislo', '')), + parse_date(data.get('DatumVyd', '')), + parse_date(data.get('DatKonec', '')), + fmt(data.get('DruhProh', '')), + posouzeni, + fmt(data.get('ZpusobPodminka', '')), + fmt(data.get('SkupinaPodminka', '')), + fmt(data.get('ZpusobJe', '')), + 'ANO' if (idpac, datum) in eposmro_keys else 'NE', + fmt(stav), fmt(printed), fmt(iduziv), fmt(created), + ]) + + if i % 2 == 0: + for cell in ws[i]: + cell.fill = ZEBRA_FILL + cell = ws.cell(row=i, column=epos_col_idx) + if cell.value == 'ANO': + cell.fill = GREEN_FILL + cell.font = GREEN_FONT + +style_header(ws) +ws.freeze_panes = 'A2' +autofit_ws(ws) + +# ===================== +# List: Posudky řidičák – EPOSMRO (elektronická podání) +# ===================== +cur.execute(""" + SELECT h.ID, h.DATUM, h.IDPACI, + k.PRIJMENI, k.JMENO, k.RODCIS, + h.DATA, h.STAV, h.CREATED, + e.ID_PODANI, e.ODESLANO, e.STATUS + FROM HISTDOC h + JOIN KAR k ON k.IDPAC = h.IDPACI + LEFT JOIN HISTDOC_EPOSUDEK e ON e.ID_HISTDOC = h.ID + WHERE h.TYP = 'EPOSMRO' + ORDER BY h.ID DESC +""") +epos_rows = cur.fetchall() +print(f"EPOSMRO: {len(epos_rows)}") + +ws = wb.create_sheet("ePosudky registr") +ws.append([ + 'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS', + 'DatumVyd', 'DatKonec', 'DruhProhlidky', 'DruhPosudku', + 'Vysledek', 'StavPosudku', 'TypAkce', + 'STAV', 'CREATED', 'ID_PODANI', 'ODESLANO', 'STATUS_ODESL' +]) + +for i, row in enumerate(epos_rows, start=2): + (hid, datum, idpac, prijmeni, jmeno, rodcis, + data_blob, stav, created, id_podani, odeslano, status_odesl) = row + data = parse_data(data_blob) + + ws.append([ + hid, fmt(datum), idpac, fmt(prijmeni), fmt(jmeno), fmt(rodcis), + parse_date(data.get('DatumVystaveni', '')), + parse_date(data.get('PlatnostDo', '')), + fmt(data.get('DruhProhlidkyNazev', '')), + fmt(data.get('DruhPosudkuNazev', '')), + fmt(data.get('VysledekNazev', '')), + fmt(data.get('StavPosudkuNazev', '')), + fmt(data.get('TypAkceNazev', '')), + fmt(stav), fmt(created), fmt(id_podani), fmt(odeslano), fmt(status_odesl), + ]) + + if i % 2 == 0: + for cell in ws[i]: + cell.fill = ZEBRA_FILL + +style_header(ws) +ws.freeze_panes = 'A2' +autofit_ws(ws) + +# ===================== +# Autofilter na všech listech +# ===================== +for ws in wb.worksheets: + ws.auto_filter.ref = f"A1:{get_column_letter(ws.max_column)}{ws.max_row}" + +# ===================== +# Uložení +# ===================== +con.close() +wb.save(output_path) +print(f"Uloženo: {output_path}") + +# ===================== +# xlwings: autofit + centrování Recepty +# ===================== +with xw.App(visible=False) as app: + wb_xw = xw.Book(output_path) + for sheet in wb_xw.sheets: + sheet.autofit() + for sloupec in ["C:C", "E:E", "F:F", "G:G", "I:I", "M:M", "N:N"]: + wb_xw.sheets['Recepty'].range(sloupec).api.HorizontalAlignment = 3 + wb_xw.save() + wb_xw.close() + +print("Hotovo.") diff --git a/MedicusWithClaudePosudek/CLAUDE_NOTES.md b/MedicusWithClaudePosudek/CLAUDE_NOTES.md new file mode 100644 index 0000000..4805c54 --- /dev/null +++ b/MedicusWithClaudePosudek/CLAUDE_NOTES.md @@ -0,0 +1,143 @@ +# MedicusWithClaudePosudek – poznámky pro Clauda + +## O co jde + +Lékařské posudky vystavované MUDr. Buzalkovou. Prozatím řešíme posudky k řízení motorových vozidel. + +Nový zákon ukládá povinnost odesílat posudky k řízení do **centrálního registru** – tuto funkci Medicus přidal v aktualizaci z konce března 2026. + +--- + +## Tabulky + +### HISTDOC – hlavní tabulka pro všechny posudky + +Všechny posudky jsou záznamy v `HISTDOC`, lišící se hodnotou sloupce `TYP`. + +Klíčové sloupce: +| Sloupec | Popis | +|---|---| +| `ID` | primární klíč | +| `TYP` | typ dokumentu (viz níže) | +| `DATUM` | datum vystavení posudku | +| `IDPACI` | FK → KAR.IDPAC (pacient) | +| `DATA` | obsah posudku – text ve formátu key=value (viz níže) | +| `PORCISLO` | pořadové číslo posudku (= PorCislo v DATA) | +| `STAV` | stav záznamu (Z = zavřeno) | +| `PRINTED` | T/F – byl vytištěn | +| `IDUZIV` | FK → UZIVATEL.IDUZI – kdo vystavil (4 = MUDr. Buzalková) | +| `CREATED` | timestamp vytvoření záznamu | + +**Vazba:** žádná přímá vazba na jiné tabulky (vyšetření, dekurz apod.) – posudek je svébytný dokument. + +### TYP hodnoty relevantní pro posudky řidičů + +| TYP | Popis | Počet (k 2026-03-31) | +|---|---|---| +| `MOTORVO` | ruční posudek k řízení motorových vozidel | 1530 | +| `EPOSMRO` | elektronické podání posudku do centrálního registru | 2 | + +Ostatní typy posudků v HISTDOC (pro referenci): +- `ZBROJPR`, `ZBROJP2` – zbrojní průkaz +- `ZPUPRN` – způsobilost pro práci +- `ZDRSTA3`–`ZDRSTA5`, `ZDRSTAV`, `ZDRINF` – zdravotní stav (různé varianty) +- ... (celkem desítky typů) + +### HISTDOC_EPOSUDEK – evidence odeslání do registru + +Doplňková tabulka k EPOSMRO záznamům v HISTDOC. + +| Sloupec | Popis | +|---|---| +| `ID_HISTDOC` | FK → HISTDOC.ID (záznam EPOSMRO) | +| `ID_PODANI` | UUID přidělené centrálním registrem | +| `ODESLANO` | timestamp odeslání | +| `STATUS` | O = odesláno | +| `VERZE` | verze záznamu (base64 interní hodnota) | + +### VS_POSUDKY – prázdná, zatím nepoužívaná + +Sloupce: ID, IDPAC, DATA (BLOB), DATUM, POSTYPE. Pravděpodobně připravena pro budoucí využití. + +--- + +## Workflow: ruční posudek → elektronické podání + +1. Lékař v Medicusu vyplní posudek → vznikne `HISTDOC` TYP=`MOTORVO` +2. Medicus automaticky odešle do centrálního registru → vznikne `HISTDOC` TYP=`EPOSMRO` + záznam v `HISTDOC_EPOSUDEK` +3. Oba záznamy mají stejné `IDPACI` + `DATUM` → podle toho je párujeme + +Příklad (pacient Vráček, 30.3.2026): +- HISTDOC ID=34743, TYP=MOTORVO, CREATED=13:12 +- HISTDOC ID=34746, TYP=EPOSMRO, CREATED=13:21 +- HISTDOC_EPOSUDEK: STATUS=O, ODESLANO=13:21 + +--- + +## Formát DATA (key=value) – MOTORVO + +``` +JmenoPac=Radomil Vráček +DatNar=D:27.03.1956 +Prukaz=207069669 ← číslo řidičského průkazu +DatKonec=D:30.03.2028 ← platnost posudku do +DatumVyd=D:30.03.2026 ← datum vydání +Bydliste=K Šafránce 507/16, 19000 Praha 9-Střížkov +DruhProh=periodická ← druh prohlídky +Posouzeni=T ← T = způsobilý (F = nezpůsobilý?) +Posouzeni2=F ← T = nezpůsobilý (druhá volba) +ZpusobJe=B:0 ← skupiny bez podmínky +ZpusobPodminka=B:1 ← B:1 = má podmínku +SkupinaPodminka=sk. B brýle +PorCislo=2600037 +KonecDleZakona=D +DatumPrevzeti=D:30.03.2026 +``` + +**Výsledek posouzení** (kombinace Posouzeni + Posouzeni2 + ZpusobPodminka): +- `Posouzeni=T` + `Posouzeni2=F` + `ZpusobPodminka=B:0` → způsobilý +- `Posouzeni=T` + `Posouzeni2=F` + `ZpusobPodminka=B:1` → způsobilý s podmínkou +- `Posouzeni=T` + `Posouzeni2=T` → nezpůsobilý + +## Formát DATA (key=value) – EPOSMRO + +``` +Lekar=MUDr. Michaela Buzalková +KRZPID=130153584 ← ID lékaře v registru +ICO=68366370 +ICP=09305001 +Pacient=Radomil Vráček +RID=8705636888 ← číslo řidičáku +DatumNarozeni=D:27.03.1956 +StavPosudkuKodVerze=zneplatneny|1.0.0 +StavPosudkuNazev=Zneplatněný ← stav posudku v registru +TypAkceNazev=vytvoření +TypAkceKodVerze=akce_ro_1|1.0.0 +DruhProhlidkyNazev=pravidelná +DruhProhlidkyKodVerze=Pravidelna|1.0.0 +DruhPosudkuNazev=řidičské oprávnění pro seniory +DruhPosudkuKodVerze=SenioriRo|1.0.0 +SkupinaZadatelRidicNazev=skupina 1 +SkupinyRidicskehoOpravneniSeznam=B +HarmonizovaneNarodniKody=$:~HNK1:011:01.01 Brýle5:01.012:HK1:B0: ← kódy omezení (brýle) +VysledekKodVerze=ZpusobilySPodminkou|1.0.0 +VysledekNazev=způsobilý s podmínkou +DatumVystaveni=D:30.03.2026 +PlatnostDo=D:30.03.2028 +``` + +**StavPosudku = "Zneplatněný"** neznamená chybu – jde o akci, kdy lékař odvolá způsobilost pacienta (např. po mrtvici, epileptickém záchvatu apod.). Medicus pak odešle do registru zneplatnění existujícího posudku. + +--- + +## Soubory v projektu + +- `posudky_report.py` – generuje Excel s listy MOTORVO a EPOSMRO +- `CLAUDE_NOTES.md` – tento soubor + +## Report (posudky_report.py) + +- Výstup: `u:\Dropbox\!!!Days\Downloads Z230\YYYY-MM-DD_HH-MM-SS_Přehled posudků řidičák.xlsx` +- Maže předchozí verzi před zápisem nové +- List MOTORVO: 1530 záznamů, sloupec `ePosudek` = ANO (zeleně) / NE podle toho, zda byl odeslán ePosudek (párování IDPACI + DATUM) +- List EPOSMRO: 2 záznamy, detail elektronického podání diff --git a/MedicusWithClaudePosudek/posudky_report.py b/MedicusWithClaudePosudek/posudky_report.py new file mode 100644 index 0000000..51f4e7e --- /dev/null +++ b/MedicusWithClaudePosudek/posudky_report.py @@ -0,0 +1,237 @@ +import fdb +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment +from openpyxl.utils import get_column_letter +from datetime import datetime +import os +import sys + +# --- Připojení --- +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +# --- Výstupní soubor --- +output_dir = r'u:\Dropbox\!!!Days\Downloads Z230' +now = datetime.now() +filename = now.strftime('%Y-%m-%d_%H-%M-%S') + '_Přehled posudků řidičák.xlsx' +output_path = os.path.join(output_dir, filename) + +# --- Smazání předchozích verzí --- +for f in os.listdir(output_dir): + if f.endswith('_Přehled posudků řidičák.xlsx'): + os.remove(os.path.join(output_dir, f)) + +wb = openpyxl.Workbook() + +# ===================== +# Pomocné funkce +# ===================== + +HEADER_FILL = PatternFill('solid', fgColor='2F5496') +HEADER_FONT = Font(bold=True, color='FFFFFF') +ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1') +GREEN_FILL = PatternFill('solid', fgColor='C6EFCE') +GREEN_FONT = Font(bold=True, color='276221') + +def style_header(ws): + for cell in ws[1]: + cell.fill = HEADER_FILL + cell.font = HEADER_FONT + cell.alignment = Alignment(horizontal='center') + +def autofit(ws): + for col in ws.columns: + max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col) + ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 50) + +def fmt(val): + if val is None: + return '' + return val + +def parse_data(data_str): + """Parsuje key=value text z HISTDOC.DATA do slovníku.""" + result = {} + if not data_str: + return result + for line in data_str.splitlines(): + if '=' in line: + key, _, val = line.partition('=') + result[key.strip()] = val.strip() + return result + +def parse_date(val): + """Převede 'D:DD.MM.YYYY' na datetime.date, nebo vrátí původní hodnotu.""" + if val and val.startswith('D:'): + try: + return datetime.strptime(val[2:], '%d.%m.%Y').date() + except ValueError: + return val + return val + +# ===================== +# List 1 – MOTORVO (ruční posudky k řízení) +# ===================== + +ws1 = wb.active +ws1.title = 'MOTORVO' + +# Množina (IDPACI, DATUM) kde existuje EPOSMRO +cur.execute(""" + SELECT IDPACI, DATUM FROM HISTDOC WHERE TYP = 'EPOSMRO' +""") +eposmro_keys = set((r[0], r[1]) for r in cur.fetchall()) + +cur.execute(""" + SELECT h.ID, h.DATUM, h.IDPACI, + k.PRIJMENI, k.JMENO, k.RODCIS, + h.DATA, h.PORCISLO, h.STAV, h.PRINTED, h.IDUZIV, h.CREATED + FROM HISTDOC h + JOIN KAR k ON k.IDPAC = h.IDPACI + WHERE h.TYP = 'MOTORVO' + ORDER BY h.ID DESC +""") +raw_rows = cur.fetchall() + +headers = [ + 'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS', + 'PorCislo', 'DatumVyd', 'DatKonec', 'DruhProh', + 'Posouzeni', 'ZpusobPodminka', 'SkupinaPodminka', + 'Skupiny', + 'ePosudek', + 'STAV', 'PRINTED', 'IDUZIV', 'CREATED' +] +ws1.append(headers) + +for i, row in enumerate(raw_rows, start=2): + (hid, datum, idpac, prijmeni, jmeno, rodcis, + data_blob, porcislo, stav, printed, iduziv, created) = row + + data = parse_data(data_blob) + + posouzeni = '' + if data.get('Posouzeni') == 'T': + if data.get('Posouzeni2') == 'T': + posouzeni = 'nezpůsobilý' + elif data.get('ZpusobPodminka') == 'B:1': + posouzeni = 'způsobilý s podmínkou' + else: + posouzeni = 'způsobilý' + + skupiny = data.get('SkupinyRidicskehoOpravneniSeznam', '') + if not skupiny: + # MOTORVO nemá SkupinyRidicskehoOpravneniSeznam, zkusíme ZpusobJe + skupiny = data.get('ZpusobJe', '') + + ws1.append([ + hid, + fmt(datum), + idpac, + fmt(prijmeni), + fmt(jmeno), + fmt(rodcis), + fmt(porcislo or data.get('PorCislo', '')), + parse_date(data.get('DatumVyd', '')), + parse_date(data.get('DatKonec', '')), + fmt(data.get('DruhProh', '')), + posouzeni, + fmt(data.get('ZpusobPodminka', '')), + fmt(data.get('SkupinaPodminka', '')), + fmt(skupiny), + 'ANO' if (idpac, datum) in eposmro_keys else 'NE', + fmt(stav), + fmt(printed), + fmt(iduziv), + fmt(created), + ]) + + if i % 2 == 0: + for cell in ws1[i]: + cell.fill = ZEBRA_FILL + + # Sloupec ePosudek – zvýraznit ANO zeleně + epos_col = headers.index('ePosudek') + 1 + cell = ws1.cell(row=i, column=epos_col) + if cell.value == 'ANO': + cell.fill = GREEN_FILL + cell.font = GREEN_FONT + +style_header(ws1) +ws1.freeze_panes = 'A2' +autofit(ws1) + +# ===================== +# List 2 – EPOSMRO (elektronická podání do registru) +# ===================== + +ws2 = wb.create_sheet('EPOSMRO') + +cur.execute(""" + SELECT h.ID, h.DATUM, h.IDPACI, + k.PRIJMENI, k.JMENO, k.RODCIS, + h.DATA, h.STAV, h.CREATED, + e.ID_PODANI, e.ODESLANO, e.STATUS + FROM HISTDOC h + JOIN KAR k ON k.IDPAC = h.IDPACI + LEFT JOIN HISTDOC_EPOSUDEK e ON e.ID_HISTDOC = h.ID + WHERE h.TYP = 'EPOSMRO' + ORDER BY h.ID DESC +""") +epos_rows = cur.fetchall() + +headers2 = [ + 'ID', 'DATUM', 'IDPACI', 'PRIJMENI', 'JMENO', 'RODCIS', + 'DatumVyd', 'DatKonec', 'DruhProhlidky', 'DruhPosudku', + 'Vysledek', 'StavPosudku', 'TypAkce', + 'STAV', 'CREATED', + 'ID_PODANI', 'ODESLANO', 'STATUS_ODESL' +] +ws2.append(headers2) + +for i, row in enumerate(epos_rows, start=2): + (hid, datum, idpac, prijmeni, jmeno, rodcis, + data_blob, stav, created, + id_podani, odeslano, status_odesl) = row + + data = parse_data(data_blob) + + ws2.append([ + hid, + fmt(datum), + idpac, + fmt(prijmeni), + fmt(jmeno), + fmt(rodcis), + parse_date(data.get('DatumVystaveni', '')), + parse_date(data.get('PlatnostDo', '')), + fmt(data.get('DruhProhlidkyNazev', '')), + fmt(data.get('DruhPosudkuNazev', '')), + fmt(data.get('VysledekNazev', '')), + fmt(data.get('StavPosudkuNazev', '')), + fmt(data.get('TypAkceNazev', '')), + fmt(stav), + fmt(created), + fmt(id_podani), + fmt(odeslano), + fmt(status_odesl), + ]) + + if i % 2 == 0: + for cell in ws2[i]: + cell.fill = ZEBRA_FILL + +style_header(ws2) +ws2.freeze_panes = 'A2' +autofit(ws2) + +# ===================== +# Uložení +# ===================== + +conn.close() +wb.save(output_path) +sys.stdout.buffer.write(f'Ulozeno: {output_path}\n'.encode('utf-8')) +sys.stdout.buffer.write(f'MOTORVO: {len(raw_rows)} radku, EPOSMRO: {len(epos_rows)} radku\n'.encode('utf-8')) From 3b30c35400add54232f8b51e547cb8c097f61019 Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Wed, 1 Apr 2026 06:04:40 +0200 Subject: [PATCH 3/4] notebook vb --- MedicusWithClaudeDekurz/dekurz_report.py | 289 ++++++++++++++++++ .../registrace_2025_dnes.xlsx | Bin 0 -> 20301 bytes MedicusWithClaudePoj/registrace_report.py | 117 +++++++ 3 files changed, 406 insertions(+) create mode 100644 MedicusWithClaudeDekurz/dekurz_report.py create mode 100644 MedicusWithClaudePoj/registrace_2025_dnes.xlsx create mode 100644 MedicusWithClaudePoj/registrace_report.py diff --git a/MedicusWithClaudeDekurz/dekurz_report.py b/MedicusWithClaudeDekurz/dekurz_report.py new file mode 100644 index 0000000..d641bab --- /dev/null +++ b/MedicusWithClaudeDekurz/dekurz_report.py @@ -0,0 +1,289 @@ +import sys, io, re, os, glob +from datetime import date, datetime +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side + +VYSTUPNI_ADRESAR = r'u:\Dropbox\Ordinace\Reporty' +NAZEV_REPORTU = 'Dekurzy' +DATUM_OD = '2025-01-01' +DATUM_DO = date.today().strftime('%Y-%m-%d') + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +cur.execute(f""" + SELECT d.DATUM, d.CAS, u.ZKRATKA, k.PRIJMENI, k.JMENO, k.RODCIS, k.POJ, d.DEKURS + FROM DEKURS d + JOIN KAR k ON k.IDPAC = d.IDPAC + LEFT JOIN UZIVATEL u ON u.IDUZI = d.IDUZI + WHERE d.DATUM >= '{DATUM_OD}' AND d.DATUM <= '{DATUM_DO}' + ORDER BY d.DATUM DESC, d.CAS DESC, k.PRIJMENI, k.JMENO +""") +raw_rows = cur.fetchall() + +TOP_TYPY = ['Rec', 'VykA', 'Files', 'MEDLAB', 'Lab', 'Ock', 'Nes', 'Lec', 'SpecVys', 'PlaPac'] + +# Parse dekurzů +rows = [] +for datum, cas, zkratka, prijmeni, jmeno, rodcis, poj, dekurs_blob in raw_rows: + rtf = dekurs_blob.read() if hasattr(dekurs_blob, 'read') else (dekurs_blob or '') + pocty = {} + ids_by_typ = {t: [] for t in TOP_TYPY} + ids_ostatni = [] + for nazev, typ, rid in re.findall(r'"([^"]+)","([A-Za-z]+):(\d+)"', rtf): + pocty[typ] = pocty.get(typ, 0) + 1 + if typ in ids_by_typ: + ids_by_typ[typ].append(int(rid)) + else: + ids_ostatni.append((typ, int(rid), nazev)) + top = [pocty.get(t, 0) for t in TOP_TYPY] + ostatni = sum(v for k, v in pocty.items() if k not in TOP_TYPY) + iniciala = jmeno[0] + '.' if jmeno and jmeno.strip() else '' + jmeno_cel = f"{prijmeni.strip()}, {iniciala}" if prijmeni else iniciala + rows.append((datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni)) + +# ── Načtení detailů z DB ──────────────────────────────────────────────────── +def fetch_details(cur, table, pk, id_col, fields, ids): + if not ids: + return {} + result = {} + batch_size = 1000 + for i in range(0, len(ids), batch_size): + batch = ids[i:i+batch_size] + ph = ','.join('?' * len(batch)) + cur.execute(f"SELECT {pk}, {','.join(fields)} FROM {table} WHERE {id_col} IN ({ph})", batch) + for row in cur.fetchall(): + result[row[0]] = row[1:] + return result + +def get_ids(rows, typ): + return list({rid for _, _, _, _, _, _, _, _, ids_by_typ, _ in rows for rid in ids_by_typ[typ]}) + +rec_det = fetch_details(cur, 'RECEPT', 'ID', 'ID', ['LEK','DSIG'], get_ids(rows,'Rec')) +vyka_det = fetch_details(cur, 'DOKLADD', 'ID', 'ID', ['KOD','DDGN'], get_ids(rows,'VykA')) +files_det = fetch_details(cur, 'FILES', 'ID', 'ID', ['FILENAME','DATUM'], get_ids(rows,'Files')) +medlab_det = fetch_details(cur, 'HISTDOC', 'ID', 'ID', ['DATUM','TYP'], get_ids(rows,'MEDLAB')) +lab_det = fetch_details(cur, 'LABVH', 'IDVH', 'IDVH', ['DATUM','CISLO'], get_ids(rows,'Lab')) +ock_det = fetch_details(cur, 'OCKZAZ', 'ID', 'ID', ['DATUM','LATKA'], get_ids(rows,'Ock')) +nes_det = fetch_details(cur, 'NES', 'ID', 'ID', ['ZACNES','KONNES'], get_ids(rows,'Nes')) +lec_det = fetch_details(cur, 'LECD', 'ID', 'ID', ['KOD','DATOSE'], get_ids(rows,'Lec')) +spec_det = fetch_details(cur, 'SPECVYS', 'IDSPECVYS','IDSPECVYS',['TYP','DATUM'], get_ids(rows,'SpecVys')) +pla_det = fetch_details(cur, 'PLA', 'IDPLA', 'IDPLA', ['DATUM','CENA','DOKLAD'], get_ids(rows,'PlaPac')) + +conn.close() +print(f"Načteno {len(rows)} dekurzů") + +# ── Styly ────────────────────────────────────────────────────────────────── +tenka_cara = Side(style='thin', color='AAAAAA') +ohraniceni = Border(left=tenka_cara, right=tenka_cara, top=tenka_cara, bottom=tenka_cara) +hl_font = Font(bold=True, color="FFFFFF") +hl_fill = PatternFill("solid", fgColor="2E75B6") +r_fill = [PatternFill("solid", fgColor="FFFFFF"), PatternFill("solid", fgColor="DCE6F1")] + +BARVY_LISTU = { + 'Recepty': ('1F6B33', 'E2EFDA'), + 'Výkony': ('2E4057', 'D6E4F0'), + 'Soubory': ('7B3F00', 'FAE5D3'), + 'MedLab': ('4A235A', 'F5EEF8'), + 'Lab': ('145A32', 'D5F5E3'), + 'Očkování': ('7E5109', 'FDEBD0'), + 'Neschop.': ('922B21', 'FADBD8'), + 'Léčiva': ('1A5276', 'D6EAF8'), + 'SpecVys': ('0B5345', 'D1F2EB'), + 'Platby': ('4D5656', 'EAECEE'), + 'Ostatní': ('2C3E50', 'EBF5FB'), +} + +def zapis_hlavicku(ws, sloupce, sirky, barva_hex): + hl_fill_l = PatternFill("solid", fgColor=barva_hex) + for col, (nazev, sirka) in enumerate(zip(sloupce, sirky), start=1): + cell = ws.cell(row=1, column=col, value=nazev) + cell.font = hl_font + cell.fill = hl_fill_l + cell.alignment = Alignment(horizontal='center') + cell.border = ohraniceni + ws.column_dimensions[cell.column_letter].width = sirka + +def zapis_radek(ws, row_i, hodnoty, zarovnani, barva_hex): + fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \ + else PatternFill("solid", fgColor=barva_hex) + for col_i, (val, align) in enumerate(zip(hodnoty, zarovnani), start=1): + cell = ws.cell(row=row_i, column=col_i, value=val) + cell.fill = fill + cell.border = ohraniceni + cell.alignment = Alignment(horizontal=align) + if col_i == 1 and isinstance(val, __import__('datetime').date): + cell.number_format = 'DD.MM.YYYY' + +def hyperlink_cell(ws, row_i, col_i, cil_list, cil_radek, text, barva_hex): + fill = PatternFill("solid", fgColor="FFFFFF") if row_i % 2 == 0 \ + else PatternFill("solid", fgColor=barva_hex) + cell = ws.cell(row=row_i, column=col_i) + cell.value = f'=HYPERLINK("#{cil_list}!A{cil_radek}","{text}")' + cell.font = Font(color="0000FF", underline='single') + cell.fill = fill + cell.border = ohraniceni + cell.alignment = Alignment(horizontal='center') + +# ── Workbook ─────────────────────────────────────────────────────────────── +wb = openpyxl.Workbook() + +# Pořadí listů a jejich konfigurace: (název, typ_bookmarku, detail_dict, sloupce, šířky, pk_label) +LISTY = [ + ('Recepty', 'Rec', rec_det, ['Datum','Jméno','Recept','Dávkování'], [12,25,25,12], None), + ('Výkony', 'VykA', vyka_det, ['Datum','Jméno','Kód výkonu','Diagnóza'], [12,25,14,10], None), + ('Soubory', 'Files', files_det, ['Datum','Jméno','Soubor','Datum souboru'], [12,25,35,14], None), + ('MedLab', 'MEDLAB', medlab_det, ['Datum','Jméno','Typ'], [12,25,15], None), + ('Lab', 'Lab', lab_det, ['Datum','Jméno','Číslo'], [12,25,20], None), + ('Očkování', 'Ock', ock_det, ['Datum','Jméno','Datum očkování','Vakcína'], [12,25,14,30], None), + ('Neschop.', 'Nes', nes_det, ['Datum','Jméno','Od','Do'], [12,25,12,12], None), + ('Léčiva', 'Lec', lec_det, ['Datum','Jméno','Kód','Datum výkonu'], [12,25,12,14], None), + ('SpecVys', 'SpecVys', spec_det, ['Datum','Jméno','Typ vyšetření','Datum vyšetření'], [12,25,25,14], None), + ('Platby', 'PlaPac', pla_det, ['Datum','Jméno','Datum platby','Částka','Doklad'], [12,25,14,12,15], None), + ('Ostatní', None, None, ['Datum','Jméno','Typ','ID','Název'], [12,25,12,10,30], None), +] + +# Vytvoříme listy +ws_d = wb.active +ws_d.title = "Dekurz" +ws_listy = {} +for nazev, *_ in LISTY: + ws_listy[nazev] = wb.create_sheet(nazev) + +# Záhlaví listů +for nazev, typ, det, sloupce, sirky, _ in LISTY: + barva_hl, _ = BARVY_LISTU[nazev] + zapis_hlavicku(ws_listy[nazev], sloupce, sirky, barva_hl) + ws_listy[nazev].freeze_panes = 'A2' + +# Záhlaví Dekurz +nazvy_d = ['Datum', 'Čas', 'Lékař', 'Jméno', 'Rodné číslo', 'Pojišťovna'] + TOP_TYPY + ['Ostatní'] +sirky_d = [12, 8, 8, 25, 14, 12 ] + [8]*10 + [8] +zapis_hlavicku(ws_d, nazvy_d, sirky_d, '2E75B6') +ws_d.freeze_panes = 'A2' +ws_d.auto_filter.ref = f"A1:Q{len(rows)+1}" + +# Aktuální řádek pro každý list +row_ptr = {nazev: 2 for nazev, *_ in LISTY} + +# ── Plnění dat ───────────────────────────────────────────────────────────── +def get_det_hodnoty(typ, rid, datum, jmeno_cel): + """Vrátí seznam hodnot pro řádek detailního listu.""" + if typ == 'Rec': + d = rec_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[0] or '', d[1] or ''] + elif typ == 'VykA': + d = vyka_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[0] or '', (d[1] or '').strip()] + elif typ == 'Files': + d = files_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[0] or '', d[1]] + elif typ == 'MEDLAB': + d = medlab_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[1] or ''] + elif typ == 'Lab': + d = lab_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[1] or ''] + elif typ == 'Ock': + d = ock_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[0], d[1] or ''] + elif typ == 'Nes': + d = nes_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[0], d[1]] + elif typ == 'Lec': + d = lec_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[0] or '', d[1]] + elif typ == 'SpecVys': + d = spec_det.get(rid, ('', '')) + return [datum, jmeno_cel, d[0] or '', d[1]] + elif typ == 'PlaPac': + d = pla_det.get(rid, ('', '', '')) + return [datum, jmeno_cel, d[0], d[1], d[2] or ''] + return [] + +ZAROVNANI = { + 'Recepty': ['left','left','left','center'], + 'Výkony': ['left','left','center','center'], + 'Soubory': ['left','left','left','left'], + 'MedLab': ['left','left','center'], + 'Lab': ['left','left','center'], + 'Očkování': ['left','left','left','left'], + 'Neschop.': ['left','left','left','left'], + 'Léčiva': ['left','left','center','left'], + 'SpecVys': ['left','left','left','left'], + 'Platby': ['left','left','left','right','center'], + 'Ostatní': ['left','left','center','center','left'], +} + +for row_i, (datum, cas, zkratka, jmeno_cel, rodcis, poj, top, ostatni, ids_by_typ, ids_ostatni) in enumerate(rows, start=2): + # Ohraničení řádku Dekurz + fill_d = r_fill[row_i % 2] + for col_i in range(1, len(nazvy_d) + 1): + ws_d.cell(row=row_i, column=col_i).fill = fill_d + ws_d.cell(row=row_i, column=col_i).border = ohraniceni + + ws_d.cell(row=row_i, column=1, value=datum).number_format = 'DD.MM.YYYY' + ws_d.cell(row=row_i, column=2, value=str(cas)[:5] if cas else '').alignment = Alignment(horizontal='center') + ws_d.cell(row=row_i, column=3, value=zkratka or '').alignment = Alignment(horizontal='center') + ws_d.cell(row=row_i, column=4, value=jmeno_cel) + ws_d.cell(row=row_i, column=5, value=rodcis or '') + ws_d.cell(row=row_i, column=6, value=poj or '').alignment = Alignment(horizontal='center') + + # Sloupce bookmarků + for col_off, (typ, pocet) in enumerate(zip(TOP_TYPY, top)): + col_i = 7 + col_off + if pocet == 0: + continue + # Najdi název listu pro tento typ + nazev_listu = next((n for n, t, *_ in LISTY if t == typ), None) + if nazev_listu and ids_by_typ[typ]: + _, barva_ll = BARVY_LISTU[nazev_listu] + hyperlink_cell(ws_d, row_i, col_i, nazev_listu, row_ptr[nazev_listu], pocet, barva_ll[1:] if len(barva_ll) > 6 else 'DCE6F1') + # Zapiš řádky na detailní list + ws_det = ws_listy[nazev_listu] + barva_hl, barva_r = BARVY_LISTU[nazev_listu] + for rid in ids_by_typ[typ]: + hodnoty = get_det_hodnoty(typ, rid, datum, jmeno_cel) + zarovnani_l = ZAROVNANI.get(nazev_listu, ['left']*10) + zapis_radek(ws_det, row_ptr[nazev_listu], hodnoty, zarovnani_l, barva_r) + row_ptr[nazev_listu] += 1 + else: + ws_d.cell(row=row_i, column=col_i, value=pocet).alignment = Alignment(horizontal='center') + + # Ostatní + if ostatni: + ws_det = ws_listy['Ostatní'] + barva_hl, barva_r = BARVY_LISTU['Ostatní'] + hyperlink_cell(ws_d, row_i, 17, 'Ostatní', row_ptr['Ostatní'], ostatni, barva_r) + for typ, rid, nazev in ids_ostatni: + zapis_radek(ws_det, row_ptr['Ostatní'], + [datum, jmeno_cel, typ, rid, nazev], + ZAROVNANI['Ostatní'], barva_r) + row_ptr['Ostatní'] += 1 + +# Autofiltr na detailních listech +for nazev, *_ in LISTY: + ws = ws_listy[nazev] + max_col = ws.max_column + max_row = ws.max_row + if max_row > 1: + ws.auto_filter.ref = f"A1:{ws.cell(row=1, column=max_col).column_letter}{max_row}" + +# Smazat starý report +for stary in glob.glob(os.path.join(VYSTUPNI_ADRESAR, f'* {NAZEV_REPORTU}.xlsx')): + os.remove(stary) + print(f"Smazán: {stary}") + +# Uložit nový +os.makedirs(VYSTUPNI_ADRESAR, exist_ok=True) +casova_znacka = datetime.now().strftime('%Y-%m-%d %H-%M-%S') +vystup = os.path.join(VYSTUPNI_ADRESAR, f'{casova_znacka} {NAZEV_REPORTU}.xlsx') +wb.save(vystup) +print(f"Uloženo: {vystup}") +for nazev, *_ in LISTY: + print(f" {nazev}: {row_ptr[nazev]-2} řádků") diff --git a/MedicusWithClaudePoj/registrace_2025_dnes.xlsx b/MedicusWithClaudePoj/registrace_2025_dnes.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..db695e6f5f4793f372e4364f710795cd1d59eb4b GIT binary patch literal 20301 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 MJV4_+n4@q11M=v}X#fBK literal 0 HcmV?d00001 diff --git a/MedicusWithClaudePoj/registrace_report.py b/MedicusWithClaudePoj/registrace_report.py new file mode 100644 index 0000000..286e2b4 --- /dev/null +++ b/MedicusWithClaudePoj/registrace_report.py @@ -0,0 +1,117 @@ +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment +from datetime import date, timedelta +import os + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +zacatek = date(2025, 1, 1) +konec = date.today() +dny = [] +d = zacatek +while d <= konec: + dny.append(d) + d += timedelta(days=1) + +print(f"Počítám {len(dny)} dní ({zacatek} – {konec})...") + +vysledky = [] +for i, den in enumerate(dny): + # Počet registrovaných + cur.execute(f""" + SELECT COUNT(*) FROM KAR + WHERE vyrazen = 'N' + AND EXISTS ( + SELECT id FROM registr r + JOIN icp i ON r.idicp = i.idicp + WHERE r.idpac = kar.idpac + AND r.datum <= '{den}' + AND (r.datum_zruseni IS NULL OR r.datum_zruseni >= '{den}') + AND r.priznak IN ('V','D','A') + AND i.icp = '09305001' + AND i.odb = '001' + ) + """) + pocet = cur.fetchone()[0] + + # Zaregistrovaní tento den + cur.execute(f""" + SELECT k.RODCIS, k.PRIJMENI, k.JMENO + FROM REGISTR r JOIN KAR k ON k.IDPAC = r.IDPAC + WHERE r.datum = '{den}' + AND r.priznak IN ('V','D','A') + ORDER BY k.PRIJMENI, k.JMENO + """) + zaregistrovani = [f"{row[0]} {row[1].strip()} {row[2]}" for row in cur.fetchall()] + + # Odregistrovaní tento den + cur.execute(f""" + SELECT k.RODCIS, k.PRIJMENI, k.JMENO + FROM REGISTR r JOIN KAR k ON k.IDPAC = r.IDPAC + WHERE r.datum_zruseni = '{den}' + ORDER BY k.PRIJMENI, k.JMENO + """) + odregistrovani = [f"{row[0]} {row[1].strip()} {row[2]}" for row in cur.fetchall()] + + vysledky.append((den, pocet, zaregistrovani, odregistrovani)) + if (i + 1) % 30 == 0: + print(f" {i+1}/{len(dny)}: {den} → {pocet}") + +conn.close() + +# Excel +wb = openpyxl.Workbook() +ws = wb.active +ws.title = "Registrace" + +hlavicka_font = Font(bold=True, color="FFFFFF") +hlavicka_fill = PatternFill("solid", fgColor="2E75B6") +ws.column_dimensions['A'].width = 14 +ws.column_dimensions['B'].width = 14 +ws.column_dimensions['C'].width = 10 +ws.column_dimensions['D'].width = 45 +ws.column_dimensions['E'].width = 45 + +for col, nazev in enumerate(['Datum', 'Registrovaných', 'Změna', 'Zaregistrováno', 'Odregistrováno'], start=1): + cell = ws.cell(row=1, column=col, value=nazev) + cell.font = hlavicka_font + cell.fill = hlavicka_fill + cell.alignment = Alignment(horizontal='center') + +predchozi = None +for row_i, (den, pocet, zaregistrovani, odregistrovani) in enumerate(vysledky, start=2): + ws.cell(row=row_i, column=1, value=den).number_format = 'DD.MM.YYYY' + ws.cell(row=row_i, column=2, value=pocet).alignment = Alignment(horizontal='center') + + if predchozi is not None: + zmena = pocet - predchozi + cell = ws.cell(row=row_i, column=3, value=zmena) + cell.alignment = Alignment(horizontal='center') + if zmena > 0: + cell.font = Font(color="00AA00", bold=True) + elif zmena < 0: + cell.font = Font(color="CC0000", bold=True) + predchozi = pocet + + if zaregistrovani: + cell = ws.cell(row=row_i, column=4, value="\n".join(zaregistrovani)) + cell.alignment = Alignment(wrap_text=True, vertical='top') + cell.font = Font(color="00AA00") + + if odregistrovani: + cell = ws.cell(row=row_i, column=5, value="\n".join(odregistrovani)) + cell.alignment = Alignment(wrap_text=True, vertical='top') + cell.font = Font(color="CC0000") + +ws.freeze_panes = 'A2' + +vystup = os.path.join(os.path.dirname(__file__), 'registrace_2025_dnes.xlsx') +wb.save(vystup) +print(f"\nUloženo: {vystup}") From ba594c373b18a059ec4a5093b45aa2149b40ffa8 Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Wed, 1 Apr 2026 06:05:21 +0200 Subject: [PATCH 4/4] notebook vb --- MedicusWithClaudeDekurz/DEKURZY_REPORT.md | 140 ++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 MedicusWithClaudeDekurz/DEKURZY_REPORT.md diff --git a/MedicusWithClaudeDekurz/DEKURZY_REPORT.md b/MedicusWithClaudeDekurz/DEKURZY_REPORT.md new file mode 100644 index 0000000..a0b9033 --- /dev/null +++ b/MedicusWithClaudeDekurz/DEKURZY_REPORT.md @@ -0,0 +1,140 @@ +# Dekurzy report – dokumentace + +## Co report dělá + +Generuje Excel soubor s přehledem všech dekurzů z ordinace MUDr. Buzalkové za zadané období. +Hlavní list **Dekurz** zobrazuje každý dekurz jako jeden řádek. Čísla v sloupcích jsou klikatelné hyperlinkové zkratky, které přeskočí na příslušný detailní list (Recepty, Výkony, Soubory apod.). + +--- + +## Spuštění + +``` +python dekurz_report.py +``` + +**Vstupní parametry** (nastavit přímo v souboru): + +| Proměnná | Výchozí hodnota | Popis | +|---|---|---| +| `DATUM_OD` | `2025-01-01` | Začátek období | +| `DATUM_DO` | dnešní datum | Konec období (automaticky) | +| `VYSTUPNI_ADRESAR` | `u:\Dropbox\Ordinace\Reporty` | Kam se ukládá | +| `NAZEV_REPORTU` | `Dekurzy` | Část názvu souboru | + +**Výstupní soubor:** `YYYY-MM-DD HH-MM-SS Dekurzy.xlsx` +Starý soubor se stejným názvem je automaticky smazán. + +--- + +## Zdroj dat + +Databáze Firebird: `localhost:c:\medicus 3\data\medicus.fdb` +Připojení: SYSDBA / masterkey, charset win1250 + +### Hlavní dotaz + +```sql +SELECT d.DATUM, d.CAS, u.ZKRATKA, k.PRIJMENI, k.JMENO, k.RODCIS, k.POJ, d.DEKURS +FROM DEKURS d +JOIN KAR k ON k.IDPAC = d.IDPAC +LEFT JOIN UZIVATEL u ON u.IDUZI = d.IDUZI +WHERE d.DATUM >= '2025-01-01' AND d.DATUM <= dnes +ORDER BY d.DATUM DESC, d.CAS DESC, k.PRIJMENI, k.JMENO +``` + +Řazení: nejnovější záznamy nahoře. + +--- + +## Jak funguje parsování RTF bookmarků + +Každý dekurz je uložen jako RTF blob ve sloupci `DEKURS.DEKURS`. +Medicus do RTF hlavičky zapisuje **bookmarky** – hypertextové odkazy na propojené záznamy: + +``` +{\info{\bookmarks "ATORIS","Rec:322528",17;"01543","VykA:189603",8}} +``` + +Formát: `"název","TYP:ID",číslo_stylu` + +Skript parsuje regex `"([^"]+)","([A-Za-z]+):(\d+)"` a extrahuje typ a ID záznamu. + +### Typy bookmarků a jejich tabulky + +| Bookmark | List v Excelu | Tabulka v DB | PK | Zobrazované sloupce | +|---|---|---|---|---| +| `Rec` | Recepty | `RECEPT` | `ID` | LEK, DSIG (lék, dávkování) | +| `VykA` | Výkony | `DOKLADD` | `ID` | KOD, DDGN (kód výkonu, diagnóza) | +| `Files` | Soubory | `FILES` | `ID` | FILENAME, DATUM | +| `MEDLAB` | MedLab | `HISTDOC` | `ID` | DATUM, TYP (žádanka do laboratoře) | +| `Lab` | Lab | `LABVH` | `IDVH` | DATUM, CISLO (výsledky laboratoře) | +| `Ock` | Očkování | `OCKZAZ` | `ID` | DATUM, LATKA (vakcína) | +| `Nes` | Neschop. | `NES` | `ID` | ZACNES, KONNES (od – do) | +| `Lec` | Léčiva | `LECD` | `ID` | KOD, DATOSE (léčivo podané v ordinaci) | +| `SpecVys` | SpecVys | `SPECVYS` | `IDSPECVYS` | TYP, DATUM (Tonotrack, holter…) | +| `PlaPac` | Platby | `PLA` | `IDPLA` | DATUM, CENA, DOKLAD | +| ostatní | Ostatní | – | – | TYP, ID, Název (formuláře, poukazy…) | + +**Ostatní typy** (méně časté): +`ORTOPE` – ePoukaz na ortopedickou pomůcku +`ZDRINF` – Žádost o předání zdravotních informací +`PROHLAS` – Prohlášení +`POTDPN` – Potvrzení DPN +`MOTORVO` – Posudek motorového vozidla +`LAZPEC` – Lázně +`VypZdrD` – Výpis ze zdravotní dokumentace +`VYMLIST` – Výměnný list +`PouRTG` – Poukaz RTG +`ZPUPRN` – Způsobilost k práci/řízení +`EPOSMRO` – ePosudek MRO +`ZNESUP` – Potvrzení neschopnosti uchazeče o zaměstnání + +> Všechny výše uvedené jdou do listu **Ostatní** s uvedením typu, ID a názvu. + +--- + +## Struktura Excel souboru + +### List Dekurz (hlavní) + +| Sloupec | Zdroj | Popis | +|---|---|---| +| Datum | `DEKURS.DATUM` | Datum dekurzu | +| Čas | `DEKURS.CAS` | Čas (HH:MM) | +| Lékař | `UZIVATEL.ZKRATKA` | MBU / VBU / ISE | +| Jméno | `KAR.PRIJMENI + JMENO` | Formát: `Příjmení, I.` | +| Rodné číslo | `KAR.RODCIS` | | +| Pojišťovna | `KAR.POJ` | Kód pojišťovny (111, 201…) | +| Rec … PlaPac | RTF bookmark | Počet záznamů – **klikací hyperlink** na detailní list | +| Ostatní | RTF bookmark | Počet ostatních typů – hyperlink na list Ostatní | + +### Detailní listy + +Každý list má: +- Záhlaví s vlastní barevnou kombinací +- Sloupce: Datum, Jméno + specifické sloupce dle typu +- Střídání bílých a barevných řádků +- Tenké šedé ohraničení všech buněk +- Zmrazený první řádek +- Autofiltr + +--- + +## Technické poznámky + +- Firebird limit `IN (...)` je 1500 hodnot – dotazy na detaily se automaticky dělí do dávek po 1000 +- RTF blob je čten přes `blob.read()` nebo přímo jako string +- Jméno pacienta: `Příjmení, I.` (iniciála prvního písmene jména) +- Chybová hláška `BlobReader.close: invalid BLOB handle` je neškodná – GC uzavírá handlery po odpojení DB + +--- + +## Scheduled Task + +Spouštěcí příkaz: +``` +python "C:\Users\vlado\PycharmProjects\Medicus\MedicusWithClaudeDekurz\dekurz_report.py" +``` + +Doporučené spouštění: každý den ráno (např. 6:00), aby byl vždy čerstvý soubor v Dropboxu.