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