Files

92 lines
4.4 KiB
Python
Raw Permalink Normal View History

2026-10-29 20:40:02 +00:00
import re
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import HTMLResponse, Response
from sqlalchemy.orm import Session
from ..audit import record
from ..config import settings
from ..db import get_db
from ..models import BudgetYear, Site, User
from ..reports import excel, render
from ..security import current_user
from ..summary import build_year_view
from ..template import build_template
from .years import get_year
router = APIRouter(prefix="/api", tags=["reports"])
XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
KINDS = ("cost-report", "budget")
FORMATS = ("html", "pdf", "xlsx")
MIME = {"html": "text/html; charset=utf-8", "pdf": "application/pdf", "xlsx": XLSX}
def _filename(kind: str, year: int, dept: str, site: str | None, ext: str) -> str:
stem = {"cost-report": "Capex_e_Custos", "budget": "Budget"}[kind]
clean = lambda text: re.sub(r"[^A-Za-z0-9]+", "_", text).strip("_")
suffix = f"_{clean(site)}" if site else ""
return f"{stem}_{clean(dept)}_{year}{suffix}.{ext}"
def disposition(name: str) -> str:
"""Content-Disposition for any file name (accents included)."""
from urllib.parse import quote
ascii_name = name.encode("ascii", "replace").decode().replace("?", "_").replace('"', "")
return f"attachment; filename=\"{ascii_name}\"; filename*=UTF-8''{quote(name)}"
def report_lang(lang: str | None, user: User) -> str:
return lang if lang in ("pt", "en") else (user.language or settings.default_language)
def render_report(db: Session, year: BudgetYear, kind: str, fmt: str, lang: str, site_id: int | None = None,
include_empty: bool = False, screen: bool = False) -> tuple[bytes, str]:
"""The report file and its download name. Shared by the download links and by email."""
site = db.get(Site, site_id).name if site_id and db.get(Site, site_id) else None
view = build_year_view(db, year, site_id if site else None)
name = _filename(kind, year.year, year.department.name, site, fmt)
if fmt == "xlsx":
data = (excel.cost_report_xlsx(view, lang, site) if kind == "cost-report"
else excel.budget_xlsx(view, lang, site, include_empty=True))
return data, name
html = (render.cost_report_html(view, lang, site, screen=screen) if kind == "cost-report"
else render.budget_html(view, lang, site, include_empty))
if fmt == "html":
return html.encode(), name
return render.to_pdf(html), name
@router.get("/years/{year_id}/reports/{kind}.{fmt}")
def report(year_id: int, kind: str, fmt: str, lang: str | None = None, site_id: int | None = None,
include_empty: bool = False, db: Session = Depends(get_db), user: User = Depends(current_user)):
if kind not in KINDS or fmt not in FORMATS:
raise HTTPException(404, "not_found")
year = get_year(db, year_id)
data, name = render_report(db, year, kind, fmt, report_lang(lang, user), site_id, include_empty,
screen=(fmt == "html"))
record(db, user, "report", year.id, "view" if fmt == "html" else "download",
f"{year.department.name} {year.year} · {kind}.{fmt}", year_id=year.id)
db.commit()
if fmt == "html":
return HTMLResponse(data.decode())
return Response(data, media_type=MIME[fmt], headers={"Content-Disposition": f'attachment; filename="{name}"'})
def workbook_file(db: Session, year: BudgetYear, lang: str) -> tuple[bytes, str]:
lines = [lv for cv in build_year_view(db, year).categories for lv in cv.lines]
data = build_template(db, year.department, lang, year.year, year.default_rate or 0, lines)
clean = re.sub(r"[^A-Za-z0-9]+", "_", year.department.name).strip("_")
return data, year.source_file or f"{'Orcamento' if lang == 'pt' else 'Budget'}_{clean}_{year.year}.xlsx"
@router.get("/years/{year_id}/workbook.xlsx")
def budget_workbook(year_id: int, lang: str | None = None, db: Session = Depends(get_db),
user: User = Depends(current_user)):
"""The budget as an editable Excel template, ready to be changed and uploaded again."""
year = get_year(db, year_id)
data, name = workbook_file(db, year, report_lang(lang, user))
record(db, user, "year", year.id, "download", f"{year.department.name} {year.year} · {name}", year_id=year.id)
db.commit()
return Response(data, media_type=XLSX, headers={"Content-Disposition": disposition(name)})