feat: paso 4 ventas en automation + fixes varios

- Automation: agrega Paso 4 (Ventas/Crear) con fetch _SQL_VENTAS,
  filtro por excluded_ventas_set, loop de envío y log de actividad
- Automation: _guardar_envio ahora almacena idrecepcion y contrato
  para que /ventas reconozca registros enviados por automation
- Contratos: corrige NameError en toggle_excluir (faltaba request: Request)
- Ventas: agrega check if not q en send_one y send para evitar TypeError
- Ventas HTML: agrega columna Contrato en la tabla de facturas
- json_generator: normaliza etnia >2 chars a '99' (ej: 999 → 99)
- json_generator: agrega campo ciudadExp usando el mismo cod_ciudad

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-11 14:29:39 -05:00
co-authored by Claude Sonnet 4.6
parent 21eb99d18d
commit 73062af8bc
7 changed files with 249 additions and 68 deletions
+114 -9
View File
@@ -11,10 +11,11 @@ from app.services.json_generator import (
generar_tercero_api,
generar_rda_paciente,
agrupar_por_recepcion,
generar_factura_venta,
)
from app.services.api_client import get_tns_token, TNS_BASE
from app.services.whatsapp_sync import sync_paciente, sync_todos, guardar_sync_log
from app.routes.contratos import load_contrato_map, load_excluded_set, load_sin_contrato_set
from app.routes.contratos import load_contrato_map, load_excluded_set, load_sin_contrato_set, load_excluded_ventas_set
from app.utils.activity import log_activity, get_ip
router = APIRouter(prefix="/automation", tags=["automation"])
@@ -152,6 +153,31 @@ ORDER BY ps.ID_PS, rel.COD_EXAMEN
"""
_SQL_VENTAS = """
SELECT
r.IDRECEPCION,
r.PREFIJO,
r.NUM_FACTURA,
r.FECHA_RECEPCION,
r.COD_PACIENTE,
r.NIT_EMPRESA,
r.VALORTOTAL,
r.VALORDESC,
rel.COD_EXAMEN,
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
rel.PRECIO,
COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA,
TRIM(e.CODCONTRATO) AS CODCONTRATO
FROM RECEPCION r
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
LEFT JOIN TARIFA t ON TRIM(t.COD_EXAMEN) = TRIM(rel.COD_EXAMEN) AND t.TARIFA = e.TARIFA
WHERE r.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin
ORDER BY r.IDRECEPCION
"""
def _agrupar_por_presserv(rows: list) -> dict:
"""Agrupa filas de pre-servicios por ID_PS."""
from collections import defaultdict
@@ -267,11 +293,17 @@ async def preview_automation(
return JSONResponse({"success": False, "message": f"Error BD RDA: {err2}"})
ok3, err3, rows_ps = fb.execute_query(_SQL_PRESERV, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok3:
fb.disconnect()
return JSONResponse({"success": False, "message": f"Error BD Pre-servicios: {err3}"})
ok4, err4, rows_vta = fb.execute_query(_SQL_VENTAS, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok4:
return JSONResponse({"success": False, "message": f"Error BD Ventas: {err4}"})
excluded = load_excluded_set()
excluded_ventas = load_excluded_ventas_set()
grupos_all = agrupar_por_recepcion(rows_rda)
grupos = {k: v for k, v in grupos_all.items()
@@ -345,6 +377,25 @@ async def preview_automation(
if rda_por_pac.get(str(p.get("CODIGO", "")))
]
grupos_vta_all = agrupar_por_recepcion(rows_vta)
grupos_vta = {k: v for k, v in grupos_vta_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded_ventas}
ventas_preview = []
for id_rec, grupo_rows in grupos_vta.items():
num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
prefijo = str(grupo_rows[0].get("PREFIJO") or "").strip()
venta_json = generar_factura_venta(grupo_rows, default_vendedor="00",
default_prefijo=prefijo_def, numero_override=num_fac)
ventas_preview.append({
"factura": f"{prefijo}-{num_fac}",
"paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
"fecha": str(grupo_rows[0].get("FECHA_RECEPCION", ""))[:10],
"contrato": str(grupo_rows[0].get("CODCONTRATO") or "").strip(),
"examenes": len(grupo_rows),
"json": venta_json,
})
return JSONResponse({
"success": True,
"fecha": fecha,
@@ -353,8 +404,10 @@ async def preview_automation(
"examenes": total_examenes,
"excluidos": excluidos_count,
"preservicios": len(grupos_ps),
"ventas": len(grupos_vta),
"pacientes_preview": pacientes_preview,
"preservicios_preview": preservicios_preview,
"ventas_preview": ventas_preview,
})
@@ -401,15 +454,21 @@ async def run_automation(
# ── Pacientes EPS (para registrar terceros en paso 1) ─────────────────────
ok4, err4, rows_pac_eps = fb.execute_query(_SQL_PACIENTES_EPS, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok4:
fb.disconnect()
return JSONResponse({"success": False, "message": f"Error Firebird pacientes EPS: {err4}"})
# ── Ventas (Factura Venta) ────────────────────────────────────────────────
ok5, err5, rows_vta = fb.execute_query(_SQL_VENTAS, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
fb.disconnect()
if not ok5:
return JSONResponse({"success": False, "message": f"Error Firebird Ventas: {err5}"})
# Fusionar pacientes: particulares + EPS, deduplicando por CODIGO
codigos_vistos = {str(r.get("CODIGO")) for r in rows_pac}
rows_pac_todos = list(rows_pac) + [r for r in rows_pac_eps if str(r.get("CODIGO")) not in codigos_vistos]
if not rows_pac_todos and not rows_rda and not rows_ps:
if not rows_pac_todos and not rows_rda and not rows_ps and not rows_vta:
return JSONResponse({"success": False, "message": f"No hay datos para la fecha {fecha}"})
# ── Login TNS ─────────────────────────────────────────────────────────────
@@ -430,6 +489,7 @@ async def run_automation(
"paso1_terceros": {"enviados": 0, "errores": 0, "detalle": []},
"paso2_rda": {"enviados": 0, "errores": 0, "detalle": []},
"paso3_preserv": {"enviados": 0, "errores": 0, "detalle": []},
"paso4_ventas": {"enviados": 0, "errores": 0, "detalle": []},
}
# ── PASO 1: Enviar Terceros (particulares + EPS) ─────────────────────────
@@ -558,27 +618,72 @@ async def run_automation(
fecha_inicio=fecha, fecha_fin=fecha,
servicios=len(grupo_rows))
# ── PASO 4: Enviar Facturas Venta ─────────────────────────────────────────
excluded_ventas = load_excluded_ventas_set()
grupos_vta_all = agrupar_por_recepcion(rows_vta)
grupos_vta = {k: v for k, v in grupos_vta_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded_ventas}
resultado["paso4_ventas"]["excluidos"] = len(grupos_vta_all) - len(grupos_vta)
endpoint_venta = f"{TNS_BASE}/v2/facturacion/Ventas/Crear"
async with httpx.AsyncClient(timeout=timeout) as client:
for id_rec, grupo_rows in grupos_vta.items():
num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
prefijo = str(grupo_rows[0].get("PREFIJO") or "").strip()
venta_json = generar_factura_venta(grupo_rows, default_vendedor="00",
default_prefijo=prefijo_def, numero_override=num_fac)
factura_display = f"{prefijo}-{num_fac}"
contrato_vta = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
try:
resp = await client.post(endpoint_venta, json=venta_json, headers=headers)
ok = resp.is_success
msg = resp.json().get("message", "") if ok else resp.text[:200]
except Exception as e:
ok = False
msg = str(e)
if ok:
resultado["paso4_ventas"]["enviados"] += 1
else:
resultado["paso4_ventas"]["errores"] += 1
resultado["paso4_ventas"]["detalle"].append({
"factura": factura_display,
"paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
"examenes": len(grupo_rows),
"ok": ok, "msg": msg,
})
_guardar_envio(user["user_id"], "ventas", factura_display, venta_json, msg, ok,
fecha_inicio=fecha, fecha_fin=fecha, servicios=len(grupo_rows),
idrecepcion=id_rec, contrato=contrato_vta)
r1 = resultado["paso1_terceros"]
r2 = resultado["paso2_rda"]
r3 = resultado["paso3_preserv"]
r4 = resultado["paso4_ventas"]
log_activity(user["user_id"], user["username"], "automation_run",
f"Fecha {fecha} | Terceros: {r1['enviados']} OK/{r1['errores']} err | "
f"RDA: {r2['enviados']} OK/{r2['errores']} err | "
f"PreServ: {r3['enviados']} OK/{r3['errores']} err",
f"PreServ: {r3['enviados']} OK/{r3['errores']} err | "
f"Ventas: {r4['enviados']} OK/{r4['errores']} err",
get_ip(request))
return JSONResponse({"success": True, "resultado": resultado})
def _guardar_envio(user_id, tipo, factura, json_data, respuesta, ok,
fecha_inicio=None, fecha_fin=None, servicios=0):
fecha_inicio=None, fecha_fin=None, servicios=0,
idrecepcion=None, contrato=None):
try:
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, fecha_inicio, fecha_fin,
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
fecha_inicio, fecha_fin,
pacientes_count, servicios_count, status, json_enviado, respuesta_api, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
user_id, tipo, factura, fecha_inicio, fecha_fin,
user_id, tipo, factura, idrecepcion, contrato,
fecha_inicio, fecha_fin,
1, servicios,
"success" if ok else "error",
json_lib.dumps(json_data, indent=2, ensure_ascii=False)[:10000],