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
This commit is contained in:
154
invoice-extractor/excel_export.py
Normal file
154
invoice-extractor/excel_export.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user