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. 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/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/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() diff --git a/MedicusWithClaudePoj/registrace_2025_dnes.xlsx b/MedicusWithClaudePoj/registrace_2025_dnes.xlsx new file mode 100644 index 0000000..db695e6 Binary files /dev/null and b/MedicusWithClaudePoj/registrace_2025_dnes.xlsx differ 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}") 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'))