Initial commit: mini-erp (backend FastAPI + frontend React/Vite)
Base para el flujo de ramas por cliente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
ab3c613165
154 files changed
+16872
No files matched your search
@@ -0,0 +1,172 @@
|
||||
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 los
|
||||
# 0.3in de margen superior e inferior definidos en el @page de stickers.html).
|
||||
PAGINA_ALTO_LANDSCAPE = {"A4": "7.67in", "Carta": "7.9in"}
|
||||
|
||||
# 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"'},
|
||||
)
|
||||
Reference in new issue
Block a user