feat: módulos de envío TNS/ERP Lab + sync de médico, empresa y valor total
- Hub /envios/tns con 5 sub-tabs (Terceros, Transacción RIPS, Facturas Venta, Prueba RDA, Automatización) via iframe lazy - Hub /envios/erp con sub-tabs Pacientes y Sync Automático (historial scheduler) - Modo embed (?embed=1) en base.html para cargar páginas sin sidebar/header dentro de iframes - Sidebar simplificado: 6 ítems individuales reemplazados por módulos TNS y ERP Lab - Scheduler y endpoint /pacientes/examenes ahora incluyen médico ordenante (DOCIDMEDICO), empresa/EPS (NIT_EMPRESA) y valor total (VALORTOTAL) desde Firebird Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
31c0b71c90
commit
8e27225326
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Scheduler: sync automático de pacientes con recepción reciente → WhatsApp Lab.
|
||||
Corre cada minuto y envía solo los pacientes con HORAINICIORECEPCION en los últimos 2 min.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.database import get_connection
|
||||
from app.services.firebird_service import get_firebird_from_config
|
||||
from app.services.whatsapp_sync import sync_todos, guardar_sync_log
|
||||
|
||||
_SQL_HOY = """
|
||||
SELECT DISTINCT
|
||||
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,
|
||||
r.HORAINICIORECEPCION
|
||||
FROM PACIENTE p
|
||||
LEFT JOIN CIUDAD c ON c.CODIGO = p.CIUDAD
|
||||
JOIN RECEPCION r ON r.COD_PACIENTE = p.CODIGO
|
||||
WHERE r.FECHA_RECEPCION = CURRENT_DATE
|
||||
AND r.NUM_FACTURA > 0
|
||||
"""
|
||||
|
||||
_SQL_EXAMENES_HOY = """
|
||||
SELECT
|
||||
TRIM(p.DOCIDENT) AS DOCIDENT,
|
||||
r.IDRECEPCION,
|
||||
r.HORAINICIORECEPCION,
|
||||
TRIM(rel.COD_EXAMEN) AS COD_EXAMEN,
|
||||
TRIM(ex.NOMBRE) AS NOM_EXAMEN,
|
||||
COALESCE(NULLIF(TRIM(ex.NUM_ISS), ''), TRIM(rel.COD_EXAMEN)) AS CUPS,
|
||||
rel.PRECIO,
|
||||
TRIM(r.DIAG_PPAL) AS DIAG_PPAL,
|
||||
TRIM(d.CONCEPTO) AS DIAG_CONCEPTO,
|
||||
TRIM(m.DOCIDMEDICO) AS DOCIDMEDICO,
|
||||
TRIM(r.NIT_EMPRESA) AS NIT_EMPRESA,
|
||||
r.VALORTOTAL
|
||||
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 DIAGNOSTICO d ON TRIM(d.COD_DIAG) = TRIM(r.DIAG_PPAL)
|
||||
LEFT JOIN MEDICO m ON m.CODIGO = r.COD_MEDICO
|
||||
JOIN PACIENTE p ON p.CODIGO = r.COD_PACIENTE
|
||||
WHERE r.FECHA_RECEPCION = CURRENT_DATE
|
||||
AND r.NUM_FACTURA > 0
|
||||
ORDER BY r.IDRECEPCION
|
||||
"""
|
||||
|
||||
|
||||
def _parse_hora(val) -> datetime | None:
|
||||
if not val:
|
||||
return None
|
||||
s = str(val)
|
||||
try:
|
||||
if "T" in s:
|
||||
dt = datetime.fromisoformat(s)
|
||||
ahora = datetime.now()
|
||||
return ahora.replace(hour=dt.hour, minute=dt.minute, second=dt.second, microsecond=0)
|
||||
partes = s.split(":")
|
||||
ahora = datetime.now()
|
||||
return ahora.replace(hour=int(partes[0]), minute=int(partes[1]),
|
||||
second=int(partes[2].split(".")[0]), microsecond=0)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
async def sync_recientes():
|
||||
"""Job que corre cada minuto: sincroniza pacientes con recepción en los últimos 2 min."""
|
||||
conn = get_connection()
|
||||
configs = {row["key"]: row["value"] for row in conn.execute("SELECT * FROM config").fetchall()}
|
||||
conn.close()
|
||||
|
||||
wa_url = configs.get("whatsapp_url", "").rstrip("/")
|
||||
wa_key = configs.get("whatsapp_api_key", "")
|
||||
if not wa_url or not wa_key:
|
||||
return
|
||||
|
||||
fb, ok, _ = get_firebird_from_config(configs)
|
||||
if not ok:
|
||||
return
|
||||
|
||||
ok_pac, _, rows_pac = fb.execute_query(_SQL_HOY, None)
|
||||
ok_ex, _, rows_ex = fb.execute_query(_SQL_EXAMENES_HOY, None)
|
||||
fb.disconnect()
|
||||
|
||||
if not ok_pac or not rows_pac:
|
||||
return
|
||||
|
||||
limite = datetime.now() - timedelta(minutes=2)
|
||||
|
||||
recientes = [
|
||||
row for row in rows_pac
|
||||
if (h := _parse_hora(row.get("HORAINICIORECEPCION"))) is None or h >= limite
|
||||
]
|
||||
if not recientes:
|
||||
return
|
||||
|
||||
# Construir mapa DOCIDENT → lista de exámenes filtrados por la misma ventana de 2 min
|
||||
examenes_map: dict[str, list] = {}
|
||||
if ok_ex and rows_ex:
|
||||
for ex in rows_ex:
|
||||
h = _parse_hora(ex.get("HORAINICIORECEPCION"))
|
||||
if h is not None and h < limite:
|
||||
continue
|
||||
doc = str(ex.get("DOCIDENT") or "").strip()
|
||||
if not doc:
|
||||
continue
|
||||
examenes_map.setdefault(doc, []).append({
|
||||
"cod_examen": (ex.get("COD_EXAMEN") or "").strip(),
|
||||
"nombre": (ex.get("NOM_EXAMEN") or "").strip(),
|
||||
"cups": (ex.get("CUPS") or "").strip(),
|
||||
"precio": ex.get("PRECIO"),
|
||||
"recepcion_id": ex.get("IDRECEPCION"),
|
||||
"hora": str(ex.get("HORAINICIORECEPCION") or "")[:8],
|
||||
"diagnostico_cod": (ex.get("DIAG_PPAL") or "").strip(),
|
||||
"diagnostico_nombre": (ex.get("DIAG_CONCEPTO") or "").strip(),
|
||||
"medico_docidmedico": (ex.get("DOCIDMEDICO") or "").strip(),
|
||||
"nit_empresa": (ex.get("NIT_EMPRESA") or "").strip(),
|
||||
"valor_total": ex.get("VALORTOTAL"),
|
||||
})
|
||||
|
||||
ingest_url = f"{wa_url}/api/lab/ingest_paciente.php"
|
||||
timeout = int(configs.get("api_timeout", 30))
|
||||
resultado = await sync_todos(recientes, ingest_url, wa_key, timeout,
|
||||
modo="upsert", examenes_map=examenes_map)
|
||||
|
||||
if resultado["total"] > 0:
|
||||
guardar_sync_log(resultado, 0, origen="scheduler", modo="upsert")
|
||||
@@ -106,10 +106,13 @@ async def sync_paciente(
|
||||
api_key: str,
|
||||
client: Optional[httpx.AsyncClient] = None,
|
||||
modo: str = "upsert",
|
||||
examenes: Optional[list] = None,
|
||||
) -> dict:
|
||||
"""Envía un paciente al endpoint de WhatsApp. Retorna {ok, action, message}."""
|
||||
payload = mapear_paciente(row)
|
||||
payload["modo"] = modo
|
||||
if examenes:
|
||||
payload["examenes"] = examenes
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Lab-Key": api_key,
|
||||
@@ -139,13 +142,16 @@ async def sync_paciente(
|
||||
}
|
||||
|
||||
|
||||
async def sync_todos(rows: list, url: str, api_key: str, timeout: int = 30, modo: str = "upsert") -> dict:
|
||||
async def sync_todos(rows: list, url: str, api_key: str, timeout: int = 30, modo: str = "upsert",
|
||||
examenes_map: Optional[dict] = None) -> dict:
|
||||
"""Envía una lista de filas de pacientes. Retorna resumen {total, created, skipped, updated, errores}."""
|
||||
resultado = {"total": len(rows), "created": 0, "skipped": 0, "updated": 0, "errores": 0, "detalle": []}
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
for row in rows:
|
||||
r = await sync_paciente(row, url, api_key, client, modo)
|
||||
doc = str(row.get("DOCIDENT") or "").strip()
|
||||
examenes = examenes_map.get(doc) if examenes_map else None
|
||||
r = await sync_paciente(row, url, api_key, client, modo, examenes)
|
||||
if r["ok"]:
|
||||
action = r["action"]
|
||||
if action == "created":
|
||||
|
||||
Reference in New Issue
Block a user