From 774ce7d72810d1253f3ad0cd6a73115295a848a0 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:27:00 -0500 Subject: [PATCH] feat: queries reales Firebird y generadores JSON para API ERP - json_generator: nuevas funciones generar_tercero_api y generar_rda_paciente mapeando PACIENTE/RECEPCION/RELACION al formato de los endpoints Tercero/Crear y RdaPaciente/Insertar - queries: reemplaza placeholders por consultas reales sobre PACIENTE, RECEPCION, RELACION, MEDICO y CIUDAD - config: agrega api_version y api_sucursal; actualiza url y credenciales Firebird - terceros/transaccion routes: usan nuevos generadores y endpoints correctos Co-Authored-By: Claude Sonnet 4.6 --- app/routes/config.py | 4 +- app/routes/queries.py | 126 +++++++++++++++------------ app/routes/terceros.py | 17 ++-- app/routes/transaccion.py | 51 ++++++----- app/services/json_generator.py | 155 +++++++++++++++++++++++++++++++++ rips_manager.db | Bin 40960 -> 40960 bytes 6 files changed, 260 insertions(+), 93 deletions(-) diff --git a/app/routes/config.py b/app/routes/config.py index 5a5361b..6eba0b0 100644 --- a/app/routes/config.py +++ b/app/routes/config.py @@ -11,10 +11,12 @@ DEFAULT_KEYS = [ ("firebird_database", "/path/to/database.fdb"), ("firebird_user", "SYSDBA"), ("firebird_password", "masterkey"), - ("api_url", "https://api.example.com/rips"), + ("api_url", "http://192.168.0.125"), ("api_method", "POST"), ("api_key", ""), ("api_timeout", "30"), + ("api_version", "1"), + ("api_sucursal", ""), ("num_documento_obligado", ""), ("cod_prestador", ""), ] diff --git a/app/routes/queries.py b/app/routes/queries.py index 69aea0c..35345d8 100644 --- a/app/routes/queries.py +++ b/app/routes/queries.py @@ -8,73 +8,85 @@ router = APIRouter(prefix="/queries", tags=["queries"]) QUERY_DEFAULTS = [ { - "name": "Terceros - Datos del paciente", + "name": "Tercero - Datos del paciente", "query_type": "terceros", "query_text": """SELECT - p.TIPO_DOCUMENTO as tipo_documento, - p.NUMERO_DOCUMENTO as numero_documento, - p.PRIMER_NOMBRE as primer_nombre, - p.SEGUNDO_NOMBRE as segundo_nombre, - p.PRIMER_APELLIDO as primer_apellido, - p.SEGUNDO_APELLIDO as segundo_apellido, - p.FECHA_NACIMIENTO as fecha_nacimiento, - p.SEXO as cod_sexo, - p.COD_ENTIDAD as cod_entidad, - p.TIPO_USUARIO as tipo_usuario, - p.COD_MUNICIPIO as cod_municipio, - p.ZONA as cod_zona, - p.DIRECCION as direccion -FROM USUAHOS p -WHERE p.NUMERO_DOCUMENTO = :doc_num""", - "description": "Consulta datos maestros del paciente por documento" + p.CODIGO, + p.TIPOIDENT, + p.DOCIDENT, + p.NOMBRES, + p.APELLIDOS, + p.DIRECCION, + p.CIUDAD AS COD_CIUDAD, + c.NOMBRE AS NOM_CIUDAD, + p.TELEFONOS, + p.EMAIL, + p.F_NACIMIENTO, + p.SEXO, + p.TIPORES, + p.CODETNIA +FROM PACIENTE p +LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD +WHERE p.DOCIDENT = :doc_num""", + "description": "Datos del paciente por número de documento (para Tercero/Crear)" }, { - "name": "Procedimientos por factura", + "name": "RDA Paciente por factura", "query_type": "transaccion", "query_text": """SELECT - s.CODIGO_CUP as cod_procedimiento, - s.FECHA_ATENCION as fecha_atencion, - s.COD_DIAGNOSTICO as cod_diagnostico, - s.FINALIDAD as finalidad, - s.VIA_INGRESO as via_ingreso, - s.MODALIDAD as modalidad, - s.GRUPO_SERVICIO as grupo_servicio, - s.COD_SERVICIO as cod_servicio, - s.COD_PRESTADOR as cod_prestador, - s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional, - s.NUM_DOC_PROFESIONAL as num_doc_profesional, - s.VR_SERVICIO as vr_servicio, - s.VALOR_PAGO_MODERADOR as valor_pago_moderador, - s.CONCEPTO_RECAUDO as concepto_recaudo, - s.NUM_AUTORIZACION as num_autorizacion -FROM SERVICIOS s -WHERE s.NUM_FACTURA = :factura - AND s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""", - "description": "Consulta procedimientos por factura y rango de fechas" + 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, + rel.COD_EXAMEN, + rel.PRECIO, + rel.FECHA_REPORTADO, + m.COD_ESPECIALIDAD, + r.USUARIO AS profesional +FROM RECEPCION r +JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION +LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO +WHERE r.NUM_FACTURA = :num_factura + AND r.PREFIJO = :prefijo""", + "description": "Servicios de una recepción por número de factura y prefijo" }, { - "name": "Procedimientos por fecha", + "name": "RDA Paciente por fecha", "query_type": "transaccion", "query_text": """SELECT - s.FACTURA as num_factura, - s.CODIGO_CUP as cod_procedimiento, - s.FECHA_ATENCION as fecha_atencion, - s.COD_DIAGNOSTICO as cod_diagnostico, - s.FINALIDAD as finalidad, - s.VIA_INGRESO as via_ingreso, - s.MODALIDAD as modalidad, - s.GRUPO_SERVICIO as grupo_servicio, - s.COD_SERVICIO as cod_servicio, - s.COD_PRESTADOR as cod_prestador, - s.TIPO_DOC_PROFESIONAL as tipo_doc_profesional, - s.NUM_DOC_PROFESIONAL as num_doc_profesional, - s.VR_SERVICIO as vr_servicio, - p.TIPO_DOCUMENTO as tipo_doc_paciente, - p.NUMERO_DOCUMENTO as num_doc_paciente -FROM SERVICIOS s -JOIN USUAHOS p ON s.COD_PACIENTE = p.COD_PACIENTE -WHERE s.FECHA_ATENCION BETWEEN :fecha_ini AND :fecha_fin""", - "description": "Consulta todos los procedimientos en rango de fechas" + 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, + rel.COD_EXAMEN, + rel.PRECIO, + rel.FECHA_REPORTADO, + m.COD_ESPECIALIDAD, + r.USUARIO AS profesional +FROM RECEPCION r +JOIN RELACION rel ON rel.IDRECEPCION = r.IDRECEPCION +LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO +WHERE r.FECHA_RECEPCION BETWEEN :fecha_ini AND :fecha_fin +ORDER BY r.IDRECEPCION""", + "description": "Servicios en un rango de fechas (para RdaPaciente/Insertar)" }, ] diff --git a/app/routes/terceros.py b/app/routes/terceros.py index dc4366d..b8c041a 100644 --- a/app/routes/terceros.py +++ b/app/routes/terceros.py @@ -6,7 +6,7 @@ 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_terceros +from app.services.json_generator import generar_tercero_api router = APIRouter(prefix="/terceros", tags=["terceros"]) @@ -93,7 +93,7 @@ async def preview_query( if not success: return JSONResponse({"success": False, "message": error}) - json_result = generar_terceros(rows[0]) if rows else None + json_result = generar_tercero_api(rows[0]) if rows else None return JSONResponse({ "success": True, @@ -135,26 +135,25 @@ async def send_terceros( if not rows: return JSONResponse({"success": False, "message": "No se encontraron datos"}) - tercero_json = generar_terceros(rows[0]) + tercero_json = generar_tercero_api(rows[0]) api_url = configs.get("api_url", "") + api_version = configs.get("api_version", "1") api_key = configs.get("api_key", "") - api_method = configs.get("api_method", "POST") headers = {"Content-Type": "application/json"} if api_key: headers["Authorization"] = f"Bearer {api_key}" + endpoint = f"{api_url}/v{api_version}/tablas/Tercero/Crear" + resp_ok = False resp_text = "" resp_code = 0 try: async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: - if api_method == "POST": - resp = await client.post(api_url + "/terceros", json=tercero_json, headers=headers) - else: - resp = await client.put(api_url + "/terceros", json=tercero_json, headers=headers) + resp = await client.post(endpoint, json=tercero_json, headers=headers) resp_ok = resp.is_success - resp_text = resp.text + resp_text = resp.text[:2000] resp_code = resp.status_code except Exception as e: resp_text = str(e) diff --git a/app/routes/transaccion.py b/app/routes/transaccion.py index c0f8cc4..b0a5e8c 100644 --- a/app/routes/transaccion.py +++ b/app/routes/transaccion.py @@ -6,7 +6,7 @@ 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_transaccion, agrupar_por_factura +from app.services.json_generator import generar_rda_paciente, agrupar_por_recepcion router = APIRouter(prefix="/transaccion", tags=["transaccion"]) @@ -53,8 +53,11 @@ async def preview_transaccion( return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"}) params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} - if ":factura" in q["query_text"] and factura: - params["factura"] = factura + if ":num_factura" in q["query_text"] and factura: + params["num_factura"] = factura + prefijo = configs.get("api_prefijo", "") + if prefijo: + params["prefijo"] = prefijo success, error, rows = fb.execute_query(q["query_text"], params) fb.disconnect() @@ -62,14 +65,8 @@ async def preview_transaccion( if not success: return JSONResponse({"success": False, "message": error}) - grupos = agrupar_por_factura(rows, factura) - json_result = [] - for (fact, doc_key), grupo in grupos.items(): - paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} - json_result.append(generar_transaccion( - fact, configs.get("num_documento_obligado", ""), - paciente_data, grupo["procedimientos"], - )) + grupos = agrupar_por_recepcion(rows) + json_result = [generar_rda_paciente(grupo) for grupo in grupos.values()] return JSONResponse({ "success": True, @@ -104,8 +101,11 @@ async def send_transaccion( return JSONResponse({"success": False, "message": f"Error Firebird: {msg}"}) params = {"fecha_ini": fecha_inicio, "fecha_fin": fecha_fin} - if ":factura" in q["query_text"] and factura: - params["factura"] = factura + if ":num_factura" in q["query_text"] and factura: + params["num_factura"] = factura + prefijo = configs.get("api_prefijo", "") + if prefijo: + params["prefijo"] = prefijo success, error, rows = fb.execute_query(q["query_text"], params) fb.disconnect() @@ -115,33 +115,31 @@ async def send_transaccion( if not rows: return JSONResponse({"success": False, "message": "No se encontraron datos"}) - grupos = agrupar_por_factura(rows, factura) + grupos = agrupar_por_recepcion(rows) api_url = configs.get("api_url", "") + api_version = configs.get("api_version", "1") + api_sucursal = configs.get("api_sucursal", "") api_key = configs.get("api_key", "") - api_method = configs.get("api_method", "POST") headers = {"Content-Type": "application/json"} if api_key: headers["Authorization"] = f"Bearer {api_key}" + endpoint = f"{api_url}/v{api_version}/rda/RdaPaciente/Insertar" + if api_sucursal: + endpoint += f"?codigosucursal={api_sucursal}" + total_enviados = 0 total_errores = 0 resultados = [] async with httpx.AsyncClient(timeout=int(configs.get("api_timeout", 30))) as client: - for (fact, doc_key), grupo in grupos.items(): - paciente_data = {"tipoDocumentoIdentificacion": doc_key[0], "numDocumentoIdentificacion": doc_key[1]} - trans_json = generar_transaccion( - fact, configs.get("num_documento_obligado", ""), - paciente_data, grupo["procedimientos"], - ) + for id_recepcion, grupo_rows in grupos.items(): + trans_json = generar_rda_paciente(grupo_rows) status_ok = False response_text = "" try: - if api_method == "POST": - resp = await client.post(api_url + "/transaccion", json=trans_json, headers=headers) - else: - resp = await client.put(api_url + "/transaccion", json=trans_json, headers=headers) + resp = await client.post(endpoint, json=trans_json, headers=headers) status_ok = resp.is_success response_text = resp.text[:1000] except Exception as e: @@ -152,6 +150,7 @@ async def send_transaccion( else: total_errores += 1 + fact = str(grupo_rows[0].get("NUM_FACTURA", id_recepcion)) resultados.append({"factura": fact, "success": status_ok}) conn = get_connection() @@ -162,7 +161,7 @@ async def send_transaccion( """, ( user["user_id"], "transaccion", fact, fecha_inicio, fecha_fin, - 1, len(grupo["procedimientos"]), + 1, len(grupo_rows), "success" if status_ok else "error", json_lib.dumps(trans_json, indent=2, ensure_ascii=False)[:5000], response_text, diff --git a/app/services/json_generator.py b/app/services/json_generator.py index b19e04d..49d2203 100644 --- a/app/services/json_generator.py +++ b/app/services/json_generator.py @@ -3,6 +3,161 @@ from datetime import datetime from typing import Optional +# ── helpers de formato de fecha ────────────────────────────────────────────── + +def _fmt_fecha(val) -> str: + if not val: + return "" + if hasattr(val, "strftime"): + return val.strftime("%d/%m/%Y") + s = str(val)[:10] + parts = s.split("-") + if len(parts) == 3: + return f"{parts[2]}/{parts[1]}/{parts[0]}" + return s + + +def _fmt_datetime(val) -> str: + if not val: + return "" + if hasattr(val, "strftime"): + return val.strftime("%d/%m/%Y %H:%M:%S") + s = str(val)[:19].replace("T", " ") + parts = s.split(" ") + if len(parts) == 2: + dp = parts[0].split("-") + if len(dp) == 3: + return f"{dp[2]}/{dp[1]}/{dp[0]} {parts[1]}" + return s + + +# ── Tercero/Crear ───────────────────────────────────────────────────────────── + +_TIPO_DOC_MAP = { + "CC": "C", "TI": "T", "RC": "R", "CE": "E", + "PA": "P", "AS": "A", "MS": "M", "NU": "U", + "SC": "S", "PE": "PE", "PT": "PT", "SI": "A", + "CN": "C", "DE": "E", "CD": "P", +} + +_ZONA_MAP = {"U": "01", "R": "02"} + + +def generar_tercero_api(row: dict) -> dict: + nombres = (row.get("NOMBRES") or "").strip().upper().split() + apellidos = (row.get("APELLIDOS") or "").strip().upper().split() + + nombre_completo = f"{row.get('NOMBRES', '')} {row.get('APELLIDOS', '')}".strip().upper() + tipo_doc = _TIPO_DOC_MAP.get(str(row.get("TIPOIDENT") or "CC").strip(), "C") + zona = _ZONA_MAP.get(str(row.get("TIPORES") or "U").strip(), "01") + + return { + "Codigo": str(row.get("CODIGO") or "").strip(), + "NatJuridica": "N", + "TipoDocumento": tipo_doc, + "Nit": str(row.get("DOCIDENT") or "").strip(), + "Nombre": nombre_completo, + "Nombre1": nombres[0] if nombres else "", + "Nombre2": nombres[1] if len(nombres) > 1 else "", + "Apellido1": apellidos[0] if apellidos else "", + "Apellido2": apellidos[1] if len(apellidos) > 1 else "", + "Direccion": (row.get("DIRECCION") or "").strip(), + "CodigoCiudad": str(row.get("COD_CIUDAD") or "00").strip(), + "NombreCiudad": (row.get("NOM_CIUDAD") or "SIN CIUDAD").strip().upper(), + "Zona1": "00", + "Clasificacion": "00", + "Telefono": str(row.get("TELEFONOS") or "").strip(), + "Email": (row.get("EMAIL") or "").strip(), + "Inactivo": False, + "Privada": "N", + "Mixta": "N", + "CodigoBarrio": "00", + "Comision": 0, + "FechaNacimiento": _fmt_fecha(row.get("F_NACIMIENTO")), + "Sexo": str(row.get("SEXO") or "M").strip(), + "Zona": zona, + "Etnia": str(row.get("CODETNIA") or "99").strip(), + "Cliente": "S", + "EnfermedadCronica": False, + "NoEnviarMinSalud": False, + } + + +# ── RdaPaciente/Insertar ────────────────────────────────────────────────────── + +def agrupar_por_recepcion(rows: list) -> dict: + grupos = defaultdict(list) + for row in rows: + key = row.get("IDRECEPCION") + grupos[key].append(dict(row)) + return grupos + + +def generar_rda_paciente(rows: list) -> dict: + if not rows: + return {} + h = rows[0] + + fecha = _fmt_fecha(h.get("FECHA_RECEPCION")) + ingreso = _fmt_datetime(h.get("HORAINICIORECEPCION") or h.get("FECHA_RECEPCION")) + egreso = _fmt_datetime(h.get("FECHA_RECEPCION")) + + detalle_pedido = [] + for row in rows: + fecha_real = _fmt_datetime( + row.get("FECHA_REPORTADO") if str(row.get("FECHA_REPORTADO") or "")[:4] != "1900" + else h.get("FECHA_RECEPCION") + ) + detalle_pedido.append({ + "CodigoMaterial": str(row.get("COD_EXAMEN") or "").strip(), + "CodigoBodega": "00", + "Cantidad": 1, + "TipoUnidad": "D", + "Valor": float(row.get("PRECIO") or 0), + "Descuento": 0, + "PorcentajeIva": 0, + "ImpConsumo": 0, + "Observacion": "", + "profesional": str(h.get("profesional") or "").strip(), + "especialidad": str(row.get("COD_ESPECIALIDAD") or "").strip(), + "diagnosticoprincipal": str(h.get("DIAG_PPAL") or "").strip(), + "fechaHoraRealizacion": fecha_real or egreso, + }) + + autorizacion = str(h.get("AUTORIZACION") or "").strip() or None + + return { + "CodigoPrefijo": str(h.get("PREFIJO") or "00").strip(), + "Numero": "", + "Fecha": fecha, + "CodTercero": str(h.get("COD_PACIENTE") or "").strip(), + "CodVendedor": "00", + "CodFormaPago": "CO", + "CodBanco": "00", + "CodigoCentroCosto": "00", + "tipoIngreso": str(h.get("CLASEPROC") or "1").strip() or "1", + "fechaHoraIngreso": ingreso, + "fechaHoraEgreso": egreso, + "modalidadAtencion": "01", + "numeroContrato": str(h.get("NIT_EMPRESA") or "").strip(), + "diagnosticoprincipal": str(h.get("DIAG_PPAL") or "").strip(), + "tipousuario": str(h.get("TIPOUSUSISPRO") or "12").strip(), + "viaIngreso": "01", + "esTerapia": False, + "esProcedimiento": False, + "numeroAutorizacion": autorizacion, + "DetallePedido": detalle_pedido, + "DetalleFormaPago": [{ + "CodigoFormaPago": "CO", + "PlazoDias": "0", + "FechaVencimiento": fecha, + "Valor": str(int(float(h.get("VALORTOTAL") or 0))), + }], + } + + +# ── funciones legacy RIPS 2.0 (se mantienen) ───────────────────────────────── + def generar_terceros(row: dict) -> dict: return { "tipoDocumentoIdentificacion": row.get("tipo_documento", "CC"), diff --git a/rips_manager.db b/rips_manager.db index 0ca5a67cea7fe5e6827bbe1d0c91fbfcb95458bc..2dc52c71f081f034ac5ed0e97373c5e33691d7dd 100644 GIT binary patch delta 2113 zcmeHIU2oe|7NkuazbYh>hXG(l5+Zi3&W<1APj8fZj zlJOx;X$6(aCCV>=J1&rrdPljKv^yjue!wod+1366uj4pLwfzIIlsM--@5l3gJ=uRc zzyEapw-<|TDW?3V%+w-z4fi4xW4(ZxF)U_OT*_6>{axWuoxj)UPudXm#n}w`(dZu z>jmXP;k0(!+}cL}#M=36TDX#pw7Y$~A9k%y{#;sEL6PLK6LwwSTfZ<5;V(?W2Y%Sw zSSyww);m9vi|3pF{4Dyw zR5e;c82L6_8+OymRtK7I+!cbYql}?7IbB%QJEU88hB(`r!tc`-ixe8nAkQS zsYmeQ%gkpxRHNV_No3q^T%p65$d;QRY%_NFE<)G0Hfld%QE%!@*I+HMmeb0hL zv7NwnpB;M`XcT+Te&2J#cuHf3_#4>2k?m`S^1BD=C4s(s$TA1n4Z{g-upJ&f`7ojd z*rRx(12Li_CIhJ@0+~yv&I+$rfZ&pAqS6{r0-p$E1}i`1tw5Am0~lkJT+9(s|6^g~ zWPq_P#!hE+Kro|&F#w}L1YDU0qIJT?^cIL!Qvp(`M{GPvMW?%y-%(mKr~v{s6h?rf z?f^+8JdT4Iwi)BLBet<428`JzNodS8ftVC1YH!oGO=z)f^@e{B&VMq~BDm@A!C#R- S=8|y-3R392o{A1w>mS=-CjDl7_YP`aj@$RJ?uv$6dfOR5b;+7NBgR5VG|HHJ-- z+9<+4OF0(LmQ}H-=^ay*^dhM1p`H&d@ElSg8>%L5s>d$ZoN+E(t}oxZUcYty#)@#`owfSv+FQ%_-kYB1 z#HE9!&nD($bnwOHB(3cXG#@MO|73Y`R+>K}mM8h)6bD!k_!P~b5zC?fXQTOlVR=~H z5;t`N7zgqNQlJ>cZ0?`|p>}|J#0FKs6eVLbD(RY}v`yVM6iKw?T%ddh2NAI;c2Mk= z(!h!qEmJqtJ3#A9_1J8nI3m>BgXtiCr7<}g1<~2=OnWbhFb-?E@Ph7&ntW;yJ}w7g zg8r$#XPPa1euhc-W`1g(YrcGc_0ts-QwLKLj+Vc?n*6gS#6#vJ{~`zwhY8Y2Y}-&l zJ#0;Hy$`buN*7RXh^g(9;WjNLrlT}1Es^yxT!?$I?~(~%9!_j4n=moy(c}S9fCpd? zWszw}h&T={VoqW+!AUeY!WPq}G;U^IJ{)1@qs7~Il#g;sFc;V@6Ebasy@g#dlYpij z8&@xjnP~0H`C!?bW$qO%)XZ#eA{c&IT E8!QI@n*aa+