Files
rips_manager/app/routes/transaccion.py
T
Lizandro GuarnizoandClaude Sonnet 4.6 daa50a70cf Add pre-servicios support in /transaccion + letter search fix
- Add 'RDA Pre-servicios por fecha' and 'RDA Pre-servicios por número'
  default queries joining PRESSERV_DIAN (ID_PS, PS_PREFIJO, PS_NUMERO)
- Refactor _query_rows: only pass params the query actually uses;
  prefix filter works for both PREFIJO and PS_PREFIJO columns
- Add _is_preserv(), _agrupar(), _build_rda() helpers that detect
  pre-servicio rows and produce correct prefijo/numero overrides
- All 4 send endpoints (preview, send-one, send, send-direct) now
  group by ID_PS for pre-servicio queries and pass correct RDA fields
- RCXC prefix still maps to '00' in the RDA codigoPrefijo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-11 10:14:13 -05:00

479 lines
19 KiB
Python

import json as json_lib
import httpx
from datetime import datetime
from fastapi import APIRouter, Request, Form, Depends
from fastapi.responses import JSONResponse
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_rda_paciente
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
from app.utils.activity import log_activity, get_ip
router = APIRouter(prefix="/transaccion", tags=["transaccion"])
def _cfg_and_fb():
conn = get_connection()
cfg = {r["key"]: r["value"] for r in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
return cfg
def _query_rows(cfg, query_text, factura, fecha_inicio, fecha_fin):
import re as _re
fb, ok, msg = get_firebird_from_config(cfg)
if not ok:
return None, msg, None
nums = _re.findall(r'\d+', factura or "")
num_val = int(nums[-1]) if nums else 0
prefix = _re.sub(r'[\d\s]', '', factura or "").strip().upper()
# Solo agregar los parámetros que la query realmente usa
params = {}
if ":fecha_ini" in query_text:
params["fecha_ini"] = f"{fecha_inicio} 00:00:00"
if ":fecha_fin" in query_text:
params["fecha_fin"] = f"{fecha_fin} 23:59:59"
if ":num_factura" in query_text:
params["num_factura"] = num_val
if ":prefijo" in query_text:
params["prefijo"] = prefix
if ":ps_numero" in query_text:
params["ps_numero"] = num_val
ok2, err, rows = fb.execute_query(query_text, params)
fb.disconnect()
if not ok2:
return None, err, None
# Filtro Python por prefijo (PREFIJO o PS_PREFIJO)
if prefix and rows:
if "PS_PREFIJO" in rows[0]:
rows = [r for r in rows if str(r.get("PS_PREFIJO") or "").strip().upper() == prefix]
elif "PREFIJO" in rows[0]:
rows = [r for r in rows if str(r.get("PREFIJO") or "").strip().upper() == prefix]
return rows, None, fb
def _is_preserv(rows: list) -> bool:
return bool(rows) and "ID_PS" in rows[0]
def _agrupar(rows: list) -> dict:
"""Agrupa por ID_PS (pre-servicios) o IDRECEPCION (regulares)."""
from collections import defaultdict
grupos = defaultdict(list)
key_field = "ID_PS" if _is_preserv(rows) else "IDRECEPCION"
for row in rows:
grupos[row.get(key_field)].append(dict(row))
return dict(grupos)
def _build_rda(grupo_rows: list, cfg: dict, contrato_map: dict, sin_contrato_set_val: set) -> tuple:
"""Devuelve (rda_json, numero_override, prefijo_override, factura_display)."""
prof_def = cfg.get("profesional_default", "")
esp_def = cfg.get("especialidad_default", "")
remis_def = cfg.get("remisionante_default", "00")
prefijo_def = cfg.get("prefijo_tns_default", "00")
if _is_preserv(grupo_rows):
ps_prefijo = str(grupo_rows[0].get("PS_PREFIJO") or "SC").strip()
ps_numero = str(grupo_rows[0].get("PS_NUMERO") or "").strip()
prefijo_override = "00" if ps_prefijo == "RCXC" else ps_prefijo
rda = generar_rda_paciente(
grupo_rows, prof_def, esp_def, remis_def, prefijo_def,
numero_override=ps_numero,
contrato_map=contrato_map,
prefijo_override=prefijo_override,
sin_contrato_set=sin_contrato_set_val,
)
factura_display = f"{ps_prefijo}-{ps_numero.zfill(5)}"
return rda, ps_numero, prefijo_override, factura_display
else:
num_fac = str(grupo_rows[0].get("NUM_FACTURA") or "").strip()
rda = generar_rda_paciente(
grupo_rows, prof_def, esp_def, remis_def, prefijo_def,
numero_override=num_fac,
contrato_map=contrato_map,
sin_contrato_set=sin_contrato_set_val,
)
return rda, num_fac, "", str(grupo_rows[0].get("NUM_FACTURA", ""))
def _is_sent(idrecepcion: int) -> dict:
"""Returns the latest envio record for this idrecepcion, or None."""
conn = get_connection()
row = conn.execute(
"SELECT status, mensaje_tns, created_at FROM envios WHERE idrecepcion=? AND tipo='transaccion' ORDER BY id DESC LIMIT 1",
(idrecepcion,)
).fetchone()
conn.close()
if row:
return {"status": row["status"], "mensaje": row["mensaje_tns"], "at": row["created_at"]}
return None
@router.get("")
async def transaccion_page(request: Request, user: dict = Depends(get_current_user)):
conn = get_connection()
queries = conn.execute(
"SELECT * FROM queries WHERE query_type = 'transaccion' ORDER BY name"
).fetchall()
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
conn.close()
return request.app.state.templates.TemplateResponse("transaccion.html", {
"request": request, "user": user, "queries": queries, "configs": configs,
})
@router.post("/preview")
async def preview_transaccion(
request: Request, user: dict = Depends(get_current_user),
query_id: int = Form(...), factura: str = Form(""),
fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
contrato: str = Form(""),
):
conn = get_connection()
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:
return JSONResponse({"success": False, "message": err})
if not rows:
return JSONResponse({"success": False, "message": "Sin datos"})
excluded = load_excluded_set()
contrato_map = load_contrato_map()
sin_contrato_set_val = load_sin_contrato_set()
grupos_all = _agrupar(rows)
if contrato:
grupos = {k: v for k, v in grupos_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() == contrato.strip()}
else:
grupos = {k: v for k, v in grupos_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded}
items = []
for key, grupo_rows in grupos.items():
enviado = _is_sent(key)
cod_c = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
is_excluded = cod_c in excluded
rda, _, _, factura_display = _build_rda(grupo_rows, cfg, contrato_map, sin_contrato_set_val)
items.append({
"idrecepcion": key,
"factura": factura_display,
"paciente": grupo_rows[0].get("COD_PACIENTE", ""),
"contrato": grupo_rows[0].get("CODCONTRATO", ""),
"examenes": [r.get("COD_EXAMEN", "") for r in grupo_rows],
"valor": float(grupo_rows[0].get("VALORTOTAL") or 0),
"enviado": enviado,
"excluido": is_excluded,
"json": rda,
})
pendientes = sum(1 for i in items if not i["enviado"] and not i["excluido"])
enviados_ok = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "success")
enviados_err = sum(1 for i in items if i["enviado"] and i["enviado"]["status"] == "error")
excluidos = sum(1 for i in items if i["excluido"])
return JSONResponse({
"success": True,
"total": len(items),
"pendientes": pendientes,
"enviados_ok": enviados_ok,
"enviados_err": enviados_err,
"excluidos": excluidos,
"items": items,
})
@router.post("/send-one")
async def send_one(
request: Request, user: dict = Depends(get_current_user),
idrecepcion: int = Form(...), query_id: int = Form(...),
factura: str = Form(""), fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
contrato: str = Form(""),
):
conn = get_connection()
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()
rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
if rows is None:
return JSONResponse({"success": False, "message": err})
grupos = _agrupar(rows)
grupo_rows = grupos.get(idrecepcion)
if not grupo_rows:
return JSONResponse({"success": False, "message": f"Registro {idrecepcion} no encontrado"})
cod_c = str(grupo_rows[0].get("CODCONTRATO") or "").strip()
if cod_c in load_excluded_set():
return JSONResponse({"success": False, "message": f"Contrato {cod_c} está excluido del envío TNS"})
token, token_err = await get_tns_token(
cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "")
)
if not token:
return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"})
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
api_sucursal = cfg.get("api_sucursal", "") or "00"
endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
rda_json, numero_override_one, _, factura_display = _build_rda(
grupo_rows, cfg, load_contrato_map(), load_sin_contrato_set()
)
raw_resp = ""
ok_rda = False
msg_tns = ""
try:
async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client:
r = await client.post(endpoint, 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))
msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300])
except Exception:
ok_rda = r.status_code < 300
msg_tns = raw_resp[:300]
except Exception as ex:
msg_tns = str(ex)
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
fecha_inicio, fecha_fin, pacientes_count, servicios_count,
status, json_enviado, respuesta_api, mensaje_tns, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (
user["user_id"], "transaccion",
factura_display,
idrecepcion,
str(grupo_rows[0].get("CODCONTRATO", "")),
fecha_inicio, fecha_fin, 1, len(grupo_rows),
"success" if ok_rda else "error",
json_lib.dumps(rda_json, ensure_ascii=False)[:10000],
raw_resp[:2000],
msg_tns,
datetime.now().isoformat(),
))
conn.commit()
conn.close()
log_activity(user["user_id"], user["username"], "rda_enviado",
f"Factura {factura_display} | Contrato {grupo_rows[0].get('CODCONTRATO','')} | {'OK' if ok_rda else 'ERROR: '+msg_tns[:80]}",
get_ip(request))
return JSONResponse({"success": ok_rda, "message": msg_tns, "raw_tns": raw_resp, "idrecepcion": idrecepcion})
@router.post("/send")
async def send_transaccion(
request: Request, user: dict = Depends(get_current_user),
query_id: int = Form(...), factura: str = Form(""),
fecha_inicio: str = Form(...), fecha_fin: str = Form(...),
solo_pendientes: str = Form("0"), contrato: str = Form(""),
):
conn = get_connection()
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()
rows, err, _ = _query_rows(cfg, q["query_text"], factura, fecha_inicio, fecha_fin)
if rows is None:
return JSONResponse({"success": False, "message": err})
if not rows:
return JSONResponse({"success": False, "message": "Sin datos"})
excluded = load_excluded_set()
contrato_map = load_contrato_map()
sin_contrato_set_val = load_sin_contrato_set()
grupos_all = _agrupar(rows)
if contrato:
grupos = {k: v for k, v in grupos_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() == contrato.strip()}
else:
grupos = {k: v for k, v in grupos_all.items()
if str(v[0].get("CODCONTRATO") or "").strip() not in excluded}
if solo_pendientes == "1":
grupos = {k: v for k, v in grupos.items() if not _is_sent(k)}
excluidos_count = len(grupos_all) - len(grupos)
token, token_err = await get_tns_token(
cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "")
)
if not token:
return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"})
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
api_sucursal = cfg.get("api_sucursal", "") or "00"
endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
resultados = []
async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client:
for key, grupo_rows in grupos.items():
rda_json, _, _, factura_display = _build_rda(grupo_rows, cfg, contrato_map, sin_contrato_set_val)
raw_resp = ""
ok_rda = False
msg_tns = ""
try:
r = await client.post(endpoint, 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))
msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300])
except Exception:
ok_rda = r.status_code < 300
msg_tns = raw_resp[:300]
except Exception as ex:
msg_tns = str(ex)
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
fecha_inicio, fecha_fin, pacientes_count, servicios_count,
status, json_enviado, respuesta_api, mensaje_tns, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (
user["user_id"], "transaccion",
factura_display, key,
str(grupo_rows[0].get("CODCONTRATO", "")),
fecha_inicio, fecha_fin, 1, len(grupo_rows),
"success" if ok_rda else "error",
json_lib.dumps(rda_json, ensure_ascii=False)[:10000],
raw_resp[:2000], msg_tns,
datetime.now().isoformat(),
))
conn.commit()
conn.close()
resultados.append({"idrecepcion": key, "success": ok_rda, "msg": msg_tns})
ok_count = sum(1 for r in resultados if r["success"])
err_count = len(resultados) - ok_count
log_activity(user["user_id"], user["username"], "rda_masivo",
f"Enviados: {ok_count} OK, {err_count} errores, {excluidos_count} excluidos | {fecha_inicio}{fecha_fin}",
get_ip(request))
return JSONResponse({
"success": err_count == 0,
"total_enviados": ok_count,
"total_errores": err_count,
"total_excluidos": excluidos_count,
"resultados": resultados,
})
@router.post("/send-direct")
async def send_direct(
request: Request,
user: dict = Depends(get_current_user),
prefijo: str = Form(...),
numero: str = Form(...),
query_id: int = Form(...),
):
conn = get_connection()
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"})
prefijo = prefijo.strip().upper()
numero = numero.strip()
factura_str = f"{prefijo}{numero}"
rows, err, _ = _query_rows(cfg, q["query_text"], factura_str, "2000-01-01", "2099-12-31")
if rows is None:
return JSONResponse({"success": False, "message": err})
if not rows:
return JSONResponse({"success": False, "message": f"No se encontró factura {factura_str} en Firebird"})
grupos = _agrupar(rows)
if not grupos:
return JSONResponse({"success": False, "message": "Sin datos agrupables"})
excluded = load_excluded_set()
primer_contrato = str(list(grupos.values())[0][0].get("CODCONTRATO") or "").strip()
if primer_contrato in excluded:
return JSONResponse({"success": False, "message": f"Contrato {primer_contrato} está excluido del envío TNS"})
token, token_err = await get_tns_token(
cfg.get("tns_empresa", ""), cfg.get("tns_usuario", ""), cfg.get("tns_password", "")
)
if not token:
return JSONResponse({"success": False, "message": f"Error login TNS: {token_err}"})
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
api_sucursal = cfg.get("api_sucursal", "") or "00"
endpoint = f"{TNS_BASE}/v2/rda/RdaPaciente/Insertar?codigosucursal={api_sucursal}"
contrato_map = load_contrato_map()
sin_contrato_set_val = load_sin_contrato_set()
resultados = []
async with httpx.AsyncClient(timeout=int(cfg.get("api_timeout", 30))) as client:
for key, grupo_rows in grupos.items():
rda_json, _, _, factura_display = _build_rda(grupo_rows, cfg, contrato_map, sin_contrato_set_val)
raw_resp = ""
ok_rda = False
msg_tns = ""
try:
r = await client.post(endpoint, 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))
msg_tns = ((data.get("data") or {}).get("response") or data.get("message") or raw_resp[:300])
except Exception:
ok_rda = r.status_code < 300
msg_tns = raw_resp[:300]
except Exception as ex:
msg_tns = str(ex)
conn = get_connection()
conn.execute("""
INSERT INTO envios (user_id, tipo, factura, idrecepcion, contrato,
fecha_inicio, fecha_fin, pacientes_count, servicios_count,
status, json_enviado, respuesta_api, mensaje_tns, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
""", (
user["user_id"], "transaccion", factura_display, key,
str(grupo_rows[0].get("CODCONTRATO", "")),
"2000-01-01", "2099-12-31", 1, len(grupo_rows),
"success" if ok_rda else "error",
json_lib.dumps(rda_json, ensure_ascii=False)[:10000],
raw_resp[:2000], msg_tns, datetime.now().isoformat(),
))
conn.commit()
conn.close()
resultados.append({
"idrecepcion": key,
"factura": factura_display,
"success": ok_rda,
"message": msg_tns,
"raw_tns": raw_resp[:1000],
"json": rda_json,
})
ok_count = sum(1 for r in resultados if r["success"])
log_activity(user["user_id"], user["username"], "rda_directo",
f"Factura {factura_str} | {'OK' if ok_count > 0 else 'ERROR'}",
get_ip(request))
return JSONResponse({
"success": ok_count > 0,
"total": len(resultados),
"resultados": resultados,
})