2 Commits

Author SHA1 Message Date
administrator 8f61cf04da Klikací hyperlinky na soubory v ED_PODANI_DATA bez modrého podtržení
- Při generování reportu se vytvoří složka SOUBORY_PRO_FAKTURY_REPORT
- KDAVKA → .txt, REQUEST → .xml, SERVERRESPONSE → .xml, PROTOCOL → .html
- Buňky s obsahem jsou klikací (otevřou soubor v Notepadu/prohlížeči)
- Font normální (černý, bez podtržení) – hyperlink funguje bez vizuální změny
- Při každém spuštění se složka smaže a vygeneruje znovu čerstvá

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:12:15 +02:00
administrator 4f586f4b57 Přidán list ED_PODANI + ED_PODANI_DATA do faktury_report.py; doplněny poznámky o eDávkách
- 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 <noreply@anthropic.com>
2026-03-30 07:42:46 +02:00
9 changed files with 1222 additions and 1 deletions
@@ -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 <Soubor ...>...</Soubor>
m = re.search(r'<Soubor[^>]*Format="BASE64"[^>]*>(.*?)</Soubor>', 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'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL)
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL)
text = re.sub(r'<[^>]+>', ' ', text)
text = re.sub(r'&nbsp;', ' ', text)
text = re.sub(r'&amp;', '&', 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'<Soubor[^>]*>(.*?)</Soubor>', data, re.DOTALL)
if m2:
print(f"\nObsah Soubor (plain): {m2.group(1)[:500]}")
conn.close()
@@ -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.")
+102
View File
@@ -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.")
@@ -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.")
+266 -1
View File
@@ -19,6 +19,13 @@ now = datetime.now()
filename = now.strftime('%Y-%m-%d_%H-%M-%S') + '_faktury.xlsx' filename = now.strftime('%Y-%m-%d_%H-%M-%S') + '_faktury.xlsx'
output_path = os.path.join(output_dir, filename) output_path = os.path.join(output_dir, filename)
# --- Složka pro soubory (KDAVKA, REQUEST atd.) ---
files_dir = os.path.join(output_dir, 'SOUBORY_PRO_FAKTURY_REPORT')
import shutil
if os.path.exists(files_dir):
shutil.rmtree(files_dir)
os.makedirs(files_dir)
# --- Smazání předchozích verzí --- # --- Smazání předchozích verzí ---
for f in os.listdir(output_dir): for f in os.listdir(output_dir):
if f.endswith('_faktury.xlsx'): if f.endswith('_faktury.xlsx'):
@@ -33,6 +40,7 @@ wb = openpyxl.Workbook()
HEADER_FILL = PatternFill('solid', fgColor='2F5496') HEADER_FILL = PatternFill('solid', fgColor='2F5496')
HEADER_FONT = Font(bold=True, color='FFFFFF') HEADER_FONT = Font(bold=True, color='FFFFFF')
LINK_FONT = Font(color='0563C1', underline='single') LINK_FONT = Font(color='0563C1', underline='single')
PLAIN_FONT = Font(color='000000') # normální font bez modrého podtržení
ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1') ZEBRA_FILL = PatternFill('solid', fgColor='DCE6F1')
def style_header(ws): def style_header(ws):
@@ -262,6 +270,263 @@ for i, row in enumerate(fak_rows, start=2):
else: else:
cell.value = '' 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')
# Ulož soubory pro klikací hyperlinky
rel_prefix = 'SOUBORY_PRO_FAKTURY_REPORT'
kdavka_link = ''
request_link = ''
serverresp_link = ''
protocol_link = ''
if kdavka_txt:
fname = f'KDAVKA_{eid}.txt'
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
f.write(kdavka_txt)
kdavka_link = f'{rel_prefix}/{fname}'
if request_txt:
fname = f'REQUEST_{eid}.xml'
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
f.write(request_txt)
request_link = f'{rel_prefix}/{fname}'
if serverresp_txt:
fname = f'SERVERRESPONSE_{eid}.xml'
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
f.write(serverresp_txt)
serverresp_link = f'{rel_prefix}/{fname}'
if protocol_txt:
fname = f'PROTOCOL_{eid}.html'
with open(os.path.join(files_dir, fname), 'w', encoding='utf-8') as f:
f.write(protocol_txt)
protocol_link = f'{rel_prefix}/{fname}'
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
# Hyperlinky na soubory F=KDAVKA, G=REQUEST, H=SERVERRESPONSE, I=PROTOCOL
for col_idx, link in [(6, kdavka_link), (7, request_link),
(8, serverresp_link), (9, protocol_link)]:
if link:
cell = ws6.cell(row=i, column=col_idx)
cell.hyperlink = link
cell.font = PLAIN_FONT # bez modrého podtržení
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í # Uložení
# ===================== # =====================
@@ -269,4 +534,4 @@ for i, row in enumerate(fak_rows, start=2):
conn.close() conn.close()
wb.save(output_path) wb.save(output_path)
sys.stdout.buffer.write(f'Ulozeno: {output_path}\n'.encode('utf-8')) 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'))
+146
View File
@@ -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.")
@@ -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.")
@@ -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íč)")
+240
View File
@@ -235,6 +235,246 @@ P ... preventivní prohlídka
- Kapitace se v FAK.KAPITACE neukazuje (je 0), ale v FAKDET.CENAKAP ano nutno ověřit - 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 - 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
<Settings>
<PortalZP>
<Portal Code="201">
<SigningCertificate Issuer="..." SerialNumber="..." />
</Portal>
<!-- Code: 201, 205, 207, 209, 212, 213, 217, 228, 333 -->
</PortalZP>
<PortalZPMV>
<!-- Pro ZPMV (211): PIN, Password, Email uloženy v plaintextu! -->
<Portal Code="211" PIN="..." Password="..." Email="ordinace@buzalkova.cz" />
</PortalZPMV>
<PortalVZP>
<!-- Pro VZP (111): SigningCertificate + AuthenticationCertificate -->
<Portal Code="111" UseAlternativePortal="true">
<SigningCertificate Issuer="..." SerialNumber="..." />
<AuthenticationCertificate Issuer="..." SerialNumber="..." />
</Portal>
</PortalVZP>
</Settings>
```
**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
<?xml version="1.0" encoding="utf-16"?>
<SubmissionRequest>
<SubmissionType>RegistrationCards</SubmissionType>
<InvoiceType>None</InvoiceType>
<PeriodFrom>2026-03-01T00:00:00.000</PeriodFrom>
<PeriodTo>2026-03-31T00:00:00.000</PeriodTo>
<TotalSum>0</TotalSum>
<HcpCode>09305000</HcpCode> <!-- IČZ ordinace -->
<FICZ>09305000</FICZ>
<HcpOrgNum>68366370</HcpOrgNum> <!-- IČO -->
<HcpPersonName>MUDr. Buzalka Vladimír</HcpPersonName>
<HicCode>111</HicCode> <!-- kód pojišťovny -->
<HicName>Všeobecná zdravotní pojišťovna ČR</HicName>
<HicCity>0900</HicCity>
<FPOB>0900</FPOB>
<VDPOJ>1</VDPOJ>
<InvoiceItems>
<InvoiceItem>
<BatchNumber>8</BatchNumber>
<Type>DP80</Type> <!-- DP80 = registrační dávka -->
<Price>0.00</Price>
<Year>2026</Year>
<Month>3</Month>
<DBODY>0</DBODY>
<DUHR>2</DUHR>
</InvoiceItem>
</InvoiceItems>
</SubmissionRequest>
```
### Formát REQUEST XML výkonová dávka s fakturou (REQUESTTYPE=1)
```xml
<?xml version="1.0" encoding="utf-16"?>
<SubmissionRequest>
<SubmissionType>HealthCareBilling</SubmissionType>
<InvoiceNumber>0000260026</InvoiceNumber>
<InvoiceType>HealthCareInvoice</InvoiceType>
<DateOfIssue>2026-03-01T00:00:00.000</DateOfIssue>
<DateOfDispatch>2026-03-01T00:00:00.000</DateOfDispatch>
<DateOfMaturity>2026-03-31T00:00:00.000</DateOfMaturity>
<PeriodFrom>2025-12-01T00:00:00.000</PeriodFrom>
<PeriodTo>2025-12-31T00:00:00.000</PeriodTo>
<TotalSum>85</TotalSum>
<HcpCode>09305000</HcpCode>
<HcpOrgNum>68366370</HcpOrgNum>
<HcpAccountNumber>2800046620</HcpAccountNumber>
<HcpAccountPrefix>000000</HcpAccountPrefix>
<HcpAccountBankCode>2010</HcpAccountBankCode>
<HcpName>Praktický lékař pro dospělé</HcpName>
<HcpStreet>Lovosická 440/40</HcpStreet>
<HcpCity>Praha 9-Prosek</HcpCity>
<HcpZipCode>19000</HcpZipCode>
<HcpPersonName>MUDr. Buzalková Michaela</HcpPersonName>
<HicCode>207</HicCode>
<HicStreet>Ročkotova 1225/1</HicStreet>
<HicCity>Praha 4</HicCity>
<HicZipCode>140 21</HicZipCode>
<FPOB>0900</FPOB>
<VDPECE>51</VDPECE>
<InvoiceItems>
<InvoiceItem>
<BatchNumber>34</BatchNumber>
<Type>DP05</Type> <!-- DP05 = výkonová dávka -->
<Year>2025</Year>
<Month>12</Month>
<DBODY>85</DBODY>
<DUHR>1</DUHR>
</InvoiceItem>
</InvoiceItems>
</SubmissionRequest>
```
**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 `<Komunikace><Data ...><Soubor ...>text</Soubor></Data><Podpis>PKCS7</Podpis></Komunikace>`
- `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 `<ekomunikace><statuscode>100</statuscode><idpodani>D01F...</idpodani>`
- **OZP a ostatní** (starý portál): XML `<Komunikace><Data ...><Soubor Format="BASE64">HTML protokol</Soubor></Data></Komunikace>`
- 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 0255 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é! ## 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í). Dávkové soubory (KDAVKA, FDAVKA) jsou uloženy v **CP852** (DOS Latin-2, prahistorické kódování).