From c2a092c0e7eba8c5a216473bb514165654fa9e8d Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Fri, 10 Jul 2026 20:43:02 -0500
Subject: [PATCH] =?UTF-8?q?feat:=20paso=203=20pre-servicios=20RCXC/SC=20y?=
=?UTF-8?q?=20limpieza=20de=20c=C3=B3digo=20copago?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Agrega paso 3 en automatización: envía RDA desde PRESSERV_DIAN (prefijos RCXC/SC) para pacientes EPS con PS_NUM IS NOT NULL
- Filtra paso 2 con PS_NUM IS NULL para separar particulares/empresas de EPS
- Elimina parámetro copago y función _calcular_descuento_pct (código muerto)
- Limpia referencia a d.copago en automation.html (UI paso 3)
Co-Authored-By: Claude Sonnet 4.6
---
app/routes/automation.py | 123 +++++++++++++++++++++++++++++++--
app/services/json_generator.py | 13 +---
app/templates/automation.html | 36 +++++++++-
3 files changed, 155 insertions(+), 17 deletions(-)
diff --git a/app/routes/automation.py b/app/routes/automation.py
index 4ec114a..7c83b3d 100644
--- a/app/routes/automation.py
+++ b/app/routes/automation.py
@@ -42,6 +42,7 @@ LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD
WHERE r.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin
AND r.NUM_FACTURA > 0
+ AND r.PS_NUM IS NULL
{filtro_contrato_pac}
"""
@@ -79,11 +80,63 @@ 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 r.NUM_FACTURA > 0
+ AND r.PS_NUM IS NULL
{filtro_contrato_rda}
ORDER BY r.IDRECEPCION
"""
+_SQL_PRESERV = """
+SELECT
+ ps.ID_PS,
+ ps.PS_PREFIJO,
+ ps.PS_NUMERO,
+ r.IDRECEPCION,
+ r.FECHA_RECEPCION,
+ r.COD_PACIENTE,
+ r.NIT_EMPRESA,
+ r.DIAG_PPAL,
+ r.TIPOUSU,
+ r.TIPOUSUSISPRO,
+ r.AUTORIZACION,
+ r.CLASEPROC,
+ r.HORAINICIORECEPCION,
+ r.VALORTOTAL,
+ r.VALORDESC,
+ COALESCE((SELECT FIRST 1 pg.VALOR FROM PAGOS pg
+ WHERE pg.NUMRECEP = r.IDRECEPCION
+ AND pg.ESINICIAL = 'T'), 0) AS COPAGO,
+ 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,
+ rel.FECHA_REPORTADO,
+ m.COD_ESPECIALIDAD,
+ COALESCE(NULLIF(TRIM(m.CODIGO), ''), NULLIF(TRIM(r.USUARIO), ''), '') AS profesional,
+ e.CODCONTRATO,
+ TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO
+FROM PRESSERV_DIAN ps
+JOIN RECEPCION r ON r.IDRECEPCION = ps.ID_RECEP
+JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
+LEFT JOIN EXAMEN ex ON TRIM(ex.CODIGO) = TRIM(rel.COD_EXAMEN)
+LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
+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 (ps.PS_ANULADA IS NULL OR ps.PS_ANULADA = 'F')
+ORDER BY ps.ID_PS, rel.COD_EXAMEN
+"""
+
+
+def _agrupar_por_presserv(rows: list) -> dict:
+ """Agrupa filas de pre-servicios por ID_PS."""
+ from collections import defaultdict
+ grupos = defaultdict(list)
+ for row in rows:
+ grupos[row.get("ID_PS")].append(dict(row))
+ return grupos
+
+
@router.get("")
async def automation_page(request: Request, user: dict = Depends(get_current_user)):
today = date.today().isoformat()
@@ -134,17 +187,27 @@ async def preview_automation(
return JSONResponse({"success": False, "message": f"Error BD pacientes: {err1}"})
ok2, err2, rows_rda = fb.execute_query(sql_rda, params)
- fb.disconnect()
if not ok2:
+ fb.disconnect()
return JSONResponse({"success": False, "message": f"Error BD RDA: {err2}"})
- grupos_all = agrupar_por_recepcion(rows_rda)
+ ok3, err3, rows_ps = fb.execute_query(_SQL_PRESERV, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
+ fb.disconnect()
+ if not ok3:
+ return JSONResponse({"success": False, "message": f"Error BD Pre-servicios: {err3}"})
+
excluded = load_excluded_set()
+
+ grupos_all = agrupar_por_recepcion(rows_rda)
grupos = {k: v for k, v in grupos_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded}
excluidos_count = len(grupos_all) - len(grupos)
total_examenes = sum(len(v) for v in grupos.values())
+ grupos_ps_all = _agrupar_por_presserv(rows_ps)
+ grupos_ps = {k: v for k, v in grupos_ps_all.items()
+ if str(v[0].get("CODCONTRATO") or "").strip() not in excluded}
+
# Agrupar RDA por COD_PACIENTE
rda_por_pac = {}
for id_rec, filas in grupos.items():
@@ -180,6 +243,7 @@ async def preview_automation(
"recepciones": len(grupos),
"examenes": total_examenes,
"excluidos": excluidos_count,
+ "preservicios": len(grupos_ps),
"pacientes_preview": pacientes_preview,
})
@@ -215,10 +279,16 @@ async def run_automation(
# ── Paso 2: obtener recepciones ───────────────────────────────────────────
ok2, err2, rows_rda = fb.execute_query(sql_rda, params)
- fb.disconnect()
if not ok2:
+ fb.disconnect()
return JSONResponse({"success": False, "message": f"Error Firebird RDA: {err2}"})
+ # ── Paso 3: obtener pre-servicios ─────────────────────────────────────────
+ ok3, err3, rows_ps = fb.execute_query(_SQL_PRESERV, {"fecha_ini": fecha_ini, "fecha_fin": fecha_fin})
+ fb.disconnect()
+ if not ok3:
+ return JSONResponse({"success": False, "message": f"Error Firebird Pre-servicios: {err3}"})
+
if not rows_pac and not rows_rda:
return JSONResponse({"success": False, "message": f"No hay datos para la fecha {fecha}"})
@@ -239,6 +309,7 @@ async def run_automation(
"fecha": fecha,
"paso1_terceros": {"enviados": 0, "errores": 0, "detalle": []},
"paso2_rda": {"enviados": 0, "errores": 0, "detalle": []},
+ "paso3_preserv": {"enviados": 0, "errores": 0, "detalle": []},
}
# ── PASO 1: Enviar Terceros ───────────────────────────────────────────────
@@ -324,10 +395,54 @@ async def run_automation(
fecha_inicio=fecha, fecha_fin=fecha,
servicios=len(grupo_rows))
+ # ── PASO 3: Enviar Pre-servicios (RCXC / SC) ──────────────────────────────
+ grupos_ps_all = _agrupar_por_presserv(rows_ps)
+ grupos_ps = {k: v for k, v in grupos_ps_all.items()
+ if str(v[0].get("CODCONTRATO") or "").strip() not in excluded}
+ resultado["paso3_preserv"]["excluidos"] = len(grupos_ps_all) - len(grupos_ps)
+
+ async with httpx.AsyncClient(timeout=timeout) as client:
+ for id_ps, grupo_rows in grupos_ps.items():
+ ps_prefijo = str(grupo_rows[0].get("PS_PREFIJO") or "SC").strip()
+ ps_numero = str(grupo_rows[0].get("PS_NUMERO") or "").strip()
+ rda_json = generar_rda_paciente(
+ grupo_rows, prof_def, esp_def, remis_def, prefijo_def,
+ numero_override=ps_numero,
+ contrato_map=contrato_map,
+ prefijo_override=ps_prefijo,
+ )
+ factura_ps = f"{ps_prefijo}-{ps_numero.zfill(5)}"
+ try:
+ resp = await client.post(endpoint, json=rda_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["paso3_preserv"]["enviados"] += 1
+ else:
+ resultado["paso3_preserv"]["errores"] += 1
+
+ resultado["paso3_preserv"]["detalle"].append({
+ "factura": factura_ps,
+ "paciente": str(grupo_rows[0].get("COD_PACIENTE", "")),
+ "examenes": len(grupo_rows),
+ "ok": ok, "msg": msg,
+ })
+
+ _guardar_envio(user["user_id"], "preserv", factura_ps, rda_json, msg, ok,
+ fecha_inicio=fecha, fecha_fin=fecha,
+ servicios=len(grupo_rows))
+
r1 = resultado["paso1_terceros"]
r2 = resultado["paso2_rda"]
+ r3 = resultado["paso3_preserv"]
log_activity(user["user_id"], user["username"], "automation_run",
- f"Fecha {fecha} | Terceros: {r1['enviados']} OK/{r1['errores']} err | RDA: {r2['enviados']} OK/{r2['errores']} err/{r2.get('excluidos',0)} excluidos",
+ 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",
get_ip(request))
return JSONResponse({"success": True, "resultado": resultado})
diff --git a/app/services/json_generator.py b/app/services/json_generator.py
index 4b4eaf7..9ec998f 100644
--- a/app/services/json_generator.py
+++ b/app/services/json_generator.py
@@ -124,17 +124,10 @@ def agrupar_por_recepcion(rows: list) -> dict:
return grupos
-def _calcular_descuento_pct(rows: list) -> int:
- total_cobrado = sum(float(r.get("PRECIO") or 0) for r in rows)
- total_tarifa = sum(float(r.get("PRECIO_TARIFA") or r.get("PRECIO") or 0) for r in rows)
- if total_tarifa > 0 and total_tarifa > total_cobrado:
- return round((total_tarifa - total_cobrado) / total_tarifa * 100)
- return 0
-
-
def generar_rda_paciente(rows: list, default_profesional: str = "", default_especialidad: str = "",
default_remisionante: str = "00", default_prefijo: str = "00",
- numero_override: str = "", contrato_map: dict = None) -> dict:
+ numero_override: str = "", contrato_map: dict = None,
+ prefijo_override: str = "") -> dict:
if not rows:
return {}
h = rows[0]
@@ -220,7 +213,7 @@ def generar_rda_paciente(rows: list, default_profesional: str = "", default_espe
descuento_pct = round(_desc * 100 / _total, 2) if _total > 0 else 0
result = {
- "codigoPrefijo": str(h.get("PREFIJO") or "").strip() or default_prefijo or "00",
+ "codigoPrefijo": prefijo_override or str(h.get("PREFIJO") or "").strip() or default_prefijo or "00",
"numero": (numero_override if numero_override else (
str(h.get("NUM_FACTURA") or "").strip() if (h.get("NUM_FACTURA") or 0) != 0 else ""
)).zfill(5) or "",
diff --git a/app/templates/automation.html b/app/templates/automation.html
index b53ec87..0c1b0af 100644
--- a/app/templates/automation.html
+++ b/app/templates/automation.html
@@ -48,7 +48,7 @@
-
+
0
Pacientes
@@ -61,6 +61,11 @@
0
Exámenes
+
+
0
+
Pre-servicios
+
RCXC / SC
+
Pacientes — clic para ver sus recepciones:
@@ -91,6 +96,16 @@
+
+
+
+
3
+
Pre-servicios RDA (RCXC / SC)
+
+
Esperando...
+
+
+
@@ -114,7 +129,7 @@
:
-
+
0
Terceros
@@ -130,6 +145,11 @@
Exámenes
detallePedido
+
+
0
+
Pre-servicios
+
RCXC / SC
+
@@ -183,6 +203,7 @@ async function previewAutomation() {
document.getElementById('cnt-pacientes').textContent = data.pacientes;
document.getElementById('cnt-recepciones').textContent = data.recepciones;
document.getElementById('cnt-examenes').textContent = data.examenes;
+ document.getElementById('cnt-preservicios').textContent = data.preservicios || 0;
document.getElementById('preview-fecha').textContent = data.fecha;
const table = document.getElementById('preview-table');
@@ -232,6 +253,7 @@ function abrirModalConfirmar() {
document.getElementById('modal-cnt-pac').textContent = d.pacientes;
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-lista-pac').innerHTML = d.pacientes_preview.map(p => `
@@ -260,6 +282,8 @@ async function confirmarEnvio() {
document.getElementById('p1-badge').className = 'text-sm text-blue-500';
document.getElementById('p2-badge').textContent = 'Esperando...';
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';
const contrato = document.getElementById('contrato-input').value.trim();
const form = new FormData();
@@ -291,8 +315,14 @@ async function confirmarEnvio() {
col2: `Pac. ${d.paciente} — ${d.examenes} examen(es)`,
col3: `${d.ok ? '✅' : '❌'} ${d.msg || ''}`, ok: d.ok,
})));
+ renderBadge('p3-badge', r.paso3_preserv.enviados, r.paso3_preserv.errores);
+ renderDetalle('p3-detalle', r.paso3_preserv.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;
+ const totalErr = r.paso1_terceros.errores + r.paso2_rda.errores + r.paso3_preserv.errores;
showToast(totalErr === 0 ? 'Envío completado sin errores' : `Completado con ${totalErr} error(es)`,
totalErr === 0 ? 'success' : 'warning');
} catch (e) {