175 lines
4.6 KiB
Python
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}"'
|
|
},
|
|
)
|