notebook vb

This commit is contained in:
2026-03-28 11:05:19 +01:00
parent 45a1642df8
commit d4b9531f57
4 changed files with 576 additions and 0 deletions
Submodule .claude/worktrees/vigorous-davinci added at 45a1642df8
+51
View File
@@ -0,0 +1,51 @@
# Medicus + Claude kontext projektu
## Přečti si prosím tyto soubory na začátku každé konverzace
1. `MedicusWithClaude/CLAUDE_NOTES.md` hlavní poznámky: DB připojení, klíčové tabulky, RTF formát, import pipeline
2. `MedicusWithClaudeSelects/SELECTS.md` SQL dotazy (registrovaní pacienti atd.)
3. `MedicusWithClaudeSelects/FakturaceADavky.md` tabulky FAK, FAKDET, FAKDAV, PORTAL, kódování dávek
## O projektu
Firebird DB lékařského SW **Medicus** pro praktického lékaře. Lékařka je **MUDr. Buzalková Michaela** (IDUZI=4). Vladimír Buzalka (IDUZI=6) je manžel a správce systému s ním probíhají tyto konverzace.
## Připojení k DB
```python
import fdb
conn = fdb.connect(
dsn=r'localhost:c:\medicus 3\data\medicus.fdb',
user='SYSDBA', password='masterkey', charset='win1250'
)
```
## Počítač "reporter"
Na tomto stroji běží pravidelná automatická tvorba reportů. Připojení k DB funguje stejně jako výše (ostrá DB, čerstvá data). Hlavní report skript: `MedicusWithClaudeFaktury/faktury_report.py`
### Co report dělá
- Generuje Excel s listy: **FAK**, **FAKDET**, **PORTAL**, **PORTAL_DATA**
- Ukládá do `u:\Dropbox\!!!Days\Downloads Z230\`
- Název souboru: `YYYY-MM-DD_HH-MM-SS_faktury.xlsx`
- Maže předchozí verze ze stejné složky
- Řazení: nejnovější záznamy nahoře
- Hyperlinky: FAK → FAKDET, PORTAL ↔ PORTAL_DATA
### Důležité kódování dávek
KDAVKA/FDAVKA jsou v **CP852** (DOS). fdb je vrací jako string dekódovaný win1250 nutno re-enkódovat:
```python
spravny_text = s.encode('cp1250', errors='replace').decode('cp852', errors='replace')
```
## Klíčové adresáře
| Adresář | Obsah |
|---|---|
| `MedicusWithClaude/` | průzkumné skripty, import pipeline (s03soubory.py) |
| `MedicusWithClaudeSelects/` | SQL dotazy, dokumentace tabulek |
| `MedicusWithClaudeFaktury/` | fakturační reporty |
## Co chceme na reporteru nastavit
Pravidelná automatická tvorba reportu `faktury_report.py` např. každý den ráno, aby byl vždy čerstvý Excel s fakturami v Dropboxu.
+272
View File
@@ -0,0 +1,272 @@
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') + '_faktury.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('_faktury.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')
LINK_FONT = Font(color='0563C1', underline='single')
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
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, 40)
def fmt(val):
if val is None:
return ''
return val
# =====================
# List 1 FAK
# =====================
ws1 = wb.active
ws1.title = 'FAK'
cur.execute('''
SELECT
ID, CISFAK, POJ, DATUMOD, DATUMDO, DATVYS, DATODE,
VYKONY, KAPITACE, ZALOHA, CENA, ZAPLACENO, ZUM, HOSPAUSAL,
PROPLACENO, SPLAT, DRUH, TYP, ROK, OBDOB,
NAZFAK, POZFAK, OBDFAK,
ICO, BANKA, UCET,
ODJMENO, ODULICE, ODMISTO, ODPSC,
PLNAZEV, PLULICE, PLMISTO, PLPSC,
ICZ, ICZ1, IDICZ, PORCISLO, DRUHPOJ, POZNAMKA
FROM FAK
ORDER BY ID DESC
''')
fak_cols = [d[0] for d in cur.description]
fak_rows = cur.fetchall()
ws1.append(fak_cols)
for i, row in enumerate(fak_rows, start=2):
ws1.append([fmt(v) for v in row])
if i % 2 == 0:
for cell in ws1[i]:
cell.fill = ZEBRA_FILL
style_header(ws1)
ws1.freeze_panes = 'A2'
autofit(ws1)
# =====================
# List 2 FAKDET
# =====================
ws2 = wb.create_sheet('FAKDET')
cur.execute('''
SELECT
fd.ID, fd.IDFAK,
f.CISFAK, f.POJ, f.DATUMOD, f.DATUMDO, f.ROK,
fd.ICP, fd.ODB, fd.IDUZI,
u.PRIJMENI, u.JMENO,
fd.CENAVYK, fd.CENALEC, fd.CENAKAP
FROM FAKDET fd
JOIN FAK f ON f.ID = fd.IDFAK
LEFT JOIN UZIVATEL u ON u.IDUZI = fd.IDUZI
ORDER BY fd.IDFAK DESC, fd.ID DESC
''')
det_cols = [d[0] for d in cur.description]
det_rows = cur.fetchall()
ws2.append(det_cols)
for i, row in enumerate(det_rows, start=2):
ws2.append([fmt(v) for v in row])
if i % 2 == 0:
for cell in ws2[i]:
cell.fill = ZEBRA_FILL
style_header(ws2)
ws2.freeze_panes = 'A2'
autofit(ws2)
# =====================
# List 3 PORTAL (krátké sloupce)
# =====================
ws3 = wb.create_sheet('PORTAL')
cur.execute('''
SELECT ID, IDFAK, ODESLANO, CHYBA, STAV, ID_PODANI, IDPODANI, IDCERT,
DAVKA_ROK, DAVKA_DISK, DAVKA_IDICZ, DAVKA_DATUMOD, DAVKA_DATUMDO,
DAVKA_CASTKA, BB_DAVKA, BB_FAKTURA
FROM PORTAL
ORDER BY ID DESC
''')
portal_cols = [d[0] for d in cur.description]
portal_rows = cur.fetchall()
ws3.append(portal_cols)
for i, row in enumerate(portal_rows, start=2):
ws3.append([fmt(v) for v in row])
if i % 2 == 0:
for cell in ws3[i]:
cell.fill = ZEBRA_FILL
style_header(ws3)
ws3.freeze_panes = 'A2'
autofit(ws3)
# =====================
# List 4 PORTAL_DATA (BLOBy)
# =====================
ws4 = wb.create_sheet('PORTAL_DATA')
cur.execute('''
SELECT ID, ID_PODANI, DAVKA_ROK, DAVKA_DISK, DAVKA_DATUMOD, DAVKA_DATUMDO,
KDAVKA, FDAVKA, DATA
FROM PORTAL
ORDER BY ID DESC
''')
pdata_rows = cur.fetchall()
pdata_cols = ['ID', 'ID_PODANI', 'DAVKA_ROK', 'DAVKA_DISK', 'DAVKA_DATUMOD', 'DAVKA_DATUMDO',
'KDAVKA', 'FDAVKA', 'DATA']
ws4.append(pdata_cols)
WRAP = Alignment(wrap_text=True, vertical='top')
# Indexy BLOB sloupců: KDAVKA=6, FDAVKA=7, DATA=8
# DATA je XML v win1250, KDAVKA/FDAVKA jsou latin2
BLOB_ENCODINGS = {6: 'cp852', 7: 'cp852', 8: 'cp1250'}
def decode_blob(v, enc):
if hasattr(v, 'read'):
raw = v.read()
else:
raw = v
if not raw:
return ''
if isinstance(raw, str):
# fdb dekódoval jako win1250 vrátíme zpět na bytes, pak dekódujeme správně
raw = raw.encode('cp1250', errors='replace')
return raw.decode(enc, errors='replace')
for i, row in enumerate(pdata_rows, start=2):
out = []
for col_idx, v in enumerate(row):
if col_idx in BLOB_ENCODINGS:
enc = BLOB_ENCODINGS[col_idx]
out.append(decode_blob(v, enc))
else:
out.append(fmt(v))
ws4.append(out)
if i % 2 == 0:
for cell in ws4[i]:
cell.fill = ZEBRA_FILL
for cell in ws4[i]:
cell.alignment = WRAP
ws4.row_dimensions[i].height = 80
style_header(ws4)
ws4.freeze_panes = 'A2'
# Šířky sloupců PORTAL_DATA
for col, width in zip(['A','B','C','D','E','F','G','H','I'], [6, 26, 8, 6, 12, 12, 80, 20, 60]):
ws4.column_dimensions[col].width = width
# =====================
# Hyperlinky PORTAL ↔ PORTAL_DATA
# =====================
# Mapa: portal_id → řádek v každém listu
portal_id_to_ws3_row = {}
for i, row in enumerate(portal_rows, start=2):
portal_id_to_ws3_row[row[0]] = i # row[0] = ID
portal_id_to_ws4_row = {}
for i, row in enumerate(pdata_rows, start=2):
portal_id_to_ws4_row[row[0]] = i # row[0] = ID
# PORTAL → PORTAL_DATA (sloupec A = ID)
for i, row in enumerate(portal_rows, start=2):
pid = row[0]
cell = ws3.cell(row=i, column=1)
if pid in portal_id_to_ws4_row:
cell.hyperlink = f'#PORTAL_DATA!A{portal_id_to_ws4_row[pid]}'
cell.font = LINK_FONT
# PORTAL_DATA → PORTAL (sloupec A = ID)
for i, row in enumerate(pdata_rows, start=2):
pid = row[0]
cell = ws4.cell(row=i, column=1)
if pid in portal_id_to_ws3_row:
cell.hyperlink = f'#PORTAL!A{portal_id_to_ws3_row[pid]}'
cell.font = LINK_FONT
cell.alignment = WRAP
# =====================
# Hyperlinky FAK → FAKDET
# =====================
# Mapa: IDFAK → první řádek na listu FAKDET (řádek 1 = záhlaví, data od 2)
idfak_to_row = {}
for i, row in enumerate(det_rows, start=2):
idfak = row[1] # IDFAK je druhý sloupec
if idfak not in idfak_to_row:
idfak_to_row[idfak] = i
# Přidat sloupec "→FAKDET" jako první sloupec na listu FAK
ws1.insert_cols(1)
ws1.cell(row=1, column=1, value='FAKDET').fill = HEADER_FILL
ws1.cell(row=1, column=1).font = HEADER_FONT
ws1.cell(row=1, column=1).alignment = Alignment(horizontal='center')
ws1.column_dimensions['A'].width = 9
for i, row in enumerate(fak_rows, start=2):
fak_id = row[0] # ID je první sloupec z DB
cell = ws1.cell(row=i, column=1)
if fak_id in idfak_to_row:
target_row = idfak_to_row[fak_id]
cell.value = '>> det'
cell.hyperlink = f'#FAKDET!A{target_row}'
cell.font = LINK_FONT
else:
cell.value = ''
# =====================
# 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'FAK: {len(fak_rows)} radku, FAKDET: {len(det_rows)} radku, PORTAL: {len(portal_rows)} radku\n'.encode('utf-8'))
+252
View File
@@ -0,0 +1,252 @@
# Fakturace a dávky poznámky pro Clauda
## Přehled tabulek
### FAK faktury pojišťovnám
- **844 záznamů** (k 2026-03-28)
- Primární klíč: `ID`
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `CISFAK` | číslo faktury (10 znaků) |
| `POJ` | pojišťovna (3 znaky, např. 111=VZP, 205=ČPZP, 207=OZP) |
| `ICZ`, `ICZ1` | IČZ ordinace |
| `IDICZ` | FK → ICZ.IDICZ (jen jedna ordinace, neřešit) |
| `PORCISLO` | pořadové číslo |
| `DATUMOD`, `DATUMDO` | období faktury (oddo) |
| `DATVYS` | datum vystavení |
| `DATODE` | datum odeslání |
| `OBDOB` | období (5 znaků, např. "20260") |
| `VYKONY` | částka za výkony (Kč) |
| `KAPITACE` | kapitační platba (Kč) |
| `ZALOHA` | záloha |
| `CENA` | celková cena faktury (Kč) |
| `DRUH` | druh faktury (např. "konečná") |
| `TYP` | typ (1 znak) |
| `ROK` | rok |
| `SPLAT` | datum splatnosti |
| `PROPLACENO` | datum proplacení (NULL = nezaplaceno) |
| `ZAPLACENO` | zaplacená částka |
| `ZUM` | ? |
| `HOSPAUSAL` | hospitalizační/paušální? |
| `FDAVKA` | BLOB odkaz na dávku |
| `KAPDETAIL`, `AGRDETAIL` | BLOBy detaily kapitace/agregace |
| `NAZFAK`, `POZFAK`, `OBDFAK` | název, poznámka, období faktury (texty) |
| `ICO`, `BANKA`, `UCET` | IČO, banka, účet |
| `ODJMENO/ULICE/MISTO/PSC` | adresa odesílatele |
| `PLNAZEV/ULICE/MISTO/PSC` | adresa plátce (pojišťovny) |
| `DRUHPOJ` | druh pojištění |
---
### FAKDET finanční detail faktury
- **1021 záznamů**
- Vazba: `IDFAK` → FAK.ID
- Typicky 12 řádky na fakturu (breakdown podle lékaře/IDUZI)
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `IDFAK` | FK → FAK.ID |
| `ICP` | IČP ordinace (např. 09305001) |
| `ODB` | odbornost (001 = praktický lékař) |
| `IDUZI` | FK → UZIVATEL.IDUZI který lékař |
| `CENAVYK` | výkony (Kč) |
| `CENALEC` | léky (Kč) |
| `CENAKAP` | kapitace (Kč) |
**Poznámka:** IDUZI=0 = systémový záznam (kapitace bez konkrétního lékaře).
---
### FAKDAV dávky zahrnuté ve faktuře
- **1296 záznamů**
- Vazba: `IDFAK` → FAK.ID
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `IDFAK` | FK → FAK.ID |
| `CISDAV` | číslo dávky |
| `DRUH` | druh dávky |
| `CENA` | celková cena dávky |
| `CENAVYK` | výkony |
| `CENALEC` | léky |
| `CENAPAU` | paušál |
| `ODB` | odbornost |
| `ICP` | IČP |
---
### PORTAL elektronické podání pojišťovně
- **180 záznamů**
- Vazba: `IDFAK` → FAK.ID
| Sloupec | Popis |
|---|---|
| `ID` | primární klíč |
| `IDFAK` | FK → FAK.ID |
| `ODESLANO` | timestamp odeslání |
| `DATA`, `KDAVKA`, `FDAVKA` | BLOBy pravděpodobně XML dávky |
| `CHYBA` | příznak chyby (1 znak) |
| `STAV` | stav podání (smallint) |
| `ID_PODANI` | ID podání u pojišťovny (32 znaků, UUID) |
| `IDPODANI` | interní ID podání |
| `IDCERT` | certifikát použitý pro podání |
| `DAVKA_ROK`, `DAVKA_DISK`, `DAVKA_IDICZ` | metadata dávky |
| `BB_DAVKA`, `BB_FAKTURA` | ? |
| `DAVKA_DATUMOD`, `DAVKA_DATUMDO` | období dávky |
| `DAVKA_CASTKA` | částka dávky |
---
## Schéma vazeb
```
FAK (faktura)
├── FAKDET (IDFAK → FAK.ID) finanční breakdown podle lékaře
├── FAKDAV (IDFAK → FAK.ID) seznam dávek ve faktuře
├── PORTAL (IDFAK → FAK.ID) viz níže, IDFAK bývá NULL
└── ICZ (IDICZ → ICZ.IDICZ) ordinace (jen jedna, neřešit)
```
---
## UZIVATEL lékaři a uživatelé
| IDUZI | Příjmení | Jméno | Zkratka | Aktivní |
|---|---|---|---|---|
| 2 | Jourová | Jana | JOU | F (neaktivní) |
| 3 | Sestra | — | SES | F (neaktivní) |
| 4 | Buzalková | Michaela | MBU | T **lékařka, manželka** |
| 5 | Ševčíková | Ivana | ISE | T |
| 6 | Buzalka | Vladimír | VBU | T **uživatel (já)** |
---
## Ukázkové záznamy
### FAK ID=856 (poslední)
- Faktura č. 260026, pojišťovna **205 (ČPZP)**, prosinec 2025
- Vystavena: 2026-03-12, druh: konečná
- VYKONY: 2 528,99 Kč, KAPITACE: 0, CENA: **2 528,99 Kč**
- ZAPLACENO: NULL, PROPLACENO: NULL
FAKDET k FAK ID=856:
| ID | IDUZI | CENAVYK | CENALEC | CENAKAP |
|---|---|---|---|---|
| 1034 | 0 (systém) | 0 | 0 | 1 902,54 |
| 1035 | 4 (Buzalková) | 2 528,99 | 0 | 0 |
### FAK ID=855 (předposlední)
- Faktura č. 260026, pojišťovna **207 (OZP)**, prosinec 2025
- Vystavena: 2026-03-01, druh: konečná
- VYKONY: 85 Kč, KAPITACE: 0, CENA: **85 Kč**
- ZAPLACENO: NULL, PROPLACENO: NULL
FAKDET k FAK ID=855:
| ID | IDUZI | CENAVYK | CENALEC | CENAKAP |
|---|---|---|---|---|
| 1033 | 6 (Buzalka Vladimír) | 85 | 0 | 0 |
---
## PORTAL upřesnění (zjištěno 2026-03-28)
PORTAL **nesouvisí s fakturací**. Je to samostatná agenda evidence **registračních dávek** odeslaných pojišťovně.
### Co PORTAL skutečně je
- Medicus přes PORTAL odesílá pojišťovně dávky s přihlášením/odhlášením pacientů k lékaři (MUDr. Buzalková jako praktický lékař)
- Bez peněz čistě administrativní agenda registrací
- **IDFAK = NULL** u všech registračních podání (není vazba na fakturu)
### Klíčové sloupce
| Sloupec | Popis |
|---|---|
| `KDAVKA` | odeslaná dávka pevný formát (hlavička + řádky I = pacienti, RC, datum) |
| `DATA` | odpověď pojišťovny jako XML (`<statuscode>100</statuscode>` = OK) |
| `ID_PODANI` | ID přidělené pojišťovnou (např. `D01F260118593316.D01`) |
| `DAVKA_DISK` | číslo dávky v roce (sekvenční) |
| `DAVKA_ROK` | rok dávky |
| `DAVKA_DATUMOD/DO` | období registrací v dávce |
| `CHYBA` | F = bez chyby |
### Ukázka KDAVKA (hlavička dávky)
```
DP80093050000900202506 15 1 0 0.00180:6.2.46
H21109305001 179202506001
I 1Příjmení Jméno RC datum_registrace
I 2...
```
### Ukázka DATA (odpověď pojišťovny)
```xml
<ekomunikace timestamp="20260127 063105">
<session id="ff59b436-...">
<statusline>Přijetí registrací proběhlo úspěšně</statusline>
<statuscode>100</statuscode>
<idpodani>D01F260118593316.D01</idpodani>
</session>
</ekomunikace>
```
---
## PORTAL dva typy dávek
### Typ 1 Registrační dávka (DP80)
- KDAVKA začíná `DP80`
- **IDFAK = NULL** bez vazby na fakturu
- Bez peněz (`DAVKA_CASTKA` = NULL)
- Přihlašuje/odhlašuje pacienty k lékaři (MUDr. Buzalková)
- Řádky `I` = pacient (RC, datum registrace)
### Typ 2 Výkonová dávka (DP98)
- KDAVKA začíná `DP98`
- **IDFAK = ID faktury** napojeno na FAK
- Má peníze (`DAVKA_CASTKA`, např. 15 452,91 Kč)
- `STAV = 10` (registrační mají NULL)
- Obsahuje výkony vykázané pojišťovně za dané období
**Struktura výkonové dávky (KDAVKA):**
```
DP98... hlavička dávky (IČP, rok, měsíc, disk, počet případů, částka)
A ... ambulantní případ (RC pacienta, diagnóza)
V ... výkon (datum, kód výkonu, body)
Z ... ZULP (zvlášť účtovaný léčivý přípravek)
L ... lékový řádek (kód, množství, cena)
DP05... druhá část dávky (prevence / jiný typ)
P ... preventivní prohlídka
```
**Odpověď pojišťovny pro výkonovou dávku:**
```xml
<statusline>Přijetí faktury proběhlo úspěšně</statusline>
<statuscode>100</statuscode>
```
---
## Poznámky
- Faktury jsou vystavovány zvlášť pro každou pojišťovnu
- FAKDET má řádky zvlášť pro každého lékaře (IDUZI)
- IDUZI=0 v FAKDET = systémový záznam pro kapitaci
- Kapitace se v FAK.KAPITACE neukazuje (je 0), ale v FAKDET.CENAKAP ano nutno ověřit
- PORTAL = registrační dávky, nesouvisí s fakturací, IDFAK bývá NULL
## Kódování KDAVKA/FDAVKA důležité!
Dávkové soubory (KDAVKA, FDAVKA) jsou uloženy v **CP852** (DOS Latin-2, prahistorické kódování).
fdb je čte přes connection charset win1250 a vrací je jako Python `str` (špatně dekódované).
**Správný postup dekódování:**
```python
# fdb vrátil string dekódovaný jako win1250 musíme to zvrátit
raw_bytes = s.encode('cp1250', errors='replace')
spravny_text = raw_bytes.decode('cp852', errors='replace')
```
- **KDAVKA, FDAVKA** → cp852
- **DATA** (XML odpověď pojišťovny) → cp1250 (win1250), fdb dekóduje správně