All generar_rda_paciente calls in transaccion.py (4) and test_rda.py (2) now pass sin_contrato_set so contract 040 sends numeroContrato: null consistently across all send paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
381 lines
15 KiB
Python
381 lines
15 KiB
Python
import json as json_lib
|
|
from fastapi import APIRouter, Request, Depends
|
|
from fastapi.responses import JSONResponse
|
|
|
|
import httpx
|
|
|
|
from app.database import get_connection
|
|
from app.auth import get_current_user
|
|
from app.services.firebird_service import get_firebird_from_config
|
|
from app.services.json_generator import (
|
|
generar_tercero_api,
|
|
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, load_excluded_set, load_sin_contrato_set
|
|
|
|
router = APIRouter(prefix="/test-rda", tags=["test-rda"])
|
|
|
|
_SQL_RDA_BY_WHERE = """
|
|
SELECT
|
|
r.IDRECEPCION, r.PREFIJO, r.NUM_FACTURA, 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, 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 RECEPCION r
|
|
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 {where}
|
|
AND r.NUM_FACTURA > 0
|
|
AND e.CODCONTRATO IS NOT NULL
|
|
AND TRIM(e.CODCONTRATO) <> ''
|
|
ORDER BY r.IDRECEPCION
|
|
"""
|
|
|
|
_SQL_CANDIDATOS = """
|
|
SELECT FIRST 100
|
|
r.IDRECEPCION,
|
|
r.NUM_FACTURA,
|
|
r.FECHA_RECEPCION,
|
|
r.COD_PACIENTE,
|
|
r.NIT_EMPRESA,
|
|
e.CODCONTRATO,
|
|
LIST(DISTINCT rel.COD_EXAMEN, ', ') AS examenes,
|
|
COUNT(DISTINCT rel.COD_EXAMEN) AS total_examenes
|
|
FROM RECEPCION r
|
|
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
|
|
LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
|
|
WHERE r.NUM_FACTURA > 0
|
|
AND e.CODCONTRATO IS NOT NULL
|
|
AND TRIM(e.CODCONTRATO) <> ''
|
|
GROUP BY r.IDRECEPCION, r.NUM_FACTURA, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA, e.CODCONTRATO
|
|
ORDER BY r.IDRECEPCION DESC
|
|
"""
|
|
|
|
_SQL_PACIENTE = """
|
|
SELECT p.CODIGO, p.NOMBRES, p.APELLIDOS, p.DOCIDENT, p.TIPOIDENT,
|
|
p.F_NACIMIENTO, p.SEXO, p.DIRECCION, p.TELEFONOS, p.EMAIL
|
|
FROM PACIENTE p WHERE p.CODIGO = :cod
|
|
"""
|
|
|
|
|
|
@router.get("")
|
|
async def test_page(request: Request, user: dict = Depends(get_current_user)):
|
|
return request.app.state.templates.TemplateResponse(
|
|
"test_rda.html", {"request": request, "user": user}
|
|
)
|
|
|
|
|
|
@router.get("/enviados")
|
|
async def get_enviados(request: Request, user: dict = Depends(get_current_user)):
|
|
db = get_connection()
|
|
rows = db.execute(
|
|
"SELECT idrecepcion, ok FROM rda_test_log ORDER BY created_at DESC"
|
|
).fetchall()
|
|
db.close()
|
|
enviados = {r["idrecepcion"]: bool(r["ok"]) for r in rows}
|
|
return JSONResponse({"enviados": enviados})
|
|
|
|
|
|
@router.get("/candidatos")
|
|
async def get_candidatos(request: Request, user: dict = Depends(get_current_user),
|
|
contrato: str = "", factura: str = "", articulos: str = "",
|
|
tipo_usuario: str = "", offset: int = 0):
|
|
db = get_connection()
|
|
cfg = {r[0]: r[1] for r in db.execute("SELECT key, value FROM config").fetchall()}
|
|
db.close()
|
|
|
|
fb, fb_ok, fb_msg = get_firebird_from_config(cfg)
|
|
if not fb_ok:
|
|
return JSONResponse({"error": f"Error Firebird: {fb_msg}"})
|
|
|
|
arts_default = "CH4,CREA,TGP,VSG,PCR,AU,FER,TPO,TSH,TGO,GLI,BUN,COL,TRI,HDL,LDL,HBA1,VB12,VITD,PO,K,NA,MG,URO,PRL,T4L,T3,T4,T3L,HIV,VDRL,PSA,AFP,CEA,CA125,CA199,FV,TP,TPT"
|
|
arts_str = articulos.strip() if articulos.strip() else arts_default
|
|
arts_list = [a.strip().upper() for a in arts_str.split(",") if a.strip()]
|
|
arts_in = ", ".join(f"'{a}'" for a in arts_list)
|
|
|
|
es_particular = tipo_usuario.strip() == "12"
|
|
|
|
filtros = ["r.NUM_FACTURA > 0"]
|
|
|
|
if not es_particular:
|
|
# Para convenios: solo registros con contrato registrado
|
|
filtros += [
|
|
"e.CODCONTRATO IS NOT NULL",
|
|
"TRIM(e.CODCONTRATO) <> ''",
|
|
]
|
|
|
|
# Solo exámenes configurados en TNS
|
|
filtros.append(
|
|
f"NOT EXISTS (SELECT 1 FROM RELACION rx WHERE rx.IDRECEPCION = r.IDRECEPCION AND rx.COD_EXAMEN NOT IN ({arts_in}))"
|
|
)
|
|
|
|
params = {}
|
|
if contrato:
|
|
c = contrato.strip()
|
|
c_alt = c.lstrip("0") or c # "012" → "12", "12" → "12"
|
|
c_pad = c.zfill(max(3, len(c))) # "12" → "012", "011" → "011"
|
|
filtros.append("TRIM(e.CODCONTRATO) IN (:contrato, :contrato_alt, :contrato_pad)")
|
|
params["contrato"] = c
|
|
params["contrato_alt"] = c_alt
|
|
params["contrato_pad"] = c_pad
|
|
elif es_particular:
|
|
# Particulares: NIT_EMPRESA es PART/PARTIC o sin contrato
|
|
filtros.append(
|
|
"(r.TIPOUSUSISPRO = '12' OR r.NIT_EMPRESA IN ('PART', 'PARTIC', 'PARTICULAR') "
|
|
"OR COALESCE(TRIM(e.CODCONTRATO), '') = '')"
|
|
)
|
|
if tipo_usuario and not es_particular:
|
|
filtros.append("r.TIPOUSUSISPRO = :tipo_usuario")
|
|
params["tipo_usuario"] = tipo_usuario.strip()
|
|
if factura:
|
|
filtros.append("r.NUM_FACTURA = :factura")
|
|
params["factura"] = int(factura)
|
|
|
|
sql_final = f"""
|
|
SELECT FIRST 10 SKIP {int(offset)}
|
|
r.IDRECEPCION, r.NUM_FACTURA, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA,
|
|
e.CODCONTRATO,
|
|
LIST(DISTINCT rel.COD_EXAMEN, ', ') AS EXAMENES,
|
|
COUNT(DISTINCT rel.COD_EXAMEN) AS TOTAL_EXAMENES
|
|
FROM RECEPCION r
|
|
JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION
|
|
LEFT JOIN EMPRESA e ON e.NIT = r.NIT_EMPRESA
|
|
WHERE {" AND ".join(filtros)}
|
|
GROUP BY r.IDRECEPCION, r.NUM_FACTURA, r.FECHA_RECEPCION, r.COD_PACIENTE, r.NIT_EMPRESA, e.CODCONTRATO
|
|
ORDER BY r.IDRECEPCION DESC
|
|
"""
|
|
|
|
ok2, err2, rows = fb.execute_query(sql_final, params)
|
|
fb.disconnect()
|
|
|
|
if not ok2:
|
|
return JSONResponse({"error": err2})
|
|
|
|
excluded = load_excluded_set()
|
|
candidatos = []
|
|
for row in rows:
|
|
cod_c = str(row.get("CODCONTRATO") or "").strip()
|
|
if cod_c in excluded:
|
|
continue
|
|
candidatos.append({
|
|
"idrecepcion": row.get("IDRECEPCION"),
|
|
"factura": row.get("NUM_FACTURA"),
|
|
"fecha": str(row.get("FECHA_RECEPCION", ""))[:10],
|
|
"paciente": row.get("COD_PACIENTE", ""),
|
|
"empresa": row.get("NIT_EMPRESA", ""),
|
|
"contrato": cod_c,
|
|
"examenes": row.get("EXAMENES", ""),
|
|
"total": row.get("TOTAL_EXAMENES", 0),
|
|
})
|
|
|
|
return JSONResponse({"candidatos": candidatos})
|
|
|
|
|
|
@router.get("/preview")
|
|
async def preview_rda(request: Request, user: dict = Depends(get_current_user),
|
|
idrecepcion: int = 0):
|
|
if not idrecepcion:
|
|
return JSONResponse({"error": "Falta idrecepcion"})
|
|
db = get_connection()
|
|
cfg = {r[0]: r[1] for r in db.execute("SELECT key, value FROM config").fetchall()}
|
|
db.close()
|
|
prof_def = cfg.get("profesional_default", "XIMENA")
|
|
esp_def = cfg.get("especialidad_default", "02")
|
|
remis_def = cfg.get("remisionante_default", "00")
|
|
prefijo_def = cfg.get("prefijo_tns_default", "00")
|
|
fb, fb_ok, fb_msg = get_firebird_from_config(cfg)
|
|
if not fb_ok:
|
|
return JSONResponse({"error": fb_msg})
|
|
ok2, err2, rows_rda = fb.execute_query(
|
|
_SQL_RDA_BY_WHERE.format(where="r.IDRECEPCION = :val"), {"val": idrecepcion}
|
|
)
|
|
fb.disconnect()
|
|
if not ok2 or not rows_rda:
|
|
return JSONResponse({"error": err2 or "Sin registros"})
|
|
grupos = agrupar_por_recepcion(rows_rda)
|
|
grupo = list(grupos.values())[0]
|
|
cod_c = str(grupo[0].get("CODCONTRATO") or "").strip()
|
|
excluido = cod_c in load_excluded_set()
|
|
rda_json = generar_rda_paciente(grupo, prof_def, esp_def, remis_def, prefijo_def,
|
|
contrato_map=load_contrato_map(),
|
|
sin_contrato_set=load_sin_contrato_set())
|
|
return JSONResponse({"json": rda_json, "excluido": excluido, "contrato": cod_c})
|
|
|
|
|
|
@router.post("/enviar")
|
|
async def test_enviar(request: Request, user: dict = Depends(get_current_user)):
|
|
form = await request.form()
|
|
modo = form.get("modo", "idrecepcion")
|
|
valor = str(form.get("valor", "")).strip()
|
|
|
|
if not valor:
|
|
return JSONResponse({"error": "Ingresa un IDRECEPCION o número de factura"})
|
|
|
|
db = get_connection()
|
|
cfg = {r[0]: r[1] for r in db.execute("SELECT key, value FROM config").fetchall()}
|
|
db.close()
|
|
|
|
prof_def = cfg.get("profesional_default", "XIMENA")
|
|
esp_def = cfg.get("especialidad_default", "02")
|
|
remis_def = cfg.get("remisionante_default", "00")
|
|
prefijo_def = cfg.get("prefijo_tns_default", "00")
|
|
api_sucursal = cfg.get("api_sucursal", "00") or "00"
|
|
|
|
fb, fb_ok, fb_msg = get_firebird_from_config(cfg)
|
|
if not fb_ok:
|
|
return JSONResponse({"error": f"Error Firebird: {fb_msg}"})
|
|
|
|
if modo == "factura":
|
|
import re as _re
|
|
nums = _re.findall(r'\d+', valor)
|
|
num_val = int(nums[-1]) if nums else 0
|
|
prefix = _re.sub(r'[\d\s]', '', valor).strip().upper()
|
|
if prefix:
|
|
where = "r.NUM_FACTURA = :val AND TRIM(r.PREFIJO) = :prefijo"
|
|
params = {"val": num_val, "prefijo": prefix}
|
|
else:
|
|
where = "r.NUM_FACTURA = :val"
|
|
params = {"val": num_val}
|
|
else:
|
|
where = "r.IDRECEPCION = :val"
|
|
params = {"val": int(valor)}
|
|
|
|
ok2, err2, rows_rda = fb.execute_query(
|
|
_SQL_RDA_BY_WHERE.format(where=where), params
|
|
)
|
|
if not ok2:
|
|
fb.disconnect()
|
|
return JSONResponse({"error": f"Error consulta: {err2}"})
|
|
|
|
if not rows_rda:
|
|
fb.disconnect()
|
|
return JSONResponse({"error": "Sin registros (¿NUM_FACTURA = 0 o contrato vacío?)"})
|
|
|
|
# Bloquear si el contrato está excluido
|
|
cod_c_check = str(rows_rda[0].get("CODCONTRATO") or "").strip()
|
|
if cod_c_check in load_excluded_set():
|
|
fb.disconnect()
|
|
return JSONResponse({"error": f"Contrato {cod_c_check} está excluido del envío TNS"})
|
|
|
|
# Pacientes únicos
|
|
pacientes_unicos = {}
|
|
for row in rows_rda:
|
|
cod = str(row.get("COD_PACIENTE", "")).strip()
|
|
if cod and cod not in pacientes_unicos:
|
|
ok3, err3, prows = fb.execute_query(_SQL_PACIENTE, {"cod": cod})
|
|
if ok3 and prows:
|
|
pacientes_unicos[cod] = prows[0]
|
|
|
|
fb.disconnect()
|
|
|
|
token, err_tns = await get_tns_token(
|
|
cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "")
|
|
)
|
|
if not token:
|
|
return JSONResponse({"error": f"Error login TNS: {err_tns}"})
|
|
|
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
endpoint_rda = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
|
|
|
|
pasos = []
|
|
|
|
# Paso 1: crear terceros
|
|
async with httpx.AsyncClient(timeout=20) as client:
|
|
for cod, pac_row in pacientes_unicos.items():
|
|
tercero_json = generar_tercero_api(pac_row)
|
|
try:
|
|
r = await client.post(
|
|
f"{TNS_BASE}/v2/tablas/Tercero/Crear",
|
|
json=tercero_json, headers=headers,
|
|
)
|
|
try:
|
|
data = r.json()
|
|
msg = data.get("message") or r.text[:200]
|
|
except Exception:
|
|
msg = r.text[:200]
|
|
pasos.append({
|
|
"tipo": "tercero",
|
|
"codigo": cod,
|
|
"nombre": tercero_json.get("nombre", ""),
|
|
"status": r.status_code,
|
|
"ok": r.status_code in (200, 201),
|
|
"mensaje": msg,
|
|
})
|
|
except Exception as ex:
|
|
pasos.append({"tipo": "tercero", "codigo": cod, "ok": False, "mensaje": str(ex)})
|
|
|
|
# Paso 2: enviar RDA
|
|
grupos = agrupar_por_recepcion(rows_rda)
|
|
contrato_map = load_contrato_map()
|
|
|
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
for id_rec, grupo_rows in grupos.items():
|
|
num_factura = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
|
|
numero_override = num_factura
|
|
rda_json = generar_rda_paciente(grupo_rows, prof_def, esp_def, remis_def, prefijo_def, numero_override,
|
|
contrato_map=contrato_map, sin_contrato_set=load_sin_contrato_set())
|
|
examenes = [r.get("COD_EXAMEN", "") for r in grupo_rows]
|
|
try:
|
|
r = await client.post(endpoint_rda, json=rda_json, headers=headers)
|
|
raw_resp = r.text
|
|
try:
|
|
data = r.json()
|
|
ok_rda = bool(data.get("status") or (data.get("data") or {}).get("success", False)) if isinstance(data, dict) else False
|
|
msg_rda = (
|
|
((data.get("data") or {}).get("response"))
|
|
or data.get("message")
|
|
or raw_resp[:300]
|
|
)
|
|
except Exception:
|
|
ok_rda = r.status_code < 300
|
|
msg_rda = raw_resp[:300]
|
|
pasos.append({
|
|
"tipo": "rda",
|
|
"idrecepcion": id_rec,
|
|
"factura": rda_json.get("numero", ""),
|
|
"paciente": rda_json.get("codTercero", ""),
|
|
"contrato": rda_json.get("numeroContrato", ""),
|
|
"examenes": examenes,
|
|
"status": r.status_code,
|
|
"ok": ok_rda,
|
|
"mensaje": msg_rda,
|
|
"respuesta_tns": raw_resp,
|
|
"json_enviado": rda_json,
|
|
})
|
|
db_log = get_connection()
|
|
db_log.execute(
|
|
"INSERT OR REPLACE INTO rda_test_log (idrecepcion, factura, contrato, ok, mensaje) VALUES (?,?,?,?,?)",
|
|
(id_rec, rda_json.get("numero", ""), rda_json.get("numeroContrato", ""), 1 if ok_rda else 0, msg_rda)
|
|
)
|
|
db_log.commit()
|
|
db_log.close()
|
|
except Exception as ex:
|
|
pasos.append({
|
|
"tipo": "rda", "idrecepcion": id_rec, "ok": False,
|
|
"examenes": examenes, "mensaje": str(ex), "json_enviado": {},
|
|
})
|
|
|
|
rda_ok = sum(1 for p in pasos if p["tipo"] == "rda" and p["ok"])
|
|
rda_err = sum(1 for p in pasos if p["tipo"] == "rda" and not p["ok"])
|
|
|
|
return JSONResponse({
|
|
"resumen": {
|
|
"total_recepciones": len(grupos),
|
|
"rda_exitosos": rda_ok,
|
|
"rda_errores": rda_err,
|
|
"pacientes_procesados": len(pacientes_unicos),
|
|
},
|
|
"pasos": pasos,
|
|
})
|