From 9f2bd5164ea06d313cae890ee48cc93ef35b5203 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:22:09 -0500 Subject: [PATCH] feat: opcion 'sin contrato' para enviar numeroContrato null en TNS - Agrega columna sin_contrato en tabla contratos (migration automatica) - Contrato 040 marcado excluir=0, sin_contrato=1 en seed - load_sin_contrato_set() retorna contratos que deben ir con null - generar_rda_paciente acepta sin_contrato_set y fuerza numeroContrato=null - Automation paso 2 y 3 pasan sin_contrato_set al generador - Pagina contratos: columna y toggle "Sin contrato", checkbox en modal editar Co-Authored-By: Claude Sonnet 4.6 --- app/database.py | 2 + app/routes/automation.py | 8 +- app/routes/contratos.py | 143 ++++++++++++++++++++------------- app/services/json_generator.py | 7 +- app/templates/contratos.html | 31 +++++-- 5 files changed, 127 insertions(+), 64 deletions(-) diff --git a/app/database.py b/app/database.py index ae2d5a8..e32bc7d 100644 --- a/app/database.py +++ b/app/database.py @@ -51,6 +51,8 @@ def _migrate(conn): contrato_cols = {r[1] for r in conn.execute("PRAGMA table_info(contratos)")} if "excluir" not in contrato_cols: conn.execute("ALTER TABLE contratos ADD COLUMN excluir INTEGER NOT NULL DEFAULT 0") + if "sin_contrato" not in contrato_cols: + conn.execute("ALTER TABLE contratos ADD COLUMN sin_contrato INTEGER NOT NULL DEFAULT 0") conn.commit() diff --git a/app/routes/automation.py b/app/routes/automation.py index 025a09a..d6f0e22 100644 --- a/app/routes/automation.py +++ b/app/routes/automation.py @@ -14,7 +14,7 @@ from app.services.json_generator import ( ) 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 +from app.routes.contratos import load_contrato_map, load_excluded_set, load_sin_contrato_set from app.utils.activity import log_activity, get_ip router = APIRouter(prefix="/automation", tags=["automation"]) @@ -260,6 +260,7 @@ async def preview_automation( if str(v[0].get("CODCONTRATO") or "").strip() not in excluded} contrato_map = load_contrato_map() + sin_contrato_set = load_sin_contrato_set() prof_def = configs.get("profesional_defecto", "") esp_def = configs.get("especialidad_defecto", "") remis_def = configs.get("remisionante_defecto", "00") @@ -274,6 +275,7 @@ async def preview_automation( numero_override=ps_numero, contrato_map=contrato_map, prefijo_override=ps_prefijo, + sin_contrato_set=sin_contrato_set, ) preservicios_preview.append({ "factura": f"{ps_prefijo}-{ps_numero.zfill(5)}", @@ -429,6 +431,7 @@ async def run_automation( # ── PASO 2: Enviar RDA Paciente ─────────────────────────────────────────── excluded = load_excluded_set() + sin_contrato_set = load_sin_contrato_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} @@ -445,7 +448,7 @@ async def run_automation( for id_recepcion, grupo_rows in grupos.items(): num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip() num_override = num_fac - rda_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def, num_override, contrato_map=contrato_map) + rda_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def, num_override, contrato_map=contrato_map, sin_contrato_set=sin_contrato_set) factura = num_override or str(id_recepcion) try: resp = await client.post(endpoint, json=rda_json, headers=headers) @@ -486,6 +489,7 @@ async def run_automation( numero_override=ps_numero, contrato_map=contrato_map, prefijo_override=ps_prefijo, + sin_contrato_set=sin_contrato_set, ) factura_ps = f"{ps_prefijo}-{ps_numero.zfill(5)}" try: diff --git a/app/routes/contratos.py b/app/routes/contratos.py index c51bd23..e9c4e2a 100644 --- a/app/routes/contratos.py +++ b/app/routes/contratos.py @@ -6,72 +6,71 @@ from app.utils.activity import log_activity, get_ip router = APIRouter(prefix="/contratos", tags=["contratos"]) -# (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir) +# (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato) _SEED = [ - ("001", "860078828", "11", "EPS SANITAS", 0), - ("002", "830054904", "11", "EPS COMPENSAR", 0), - ("003", "900278729", "12", "PARTICULAR", 0), - ("004", "800106339", "11", "EPS NUEVA EPS", 0), - ("005", "800153424", "11", "EPS SURA", 0), - ("006", "805009741", "11", "EPS COOMEVA", 0), - ("007", "860002183", "11", "EPS FAMISANAR", 0), - ("008", "860002503", "11", "EPS CRUZ BLANCA", 0), - ("009", "860027404", "11", "EPS COLSANITAS", 0), - ("010", "860039988", "11", "EPS SALUD TOTAL", 0), - ("011", "890903790", "11", "EPS SAVIA SALUD", 0), - ("012", "900178724", "11", "EPS MUTUAL SER", 0), - ("034", "860078828", "11", "EPS SANITAS (alt)", 0), - ("035", "860078828", "11", "EPS SANITAS (alt)", 0), - ("036", "860078828", "11", "EPS SANITAS (alt)", 0), - ("20062026", "800182856", "01", "SUBSIDIADO", 0), - ("CW225489", "899999068", "07", "POLIZA / SEGURO", 0), + ("001", "860078828", "11", "EPS SANITAS", 0, 0), + ("002", "830054904", "11", "EPS COMPENSAR", 0, 0), + ("003", "900278729", "12", "PARTICULAR", 0, 0), + ("004", "800106339", "11", "EPS NUEVA EPS", 0, 0), + ("005", "800153424", "11", "EPS SURA", 0, 0), + ("006", "805009741", "11", "EPS COOMEVA", 0, 0), + ("007", "860002183", "11", "EPS FAMISANAR", 0, 0), + ("008", "860002503", "11", "EPS CRUZ BLANCA", 0, 0), + ("009", "860027404", "11", "EPS COLSANITAS", 0, 0), + ("010", "860039988", "11", "EPS SALUD TOTAL", 0, 0), + ("011", "890903790", "11", "EPS SAVIA SALUD", 0, 0), + ("012", "900178724", "11", "EPS MUTUAL SER", 0, 0), + ("034", "860078828", "11", "EPS SANITAS (alt)", 0, 0), + ("035", "860078828", "11", "EPS SANITAS (alt)", 0, 0), + ("036", "860078828", "11", "EPS SANITAS (alt)", 0, 0), + ("20062026", "800182856", "01", "SUBSIDIADO", 0, 0), + ("CW225489", "899999068", "07", "POLIZA / SEGURO", 0, 0), # Contratos excluidos del envío TNS - ("013", "", "11", "MEDILAVORO S.A.S", 0), - ("014", "", "11", "LABORATORIO UROCLINICO",0), - ("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1), - ("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1), - ("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1), - ("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1), - ("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1), - ("020", "", "11", "MARTHA LUCIA GALLARDO", 1), - ("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1), - ("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1), - ("023", "", "11", "URONORTE S.A", 1), - ("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1), - ("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1), - ("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1), - ("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1), - ("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1), - ("029", "", "11", "GENETIX S.A.S", 1), - ("030", "", "11", "NORFETUS S.A.S", 1), - ("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1), - ("032", "", "11", "COLGENES S.A.S", 1), - ("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1), - ("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1), - ("038", "", "11", "CLINICA COLSANITAS S.A.", 1), - ("039", "", "11", "GOMEZ GIL JOSE JESUS", 1), - ("040", "", "11", "MARTHA LILIANA SALGAR GALLEGO", 1), - ("041", "", "11", "GENCELL PHARMA S.A.S", 1), - ("042", "", "07", "AXA COLPATRIA HYC", 1), + ("013", "", "11", "MEDILAVORO S.A.S", 0, 0), + ("014", "", "11", "LABORATORIO UROCLINICO",0, 0), + ("015", "", "11", "ANDRES AFANADOR VILLAMIZAR", 1, 0), + ("016", "", "11", "OMAR FERNANDO RIBERO GOMEZ", 1, 0), + ("017", "", "11", "CLAUDIA BELEN JULIO SEPULVEDA", 1, 0), + ("018", "", "11", "LABORATORIO MICROBIOLOGICO - MARGIE OJEDA", 1, 0), + ("019", "", "11", "LABORATORIO TOXICOLOGICO - MARTHA MORALES", 1, 0), + ("020", "", "11", "MARTHA LUCIA GALLARDO", 1, 0), + ("021", "", "11", "LABORATORIO VILMA OROZCO AYALA", 1, 0), + ("022", "", "11", "CLINICA SAN JOSE DE CUCUTA", 1, 0), + ("023", "", "11", "URONORTE S.A", 1, 0), + ("024", "", "11", "LABORATORIO CLINICO BIOLAB S.A.S", 1, 0), + ("025", "", "11", "JOEL LEONARDO CARRILLO CORREDOR", 1, 0), + ("026", "", "11", "ROLANDO IVAN PENARANDA DEVIA", 1, 0), + ("027", "", "11", "JOSE WILMER GARCIA CALDERON", 1, 0), + ("028", "", "11", "ONCOMEDICAL IPS S.A.S", 1, 0), + ("029", "", "11", "GENETIX S.A.S", 1, 0), + ("030", "", "11", "NORFETUS S.A.S", 1, 0), + ("031", "", "11", "TBTB GLOBAL LAB S.A.S", 1, 0), + ("032", "", "11", "COLGENES S.A.S", 1, 0), + ("033", "", "11", "IPS FIGURAS SPA CUCUTA S.A.S", 1, 0), + ("037", "", "11", "CLINICA URGENCIAS LA MERCED", 1, 0), + ("038", "", "11", "CLINICA COLSANITAS S.A.", 1, 0), + ("039", "", "11", "GOMEZ GIL JOSE JESUS", 1, 0), + ("040", "", "11", "MARTHA LILIANA SALGAR GALLEGO", 0, 1), + ("041", "", "11", "GENCELL PHARMA S.A.S", 1, 0), + ("042", "", "07", "AXA COLPATRIA HYC", 1, 0), ] def ensure_defaults(): conn = get_connection() - for nc, nit, tu, desc, excluir in _SEED: + for nc, nit, tu, desc, excluir, sin_contrato in _SEED: exists = conn.execute( "SELECT id FROM contratos WHERE numero_contrato = ?", (nc,) ).fetchone() if not exists: conn.execute( - "INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir) VALUES (?,?,?,?,?)", - (nc, nit, tu, desc, excluir), + "INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato) VALUES (?,?,?,?,?,?)", + (nc, nit, tu, desc, excluir, sin_contrato), ) else: - # Forzar excluir según el seed para que el DB siempre coincida conn.execute( - "UPDATE contratos SET excluir=? WHERE numero_contrato=?", - (excluir, nc), + "UPDATE contratos SET excluir=?, sin_contrato=? WHERE numero_contrato=?", + (excluir, sin_contrato, nc), ) conn.commit() conn.close() @@ -93,6 +92,21 @@ def load_contrato_map() -> dict: return result +def load_sin_contrato_set() -> set: + """Devuelve set de numero_contrato marcados como sin_contrato=1 (se envían con numeroContrato: null).""" + conn = get_connection() + rows = conn.execute("SELECT numero_contrato FROM contratos WHERE sin_contrato=1").fetchall() + conn.close() + result = set() + for r in rows: + nc = r["numero_contrato"].strip() + result.add(nc) + nc_s = nc.lstrip("0") or nc + if nc_s != nc: + result.add(nc_s) + return result + + def load_excluded_set() -> set: """Devuelve set de numero_contrato (raw + sin ceros) marcados como excluir=1.""" conn = get_connection() @@ -127,12 +141,13 @@ async def contrato_create( tipo_usuario: str = Form(...), descripcion: str = Form(""), excluir: str = Form("0"), + sin_contrato: str = Form("0"), ): conn = get_connection() try: conn.execute( - "INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir) VALUES (?,?,?,?,?)", - (numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir)), + "INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion, excluir, sin_contrato) VALUES (?,?,?,?,?,?)", + (numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), int(sin_contrato)), ) conn.commit() log_activity(user["user_id"], user["username"], "contrato_creado", @@ -146,17 +161,19 @@ async def contrato_create( @router.post("/update/{contrato_id}") async def contrato_update( contrato_id: int, + request: Request, user: dict = Depends(get_current_user), numero_contrato: str = Form(...), nit_empresa: str = Form(""), tipo_usuario: str = Form(...), descripcion: str = Form(""), excluir: str = Form("0"), + sin_contrato: str = Form("0"), ): conn = get_connection() conn.execute( - "UPDATE contratos SET numero_contrato=?, nit_empresa=?, tipo_usuario=?, descripcion=?, excluir=? WHERE id=?", - (numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), contrato_id), + "UPDATE contratos SET numero_contrato=?, nit_empresa=?, tipo_usuario=?, descripcion=?, excluir=?, sin_contrato=? WHERE id=?", + (numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), int(excluir), int(sin_contrato), contrato_id), ) conn.commit() log_activity(user["user_id"], user["username"], "contrato_editado", @@ -182,6 +199,22 @@ async def toggle_excluir(contrato_id: int, user: dict = Depends(get_current_user return RedirectResponse("/contratos", status_code=302) +@router.post("/toggle-sin-contrato/{contrato_id}") +async def toggle_sin_contrato(contrato_id: int, request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + conn.execute( + "UPDATE contratos SET sin_contrato = CASE WHEN sin_contrato=1 THEN 0 ELSE 1 END WHERE id=?", + (contrato_id,) + ) + conn.commit() + row = conn.execute("SELECT numero_contrato, sin_contrato FROM contratos WHERE id=?", (contrato_id,)).fetchone() + if row: + log_activity(user["user_id"], user["username"], "contrato_toggle_sin_contrato", + f"Contrato {row['numero_contrato']} sin_contrato→{row['sin_contrato']}", get_ip(request)) + conn.close() + return RedirectResponse("/contratos", status_code=302) + + @router.post("/delete/{contrato_id}") async def contrato_delete(contrato_id: int, user: dict = Depends(get_current_user)): conn = get_connection() diff --git a/app/services/json_generator.py b/app/services/json_generator.py index 9ec998f..cee2ba8 100644 --- a/app/services/json_generator.py +++ b/app/services/json_generator.py @@ -127,7 +127,7 @@ def agrupar_por_recepcion(rows: list) -> dict: 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, - prefijo_override: str = "") -> dict: + prefijo_override: str = "", sin_contrato_set: set = None) -> dict: if not rows: return {} h = rows[0] @@ -189,7 +189,10 @@ def generar_rda_paciente(rows: list, default_profesional: str = "", default_espe or (_map_tipo == "12") or (not _map_tipo and cod_contrato_raw.lstrip("0") == "12") ) - cod_contrato = cod_contrato_raw or ("012" if es_particular else None) + if sin_contrato_set and cod_contrato_raw and cod_contrato_raw in sin_contrato_set: + cod_contrato = None + else: + cod_contrato = cod_contrato_raw or ("012" if es_particular else None) cod_forma_pago = "CLIP" if es_particular else "INST" autorizacion = None if es_particular else (str(h.get("AUTORIZACION") or "").strip() or None) diff --git a/app/templates/contratos.html b/app/templates/contratos.html index 153dec0..8cebbb7 100644 --- a/app/templates/contratos.html +++ b/app/templates/contratos.html @@ -68,6 +68,7 @@ Tipo Usuario Descripción Envío TNS + Sin Contrato Acciones @@ -105,7 +106,20 @@ - + {% else %} + + {% endif %} + + + + @@ -161,9 +175,15 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"> -
- - +
+ +