Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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.
|
||||
@@ -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 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`)
|
||||
@@ -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.")
|
||||
@@ -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 |
|
||||
@@ -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()
|
||||
@@ -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.
@@ -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}")
|
||||
@@ -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í
|
||||
@@ -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'))
|
||||
Reference in New Issue
Block a user