From 73062af8bc23478fe404dc034c54551c3d6569e0 Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Sat, 11 Jul 2026 14:29:39 -0500
Subject: [PATCH] feat: paso 4 ventas en automation + fixes varios
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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
---
app/routes/automation.py | 123 ++++++++++++++++++++++++++++++---
app/routes/contratos.py | 91 ++++++++++++------------
app/routes/queries.py | 16 ++---
app/routes/ventas.py | 4 ++
app/services/json_generator.py | 3 +-
app/templates/automation.html | 76 +++++++++++++++++++-
app/templates/ventas.html | 4 +-
7 files changed, 249 insertions(+), 68 deletions(-)
diff --git a/app/routes/automation.py b/app/routes/automation.py
index ce616b8..84d7a85 100644
--- a/app/routes/automation.py
+++ b/app/routes/automation.py
@@ -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],
diff --git a/app/routes/contratos.py b/app/routes/contratos.py
index ae3d3ed..417c612 100644
--- a/app/routes/contratos.py
+++ b/app/routes/contratos.py
@@ -7,52 +7,53 @@ from app.utils.activity import log_activity, get_ip
router = APIRouter(prefix="/contratos", tags=["contratos"])
# (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir_rda, sin_contrato, excluir_ventas)
+# excluir_ventas=0 → activo para Ventas/Crear | excluir_ventas=1 → bloqueado para ventas
_SEED = [
- ("001", "860078828", "11", "EPS SANITAS", 0, 0, 0),
- ("002", "830054904", "11", "EPS COMPENSAR", 0, 0, 0),
- ("003", "900278729", "12", "PARTICULAR", 0, 0, 0),
- ("004", "800106339", "11", "EPS NUEVA EPS", 0, 0, 0),
- ("005", "800153424", "11", "EPS SURA", 0, 0, 0),
- ("006", "805009741", "11", "EPS COOMEVA", 0, 0, 0),
- ("007", "860002183", "11", "EPS FAMISANAR", 0, 0, 0),
- ("008", "860002503", "11", "EPS CRUZ BLANCA", 0, 0, 0),
- ("009", "860027404", "11", "EPS COLSANITAS", 0, 0, 0),
- ("010", "860039988", "11", "EPS SALUD TOTAL", 0, 0, 0),
- ("011", "890903790", "11", "EPS SAVIA SALUD", 0, 0, 0),
- ("012", "900178724", "11", "EPS MUTUAL SER", 0, 0, 0),
- ("034", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
- ("035", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
- ("036", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 0),
- ("20062026", "800182856", "01", "SUBSIDIADO", 0, 0, 0),
- ("CW225489", "899999068", "07", "POLIZA / SEGURO", 0, 0, 0),
- # Contratos excluidos del envío TNS
- ("013", "", "11", "MEDILAVORO S.A.S", 0, 0, 0),
- ("014", "", "11", "LABORATORIO UROCLINICO",0, 0, 0),
- ("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1, 0, 0),
- ("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1, 0, 0),
- ("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1, 0, 0),
- ("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1, 0, 0),
- ("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1, 0, 0),
- ("020", "", "11", "MARTHA LUCIA GALLARDO", 1, 0, 0),
- ("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1, 0, 0),
- ("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1, 0, 0),
- ("023", "", "11", "URONORTE S.A", 1, 0, 0),
- ("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1, 0, 0),
- ("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1, 0, 0),
- ("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1, 0, 0),
- ("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1, 0, 0),
- ("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1, 0, 0),
- ("029", "", "11", "GENETIX S.A.S", 1, 0, 0),
- ("030", "", "11", "NORFETUS S.A.S", 1, 0, 0),
- ("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1, 0, 0),
- ("032", "", "11", "COLGENES S.A.S", 1, 0, 0),
- ("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1, 0, 0),
- ("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1, 0, 0),
- ("038", "", "11", "CLINICA COLSANITAS S.A.", 1, 0, 0),
- ("039", "", "11", "GOMEZ GIL JOSE JESUS", 1, 0, 0),
+ ("001", "860078828", "11", "EPS SANITAS", 0, 0, 1),
+ ("002", "830054904", "11", "EPS COMPENSAR", 0, 0, 1),
+ ("003", "900278729", "12", "PARTICULAR", 0, 0, 1),
+ ("004", "800106339", "11", "EPS NUEVA EPS", 0, 0, 1),
+ ("005", "800153424", "11", "EPS SURA", 0, 0, 1),
+ ("006", "805009741", "11", "EPS COOMEVA", 0, 0, 1),
+ ("007", "860002183", "11", "EPS FAMISANAR", 0, 0, 1),
+ ("008", "860002503", "11", "EPS CRUZ BLANCA", 0, 0, 1),
+ ("009", "860027404", "11", "EPS COLSANITAS", 0, 0, 1),
+ ("010", "860039988", "11", "EPS SALUD TOTAL", 0, 0, 1),
+ ("011", "890903790", "11", "EPS SAVIA SALUD", 0, 0, 1),
+ ("012", "900178724", "11", "EPS MUTUAL SER", 0, 0, 1),
+ ("034", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 1),
+ ("035", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 1),
+ ("036", "860078828", "11", "EPS SANITAS (alt)", 0, 0, 1),
+ ("20062026", "800182856", "01", "SUBSIDIADO", 0, 0, 1),
+ ("CW225489", "899999068", "07", "POLIZA / SEGURO", 0, 0, 1),
+ # Contratos excluidos del envío RDA
+ ("013", "", "11", "MEDILAVORO S.A.S", 0, 0, 1),
+ ("014", "", "11", "LABORATORIO UROCLINICO",0, 0, 1),
+ ("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1, 0, 1),
+ ("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1, 0, 1),
+ ("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1, 0, 1),
+ ("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1, 0, 1),
+ ("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1, 0, 1),
+ ("020", "", "11", "MARTHA LUCIA GALLARDO", 1, 0, 1),
+ ("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1, 0, 1),
+ ("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1, 0, 1),
+ ("023", "", "11", "URONORTE S.A", 1, 0, 1),
+ ("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1, 0, 1),
+ ("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1, 0, 1),
+ ("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1, 0, 1),
+ ("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1, 0, 1),
+ ("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1, 0, 1),
+ ("029", "", "11", "GENETIX S.A.S", 1, 0, 1),
+ ("030", "", "11", "NORFETUS S.A.S", 1, 0, 1),
+ ("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1, 0, 1),
+ ("032", "", "11", "COLGENES S.A.S", 1, 0, 1),
+ ("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1, 0, 1),
+ ("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1, 0, 1),
+ ("038", "", "11", "CLINICA COLSANITAS S.A.", 1, 0, 1),
+ ("039", "", "11", "GOMEZ GIL JOSE JESUS", 1, 0, 1),
("040", "", "11", "MARTHA LILIANA SALGAR GALLEGO", 1, 1, 0),
- ("041", "", "11", "GENCELL PHARMA S.A.S", 1, 0, 0),
- ("042", "", "07", "AXA COLPATRIA HYC", 1, 0, 0),
+ ("041", "", "11", "GENCELL PHARMA S.A.S", 1, 0, 1),
+ ("042", "", "07", "AXA COLPATRIA HYC", 1, 0, 1),
]
@@ -200,7 +201,7 @@ async def contrato_update(
@router.post("/toggle-excluir/{contrato_id}")
-async def toggle_excluir(contrato_id: int, user: dict = Depends(get_current_user)):
+async def toggle_excluir(contrato_id: int, request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
conn.execute(
"UPDATE contratos SET excluir = CASE WHEN excluir=1 THEN 0 ELSE 1 END WHERE id=?",
diff --git a/app/routes/queries.py b/app/routes/queries.py
index af8d538..63d03a0 100644
--- a/app/routes/queries.py
+++ b/app/routes/queries.py
@@ -188,7 +188,7 @@ ORDER BY ps.ID_PS, rel.COD_EXAMEN""",
"description": "Pre-servicio por número PS (buscar RCXC05291 → escribe solo el número)"
},
{
- "name": "Factura Venta 040 por fecha",
+ "name": "Factura Venta por fecha",
"query_type": "ventas",
"query_text": """SELECT
r.IDRECEPCION,
@@ -203,19 +203,18 @@ ORDER BY ps.ID_PS, 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,
- e.CODCONTRATO
+ 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
- AND TRIM(e.CODCONTRATO) = '040'
ORDER BY r.IDRECEPCION""",
- "description": "Facturas de venta contrato 040 en un rango de fechas"
+ "description": "Facturas de venta en un rango de fechas (filtra por excluir_ventas en /contratos)"
},
{
- "name": "Factura Venta 040 por número",
+ "name": "Factura Venta por número",
"query_type": "ventas",
"query_text": """SELECT
r.IDRECEPCION,
@@ -230,15 +229,14 @@ ORDER BY r.IDRECEPCION""",
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
rel.PRECIO,
COALESCE(t.VALOR, rel.PRECIO) AS PRECIO_TARIFA,
- e.CODCONTRATO
+ 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.NUM_FACTURA = :num_factura
- AND TRIM(e.CODCONTRATO) = '040'""",
- "description": "Factura de venta contrato 040 por número de factura"
+WHERE r.NUM_FACTURA = :num_factura""",
+ "description": "Factura de venta por número (filtra por excluir_ventas en /contratos)"
},
]
diff --git a/app/routes/ventas.py b/app/routes/ventas.py
index a5aece8..9905444 100644
--- a/app/routes/ventas.py
+++ b/app/routes/ventas.py
@@ -188,6 +188,8 @@ async def send_one(
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
+ if not q:
+ return JSONResponse({"success": False, "message": "Consulta no encontrada"})
rows, err = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
if rows is None:
@@ -243,6 +245,8 @@ async def send_ventas(
q = conn.execute("SELECT * FROM queries WHERE id = ?", (query_id,)).fetchone()
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
+ if not q:
+ return JSONResponse({"success": False, "message": "Consulta no encontrada"})
rows, err = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
if rows is None:
diff --git a/app/services/json_generator.py b/app/services/json_generator.py
index 0f3aa28..8bd0fac 100644
--- a/app/services/json_generator.py
+++ b/app/services/json_generator.py
@@ -106,7 +106,8 @@ def generar_tercero_api(row: dict) -> dict:
"sexo": sexo,
"identidadGenero": identidad_genero,
"zona": zona,
- "etnia": str(row.get("CODETNIA") or "99").strip(),
+ "etnia": (lambda v: v if v and len(v) <= 2 else "99")(str(row.get("CODETNIA") or "").strip()),
+ "ciudadExp": cod_ciudad,
"antecedentes": "",
"cliente": "S",
"enfermedadCronica": False,
diff --git a/app/templates/automation.html b/app/templates/automation.html
index 6eef50e..cb44b18 100644
--- a/app/templates/automation.html
+++ b/app/templates/automation.html
@@ -48,7 +48,7 @@
-
+
0
Pacientes
@@ -66,6 +66,11 @@
Pre-servicios
RCXC / SC
+
+
0
+
Ventas
+
Ventas/Crear
+
Pacientes — clic para ver sus recepciones:
@@ -75,6 +80,10 @@
Pre-servicios RCXC / SC — clic para ver JSON:
+
+
Facturas Venta — clic para ver JSON:
+
+
@@ -110,6 +119,16 @@
+
@@ -133,7 +152,7 @@
:
-
+
0
Terceros
@@ -154,6 +173,11 @@
Pre-servicios
RCXC / SC
+
+
0
+
Ventas
+
Ventas/Crear
+
@@ -214,6 +238,7 @@ async function previewAutomation() {
document.getElementById('cnt-recepciones').textContent = data.recepciones;
document.getElementById('cnt-examenes').textContent = data.examenes;
document.getElementById('cnt-preservicios').textContent = data.preservicios || 0;
+ document.getElementById('cnt-ventas').textContent = data.ventas || 0;
document.getElementById('preview-fecha').textContent = data.fecha;
const psTable = document.getElementById('preview-ps-table');
@@ -240,6 +265,31 @@ async function previewAutomation() {
psSection.classList.add('hidden');
}
+ const vtaTable = document.getElementById('preview-vta-table');
+ const vtaSection = document.getElementById('ventas-section');
+ if (data.ventas_preview && data.ventas_preview.length > 0) {
+ vtaTable.innerHTML = data.ventas_preview.map((vta, i) => {
+ const key = `vta-json-${i}`;
+ _rdaJsonStore[key] = vta.json;
+ return `
+
+
+
+ ${vta.factura}
+ ${vta.fecha}
+ ${vta.contrato || '—'}
+ Pac. ${vta.paciente}
+ ${vta.examenes} examen(es)
+
+
+
`;
+ }).join('');
+ vtaSection.classList.remove('hidden');
+ } else {
+ vtaSection.classList.add('hidden');
+ }
+
const table = document.getElementById('preview-table');
if (data.pacientes === 0) {
table.innerHTML = '
No hay pacientes para esta fecha
';
@@ -297,6 +347,7 @@ function abrirModalConfirmar() {
document.getElementById('modal-cnt-rec').textContent = d.recepciones;
document.getElementById('modal-cnt-exa').textContent = d.examenes;
document.getElementById('modal-cnt-ps').textContent = d.preservicios || 0;
+ document.getElementById('modal-cnt-vta').textContent = d.ventas || 0;
document.getElementById('modal-lista-pac').innerHTML = d.pacientes_preview.map(p => `
@@ -327,6 +378,8 @@ async function confirmarEnvio() {
document.getElementById('p2-badge').className = 'text-sm text-gray-400';
document.getElementById('p3-badge').textContent = 'Esperando...';
document.getElementById('p3-badge').className = 'text-sm text-gray-400';
+ document.getElementById('p4-badge').textContent = 'Esperando...';
+ document.getElementById('p4-badge').className = 'text-sm text-gray-400';
const contrato = document.getElementById('contrato-input').value.trim();
const form = new FormData();
@@ -364,8 +417,14 @@ async function confirmarEnvio() {
col2: `Pac. ${d.paciente} — ${d.examenes} examen(es)`,
col3: `${d.ok ? '✅' : '❌'} ${d.msg || ''}`, ok: d.ok,
})));
+ renderBadge('p4-badge', r.paso4_ventas.enviados, r.paso4_ventas.errores);
+ renderDetalle('p4-detalle', r.paso4_ventas.detalle.map(d => ({
+ col1: `${d.factura}`,
+ col2: `Pac. ${d.paciente} — ${d.examenes} examen(es)`,
+ col3: `${d.ok ? '✅' : '❌'} ${d.msg || ''}`, ok: d.ok,
+ })));
- const totalErr = r.paso1_terceros.errores + r.paso2_rda.errores + r.paso3_preserv.errores;
+ const totalErr = r.paso1_terceros.errores + r.paso2_rda.errores + r.paso3_preserv.errores + r.paso4_ventas.errores;
showToast(totalErr === 0 ? 'Envío completado sin errores' : `Completado con ${totalErr} error(es)`,
totalErr === 0 ? 'success' : 'warning');
} catch (e) {
@@ -405,6 +464,17 @@ function togglePs(id, row) {
if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
}
+function toggleVta(id, row) {
+ const div = document.getElementById(id);
+ const chev = row.querySelector('[id^="vtachev-"]');
+ const hidden = div.classList.contains('hidden');
+ if (hidden && _rdaJsonStore[id]) {
+ div.textContent = JSON.stringify(_rdaJsonStore[id], null, 2);
+ }
+ div.classList.toggle('hidden', !hidden);
+ if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
+}
+
function renderBadge(id, enviados, errores) {
const el = document.getElementById(id);
if (errores === 0) {
diff --git a/app/templates/ventas.html b/app/templates/ventas.html
index 19b070b..645e70c 100644
--- a/app/templates/ventas.html
+++ b/app/templates/ventas.html
@@ -178,6 +178,7 @@ function renderTabla() {
IDRECEP. |
Factura |
Paciente |
+ Contrato |
Exámenes |
Estado |
|
@@ -217,7 +218,8 @@ function filaHtml(item) {
${id} |
${escHtml(String(item.factura || '-'))} |
${escHtml(String(item.paciente || '-'))} |
- ${escHtml(examsStr)} |
+ ${escHtml(String(item.contrato || '-'))} |
+ ${escHtml(examsStr)} |
${badgeHtml} |
|