Files
medicus/MedicusWithClaudeFaktury/faktury_report.py
T
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

538 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
# --- 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í ---
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')
PLAIN_FONT = Font(color='000000') # normální font bez modrého podtržení
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 = ''
# =====================
# 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í
# =====================
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, ED_PODANI: {len(ed_rows)} radku\n'.encode('utf-8'))