92 lines
4.4 KiB
Python
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)})
|