Dockerized web app (FastAPI + React + PostgreSQL) for scoping client ratecards against the GMAL master asset database. Features: - GMAL data ingestion from Excel (390 assets, 120 roles, 5 model types) - AI-powered document parsing and asset extraction (Claude Opus 4.6) - AI matching engine with parallel batching, confidence scoring, caveats - Ratecard builder with hours x volume calculation - Excel and PDF export - GMAL browser and inline editor - AI cost tracking per project (persisted to DB) - Debug panel for AI call inspection - Dark theme UI with gold (#FFC407) accent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
"""Export caveats report to PDF."""
|
|
|
|
import io
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import A4
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.units import mm
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.gmal import GmalAsset
|
|
from app.models.project import Project, ClientAsset, Match
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CONFIDENCE_COLORS = {
|
|
"exact": colors.HexColor("#22c55e"),
|
|
"close": colors.HexColor("#f59e0b"),
|
|
"multiple": colors.HexColor("#3b82f6"),
|
|
"none": colors.HexColor("#ef4444"),
|
|
}
|
|
|
|
|
|
async def export_caveats_pdf(db: AsyncSession, project: Project) -> bytes:
|
|
"""Generate a PDF caveats report for a project.
|
|
|
|
Returns the PDF as bytes.
|
|
"""
|
|
buf = io.BytesIO()
|
|
doc = SimpleDocTemplate(buf, pagesize=A4, topMargin=20 * mm, bottomMargin=20 * mm)
|
|
|
|
styles = getSampleStyleSheet()
|
|
title_style = ParagraphStyle("CustomTitle", parent=styles["Title"], fontSize=18, spaceAfter=12)
|
|
heading_style = ParagraphStyle("CustomHeading", parent=styles["Heading2"], fontSize=13, spaceAfter=6)
|
|
body_style = styles["BodyText"]
|
|
caveat_style = ParagraphStyle("Caveat", parent=body_style, textColor=colors.HexColor("#92400e"), leftIndent=10)
|
|
|
|
elements = []
|
|
|
|
# Title
|
|
elements.append(Paragraph(f"Scope Builder - Caveats Report", title_style))
|
|
elements.append(Paragraph(f"Project: {project.name}", heading_style))
|
|
if project.client_name:
|
|
elements.append(Paragraph(f"Client: {project.client_name}", body_style))
|
|
elements.append(Paragraph(f"Generated: {datetime.utcnow().strftime('%d %B %Y')}", body_style))
|
|
elements.append(Spacer(1, 12))
|
|
|
|
# Load client assets and matches
|
|
assets_result = await db.execute(
|
|
select(ClientAsset).where(ClientAsset.project_id == project.id).order_by(ClientAsset.sort_order)
|
|
)
|
|
client_assets = assets_result.scalars().all()
|
|
|
|
for ca in client_assets:
|
|
matches_result = await db.execute(
|
|
select(Match).where(Match.client_asset_id == ca.id).order_by(Match.rank)
|
|
)
|
|
matches = matches_result.scalars().all()
|
|
|
|
elements.append(Paragraph(f"{ca.raw_name} (Volume: {ca.volume})", heading_style))
|
|
|
|
if ca.raw_description:
|
|
elements.append(Paragraph(f"<i>Client description: {ca.raw_description}</i>", body_style))
|
|
elements.append(Spacer(1, 4))
|
|
|
|
if not matches:
|
|
elements.append(Paragraph("No matches found.", caveat_style))
|
|
elements.append(Spacer(1, 8))
|
|
continue
|
|
|
|
for match in matches:
|
|
# Load GMAL asset
|
|
gmal_result = await db.execute(select(GmalAsset).where(GmalAsset.id == match.gmal_asset_id))
|
|
gmal = gmal_result.scalar_one_or_none()
|
|
if not gmal:
|
|
continue
|
|
|
|
selected_marker = " [SELECTED]" if match.is_selected else ""
|
|
conf_label = match.confidence.value.upper()
|
|
score_pct = f"{float(match.confidence_score) * 100:.0f}%" if match.confidence_score else "N/A"
|
|
|
|
elements.append(Paragraph(
|
|
f"<b>Match #{match.rank}: {gmal.gmal_id} - {gmal.unique_name or gmal.asset_name}</b>{selected_marker}",
|
|
body_style,
|
|
))
|
|
elements.append(Paragraph(f"Confidence: {conf_label} ({score_pct})", body_style))
|
|
|
|
if match.ai_reasoning:
|
|
elements.append(Paragraph(f"<b>Reasoning:</b> {match.ai_reasoning}", body_style))
|
|
|
|
if match.caveat_text:
|
|
elements.append(Paragraph(f"<b>Caveats:</b>", body_style))
|
|
elements.append(Paragraph(match.caveat_text, caveat_style))
|
|
|
|
elements.append(Spacer(1, 6))
|
|
|
|
elements.append(Spacer(1, 12))
|
|
|
|
doc.build(elements)
|
|
buf.seek(0)
|
|
return buf.read()
|