From 4f586f4b57af967d34cd7b7538d98f6bc6c85c9f Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Mon, 30 Mar 2026 07:42:46 +0200 Subject: [PATCH] =?UTF-8?q?P=C5=99id=C3=A1n=20list=20ED=5FPODANI=20+=20ED?= =?UTF-8?q?=5FPODANI=5FDATA=20do=20faktury=5Freport.py;=20dopln=C4=9Bny=20?= =?UTF-8?q?pozn=C3=A1mky=20o=20eD=C3=A1vk=C3=A1ch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - faktury_report.py: nový list ED_PODANI (ED_BOOKOFSUBMISSIONS) s přehledem podání pojišťovnám - faktury_report.py: nový list ED_PODANI_DATA s dekódovaným obsahem dávek (KDAVKA, REQUEST XML, odpovědi pojišťoven) - Opraveno kódování: KDAVKA=cp1250, REQUEST detekce BOM (utf-16/utf-8), SERVERRESPONSE/PROTOCOL=iso-8859-2 - Hyperlinky ED_PODANI ↔ ED_PODANI_DATA a Faktura → FAK - FakturaceADavky.md: dokumentace ED_* tabulek, portálů pojišťoven, formátů REQUEST XML - Průzkumné skripty: find_edavky_table, explore_hpn, explore_ed_bookofsubmissions, parse_trace_edavky aj. Co-Authored-By: Claude Sonnet 4.6 --- .../decode_portal_data.py | 68 +++++ .../explore_ed_bookofsubmissions.py | 61 +++++ MedicusWithClaudeFaktury/explore_hpn.py | 102 ++++++++ .../explore_hpn_podani.py | 106 ++++++++ MedicusWithClaudeFaktury/faktury_report.py | 220 +++++++++++++++- MedicusWithClaudeFaktury/find_edavky2.py | 146 +++++++++++ MedicusWithClaudeFaktury/find_edavky_table.py | 128 ++++++++++ .../parse_trace_edavky.py | 105 ++++++++ MedicusWithClaudeSelects/FakturaceADavky.md | 240 ++++++++++++++++++ 9 files changed, 1175 insertions(+), 1 deletion(-) create mode 100644 MedicusWithClaudeFaktury/decode_portal_data.py create mode 100644 MedicusWithClaudeFaktury/explore_ed_bookofsubmissions.py create mode 100644 MedicusWithClaudeFaktury/explore_hpn.py create mode 100644 MedicusWithClaudeFaktury/explore_hpn_podani.py create mode 100644 MedicusWithClaudeFaktury/find_edavky2.py create mode 100644 MedicusWithClaudeFaktury/find_edavky_table.py create mode 100644 MedicusWithClaudeFaktury/parse_trace_edavky.py diff --git a/MedicusWithClaudeFaktury/decode_portal_data.py b/MedicusWithClaudeFaktury/decode_portal_data.py new file mode 100644 index 0000000..38fdfa0 --- /dev/null +++ b/MedicusWithClaudeFaktury/decode_portal_data.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +import sys, io, base64, re +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +# Nacti poslednich 5 PORTAL zaznamu s DATA +cur.execute(""" + SELECT FIRST 5 ID, ODESLANO, IDFAK, ID_PODANI, DATA + FROM PORTAL + WHERE DATA IS NOT NULL + ORDER BY ID DESC +""") +rows = cur.fetchall() + +for row in rows: + pid, odeslano, idfak, id_podani, data = row + print(f"\n{'='*70}") + print(f"PORTAL.ID={pid} ODESLANO={odeslano} IDFAK={idfak} ID_PODANI={id_podani}") + + if data is None: + print("DATA: NULL") + continue + + # data je string (win1250) + if isinstance(data, bytes): + data = data.decode('win1250', errors='replace') + + # Parsuj XML obalku + print(f"DATA (prvnich 300 zn): {data[:300]}") + + # Najdi BASE64 obsah uvnitr ... + m = re.search(r']*Format="BASE64"[^>]*>(.*?)', data, re.DOTALL) + if m: + b64_raw = m.group(1).strip() + # Dekoduj + try: + b64_clean = re.sub(r'\s+', '', b64_raw) + # Padding + b64_clean += '=' * (4 - len(b64_clean) % 4) + html_bytes = base64.b64decode(b64_clean) + html = html_bytes.decode('iso-8859-2', errors='replace') + + # Extrahuj text + text = re.sub(r'', '', html, flags=re.DOTALL) + text = re.sub(r']*>.*?', '', text, flags=re.DOTALL) + text = re.sub(r']*>.*?', '', text, flags=re.DOTALL) + text = re.sub(r'<[^>]+>', ' ', text) + text = re.sub(r' ', ' ', text) + text = re.sub(r'&', '&', text) + text = re.sub(r'\s+', ' ', text).strip() + + print(f"\nDEKÓDOVANÝ HTML protokol:") + print(text[:2000]) + except Exception as e: + print(f"Chyba dekódování: {e}") + else: + # Zkus plain text odpoved (ne BASE64) + m2 = re.search(r']*>(.*?)', data, re.DOTALL) + if m2: + print(f"\nObsah Soubor (plain): {m2.group(1)[:500]}") + +conn.close() diff --git a/MedicusWithClaudeFaktury/explore_ed_bookofsubmissions.py b/MedicusWithClaudeFaktury/explore_ed_bookofsubmissions.py new file mode 100644 index 0000000..86a8f73 --- /dev/null +++ b/MedicusWithClaudeFaktury/explore_ed_bookofsubmissions.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +for table in ['ED_BOOKOFSUBMISSIONS', 'ED_BOOKOFSUBMISSIONATTACH', 'ED_MAILBOXMESSAGE', 'ED_STORAGE']: + print(f"\n{'='*70}") + print(f"TABULKA: {table}") + print('='*70) + + cur.execute(f""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = '{table}' + ORDER BY rf.RDB$FIELD_POSITION + """) + cols = [r[0].strip() for r in cur.fetchall()] + print(f"Sloupce: {cols}") + + cur.execute(f"SELECT COUNT(*) FROM {table}") + print(f"Počet: {cur.fetchone()[0]}") + + # BLOB sloupce vynech + cur.execute(f""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE + WHERE rf.RDB$RELATION_NAME = '{table}' + AND f.RDB$FIELD_TYPE = 261 + """) + blob_cols = {r[0].strip() for r in cur.fetchall()} + safe = [c for c in cols if c not in blob_cols] + + # Najdi razeni + order_col = next((c for c in ['CREATEDATE','SENTDATE','CREATED','ODESLANO','DATUM','ID'] if c in cols), cols[0]) + + try: + cur.execute(f"SELECT FIRST 10 {', '.join(safe)} FROM {table} ORDER BY {order_col} DESC") + rows = cur.fetchall() + print(f"\nPosledních 10 (ORDER BY {order_col} DESC):") + for row in rows: + print(f" {dict(zip(safe, row))}") + except Exception as e: + print(f"Chyba SELECT: {e}") + # Zkus bez order + try: + cur.execute(f"SELECT FIRST 5 {', '.join(safe)} FROM {table}") + rows = cur.fetchall() + for row in rows: + print(f" {dict(zip(safe, row))}") + except Exception as e2: + print(f"Chyba i bez ORDER: {e2}") + +conn.close() +print("\nHotovo.") diff --git a/MedicusWithClaudeFaktury/explore_hpn.py b/MedicusWithClaudeFaktury/explore_hpn.py new file mode 100644 index 0000000..9bef67c --- /dev/null +++ b/MedicusWithClaudeFaktury/explore_hpn.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +print("=" * 60) +print("1. Sloupce HPN") +print("=" * 60) +cur.execute(""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = 'HPN' + ORDER BY rf.RDB$FIELD_POSITION +""") +col_names = [r[0].strip() for r in cur.fetchall()] +print(f" Sloupce: {col_names}") + +print() +print("=" * 60) +print("2. Počet záznamů a max datum") +print("=" * 60) +cur.execute("SELECT COUNT(*) FROM HPN") +print(f" Celkem: {cur.fetchone()[0]} záznamů") + +# Najdi datumove sloupce +date_candidates = [c for c in col_names if any(x in c for x in ['DAT', 'ODE', 'VYT', 'CAS'])] +print(f" Datumové sloupce: {date_candidates}") +for dc in date_candidates[:3]: + try: + cur.execute(f"SELECT MIN({dc}), MAX({dc}) FROM HPN") + mn, mx = cur.fetchone() + print(f" {dc}: {mn} .. {mx}") + except Exception as e: + print(f" {dc}: {e}") + +print() +print("=" * 60) +print("3. Posledních 20 záznamů (bez BLOBů)") +print("=" * 60) + +# Zjisti BLOB sloupce +cur.execute(""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE + WHERE rf.RDB$RELATION_NAME = 'HPN' + AND f.RDB$FIELD_TYPE = 261 +""") +blob_cols = {r[0].strip() for r in cur.fetchall()} +print(f" BLOB sloupce: {blob_cols}") + +safe_cols = [c for c in col_names if c not in blob_cols] + +# Zjisti razeni - zkus VYTVORENO nebo ODESLANO nebo ID +order_col = next((c for c in ['VYTVORENO', 'ODESLANO', 'DATUM', 'ID'] if c in col_names), col_names[0]) + +cur.execute(f"SELECT FIRST 20 {', '.join(safe_cols)} FROM HPN ORDER BY {order_col} DESC") +rows = cur.fetchall() +for row in rows: + d = dict(zip(safe_cols, row)) + print(f" {d}") + +print() +print("=" * 60) +print("4. Záznamy od 2026-02-01") +print("=" * 60) +for dc in date_candidates[:2]: + try: + cur.execute(f"SELECT COUNT(*) FROM HPN WHERE {dc} >= '2026-02-01'") + cnt = cur.fetchone()[0] + if cnt > 0: + print(f" {dc} >= 2026-02-01: {cnt} záznamů") + cur.execute(f"SELECT FIRST 5 {', '.join(safe_cols)} FROM HPN WHERE {dc} >= '2026-02-01' ORDER BY {dc} DESC") + for row in cur.fetchall(): + print(f" {dict(zip(safe_cols, row))}") + break + except Exception as e: + print(f" {dc}: {e}") + +print() +print("=" * 60) +print("5. HPN_NOTIFIKACE_DETAIL – sloupce a ukazka") +print("=" * 60) +cur.execute(""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = 'HPN_NOTIFIKACE_DETAIL' + ORDER BY rf.RDB$FIELD_POSITION +""") +hnd_cols = [r[0].strip() for r in cur.fetchall()] +print(f" Sloupce: {hnd_cols}") +cur.execute("SELECT COUNT(*) FROM HPN_NOTIFIKACE_DETAIL") +print(f" Záznamy: {cur.fetchone()[0]}") + +conn.close() +print("\nHotovo.") diff --git a/MedicusWithClaudeFaktury/explore_hpn_podani.py b/MedicusWithClaudeFaktury/explore_hpn_podani.py new file mode 100644 index 0000000..e082e9d --- /dev/null +++ b/MedicusWithClaudeFaktury/explore_hpn_podani.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +print("=" * 60) +print("1. Sloupce HPN_PODANI") +print("=" * 60) +cur.execute(""" + SELECT rf.RDB$FIELD_NAME, rf.RDB$FIELD_POSITION + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = 'HPN_PODANI' + ORDER BY rf.RDB$FIELD_POSITION +""") +col_names = [r[0].strip() for r in cur.fetchall()] +print(f" Sloupce: {col_names}") + +print() +print("=" * 60) +print("2. Ukazka HPN_PODANI (bez BLOB ODPOVED)") +print("=" * 60) +safe_cols = [c for c in col_names if c != 'ODPOVED'] +cur.execute(f"SELECT FIRST 10 {', '.join(safe_cols)} FROM HPN_PODANI ORDER BY ODESLANO DESC") +rows = cur.fetchall() +for row in rows: + print(dict(zip(safe_cols, row))) + +print() +print("=" * 60) +print("3. Hledej tabulky s POJ nebo ZP sloupcem + data od 2026-02") +print("=" * 60) + +# Najdi vsechny tabulky ktere maji sloupec POJ nebo ZP +cur.execute(""" + SELECT DISTINCT rf.RDB$RELATION_NAME + FROM RDB$RELATION_FIELDS rf + JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME + WHERE r.RDB$SYSTEM_FLAG = 0 + AND (TRIM(rf.RDB$FIELD_NAME) = 'POJ' OR TRIM(rf.RDB$FIELD_NAME) = 'ZP') + ORDER BY rf.RDB$RELATION_NAME +""") +poj_tables = [r[0].strip() for r in cur.fetchall()] +print(f" Tabulky s POJ/ZP: {len(poj_tables)}") + +# Z techto tabulek vyber ty ktere maji ODESLANO nebo DATUM a data od 2026-02 +hits = [] +for table in poj_tables: + cur.execute(f""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = '{table}' + AND (TRIM(rf.RDB$FIELD_NAME) LIKE '%ODESLAN%' + OR TRIM(rf.RDB$FIELD_NAME) LIKE '%VYTVOR%' + OR TRIM(rf.RDB$FIELD_NAME) = 'DATUM') + """) + date_cols = [r[0].strip() for r in cur.fetchall()] + for dc in date_cols: + try: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-02-01'") + cnt = cur.fetchone()[0] + if cnt > 0: + hits.append((table, dc, cnt)) + except Exception: + pass + +hits.sort(key=lambda x: -x[2]) +for table, dc, cnt in hits: + print(f" {table:<40} {dc}: {cnt} od 2026-02") + +print() +print("=" * 60) +print("4. Hledej tabulky s PODACI_CISLO nebo PODAC nebo ID_PODANI") +print("=" * 60) +cur.execute(""" + SELECT DISTINCT rf.RDB$RELATION_NAME, rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME + WHERE r.RDB$SYSTEM_FLAG = 0 + AND (TRIM(rf.RDB$FIELD_NAME) LIKE '%PODACI%' + OR TRIM(rf.RDB$FIELD_NAME) = 'ID_PODANI' + OR TRIM(rf.RDB$FIELD_NAME) LIKE '%PODANI%') + ORDER BY rf.RDB$RELATION_NAME +""") +for r in cur.fetchall(): + print(f" {r[0].strip():<40} sloupec: {r[1].strip()}") + +print() +print("=" * 60) +print("5. PORTAL - co tam chybi od 2026-02?") +print("=" * 60) +cur.execute(""" + SELECT FIRST 5 ID, IDFAK, ODESLANO, STAV, ID_PODANI, DAVKA_ROK, BB_DAVKA, BB_FAKTURA + FROM PORTAL + ORDER BY ODESLANO DESC +""") +for row in cur.fetchall(): + print(f" {row}") + +conn.close() +print("\nHotovo.") diff --git a/MedicusWithClaudeFaktury/faktury_report.py b/MedicusWithClaudeFaktury/faktury_report.py index ec2fdbd..0c22a18 100644 --- a/MedicusWithClaudeFaktury/faktury_report.py +++ b/MedicusWithClaudeFaktury/faktury_report.py @@ -262,6 +262,224 @@ for i, row in enumerate(fak_rows, start=2): else: cell.value = '' +# ===================== +# List 5 – ED_PODANI (ED_BOOKOFSUBMISSIONS) +# ===================== + +ws5 = wb.create_sheet('ED_PODANI') + +REQUESTTYPE_MAP = {0: 'Registrační', 1: 'Výkonová'} +HICCODE_MAP = { + '111': 'VZP', '201': 'VoZP', '205': 'ČPZP', + '207': 'OZP', '209': 'ZPŠ', '211': 'ZPMV', + '212': '212', '213': '213', '217': '217', '228': '228', '333': '333', +} + +cur.execute(''' + SELECT + ID, CREATED, SENTDATE, HICCODE, REQUESTTYPE, + SUBMISSIONID, INVOICENUMBER, + PERIODFROM, PERIODTO, TOTALSUM, + STATE, CREATOR, HCPPERSONNAME, HCPCODE, UNIQUEID + FROM ED_BOOKOFSUBMISSIONS + ORDER BY ID DESC +''') +ed_cols_raw = [d[0] for d in cur.description] +ed_rows = cur.fetchall() + +# Lidštější záhlaví +ed_headers = [ + 'ID', 'Vytvořeno', 'Odesláno', 'ZP', 'Typ podání', + 'Podací č.', 'Faktura', + 'Období od', 'Období do', 'Částka (Kč)', + 'Stav', 'Autor', 'Lékař', 'IČZ', 'UUID', +] +ws5.append(ed_headers) + +# Mapa CISFAK → řádek na listu FAK (pro hyperlink) +cisfak_to_fak_row = {} +for i, row in enumerate(fak_rows, start=2): + cisfak = row[1] # CISFAK je druhý sloupec z DB (index 1) + if cisfak and cisfak not in cisfak_to_fak_row: + cisfak_to_fak_row[cisfak] = i + +for i, row in enumerate(ed_rows, start=2): + (eid, created, sentdate, hiccode, reqtype, + submid, invoicenum, + perfrom, perto, totalsum, + state, creator, hcpperson, hcpcode, uniqueid) = row + + out = [ + eid, + fmt(created), + fmt(sentdate), + f"{hiccode} {HICCODE_MAP.get(str(hiccode), '')}" if hiccode else '', + REQUESTTYPE_MAP.get(reqtype, str(reqtype)) if reqtype is not None else '', + fmt(submid), + fmt(invoicenum), + fmt(perfrom), + fmt(perto), + float(totalsum) if totalsum is not None else '', + fmt(state), + fmt(creator), + fmt(hcpperson), + fmt(hcpcode), + fmt(uniqueid), + ] + ws5.append(out) + if i % 2 == 0: + for cell in ws5[i]: + cell.fill = ZEBRA_FILL + +style_header(ws5) +ws5.freeze_panes = 'A2' +autofit(ws5) + +# Hyperlink: Faktura (sloupec 7) → list FAK +for i, row in enumerate(ed_rows, start=2): + invoicenum = row[6] # INVOICENUMBER + if invoicenum and invoicenum in cisfak_to_fak_row: + cell = ws5.cell(row=i, column=7) + # +1 kvůli vloženému sloupci FAKDET na listu FAK + cell.hyperlink = f'#FAK!B{cisfak_to_fak_row[invoicenum]}' + cell.font = LINK_FONT + +# ===================== +# List 6 – ED_PODANI_DATA (BLOBy z ED_BOOKOFSUBMISSIONS) +# ===================== + +ws6 = wb.create_sheet('ED_PODANI_DATA') + +cur.execute(''' + SELECT ID, HICCODE, SENTDATE, SUBMISSIONID, INVOICENUMBER, + KDAVKACONTENT, REQUEST, SERVERRESPONSE, PROTOCOL + FROM ED_BOOKOFSUBMISSIONS + ORDER BY ID DESC +''') +ed_data_rows = cur.fetchall() + +ed_data_headers = ['ID', 'ZP', 'Odesláno', 'Podací č.', 'Faktura', + 'KDAVKA', 'REQUEST (XML)', 'SERVERRESPONSE', 'PROTOCOL'] +ws6.append(ed_data_headers) + +def decode_ed_blob(v, enc): + """Dekóduj BLOB z ED_BOOKOFSUBMISSIONS.""" + if v is None: + return '' + if hasattr(v, 'read'): + raw = v.read() + else: + raw = v + if not raw: + return '' + if isinstance(raw, str): + # fdb vrátil jako win1250 string – zrekonstruuj bytes + raw = raw.encode('cp1250', errors='replace') + try: + text = raw.decode(enc, errors='replace') + except Exception: + return repr(raw[:200]) + # Odstraň prázdné řádky a sjednoť odřádkování + lines = [l for l in text.splitlines() if l.strip()] + return '\n'.join(lines) + +for i, row in enumerate(ed_data_rows, start=2): + eid, hiccode, sentdate, submid, invoicenum, kdavka, request, serverresp, protocol = row + + # KDAVKACONTENT – fdb vrací str přes win1250 spojení, použij přímo + if kdavka is None: + kdavka_txt = '' + else: + raw = kdavka.read() if hasattr(kdavka, 'read') else kdavka + if isinstance(raw, bytes): + raw = raw.encode('latin-1', errors='replace').decode('cp852', errors='replace') + lines = [l for l in raw.splitlines() if l.strip()] + kdavka_txt = '\n'.join(lines) + + # REQUEST – XML; může být uložen jako UTF-16 binary nebo jako ASCII/UTF-8 string + request_txt = '' + if request is not None: + raw = request.read() if hasattr(request, 'read') else request + if isinstance(raw, str): + raw_b = raw.encode('latin-1', errors='replace') + else: + raw_b = raw + if raw_b: + # Detekce podle BOM – jedině tehdy jde o skutečné UTF-16 binární data + if raw_b[:2] in (b'\xff\xfe', b'\xfe\xff'): + request_txt = raw_b.decode('utf-16', errors='replace') + elif raw_b[:1] == b'<': + # ASCII/UTF-8 XML – fdb ho vrátil jako string, použij přímo + request_txt = raw_b.decode('utf-8', errors='replace') + else: + request_txt = raw_b.decode('cp1250', errors='replace') + lines = [l for l in request_txt.splitlines() if l.strip()] + request_txt = '\n'.join(lines) + + # SERVERRESPONSE a PROTOCOL – latin-1 re-encoding, pak iso-8859-2 + def decode_latin_blob(v, enc): + if v is None: + return '' + raw = v.read() if hasattr(v, 'read') else v + if not raw: + return '' + if isinstance(raw, str): + raw = raw.encode('latin-1', errors='replace') + text = raw.decode(enc, errors='replace') + lines = [l for l in text.splitlines() if l.strip()] + return '\n'.join(lines) + + serverresp_txt = decode_latin_blob(serverresp, 'iso-8859-2') + protocol_txt = decode_latin_blob(protocol, 'iso-8859-2') + + out = [ + eid, + f"{hiccode} {HICCODE_MAP.get(str(hiccode), '')}" if hiccode else '', + fmt(sentdate), + fmt(submid), + fmt(invoicenum), + kdavka_txt, + request_txt, + serverresp_txt, + protocol_txt, + ] + ws6.append(out) + if i % 2 == 0: + for cell in ws6[i]: + cell.fill = ZEBRA_FILL + for cell in ws6[i]: + cell.alignment = WRAP + ws6.row_dimensions[i].height = 80 + +style_header(ws6) +ws6.freeze_panes = 'A2' + +# Šířky sloupců ED_PODANI_DATA +for col, width in zip(['A','B','C','D','E','F','G','H','I'], + [6, 10, 12, 16, 14, 80, 60, 60, 40]): + ws6.column_dimensions[col].width = width + +# Hyperlinky ED_PODANI ↔ ED_PODANI_DATA (přes ID) +ed_id_to_ws5_row = {row[0]: i for i, row in enumerate(ed_rows, start=2)} +ed_id_to_ws6_row = {row[0]: i for i, row in enumerate(ed_data_rows, start=2)} + +# ED_PODANI sloupec 1 (ID) → ED_PODANI_DATA +for i, row in enumerate(ed_rows, start=2): + eid = row[0] + if eid in ed_id_to_ws6_row: + cell = ws5.cell(row=i, column=1) + cell.hyperlink = f'#ED_PODANI_DATA!A{ed_id_to_ws6_row[eid]}' + cell.font = LINK_FONT + +# ED_PODANI_DATA sloupec 1 (ID) → ED_PODANI +for i, row in enumerate(ed_data_rows, start=2): + eid = row[0] + if eid in ed_id_to_ws5_row: + cell = ws6.cell(row=i, column=1) + cell.hyperlink = f'#ED_PODANI!A{ed_id_to_ws5_row[eid]}' + cell.font = LINK_FONT + cell.alignment = WRAP + # ===================== # Uložení # ===================== @@ -269,4 +487,4 @@ for i, row in enumerate(fak_rows, start=2): 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')) +sys.stdout.buffer.write(f'FAK: {len(fak_rows)} radku, FAKDET: {len(det_rows)} radku, PORTAL: {len(portal_rows)} radku, ED_PODANI: {len(ed_rows)} radku\n'.encode('utf-8')) diff --git a/MedicusWithClaudeFaktury/find_edavky2.py b/MedicusWithClaudeFaktury/find_edavky2.py new file mode 100644 index 0000000..ff6e3b3 --- /dev/null +++ b/MedicusWithClaudeFaktury/find_edavky2.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +import sys, io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +# Ze screenshotu vime: +# - sloupce: Vytvoreno, Odeslano, ZP, Stav, Zpravy, Podaci c., Faktura +# - ZP: 111, 201, 205, 207, 209, 211 +# - Podaci c.: 58933293, 174804160, 26082877, D01F260218... +# - datum: 01.03.2026, 05.03.2026, 12.03.2026, 23.03.2026, 24.03.2026 +# - typ: "Reg. listy" (bez faktury), vykonove davky (s fakturou) + +print("=" * 60) +print("1. EOCK_DAVKA - sloupce a ukazka") +print("=" * 60) +cur.execute(""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = 'EOCK_DAVKA' + ORDER BY rf.RDB$FIELD_POSITION +""") +eock_cols = [r[0].strip() for r in cur.fetchall()] +print(f" Sloupce: {eock_cols}") +cur.execute("SELECT COUNT(*) FROM EOCK_DAVKA") +print(f" Pocet: {cur.fetchone()[0]}") +safe = [c for c in eock_cols if c not in ('DATA', 'DAVKA', 'ODPOVED')] +if safe: + cur.execute(f"SELECT FIRST 3 {', '.join(safe)} FROM EOCK_DAVKA ORDER BY ID DESC") + for r in cur.fetchall(): + print(f" {dict(zip(safe, r))}") + +print() +print("=" * 60) +print("2. FAK - zaznamy z brezna 2026") +print("=" * 60) +cur.execute(""" + SELECT ID, CISFAK, POJ, DATVYS, DATODE, OBDOB, CENA, DRUH, STAV + FROM FAK + WHERE DATVYS >= '2026-03-01' OR DATODE >= '2026-03-01' + ORDER BY ID DESC +""") +rows = cur.fetchall() +print(f" Celkem: {len(rows)}") +for r in rows: + print(f" ID={r[0]} CISFAK={r[1]} POJ={r[2]} DATVYS={r[3]} DATODE={r[4]} OBDOB={r[5]} CENA={r[6]} DRUH={r[7]} STAV={r[8]}") + +print() +print("=" * 60) +print("3. Hledej tabulky ktere maji sloupec VYTVORENO nebo PODACI") +print("=" * 60) +cur.execute(""" + SELECT DISTINCT rf.RDB$RELATION_NAME, rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME + WHERE r.RDB$SYSTEM_FLAG = 0 + AND (TRIM(rf.RDB$FIELD_NAME) LIKE '%VYTVOR%' + OR TRIM(rf.RDB$FIELD_NAME) LIKE '%PODACI%') + ORDER BY rf.RDB$RELATION_NAME +""") +for r in cur.fetchall(): + print(f" {r[0].strip():<40} {r[1].strip()}") + +print() +print("=" * 60) +print("4. Hledej tabulky ktere maji POJ sloupec a zaznamy z brezna 2026") +print("=" * 60) +# Vsechny tabulky s POJ sloupcem +cur.execute(""" + SELECT DISTINCT rf.RDB$RELATION_NAME + FROM RDB$RELATION_FIELDS rf + JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME + WHERE r.RDB$SYSTEM_FLAG = 0 + AND TRIM(rf.RDB$FIELD_NAME) = 'POJ' + ORDER BY rf.RDB$RELATION_NAME +""") +poj_tables = [r[0].strip() for r in cur.fetchall()] + +for table in poj_tables: + # Najdi vsechny datumove sloupce + cur.execute(f""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = '{table}' + AND TRIM(rf.RDB$FIELD_NAME) LIKE '%DAT%' + """) + date_cols = [r[0].strip() for r in cur.fetchall()] + for dc in date_cols: + try: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-03-01'") + cnt = cur.fetchone()[0] + if cnt > 0: + print(f" {table:<40} {dc}: {cnt} od 2026-03-01") + except Exception: + pass + +print() +print("=" * 60) +print("5. Prohledej vsechny tabulky - ktere maji ~15 zaznamu od 2026-03-01") +print(" (v eDavky screenshotu je ~15 radku z brezna)") +print("=" * 60) +cur.execute(""" + SELECT RDB$RELATION_NAME FROM RDB$RELATIONS + WHERE RDB$SYSTEM_FLAG = 0 + ORDER BY RDB$RELATION_NAME +""") +all_tables = [r[0].strip() for r in cur.fetchall()] + +# Hledej tabulky s datumovym sloupcem a 10-50 zaznamy od 2026-03 +hits = [] +for table in all_tables: + cur.execute(f""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = '{table}' + AND (TRIM(rf.RDB$FIELD_NAME) IN ('ODESLANO', 'VYTVORENO', 'DATODE', 'DATVYS', 'DATUM_ODESLANI')) + """) + date_cols = [r[0].strip() for r in cur.fetchall()] + for dc in date_cols: + try: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-03-01'") + cnt = cur.fetchone()[0] + if 5 <= cnt <= 100: + hits.append((table, dc, cnt)) + except Exception: + pass + +hits.sort(key=lambda x: x[2]) +for table, dc, cnt in hits: + # Vypis sloupce teto tabulky + cur.execute(f""" + SELECT rf.RDB$FIELD_NAME + FROM RDB$RELATION_FIELDS rf + WHERE rf.RDB$RELATION_NAME = '{table}' + ORDER BY rf.RDB$FIELD_POSITION + """) + cols = [r[0].strip() for r in cur.fetchall()] + print(f" {table:<40} {dc}: {cnt} | sloupce: {cols[:10]}") + +conn.close() +print("\nHotovo.") diff --git a/MedicusWithClaudeFaktury/find_edavky_table.py b/MedicusWithClaudeFaktury/find_edavky_table.py new file mode 100644 index 0000000..7adcfc1 --- /dev/null +++ b/MedicusWithClaudeFaktury/find_edavky_table.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +""" +Hledani tabulky pro eDavky / Kniha podani v Medicus DB. +""" +import sys +import io +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +import fdb + +conn = fdb.connect( + dsn=r'localhost:c:\medicus 3\data\medicus.fdb', + user='SYSDBA', password='masterkey', charset='win1250' +) +cur = conn.cursor() + +print("=" * 60) +print("1. Tabulky s relevantními názvy") +print("=" * 60) +cur.execute(""" + SELECT RDB$RELATION_NAME + FROM RDB$RELATIONS + WHERE RDB$SYSTEM_FLAG = 0 + AND ( + TRIM(RDB$RELATION_NAME) LIKE '%DAV%' + OR TRIM(RDB$RELATION_NAME) LIKE '%PORTAL%' + OR TRIM(RDB$RELATION_NAME) LIKE '%PODANI%' + OR TRIM(RDB$RELATION_NAME) LIKE '%EDAVK%' + OR TRIM(RDB$RELATION_NAME) LIKE '%KNIHA%' + OR TRIM(RDB$RELATION_NAME) LIKE '%PODAC%' + OR TRIM(RDB$RELATION_NAME) LIKE '%ELEK%' + ) + ORDER BY RDB$RELATION_NAME +""") +tables = [row[0].strip() for row in cur.fetchall()] +for t in tables: + print(f" {t}") + +print() +print("=" * 60) +print("2. Počty záznamů a max datum v relevantních tabulkách") +print("=" * 60) + +for table in tables: + try: + # Zkus najít datumový sloupec + cur.execute(f""" + SELECT RDB$FIELD_NAME FROM RDB$RELATION_FIELDS + WHERE RDB$RELATION_NAME = '{table}' + ORDER BY RDB$FIELD_POSITION + """) + cols = [r[0].strip() for r in cur.fetchall()] + + cur.execute(f"SELECT COUNT(*) FROM {table}") + count = cur.fetchone()[0] + + # Najdi datum sloupec + date_col = None + for c in cols: + if any(x in c for x in ['DAT', 'ODE', 'VYT', 'CAS', 'TIME']): + date_col = c + break + + if date_col: + try: + cur.execute(f"SELECT MAX({date_col}) FROM {table}") + max_date = cur.fetchone()[0] + print(f" {table:<30} {count:>6} záznamů max {date_col}={max_date}") + except Exception: + print(f" {table:<30} {count:>6} záznamů cols: {cols[:5]}") + else: + print(f" {table:<30} {count:>6} záznamů cols: {cols[:5]}") + except Exception as e: + print(f" {table:<30} chyba: {e}") + +print() +print("=" * 60) +print("3. Aktuální stav PORTAL") +print("=" * 60) +try: + cur.execute("SELECT COUNT(*), MAX(ODESLANO) FROM PORTAL") + cnt, mx = cur.fetchone() + print(f" PORTAL: {cnt} záznamů, max ODESLANO={mx}") + cur.execute("SELECT FIRST 5 ID, IDFAK, ODESLANO, STAV, ID_PODANI, DAVKA_ROK FROM PORTAL ORDER BY ID DESC") + for row in cur.fetchall(): + print(f" {row}") +except Exception as e: + print(f" PORTAL chyba: {e}") + +print() +print("=" * 60) +print("4. Hledání tabulek s datem >= 2026-02-01 (čerstvá data)") +print("=" * 60) +# Projdi všechny tabulky a hledej ty které mají záznamy z 2026 +suspicious = [] +cur.execute(""" + SELECT RDB$RELATION_NAME FROM RDB$RELATIONS + WHERE RDB$SYSTEM_FLAG = 0 + ORDER BY RDB$RELATION_NAME +""") +all_tables = [r[0].strip() for r in cur.fetchall()] + +for table in all_tables: + try: + # Najdi datum sloupce + cur.execute(f""" + SELECT RDB$FIELD_NAME FROM RDB$RELATION_FIELDS + WHERE RDB$RELATION_NAME = '{table}' + AND ( + TRIM(RDB$FIELD_NAME) LIKE '%ODESLANO%' + OR TRIM(RDB$FIELD_NAME) LIKE '%VYTVORENO%' + OR TRIM(RDB$FIELD_NAME) LIKE '%DATUM_ODE%' + ) + """) + date_cols = [r[0].strip() for r in cur.fetchall()] + for dc in date_cols: + cur.execute(f"SELECT COUNT(*) FROM {table} WHERE {dc} >= '2026-02-01'") + cnt = cur.fetchone()[0] + if cnt > 0: + suspicious.append((table, dc, cnt)) + except Exception: + pass + +for table, dc, cnt in suspicious: + print(f" {table:<35} {dc}: {cnt} záznamů od 2026-02-01") + +conn.close() +print() +print("Hotovo.") diff --git a/MedicusWithClaudeFaktury/parse_trace_edavky.py b/MedicusWithClaudeFaktury/parse_trace_edavky.py new file mode 100644 index 0000000..dde9b50 --- /dev/null +++ b/MedicusWithClaudeFaktury/parse_trace_edavky.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +import sys, io, re +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + +TRACE_FILE = r'c:\Program Files\Firebird\Firebird_2_5_CGM\default_trace.log' +TIME_FROM = '2026-03-29T13:20:00' +TIME_TO = '2026-03-29T13:20:15' + +# Hledame dotazy ktere pravdepodobne patri k eDavky oknu: +# - zminka o tabulkach s davkami/podanim na pojistovnu +# - sloupce jako ZP, PODACI, ODESLANO, STAV v kontextu davek +KEYWORDS = [ + 'EDAVKY', 'EDAVKA', 'DAVKY_POJ', 'REGISTR_POJ', + 'PODACI_CISLO', 'PODACI', + 'FAKDAV', 'BB_DAVKA', 'BB_FAKTURA', + 'STAV_PODANI', 'ID_PODANI', + 'DAVKA_ROK', 'DAVKA_DISK', 'DAVKA_CASTKA', +] + +# Tabulky ktere NECHCEME (HPN = neschopenky, RECEPT = recepty, atd.) +EXCLUDE_TABLES = ['FROM HPN', 'FROM NES', 'FROM RECEPT', 'FROM CLICKDOC', + 'FROM UZIVATEL', 'FROM ZARIZENI', 'FROM ODDEL', 'FROM PRACOVISTE'] + +print(f"Čtu trace: {TRACE_FILE}") +print(f"Časové okno: {TIME_FROM} .. {TIME_TO}") +print(f"Hledám klíčová slova: {KEYWORDS}") +print("=" * 70) + +ts_re = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') + +in_window = False +current_ts = '' +current_block = [] +hits = [] +all_selects_tables = {} # tabulka -> pocet vyskytu + +def get_from_tables(text): + """Extrahuj tabulky z FROM klauzule.""" + tables = re.findall(r'\bFROM\s+([A-Z_][A-Z0-9_]*)', text, re.IGNORECASE) + tables += re.findall(r'\bJOIN\s+([A-Z_][A-Z0-9_]*)', text, re.IGNORECASE) + return [t.upper() for t in tables] + +def process_block(ts, lines): + text = '\n'.join(lines) + if not re.search(r'\bSELECT\b', text, re.IGNORECASE): + return + + text_up = text.upper() + + # Spocitej tabulky + tables = get_from_tables(text_up) + for t in tables: + all_selects_tables[t] = all_selects_tables.get(t, 0) + 1 + + # Filtr - hledej klicova slova + for kw in KEYWORDS: + if kw in text_up: + hits.append((ts, kw, text)) + return + + # Alternativne: dotazy na PORTAL nebo FAK s datumem + if ('PORTAL' in text_up or 'FAKDAV' in text_up) and 'SELECT' in text_up: + # Vynech jednoduche dotazy + if len(text) > 100: + hits.append((ts, 'PORTAL/FAKDAV', text)) + +try: + with open(TRACE_FILE, 'r', encoding='utf-8', errors='replace') as f: + for line in f: + line = line.rstrip('\n') + m = ts_re.match(line) + if m: + if current_block and in_window: + process_block(current_ts, current_block) + current_ts = line[:19] + current_block = [line] + in_window = (TIME_FROM <= current_ts <= TIME_TO) + else: + if in_window: + current_block.append(line) + if current_block and in_window: + process_block(current_ts, current_block) +except FileNotFoundError: + print(f"CHYBA: soubor nenalezen: {TRACE_FILE}") + sys.exit(1) + +print(f"\nNejčastější tabulky v SELECT dotazech v okně (top 40):") +sorted_tables = sorted(all_selects_tables.items(), key=lambda x: -x[1]) +for t, cnt in sorted_tables[:40]: + print(f" {t:<40} {cnt}x") + +print() +print(f"{'='*70}") +print(f"Nalezeno relevantních bloků (klíčová slova): {len(hits)}") +print() + +for i, (ts, kw, text) in enumerate(hits): + print(f"{'='*70}") + print(f"[{i+1}] {ts} klíčové slovo: {kw}") + print(text[:4000]) + print() + +if not hits: + print("Žádné přesné shody. Zkusíme PREPARE_STATEMENT bloky s FROM tabulkami:") + print("(viz seznam tabulek nahoře - neobvyklé názvy mohou být klíč)") diff --git a/MedicusWithClaudeSelects/FakturaceADavky.md b/MedicusWithClaudeSelects/FakturaceADavky.md index 204a49b..8c8c998 100644 --- a/MedicusWithClaudeSelects/FakturaceADavky.md +++ b/MedicusWithClaudeSelects/FakturaceADavky.md @@ -235,6 +235,246 @@ P ... – preventivní prohlídka - 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 +--- + +## eDávky – elektronické odesílání dávek pojišťovnám (zjištěno 2026-03-29) + +### Přehled tabulek + +Modul eDávky v Medicusu používá tabulky s prefixem `ED_`: + +| Tabulka | Záznamy | Popis | +|---|---|---| +| `ED_BOOKOFSUBMISSIONS` | 998 | Hlavní tabulka – Kniha podání (od 2016) | +| `ED_BOOKOFSUBMISSIONATTACH` | 0 | Přílohy k podáním (zatím nevyužito) | +| `ED_MAILBOXMESSAGE` | 0 | Schránka zpráv od pojišťoven (zatím prázdná) | +| `ED_STORAGE` | 5 | Konfigurace – certifikáty a přihlašovací údaje | + +### Jak jsme tabulku našli +Přes Firebird trace log – při otevření okna eDávky v Medicusu se nejčastěji dotazuje +na `ED_BOOKOFSUBMISSIONS` (45x) a `ED_MAILBOXMESSAGE` (4990x). + +### PORTAL vs ED_BOOKOFSUBMISSIONS +- **PORTAL** (180 záznamů, max 2026-01-27) = starý systém podávání dávek +- **ED_BOOKOFSUBMISSIONS** (998 záznamů, od 2016) = nový systém (přechod ~únor 2026) +- Nepřekrývají se – žádný záznam není v obou (různá ID_PODANI/SUBMISSIONID) +- PORTAL má data od 2014, ED_BOOKOFSUBMISSIONS od 2016 (oba systémy běžely paralelně) + +--- + +### ED_BOOKOFSUBMISSIONS – hlavní tabulka Knihy podání + +**Sloupce:** + +| Sloupec | Popis | +|---|---| +| `ID` | primární klíč | +| `CREATED` | datum vytvoření | +| `SENTDATE` | datum odeslání pojišťovně | +| `CREATOR` | jméno autora (např. "Buzalková Michaela MUDr.") | +| `HCPCODE` | IČZ ordinace (09305000) | +| `HCPPERSONNAME` | jméno lékaře | +| `HICCODE` | kód pojišťovny (111, 201, 205, 207, 209, 211...) | +| `INVOICENUMBER` | číslo faktury (např. 0000260020) – NULL pro reg. dávky | +| `PERIODFROM` / `PERIODTO` | období dávky | +| `REQUESTTYPE` | typ: **0** = registrační (Reg. listy), **1** = výkonová s fakturou | +| `STATE` | stav podání (0 = odesláno OK) | +| `SUBMISSIONID` | podací číslo přidělené pojišťovnou (např. 59135047) | +| `TOTALSUM` | celková částka (0 pro reg. dávky) | +| `UNIQUEID` | UUID záznamu | +| `USERDESCRIPTION` | popis (zobrazuje se ve sloupci "Zprávy") | +| `REQUEST` | XML žádosti odeslané pojišťovně (BLOB) | +| `SERVERRESPONSE` | odpověď pojišťovny (BLOB, bytes, kódování iso-8859-2) | +| `FDAVKACONTENT` | obsah FDAVKA (BLOB) | +| `KDAVKACONTENT` | obsah KDAVKA (BLOB) | +| `PROTOCOL` | protokol (BLOB) | + +**Pojišťovny v datech:** 111 (VZP), 201 (VoZP), 205 (ČPZP), 207 (OZP), 209 (ZPŠ), 211 (ZPMV) + +### Kódování pojišťoven (správné!) +- **111** = VZP (Všeobecná zdravotní pojišťovna) +- **201** = VoZP (Vojenská zdravotní pojišťovna) +- **205** = ČPZP (Česká průmyslová zdravotní pojišťovna) +- **207** = OZP (Oborová zdravotní pojišťovna) +- **209** = ZPŠ (Zdravotní pojišťovna Škoda) +- **211** = ZPMV (Zdravotní pojišťovna ministerstva vnitra) + +--- + +### ED_STORAGE – konfigurace certifikátů a přihlašovacích údajů + +Sloupce: `ID`, `NAME`, `VALUEB` (BLOB XML), `IDUZI` + +**Záznamy:** +- `ServerSettingsXml` (per uživatel, IDUZI=None/2/4/6) – XML s certifikáty pro každou pojišťovnu +- `LastMessagesDownloadTime` (IDUZI=None) – datum posledního stažení zpráv + +**Struktura ServerSettingsXml:** +```xml + + + + + + + + + + + + + + + + + + + +``` + +**Certifikát Buzalky Vladimíra (IDUZI=6):** +- Vydavatel: I.CA EU Qualified CA2/RSA 06/2022 (První certifikační autorita, a.s.) +- SerialNumber: `0247068517B0049E2E` +- Platí pro pojišťovny: 201, 205, 207, 209, 213, 217, 228 + +--- + +### Formát REQUEST XML – registrační dávka (REQUESTTYPE=0) + +```xml + + + RegistrationCards + None + 2026-03-01T00:00:00.000 + 2026-03-31T00:00:00.000 + 0 + 09305000 + 09305000 + 68366370 + MUDr. Buzalka Vladimír + 111 + Všeobecná zdravotní pojišťovna ČR + 0900 + 0900 + 1 + + + 8 + DP80 + 0.00 + 2026 + 3 + 0 + 2 + + + +``` + +### Formát REQUEST XML – výkonová dávka s fakturou (REQUESTTYPE=1) + +```xml + + + HealthCareBilling + 0000260026 + HealthCareInvoice + 2026-03-01T00:00:00.000 + 2026-03-01T00:00:00.000 + 2026-03-31T00:00:00.000 + 2025-12-01T00:00:00.000 + 2025-12-31T00:00:00.000 + 85 + 09305000 + 68366370 + 2800046620 + 000000 + 2010 + Praktický lékař pro dospělé + Lovosická 440/40 + Praha 9-Prosek + 19000 + MUDr. Buzalková Michaela + 207 + Ročkotova 1225/1 + Praha 4 + 140 21 + 0900 + 51 + + + 34 + DP05 + 2025 + 12 + 85 + 1 + + + +``` + +**Poznámka k diakritice v REQUEST:** Medicus ukládá XML v UTF-16, ale česká diakritika v polích jako HcpPersonName, HicName apod. bývá uložena bez háčků/čárek (bug Medicusu při tvorbě XML). Toto není chyba dekódování. + +### Formát SERVERRESPONSE (odpověď pojišťovny) +- Kódování: **iso-8859-2** (bytes) +- Struktura: XML `textPKCS7` +- `PZP_IdPodani` = přidělené podací číslo +- `PZP_Chyba="0"` = bez chyby +- Odpověď je podepsána pojišťovnou (PKCS7) + +--- + +### Plán: skript pro automatické odeslání žádosti o seznam registrovaných + +### Portály pojišťoven – 3 skupiny + +#### Skupina 1 – Společný portál (201, 205, 207, 209, 213, 217, 228) +- Jeden společný portál pro všechny tyto pojišťovny +- Autentizace a podepisování: **kvalifikovaný certifikát I.CA EU Qualified CA2/RSA 06/2022** +- SerialNumber (Buzalka): `0247068517B0049E2E` +- Vydavatel: První certifikační autorita, a.s. + +#### Skupina 2 – VZP (111) – vlastní portál +- VZP má **samostatný portál** (`UseAlternativePortal="true"` v konfiguraci) +- Podepisování: **komerční certifikát I.CA Public CA/RSA 06/2022** (nižší stupeň než kvalifikovaný) + - SerialNumber: `01DE0F46B713505F1F` +- Autentizace vůči portálu: **certifikát Komerční banky (DCS CA KB)** + - SerialNumber: `46E67A` + +#### Skupina 3 – ZPMV (211) – samostatní exoti +- Žádný certifikát, přihlašování **heslem** +- Konfigurace: PIN, Password, Email (uloženo v plaintextu v ED_STORAGE) +- Email: `ordinace@buzalkova.cz` + +### Formáty odpovědí v PORTAL.DATA (historické) +Různé pojišťovny vracely různé formáty: +- **ČPZP/ZPMV** (D01 portál): XML `100D01F...` +- **OZP a ostatní** (starý portál): XML `HTML protokol` + - HTML protokol je v iso-8859-2, obsahuje tabulku s detaily dávky + +### Kódování BLOBů v ED_BOOKOFSUBMISSIONS + +| Sloupec | Kódování | Poznámka | +|---|---|---| +| `KDAVKACONTENT` | CP1250 | fdb vrací str přes win1250 spojení – použít přímo | +| `REQUEST` | ASCII/UTF-8 nebo UTF-16 | Detekovat podle BOM: `FF FE`/`FE FF` → utf-16, začíná `<` → utf-8 | +| `SERVERRESPONSE` | iso-8859-2 | latin-1 re-encoding → decode iso-8859-2 | +| `PROTOCOL` | iso-8859-2 | latin-1 re-encoding → decode iso-8859-2 | + +**Důležité:** Pro re-encoding str→bytes vždy používat `latin-1` (ne `cp1250`), protože latin-1 zachová všechny bajty 0–255 beze změny včetně null bajtů. Pro utf-16 nikdy nezkoušet decode bez ověření BOM – bez BOM Python tiše vrátí čínské znaky místo chyby. + +**Co potřebujeme ještě zjistit:** +- URL endpointů jednotlivých portálů (zachytit přes síťový trace) +- Jak přesně se REQUEST podepisuje certifikátem (PKCS12 / Windows certificate store) + +**Co už máme:** +- Formát REQUEST XML (viz výše) +- Certifikáty a přihlašovací údaje z ED_STORAGE +- Strukturu odpovědi pojišťovny +- Rozdělení pojišťoven do 3 skupin podle způsobu přístupu + ## 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í).