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 @@
Pacientes — clic para ver sus recepciones:
@@ -75,6 +80,10 @@Pre-servicios RCXC / SC — clic para ver JSON:
Facturas Venta — clic para ver JSON:
+ +