2026-05-28 22:24:21 +02:00

175 lines
4.6 KiB
Python

import os
import re
from datetime import datetime
from decimal import Decimal
from typing import Any
import psycopg
from psycopg.rows import dict_row
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse, Response
from fastapi.encoders import jsonable_encoder
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
from weasyprint import HTML
DATABASE_URL = os.getenv("DATABASE_URL")
REPORT_TITLE = os.getenv("REPORT_TITLE", "MESAVAULT Edge-OEE UCEPSA")
if not DATABASE_URL:
raise RuntimeError("DATABASE_URL environment variable is required")
app = FastAPI(title="MESAVAULT Order Report API")
templates = Jinja2Templates(directory="/app/app/templates")
def safe_filename(value: str) -> str:
cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-")
return cleaned or "order-report"
def fmt_dt(value: Any) -> str:
if value is None:
return ""
if isinstance(value, datetime):
return value.strftime("%d/%m/%Y %H:%M:%S")
text = str(value)
text = re.sub(r"\.\d+", "", text)
text = text.replace("+02:00", "").replace("+01:00", "")
return text
def fmt_num(value: Any, decimals: int = 2) -> str:
if value is None:
return ""
try:
number = float(value)
return f"{number:.{decimals}f}"
except (ValueError, TypeError):
return str(value)
templates.env.filters["dt"] = fmt_dt
templates.env.filters["num"] = fmt_num
def fetch_one(query: str, params: tuple[Any, ...]) -> dict[str, Any] | None:
with psycopg.connect(DATABASE_URL, row_factory=dict_row) as conn:
with conn.cursor() as cur:
cur.execute(query, params)
return cur.fetchone()
def fetch_all(query: str, params: tuple[Any, ...]) -> list[dict[str, Any]]:
with psycopg.connect(DATABASE_URL, row_factory=dict_row) as conn:
with conn.cursor() as cur:
cur.execute(query, params)
return list(cur.fetchall())
def build_report_context(order_ref: str) -> dict[str, Any]:
report = fetch_one(
"""
SELECT *
FROM mv_reports_ucepsa_demo.order_execution_report_full_v1
WHERE odoo_order_ref = %s
""",
(order_ref,),
)
if report is None:
raise HTTPException(status_code=404, detail=f"Order not found: {order_ref}")
stops = fetch_all(
"""
SELECT
stop_id,
stop_started_at,
stop_ended_at,
attributed_duration_min,
cause_code,
operator_note,
classification_status,
attributed_cost_eur,
stop_quality_status
FROM mv_reports_ucepsa_demo.order_execution_stops_v1
WHERE odoo_order_ref = %s
ORDER BY attributed_started_at
""",
(order_ref,),
)
timeline = fetch_all(
"""
SELECT
event_ts,
event_type,
event_label,
severity,
cause_code,
duration_min,
cost_eur,
description
FROM mv_reports_ucepsa_demo.order_execution_timeline_v1
WHERE odoo_order_ref = %s
ORDER BY event_ts, event_type
""",
(order_ref,),
)
return {
"title": REPORT_TITLE,
"report": report,
"stops": stops,
"timeline": timeline,
}
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/api/report")
def api_report(order_ref: str = Query(..., description="Odoo order reference, e.g. WH/MO/00025")):
return jsonable_encoder(build_report_context(order_ref))
@app.get("/report.html", response_class=HTMLResponse)
def report_html(
request: Request,
order_ref: str = Query(..., description="Odoo order reference, e.g. WH/MO/00025"),
):
context = build_report_context(order_ref)
context["request"] = request
return templates.TemplateResponse(
"order_report.html",
context,
)
@app.get("/report.pdf")
def report_pdf(order_ref: str = Query(..., description="Odoo order reference, e.g. WH/MO/00025")):
context = build_report_context(order_ref)
template = templates.env.get_template("order_report.html")
html_content = template.render(**context)
pdf_bytes = HTML(
string=html_content,
base_url="/app/app",
).write_pdf()
filename = f"mesavault-order-report-{safe_filename(order_ref)}.pdf"
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'inline; filename="{filename}"'
},
)