"""Render extracted invoice data into a tidy XLSX file.""" from openpyxl import Workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter HEADER_FILL = PatternFill("solid", fgColor="2563EB") HEADER_FONT = Font(bold=True, color="FFFFFF", size=11) LABEL_FONT = Font(bold=True, size=10) THIN = Side(style="thin", color="D0D5DC") BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) def write_invoice_xlsx(data: dict, out_path: str) -> None: wb = Workbook() ws = wb.active ws.title = "Faktura" # ── Header section ── ws.cell(row=1, column=1, value="FAKTURA — extrahovaná data").font = Font(bold=True, size=14) ws.merge_cells("A1:D1") row = 3 row = _write_kv_block(ws, row, "Identifikace faktury", [ ("Číslo faktury", data.get("invoice_number")), ("Variabilní symbol", data.get("variable_symbol")), ("Konstantní symbol", data.get("constant_symbol")), ("Specifický symbol", data.get("specific_symbol")), ("Datum vystavení", data.get("issue_date")), ("Datum splatnosti", data.get("due_date")), ("DUZP", data.get("taxable_date")), ("Měna", data.get("currency") or "CZK"), ("Způsob platby", data.get("payment_method")), ("Číslo účtu", data.get("bank_account")), ("IBAN", data.get("iban")), ]) row += 1 sup = data.get("supplier") or {} row = _write_kv_block(ws, row, "Dodavatel", [ ("Název", sup.get("name")), ("IČO", sup.get("ico")), ("DIČ", sup.get("dic")), ("Adresa", sup.get("address")), ]) row += 1 cust = data.get("customer") or {} row = _write_kv_block(ws, row, "Odběratel", [ ("Název", cust.get("name")), ("IČO", cust.get("ico")), ("DIČ", cust.get("dic")), ("Adresa", cust.get("address")), ]) row += 1 # ── Line items table ── items = data.get("line_items") or [] if items: row = _write_section_header(ws, row, "Položky faktury") headers = ["Popis", "Množství", "Jednotka", "Cena/jed. bez DPH", "Sazba DPH (%)", "Bez DPH", "S DPH"] for c, h in enumerate(headers, 1): cell = ws.cell(row=row, column=c, value=h) cell.font = HEADER_FONT cell.fill = HEADER_FILL cell.alignment = Alignment(horizontal="center") cell.border = BORDER row += 1 for item in items: values = [ item.get("description") or "", item.get("quantity"), item.get("unit") or "", item.get("unit_price_excluding_vat"), item.get("vat_rate"), item.get("total_excluding_vat"), item.get("total_including_vat"), ] for c, v in enumerate(values, 1): cell = ws.cell(row=row, column=c, value=v) cell.border = BORDER if c >= 4 and isinstance(v, (int, float)): cell.number_format = "#,##0.00" row += 1 row += 1 # ── VAT breakdown ── vat = data.get("vat_breakdown") or [] if vat: row = _write_section_header(ws, row, "Rekapitulace DPH") headers = ["Sazba (%)", "Základ", "DPH", "Celkem"] for c, h in enumerate(headers, 1): cell = ws.cell(row=row, column=c, value=h) cell.font = HEADER_FONT cell.fill = HEADER_FILL cell.alignment = Alignment(horizontal="center") cell.border = BORDER row += 1 for br in vat: for c, v in enumerate([br.get("rate"), br.get("base"), br.get("vat"), br.get("total")], 1): cell = ws.cell(row=row, column=c, value=v) cell.border = BORDER if c >= 2 and isinstance(v, (int, float)): cell.number_format = "#,##0.00" row += 1 row += 1 # ── Totals ── row = _write_section_header(ws, row, "Celkem") totals = [ ("Celkem bez DPH", data.get("total_excluding_vat")), ("Celkem DPH", data.get("total_vat")), ("CELKEM K ÚHRADĚ", data.get("total_including_vat")), ] for label, val in totals: ws.cell(row=row, column=1, value=label).font = LABEL_FONT cell = ws.cell(row=row, column=2, value=val) if isinstance(val, (int, float)): cell.number_format = "#,##0.00" if label.startswith("CELKEM K"): ws.cell(row=row, column=1).font = Font(bold=True, size=12) cell.font = Font(bold=True, size=12) row += 1 if data.get("notes"): row += 1 ws.cell(row=row, column=1, value="Poznámka:").font = LABEL_FONT ws.cell(row=row, column=2, value=data["notes"]) # ── Auto-widths ── widths = {1: 32, 2: 14, 3: 10, 4: 16, 5: 14, 6: 14, 7: 14} for c, w in widths.items(): ws.column_dimensions[get_column_letter(c)].width = w wb.save(out_path) def _write_section_header(ws, row: int, text: str) -> int: cell = ws.cell(row=row, column=1, value=text) cell.font = Font(bold=True, size=12, color="0F1729") cell.fill = PatternFill("solid", fgColor="EFF4FF") ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=7) return row + 1 def _write_kv_block(ws, row: int, header: str, pairs: list[tuple]) -> int: row = _write_section_header(ws, row, header) for label, val in pairs: ws.cell(row=row, column=1, value=label).font = LABEL_FONT ws.cell(row=row, column=2, value=val if val is not None else "") row += 1 return row