""" Endpoint generazione verbale di istruttoria in HTML/PDF via Jinja2 + weasyprint. Solo ruoli istruttore/superadmin possono scaricare. """ from datetime import datetime, date from uuid import UUID from pathlib import Path from collections import OrderedDict from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import Response, HTMLResponse from jinja2 import Environment, FileSystemLoader, select_autoescape from sqlalchemy.orm import Session from sqlalchemy import text from ..db import get_db from ..auth import AuthUser, get_current_user from ..models import RemissionPractice from .practices import _compute_gate_check router = APIRouter(prefix="/api/remission-practices/instructor", tags=["verbale"]) TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates_jinja" # ---------- Jinja env & filters ---------- def _euro(v): if v is None: return "—" try: n = float(v) except (TypeError, ValueError): return "—" s = f"{n:,.2f}" # IT locale: 1,234,567.89 → 1.234.567,89 s = s.replace(",", "X").replace(".", ",").replace("X", ".") return f"€ {s}" def _datefmt(v): if v is None: return "—" if isinstance(v, str): try: v = datetime.fromisoformat(v).date() except ValueError: return v if hasattr(v, "strftime"): return v.strftime("%d/%m/%Y") return str(v) def _datetimefmt(v): if v is None: return "—" if isinstance(v, str): try: v = datetime.fromisoformat(v) except ValueError: return v if hasattr(v, "strftime"): return v.strftime("%d/%m/%Y %H:%M") return str(v) _AMEND_STATUS = { "AWAITING": "In attesa risposta", "RESPONSE_RECEIVED": "Risposta ricevuta", "CLOSED": "Chiuso", "EXPIRED": "Scaduto", "REJECTED": "Rifiutato", } def _amendstatus(s): return _AMEND_STATUS.get(s, s or "—") _env = Environment( loader=FileSystemLoader(str(TEMPLATES_DIR)), autoescape=select_autoescape(["html", "xml"]), trim_blocks=True, lstrip_blocks=True, ) _env.filters["euro"] = _euro _env.filters["datefmt"] = _datefmt _env.filters["datetimefmt"] = _datetimefmt _env.filters["amendstatus"] = _amendstatus _CONTRACT_LABELS = { "T_IND": "Tempo indeterminato", "T_DET": "Tempo determinato", "APPR": "Apprendistato", "STAGE": "Tirocinio / Stage", "COLL": "Collaborazione coordinata", "ALTRO": "Altro", } def _is_instructor(user: AuthUser) -> bool: return user.role in ("ROLE_PRE_INSTRUCTOR", "ROLE_INSTRUCTOR_MANAGER", "ROLE_SUPER_ADMIN") def _build_context(db: Session, practice: RemissionPractice, user: AuthUser) -> dict: """Prepara tutto il contesto per il template.""" # Gate check + totali gate_obj = _compute_gate_check(db, practice); gate = gate_obj.model_dump() if hasattr(gate_obj, "model_dump") else dict(gate_obj) totals = gate.get("totals") or {} # Schema sections sections = practice.schema_snapshot.get("sections") or [] cat_section = next((s for s in sections if s.get("type") == "category_grid"), {}) or {} categories = cat_section.get("categories") or [] categories_map = {c.get("code"): c.get("label") for c in categories} cat_order = {c.get("code"): i for i, c in enumerate(categories)} ula_section = next((s for s in sections if s.get("type") == "ula_block"), {}) or {} ula_enabled = bool(ula_section.get("enabled")) ula_threshold = float(ula_section.get("threshold") or 1) docs_section = next((s for s in sections if s.get("type") == "document_checklist"), {}) or {} docs_required_raw = docs_section.get("required_types") or [] docs_required = [ (r if isinstance(r, dict) else {"code": r, "label": r}) for r in docs_required_raw ] docs_by_code = {d.doc_code: { "filename": d.filename, "verification_status": d.verification_status, "verification_notes": d.verification_notes, } for d in practice.documents} # Raggruppo fatture per categoria in ordine schema sorted_invoices = sorted( practice.invoices, key=lambda i: (cat_order.get(i.category_code, 999), i.invoice_number or "") ) invoices_by_cat = OrderedDict() for c in categories: invoices_by_cat[c.get("code")] = [] for inv in sorted_invoices: invoices_by_cat.setdefault(inv.category_code, []).append(inv) per_cat_declared = totals.get("per_category_declared") or {} per_cat_verified = totals.get("per_category_verified") or {} # ULA aggregati ula_fte_decl = sum(float(e.fte_pct or 0) for e in practice.ula_employees) ula_fte_verif = sum( float((e.fte_pct_verified if e.fte_pct_verified is not None else e.fte_pct) or 0) for e in practice.ula_employees if e.verification_status in ("AMMESSA", "PARZIALE") ) ula_ok = ula_fte_verif >= ula_threshold # Anagrafica beneficiario (da gepafin_schema.company) company_row = db.execute(text(""" SELECT company_name, vat_number FROM gepafin_schema.company WHERE id = :cid """), {"cid": practice.company_id}).mappings().first() company = dict(company_row) if company_row else {} # Istruttore instructor_name = None if practice.reviewed_by: row = db.execute(text(""" SELECT first_name || ' ' || last_name AS name FROM gepafin_schema.gepafin_user WHERE id = :uid """), {"uid": practice.reviewed_by}).scalar() instructor_name = row elif user.user_id: row = db.execute(text(""" SELECT first_name || ' ' || last_name AS name FROM gepafin_schema.gepafin_user WHERE id = :uid """), {"uid": user.user_id}).scalar() instructor_name = row return { "practice": practice, "totals": totals, "categories_map": categories_map, "invoices_by_cat": invoices_by_cat, "per_cat_declared": per_cat_declared, "per_cat_verified": per_cat_verified, "ula_section_enabled": ula_enabled, "ula_threshold": ula_threshold, "ula_fte_decl": ula_fte_decl, "ula_fte_verif": ula_fte_verif, "ula_ok": ula_ok, "docs_required": docs_required, "docs_by_code": docs_by_code, "amendments": practice.amendment_requests or [], "contract_labels": _CONTRACT_LABELS, "company": company, "instructor_name": instructor_name, "generated_at": datetime.now().strftime("%d/%m/%Y"), } def _render_html(db: Session, practice_id: UUID, user: AuthUser) -> tuple[RemissionPractice, str]: practice = db.query(RemissionPractice).filter(RemissionPractice.id == practice_id).first() if not practice: raise HTTPException(status_code=404, detail=f"Pratica {practice_id} non trovata") if not _is_instructor(user): raise HTTPException(status_code=403, detail="Ruolo istruttore richiesto") ctx = _build_context(db, practice, user) tpl = _env.get_template("verbale_istruttoria.html") html = tpl.render(**ctx) return practice, html @router.get("/{practice_id}/verbale.html", response_class=HTMLResponse) def verbale_html( practice_id: UUID, db: Session = Depends(get_db), user: AuthUser = Depends(get_current_user), ): """Rendering HTML del verbale — utile per debug e preview rapida.""" _, html = _render_html(db, practice_id, user) return HTMLResponse(content=html) @router.get("/{practice_id}/verbale.pdf") def verbale_pdf( practice_id: UUID, db: Session = Depends(get_db), user: AuthUser = Depends(get_current_user), ): """Generazione PDF via weasyprint.""" # Import locale per non rallentare il cold start se weasyprint manca from weasyprint import HTML as WeasyHTML practice, html = _render_html(db, practice_id, user) pdf_bytes = WeasyHTML(string=html).write_pdf() filename = f"verbale_istruttoria_pratica_{practice.application_id}.pdf" return Response( content=pdf_bytes, media_type="application/pdf", headers={ "Content-Disposition": f'attachment; filename="{filename}"', "Cache-Control": "no-store", }, )