Merge remote-tracking branch 'origin/master'

This commit is contained in:
michaela.buzalkova
2026-04-01 06:08:07 +02:00
12 changed files with 2572 additions and 0 deletions
+140
View File
@@ -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.
+289
View File
@@ -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ů")
@@ -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()
@@ -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 11301134 |
| `TOKS` | dokladd + vykony | Kódy 1511815121 |
| `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`)
@@ -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.")
+321
View File
@@ -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 |
+320
View File
@@ -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()
+28
View File
@@ -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()
Binary file not shown.
+117
View File
@@ -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}")
+143
View File
@@ -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í
+237
View File
@@ -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'))