import logging from io import BytesIO from pathlib import Path from typing import Literal from fastapi import APIRouter, HTTPException from fastapi.responses import HTMLResponse, StreamingResponse from jinja2 import Environment, FileSystemLoader from pydantic import BaseModel from weasyprint import HTML from hdbcli import dbapi from core.hana import hana_connection, HANA_SCHEMA router = APIRouter( tags=["Reportes (Jinja/HTML)"] ) TEMPLATES_REPORTS_DIR = Path(__file__).resolve().parent.parent / "templates" / "reports" templates_env = Environment(loader=FileSystemLoader("templates/reports")) logger = logging.getLogger(__name__) # Tamaño de papel que usa cada reporte al renderizar el PDF (CSS @page.size). PAPER_SIZES = {"A4": "A4", "Carta": "letter"} # Alto útil de la página en orientación horizontal (tamaño de papel menos el # 1.6cm de margen superior + 1cm de margen inferior definidos en el @page de # stickers.html). PAGINA_ALTO_LANDSCAPE = {"A4": "18.4cm", "Carta": "18.99cm"} # Query original de Crystal Reports para DLN20015, adaptada: el placeholder # de Crystal `{?Dockey@}` se reemplaza por un bind param `?` de hdbcli, y cada # tabla queda calificada con el schema de la compañía SAP B1 (HANA_SCHEMA). STICKERS_QUERY = f""" SELECT TOP 1 T0."DocEntry", T0."DocNum", CASE WHEN IFNULL(T0."NumAtCard",'') <> '' THEN T0."NumAtCard" ELSE T0."FolioPref"||'-'||T0."FolioNum" END AS "GUIA_REMISION", T0."U_SIS_ORDCOM" AS "ORDEN_COMPRA", CASE WHEN T0."Indicator" = 'AN' THEN 'ANULADO' END AS "ESTADO", T0."CardCode", T0."CardName", T4."Phone1" AS "TELEFONO", T7."StreetS" AS "DIRECCION", T7."CountyS" AS "PROVINCIA", T14."Name" AS "DEPARTAMENTO", T7."ZipCodeS" AS "DISTRITO", T6."IndDesc" AS "GRUPO_CLIENTE", CASE WHEN T4."GroupCode" = '100' THEN 'VENTA LOCAL' WHEN T4."GroupCode" = '102' THEN 'VENTA EXTERIOR' END AS "TIPO_VENTA", T0."DocDate" AS "FECHA", T1."ItemCode", T2."ItemName", T3."Name" AS "LINEA", T10."BatchNum" AS "LOTE", T1."unitMsr" AS "UNIDADES", T10."Quantity" AS "CANTIDAD", T0."Indicator" AS "DOCUMENTO", T0."U_SIS_FECENT" AS "FECHA_ENTREGA", T8."SlpName" AS "REPRESENTANTE", UPPER(T9."U_NAME") AS "ASISTENTE_VENTA", T0."Comments" AS "COMENTARIOS", CASE T0."U_SIS_TIPDESP" WHEN 'N' THEN 'LOCAL' WHEN 'A' THEN 'PROVINCIA' WHEN 'U' THEN 'LOCAL - URGENTE' WHEN 'O' THEN 'OFICINA' WHEN 'V' THEN '-' WHEN 'J' THEN 'PROVINCIA -URGENTE' WHEN 'R' THEN 'REGULARIZACION' ELSE 'LOCAL' END AS "TIPO_DESPACHO", T11."ExpDate" AS "FECHA VENCIMIENTO", CASE T0."U_SIS_HORREC" WHEN '100' THEN 'SIN HORARIO' WHEN '101' THEN '08 AM - 10 AM' WHEN '102' THEN '10 AM - 01 PM' WHEN '103' THEN '01 PM - 03 PM' WHEN '104' THEN '03 PM - 06 PM' ELSE 'SIN HORARIO' END AS "HORARIO RECOJO", T11."LotNumber" AS "UBICACION", T1."WhsCode" AS "ALMACEN", T1."VisOrder", T0.U_EXX_NOMTRANS AS "TRANSPORTISTA", ROW_NUMBER() OVER (PARTITION BY T1."VisOrder" ORDER BY T1."VisOrder" DESC) AS row_num FROM "{HANA_SCHEMA}".ODLN T0 INNER JOIN "{HANA_SCHEMA}".DLN1 T1 ON T0."DocEntry" = T1."DocEntry" INNER JOIN "{HANA_SCHEMA}".OITM T2 ON T1."ItemCode" = T2."ItemCode" LEFT JOIN "{HANA_SCHEMA}"."@SIS_LINART" T3 ON T2.U_SIS_LINART = T3."Code" INNER JOIN "{HANA_SCHEMA}".OCRD T4 ON T0."CardCode" = T4."CardCode" INNER JOIN "{HANA_SCHEMA}".NNM1 T5 ON T0."Series"=T5."Series" LEFT JOIN "{HANA_SCHEMA}".OOND T6 ON T4."IndustryC" = T6."IndCode" LEFT JOIN "{HANA_SCHEMA}".DLN12 T7 ON T0."DocEntry" = T7."DocEntry" LEFT JOIN "{HANA_SCHEMA}".OCST T14 ON T14."Code" = T7."StateS" AND T14."Country" = T7."CountryS" LEFT JOIN "{HANA_SCHEMA}".OSLP T8 ON T0."SlpCode" = T8."SlpCode" LEFT JOIN "{HANA_SCHEMA}".OUSR T9 ON T0."UserSign" = T9."USERID" LEFT JOIN "{HANA_SCHEMA}".IBT1 T10 ON T1."ItemCode"=T10."ItemCode" AND T0."DocEntry"=T10."BaseEntry" AND T1."WhsCode"=T10."WhsCode" AND T0."ObjType"=T10."BaseType" AND T0."DocNum"=T10."BaseNum" AND T1."LineNum" = T10."BaseLinNum" AND T10."Direction"='1' LEFT JOIN "{HANA_SCHEMA}".OBTN T11 ON T11."ItemCode" = T1."ItemCode" AND T10."BatchNum" = T11."DistNumber" WHERE T0."DocEntry" = ? """ # ============================================================================ class StickersParams(BaseModel): doc_entry: int cantidad: int formato: Literal["pdf", "html"] = "pdf" papel: Literal["A4", "Carta"] = "Carta" # ============================================================================ @router.post("/api/reports/stickers") async def generar_stickers(params: StickersParams): log_extra = {"http_method": "POST", "endpoint": "reports/stickers"} logger.info(f"Generando reporte de stickers para DocEntry {params.doc_entry}", extra=log_extra) try: with hana_connection() as conn: cursor = conn.cursor() cursor.execute(STICKERS_QUERY, (params.doc_entry,)) row = cursor.fetchone() columns = [d[0] for d in cursor.description] except dbapi.Error as exc: logger.error(f"Error al consultar la guía de remisión {params.doc_entry}: {exc}", extra=log_extra) raise HTTPException(status_code=500, detail=f"Error al consultar el reporte: {exc}") if not row: logger.warning(f"DocEntry {params.doc_entry} no encontrado", extra=log_extra) raise HTTPException(status_code=404, detail=f"No se encontró el documento {params.doc_entry}") fila = dict(zip(columns, row)) lugar = " - ".join( p for p in (fila.get("DEPARTAMENTO"), fila.get("PROVINCIA"), fila.get("DISTRITO")) if p ) datos = { "cliente": fila.get("CardName"), "direccion": fila.get("DIRECCION"), "lugar": lugar, "documento": fila.get("GUIA_REMISION"), } stickers = [{"numero": i, "total": params.cantidad} for i in range(1, params.cantidad + 1)] paginas = [stickers[i:i + 4] for i in range(0, len(stickers), 4)] template = templates_env.get_template("stickers.html") html_renderizado = template.render( datos=datos, paginas=paginas, paper_size=PAPER_SIZES[params.papel], pagina_alto=PAGINA_ALTO_LANDSCAPE[params.papel], ) if params.formato == "html": logger.info(f"Reporte de stickers generado en HTML para DocEntry {params.doc_entry}", extra={**log_extra, "status_code": 200}) return HTMLResponse(content=html_renderizado) pdf_bytes = HTML(string=html_renderizado, base_url=str(TEMPLATES_REPORTS_DIR)).write_pdf() logger.info(f"Reporte de stickers generado en PDF para DocEntry {params.doc_entry}", extra={**log_extra, "status_code": 200}) return StreamingResponse( BytesIO(pdf_bytes), media_type="application/pdf", headers={"Content-Disposition": f'inline; filename="stickers_{params.doc_entry}.pdf"'}, )