From 0f3a760de43a87c3e3b5b97e76d12dd5bbaadf7b Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:00:21 -0500 Subject: [PATCH] feat(contratos): tabla de contratos con tipo_usuario por contrato MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega tabla `contratos` en SQLite sembrada con los 17 contratos del laboratorio. `generar_rda_paciente` consulta el mapa contrato→tipoUsuario como primera prioridad antes de derivar de TIPOUSU/TIPOUSUSISPRO. Incluye CRUD en /contratos con UI en sidebar. Co-Authored-By: Claude Sonnet 4.6 --- app/database.py | 13 +++ app/routes/automation.py | 4 +- app/routes/contratos.py | 117 ++++++++++++++++++++++ app/routes/transaccion.py | 8 +- app/services/json_generator.py | 23 +++-- app/templates/base.html | 3 + app/templates/contratos.html | 176 +++++++++++++++++++++++++++++++++ main.py | 5 +- 8 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 app/routes/contratos.py create mode 100644 app/templates/contratos.html diff --git a/app/database.py b/app/database.py index f2e589f..c5dfe49 100644 --- a/app/database.py +++ b/app/database.py @@ -21,6 +21,19 @@ def _migrate(conn): conn.execute("ALTER TABLE envios ADD COLUMN contrato TEXT") if "mensaje_tns" not in cols: conn.execute("ALTER TABLE envios ADD COLUMN mensaje_tns TEXT") + + tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")} + if "contratos" not in tables: + conn.execute(""" + CREATE TABLE contratos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + numero_contrato TEXT UNIQUE NOT NULL, + nit_empresa TEXT NOT NULL DEFAULT '', + tipo_usuario TEXT NOT NULL, + descripcion TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + """) conn.commit() diff --git a/app/routes/automation.py b/app/routes/automation.py index 396b445..abc796e 100644 --- a/app/routes/automation.py +++ b/app/routes/automation.py @@ -14,6 +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 router = APIRouter(prefix="/automation", tags=["automation"]) @@ -278,12 +279,13 @@ async def run_automation( esp_def = configs.get("especialidad_default", "") remis_def = configs.get("remisionante_default", "00") prefijo_def = configs.get("prefijo_tns_default", "00") + contrato_map = load_contrato_map() async with httpx.AsyncClient(timeout=timeout) as client: 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) + rda_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def, num_override, contrato_map=contrato_map) factura = num_override or str(id_recepcion) try: resp = await client.post(endpoint, json=rda_json, headers=headers) diff --git a/app/routes/contratos.py b/app/routes/contratos.py new file mode 100644 index 0000000..6202d2f --- /dev/null +++ b/app/routes/contratos.py @@ -0,0 +1,117 @@ +from fastapi import APIRouter, Request, Form, Depends +from fastapi.responses import RedirectResponse, JSONResponse +from app.database import get_connection +from app.auth import get_current_user + +router = APIRouter(prefix="/contratos", tags=["contratos"]) + +_SEED = [ + ("001", "860078828", "11", "EPS SANITAS"), + ("002", "830054904", "11", "EPS COMPENSAR"), + ("003", "900278729", "12", "PARTICULAR"), + ("004", "800106339", "11", "EPS NUEVA EPS"), + ("005", "800153424", "11", "EPS SURA"), + ("006", "805009741", "11", "EPS COOMEVA"), + ("007", "860002183", "11", "EPS FAMISANAR"), + ("008", "860002503", "11", "EPS CRUZ BLANCA"), + ("009", "860027404", "11", "EPS COLSANITAS"), + ("010", "860039988", "11", "EPS SALUD TOTAL"), + ("011", "890903790", "11", "EPS SAVIA SALUD"), + ("012", "900178724", "11", "EPS MUTUAL SER"), + ("034", "860078828", "11", "EPS SANITAS (alt)"), + ("035", "860078828", "11", "EPS SANITAS (alt)"), + ("036", "860078828", "11", "EPS SANITAS (alt)"), + ("20062026", "800182856", "01", "SUBSIDIADO"), + ("CW225489", "899999068", "07", "POLIZA / SEGURO"), +] + + +def ensure_defaults(): + conn = get_connection() + for nc, nit, tu, desc 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) VALUES (?,?,?,?)", + (nc, nit, tu, desc), + ) + conn.commit() + conn.close() + + +def load_contrato_map() -> dict: + """Devuelve {numero_contrato: tipo_usuario} incluyendo variante sin ceros a la izquierda.""" + conn = get_connection() + rows = conn.execute("SELECT numero_contrato, tipo_usuario FROM contratos").fetchall() + conn.close() + result = {} + for r in rows: + nc = r["numero_contrato"].strip() + tu = r["tipo_usuario"].strip() + result[nc] = tu + nc_s = nc.lstrip("0") or nc + if nc_s != nc: + result[nc_s] = tu + return result + + +@router.get("") +async def contratos_page(request: Request, user: dict = Depends(get_current_user)): + conn = get_connection() + rows = conn.execute("SELECT * FROM contratos ORDER BY numero_contrato").fetchall() + conn.close() + return request.app.state.templates.TemplateResponse("contratos.html", { + "request": request, "user": user, "contratos": rows, + }) + + +@router.post("/create") +async def contrato_create( + request: Request, + user: dict = Depends(get_current_user), + numero_contrato: str = Form(...), + nit_empresa: str = Form(""), + tipo_usuario: str = Form(...), + descripcion: str = Form(""), +): + conn = get_connection() + try: + conn.execute( + "INSERT INTO contratos (numero_contrato, nit_empresa, tipo_usuario, descripcion) VALUES (?,?,?,?)", + (numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip()), + ) + conn.commit() + except Exception: + pass + conn.close() + return RedirectResponse("/contratos", status_code=302) + + +@router.post("/update/{contrato_id}") +async def contrato_update( + contrato_id: int, + user: dict = Depends(get_current_user), + numero_contrato: str = Form(...), + nit_empresa: str = Form(""), + tipo_usuario: str = Form(...), + descripcion: str = Form(""), +): + conn = get_connection() + conn.execute( + "UPDATE contratos SET numero_contrato=?, nit_empresa=?, tipo_usuario=?, descripcion=? WHERE id=?", + (numero_contrato.strip(), nit_empresa.strip(), tipo_usuario.strip(), descripcion.strip(), contrato_id), + ) + conn.commit() + 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() + conn.execute("DELETE FROM contratos WHERE id = ?", (contrato_id,)) + conn.commit() + conn.close() + return RedirectResponse("/contratos", status_code=302) diff --git a/app/routes/transaccion.py b/app/routes/transaccion.py index d149224..f939e78 100644 --- a/app/routes/transaccion.py +++ b/app/routes/transaccion.py @@ -8,6 +8,7 @@ from app.auth import get_current_user from app.services.firebird_service import get_firebird_from_config from app.services.json_generator import generar_rda_paciente, agrupar_por_recepcion from app.services.api_client import get_tns_token, TNS_BASE +from app.routes.contratos import load_contrato_map router = APIRouter(prefix="/transaccion", tags=["transaccion"]) @@ -96,11 +97,12 @@ async def preview_transaccion( esp_def = cfg.get("especialidad_default", "") remis_def = cfg.get("remisionante_default", "00") prefijo_def = cfg.get("prefijo_tns_default", "00") + contrato_map = load_contrato_map() items = [] for id_rec, grupo_rows in grupos.items(): enviado = _is_sent(id_rec) - rda = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def) + rda = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def, contrato_map=contrato_map) items.append({ "idrecepcion": id_rec, "factura": grupo_rows[0].get("NUM_FACTURA", ""), @@ -166,6 +168,7 @@ async def send_one( cfg.get("remisionante_default", "00"), cfg.get("prefijo_tns_default", "00"), numero_override_one, + contrato_map=load_contrato_map(), ) raw_resp = "" @@ -234,6 +237,8 @@ async def send_transaccion( if solo_pendientes == "1": grupos = {k: v for k, v in grupos.items() if not _is_sent(k)} + contrato_map = load_contrato_map() + token, token_err = await get_tns_token( cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "") ) @@ -256,6 +261,7 @@ async def send_transaccion( cfg.get("remisionante_default", "00"), cfg.get("prefijo_tns_default", "00"), numero_override, + contrato_map=contrato_map, ) raw_resp = "" ok_rda = False diff --git a/app/services/json_generator.py b/app/services/json_generator.py index 594514a..13f9fcc 100644 --- a/app/services/json_generator.py +++ b/app/services/json_generator.py @@ -134,7 +134,7 @@ def _calcular_descuento_pct(rows: list) -> int: def generar_rda_paciente(rows: list, default_profesional: str = "", default_especialidad: str = "", default_remisionante: str = "00", default_prefijo: str = "00", - numero_override: str = "") -> dict: + numero_override: str = "", contrato_map: dict = None) -> dict: if not rows: return {} h = rows[0] @@ -191,15 +191,20 @@ def generar_rda_paciente(rows: list, default_profesional: str = "", default_espe cod_forma_pago = "CLIP" if es_particular else "INST" autorizacion = None if es_particular else (str(h.get("AUTORIZACION") or "").strip() or None) - # tipousuario: particular siempre "12"; para convenios usar TIPOUSUSISPRO o derivar + # tipousuario: prioridad 1→tabla contratos, 2→particular, 3→TIPOUSUSISPRO/TIPOUSU _TIPOUSU_MAP = {"1": "11", "5": "07"} # EPS→11, Póliza→07 - if es_particular: - tipoususispro = "12" - else: - tipoususispro = str(h.get("TIPOUSUSISPRO") or "").strip() - if not tipoususispro: - tipousu = str(h.get("TIPOUSU") or "").strip() - tipoususispro = _TIPOUSU_MAP.get(tipousu, "11") + tipoususispro = "" + if contrato_map and cod_contrato_raw: + _nc_s = cod_contrato_raw.lstrip("0") or cod_contrato_raw + tipoususispro = contrato_map.get(cod_contrato_raw) or contrato_map.get(_nc_s) or "" + if not tipoususispro: + if es_particular: + tipoususispro = "12" + else: + tipoususispro = str(h.get("TIPOUSUSISPRO") or "").strip() + if not tipoususispro: + tipousu = str(h.get("TIPOUSU") or "").strip() + tipoususispro = _TIPOUSU_MAP.get(tipousu, "11") via_ingreso = "01" modalidad = "01" diff --git a/app/templates/base.html b/app/templates/base.html index 1eebcf8..0af6659 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -46,6 +46,9 @@ Consultas SQL + + Contratos +

Envíos

diff --git a/app/templates/contratos.html b/app/templates/contratos.html new file mode 100644 index 0000000..00e2948 --- /dev/null +++ b/app/templates/contratos.html @@ -0,0 +1,176 @@ +{% extends "base.html" %} +{% block title %}Contratos{% endblock %} +{% block header %}Contratos{% endblock %} +{% block content %} +
+ + +
+
+

Agregar Contrato

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + +
+
+

+ Contratos registrados + {{ contratos|length }} +

+ El tipo de usuario se aplica automáticamente al generar RIPS por contrato +
+
+ + + + + + + + + + + + {% for c in contratos %} + + + + + + + + {% else %} + + {% endfor %} + +
N° ContratoNIT EmpresaTipo UsuarioDescripciónAcciones
{{ c.numero_contrato }}{{ c.nit_empresa }} + {% set tu = c.tipo_usuario %} + {% if tu == '11' %} + 11 — Contributivo + {% elif tu == '12' %} + 12 — Particular + {% elif tu == '07' %} + 07 — Póliza + {% elif tu == '01' %} + 01 — Subsidiado + {% else %} + {{ tu }} + {% endif %} + {{ c.descripcion }} + +
+ +
+
No hay contratos registrados
+
+
+
+ + + + + +{% endblock %} diff --git a/main.py b/main.py index 541eca4..f704972 100644 --- a/main.py +++ b/main.py @@ -56,8 +56,10 @@ async def startup(): init_db() from app.routes.config import ensure_defaults as config_defaults from app.routes.queries import ensure_defaults as query_defaults + from app.routes.contratos import ensure_defaults as contratos_defaults config_defaults() query_defaults() + contratos_defaults() @app.get("/") @@ -65,7 +67,7 @@ async def root(): return RedirectResponse(url="/dashboard") -from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation, test_rda, debug_fb, pacientes +from app.routes import auth, dashboard, config, queries, terceros, transaccion, logs, automation, test_rda, debug_fb, pacientes, contratos app.include_router(auth.router) app.include_router(dashboard.router) @@ -78,6 +80,7 @@ app.include_router(automation.router) app.include_router(test_rda.router) app.include_router(debug_fb.router) app.include_router(pacientes.router) +app.include_router(contratos.router) if __name__ == "__main__":