Files
AI_portal/invoice-extractor/excel_export.py
Ondřej Glaser 48cef99257 Initial portal commit: landing + 9 AI-powered apps
Apps:
- dwg-rooms: extract room numbers from DWG/DXF
- dwg-counting: count symbols in PDF drawings (OpenCV template matching)
- contract-check: review PDF contracts against a checklist (Claude vision + Tesseract OCR fallback)
- email-drafter: bullet notes → polished Czech/English business emails
- invoice-extractor: PDF/image invoice → structured data → Excel
- translator: Czech-first translator across 19 languages with tone control
- vv-check: find inconsistent unit prices across VV sheets in one workbook
- vv-compare: diff original vs new VV files (changes / added / removed)
- feature-request: portal users submit ideas + sample files

Infrastructure:
- LiteLLM gateway with per-app virtual keys + budgets
- Langfuse observability
- Geist font, shared theme, cross-subdomain back link + theme sync via cookie/URL
- Caddy reverse proxy on *.klas.chat
2026-05-13 15:25:04 +02:00

155 lines
5.7 KiB
Python

"""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