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
155 lines
5.7 KiB
Python
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
|