"""FastAPI: invoice PDF/image → structured data → editable form → XLSX export.""" import asyncio import logging import os import uuid from pathlib import Path from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from excel_export import write_invoice_xlsx from extractor import extract_invoice logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="Invoice Extractor") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) WORK_DIR = Path(os.getenv("WORK_DIR", "/tmp/invoice-extractor")) WORK_DIR.mkdir(parents=True, exist_ok=True) jobs: dict[str, dict] = {} ALLOWED_EXT = {".pdf", ".jpg", ".jpeg", ".png", ".webp"} @app.get("/") async def root(): return FileResponse("static/index.html") @app.post("/api/upload") async def upload(file: UploadFile = File(...)): suffix = Path(file.filename or "").suffix.lower() if suffix not in ALLOWED_EXT: raise HTTPException(400, f"Podporované formáty: {', '.join(sorted(ALLOWED_EXT))}") job_id = str(uuid.uuid4()) job_dir = WORK_DIR / job_id job_dir.mkdir() input_path = job_dir / f"input{suffix}" input_path.write_bytes(await file.read()) logger.info("Job %s: %s (%d bytes)", job_id, file.filename, input_path.stat().st_size) try: data = await extract_invoice(input_path) except Exception as exc: logger.exception("Extraction failed") raise HTTPException(500, str(exc)) jobs[job_id] = { "filename": file.filename, "job_dir": str(job_dir), "data": data, } return {"job_id": job_id, "data": data} class SaveRequest(BaseModel): data: dict @app.post("/api/save/{job_id}") async def save_data(job_id: str, req: SaveRequest): if job_id not in jobs: raise HTTPException(404, "Nenalezeno") jobs[job_id]["data"] = req.data return {"ok": True} @app.get("/api/export/{job_id}") async def export(job_id: str): if job_id not in jobs: raise HTTPException(404, "Nenalezeno") job = jobs[job_id] out_path = Path(job["job_dir"]) / "invoice.xlsx" write_invoice_xlsx(job["data"], str(out_path)) inv_no = (job["data"].get("invoice_number") or Path(job["filename"]).stem if job.get("filename") else "faktura") safe = "".join(c if c.isalnum() or c in "-_." else "_" for c in str(inv_no)) return FileResponse( str(out_path), media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename=f"faktura_{safe}.xlsx", ) @app.get("/health") async def health(): return {"status": "ok"} app.mount("/static", StaticFiles(directory="static"), name="static")