Files

92 lines
4.4 KiB
Python

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)})