For terceros, _guardar_envio stores the automation date in the factura field (not a patient id), so COALESCE(factura, cedula) still collapsed all terceros into one row. Now GROUP BY uses cedula for tipo=terceros and factura for transaccion/ventas. Template hides the date in the factura column for terceros rows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
285 lines
9.7 KiB
Python
285 lines
9.7 KiB
Python
import json as json_lib
|
|
from datetime import datetime, date as date_cls
|
|
import httpx
|
|
from fastapi import APIRouter, Request, Depends
|
|
from fastapi.responses import JSONResponse
|
|
from app.database import get_connection
|
|
from app.auth import get_current_user
|
|
from app.services.json_generator import _clean_times
|
|
from app.services.api_client import get_tns_token, TNS_BASE
|
|
from app.utils.activity import log_activity, get_ip
|
|
from app.routes.automation import _parse_tns_resp
|
|
|
|
router = APIRouter(prefix="/logs", tags=["logs"])
|
|
|
|
|
|
def _limpiar_json_enviado(raw: str) -> str:
|
|
if not raw:
|
|
return raw
|
|
try:
|
|
return json_lib.dumps(_clean_times(json_lib.loads(raw)), ensure_ascii=False, indent=2)
|
|
except Exception:
|
|
return raw
|
|
|
|
PAGE_SIZE = 50
|
|
|
|
|
|
@router.get("")
|
|
async def logs_page(
|
|
request: Request,
|
|
user: dict = Depends(get_current_user),
|
|
tipo: str = "",
|
|
status: str = "",
|
|
factura: str = "",
|
|
paciente: str = "",
|
|
fecha_desde: str = "",
|
|
fecha_hasta: str = "",
|
|
offset: int = 0,
|
|
):
|
|
conn = get_connection()
|
|
where = ["1=1"]
|
|
params = []
|
|
|
|
if tipo:
|
|
where.append("e.tipo = ?")
|
|
params.append(tipo)
|
|
if status:
|
|
where.append("e.status = ?")
|
|
params.append(status)
|
|
if factura:
|
|
where.append("e.factura LIKE ?")
|
|
params.append(f"%{factura}%")
|
|
if paciente:
|
|
where.append("(e.cedula LIKE ? OR e.factura LIKE ?)")
|
|
params += [f"%{paciente}%", f"%{paciente}%"]
|
|
if fecha_desde:
|
|
where.append("DATE(e.created_at) >= ?")
|
|
params.append(fecha_desde)
|
|
if fecha_hasta:
|
|
where.append("DATE(e.created_at) <= ?")
|
|
params.append(fecha_hasta)
|
|
|
|
w = " AND ".join(where)
|
|
|
|
total = conn.execute(f"SELECT COUNT(*) FROM envios e WHERE {w}", params).fetchone()[0]
|
|
|
|
envios = conn.execute(f"""
|
|
SELECT e.*, u.username FROM envios e
|
|
LEFT JOIN users u ON e.user_id = u.id
|
|
WHERE {w}
|
|
ORDER BY e.created_at DESC
|
|
LIMIT ? OFFSET ?
|
|
""", params + [PAGE_SIZE, offset]).fetchall()
|
|
|
|
stats = conn.execute("""
|
|
SELECT tipo, status, COUNT(*) as cnt
|
|
FROM envios
|
|
GROUP BY tipo, status
|
|
ORDER BY tipo, status
|
|
""").fetchall()
|
|
|
|
conn.close()
|
|
|
|
has_more = (offset + PAGE_SIZE) < total
|
|
|
|
return request.app.state.templates.TemplateResponse("logs.html", {
|
|
"request": request, "user": user,
|
|
"envios": envios, "stats": stats, "total": total,
|
|
"filtro_tipo": tipo, "filtro_status": status,
|
|
"filtro_factura": factura, "filtro_paciente": paciente,
|
|
"filtro_fecha_desde": fecha_desde, "filtro_fecha_hasta": fecha_hasta,
|
|
"offset": offset, "page_size": PAGE_SIZE, "has_more": has_more,
|
|
})
|
|
|
|
|
|
@router.get("/actividad")
|
|
async def actividad_page(
|
|
request: Request,
|
|
user: dict = Depends(get_current_user),
|
|
username: str = "",
|
|
accion: str = "",
|
|
fecha_desde: str = "",
|
|
fecha_hasta: str = "",
|
|
offset: int = 0,
|
|
):
|
|
conn = get_connection()
|
|
where = ["1=1"]
|
|
params = []
|
|
if username:
|
|
where.append("a.username = ?")
|
|
params.append(username)
|
|
if accion:
|
|
where.append("a.accion = ?")
|
|
params.append(accion)
|
|
if fecha_desde:
|
|
where.append("DATE(a.created_at) >= ?")
|
|
params.append(fecha_desde)
|
|
if fecha_hasta:
|
|
where.append("DATE(a.created_at) <= ?")
|
|
params.append(fecha_hasta)
|
|
w = " AND ".join(where)
|
|
total = conn.execute(f"SELECT COUNT(*) FROM activity_log a WHERE {w}", params).fetchone()[0]
|
|
rows = conn.execute(f"""
|
|
SELECT a.* FROM activity_log a
|
|
WHERE {w} ORDER BY a.created_at DESC LIMIT ? OFFSET ?
|
|
""", params + [PAGE_SIZE, offset]).fetchall()
|
|
usuarios = conn.execute("SELECT DISTINCT username FROM activity_log ORDER BY username").fetchall()
|
|
acciones = conn.execute("SELECT DISTINCT accion FROM activity_log ORDER BY accion").fetchall()
|
|
conn.close()
|
|
return request.app.state.templates.TemplateResponse("actividad.html", {
|
|
"request": request, "user": user,
|
|
"rows": rows, "total": total,
|
|
"usuarios": [u["username"] for u in usuarios],
|
|
"acciones": [a["accion"] for a in acciones],
|
|
"filtro_username": username, "filtro_accion": accion,
|
|
"filtro_fecha_desde": fecha_desde, "filtro_fecha_hasta": fecha_hasta,
|
|
"offset": offset, "page_size": PAGE_SIZE,
|
|
"has_more": (offset + PAGE_SIZE) < total,
|
|
})
|
|
|
|
|
|
@router.post("/reenviar/{envio_id}")
|
|
async def reenviar_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)):
|
|
conn = get_connection()
|
|
row = conn.execute("SELECT * FROM envios WHERE id = ?", (envio_id,)).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return JSONResponse({"success": False, "message": "Registro no encontrado"})
|
|
if row["status"] in ("success", "warning"):
|
|
return JSONResponse({"success": False, "message": "No aplica reenvío para este registro"})
|
|
if not row["json_enviado"]:
|
|
return JSONResponse({"success": False, "message": "Sin JSON guardado para reenviar"})
|
|
|
|
try:
|
|
rda_json = json_lib.loads(row["json_enviado"])
|
|
except Exception:
|
|
return JSONResponse({"success": False, "message": "JSON inválido en registro"})
|
|
|
|
conn_cfg = get_connection()
|
|
cfg = {r["key"]: r["value"] for r in conn_cfg.execute("SELECT * FROM config").fetchall()}
|
|
conn_cfg.close()
|
|
|
|
token, err = await get_tns_token(
|
|
cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "")
|
|
)
|
|
if not token:
|
|
return JSONResponse({"success": False, "message": f"Error login TNS: {err}"})
|
|
|
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
api_sucursal = cfg.get("api_sucursal", "00") or "00"
|
|
tipo = row["tipo"]
|
|
endpoint = (
|
|
f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
|
|
if tipo == "transaccion"
|
|
else f"{TNS_BASE}/v2/facturacion/Ventas/Crear?codigosucursal={api_sucursal}"
|
|
if tipo == "ventas"
|
|
else f"{TNS_BASE}/v2/tablas/Tercero/Crear"
|
|
)
|
|
|
|
new_status = "error"
|
|
msg = ""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
r = await client.post(endpoint, json=rda_json, headers=headers)
|
|
new_status, msg = _parse_tns_resp(r)
|
|
except Exception as ex:
|
|
msg = str(ex)
|
|
|
|
if new_status != "error":
|
|
conn = get_connection()
|
|
conn.execute(
|
|
"UPDATE envios SET status=?, mensaje_tns=?, respuesta_api=?, created_at=? WHERE id=?",
|
|
(new_status, msg[:500], msg[:5000], datetime.now().isoformat(), envio_id)
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
log_activity(user["user_id"], user["username"], "reenvio_ok",
|
|
f"Envío #{envio_id} factura {row['factura']} reenvío {new_status}", get_ip(request))
|
|
|
|
return JSONResponse({"success": new_status != "error", "message": msg})
|
|
|
|
|
|
@router.get("/resumen")
|
|
async def resumen_page(
|
|
request: Request,
|
|
user: dict = Depends(get_current_user),
|
|
fecha: str = "",
|
|
tipo: str = "",
|
|
):
|
|
if not fecha:
|
|
fecha = date_cls.today().isoformat()
|
|
|
|
conn = get_connection()
|
|
|
|
where_extra = ""
|
|
params_q: list = [fecha]
|
|
if tipo:
|
|
where_extra = "AND tipo = ?"
|
|
params_q.append(tipo)
|
|
|
|
rows = conn.execute(f"""
|
|
SELECT
|
|
factura, tipo,
|
|
MAX(contrato) AS contrato,
|
|
MIN(cedula) AS cedula,
|
|
MAX(idrecepcion) AS idrecepcion,
|
|
COUNT(*) AS intentos,
|
|
SUM(CASE WHEN status != 'error' THEN 1 ELSE 0 END) AS exitos,
|
|
MIN(created_at) AS primer_envio,
|
|
MAX(created_at) AS ultimo_envio,
|
|
MAX(CASE WHEN status = 'error' THEN id END) AS ultimo_error_id,
|
|
MAX(CASE WHEN status != 'error' THEN id END) AS exitoso_id,
|
|
CASE
|
|
WHEN SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) > 0 THEN 'success'
|
|
WHEN SUM(CASE WHEN status = 'warning' THEN 1 ELSE 0 END) > 0 THEN 'warning'
|
|
ELSE 'error'
|
|
END AS estado_final
|
|
FROM envios
|
|
WHERE DATE(created_at) = ? {where_extra}
|
|
GROUP BY CASE WHEN tipo = 'terceros' THEN cedula ELSE factura END, tipo
|
|
ORDER BY estado_final, tipo, CASE WHEN tipo = 'terceros' THEN cedula ELSE factura END
|
|
""", params_q).fetchall()
|
|
|
|
conn.close()
|
|
|
|
total = len(rows)
|
|
exitosos = sum(1 for r in rows if r["estado_final"] != "error")
|
|
errores = sum(1 for r in rows if r["estado_final"] == "error")
|
|
|
|
return request.app.state.templates.TemplateResponse("resumen.html", {
|
|
"request": request, "user": user,
|
|
"fecha": fecha,
|
|
"filtro_tipo": tipo,
|
|
"rows": rows,
|
|
"total": total,
|
|
"exitosos": exitosos,
|
|
"errores": errores,
|
|
})
|
|
|
|
|
|
@router.get("/detalle/{envio_id}")
|
|
async def detalle_envio(envio_id: int, request: Request, user: dict = Depends(get_current_user)):
|
|
conn = get_connection()
|
|
row = conn.execute(
|
|
"SELECT e.*, u.username FROM envios e LEFT JOIN users u ON e.user_id = u.id WHERE e.id = ?",
|
|
(envio_id,)
|
|
).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return JSONResponse({"error": "No encontrado"}, status_code=404)
|
|
return JSONResponse({
|
|
"id": row["id"],
|
|
"tipo": row["tipo"],
|
|
"factura": row["factura"],
|
|
"idrecepcion": row["idrecepcion"],
|
|
"contrato": row["contrato"],
|
|
"status": row["status"],
|
|
"mensaje_tns": row["mensaje_tns"],
|
|
"respuesta_api": row["respuesta_api"],
|
|
"json_enviado": _limpiar_json_enviado(row["json_enviado"]),
|
|
"fecha_inicio": row["fecha_inicio"],
|
|
"fecha_fin": row["fecha_fin"],
|
|
"created_at": row["created_at"],
|
|
"username": row["username"],
|
|
})
|