Saltar al contenido

Cómo funciona

De la alerta a la salida: el flujo completo en OdontoWare

Por Eduardo Gallegos · Publicado el 24 de septiembre de 2026

Este es un recorrido de producto, no el caso de ningún consultorio real: describe cómo se mueve -y dónde se desconecta de la norma- el inventario de medicamentos dentro de OdontoWare. La pregunta que resuelve es concreta: ¿en qué paso exacto el sistema deja de cruzar la alerta de caducidad con el movimiento de salida, según lo visto en en OdontoWare, el inventario avisa que un medicamento caducó, pero igual permite darle salida?

Esquema del recorrido · ilustración, no es una pantalla del sistema

  1. 1. Un medicamento de la vitrina llega a su fecha de caducidadEl artículo sigue activo (active = true) y con existencia positiva en inventory_items.
  2. 2. El panel de alertas lo detecta correctamentealerts() consulta expiry_date y lo lista dentro de "por caducar".
  3. 3. En paralelo, alguien registra una salida manual del mismo artículocreateMovement() sólo confirma existencia y pertenencia antes de mover el saldo.
  4. 4. El descuento se aplica sin mirar la caducidadapplyStockDelta() valida únicamente stock_qty >= qty.
  5. 5. O, en lugar de una salida manual, se emite una receta con ese medicamentodecrementForPrescription() resuelve por nombre y categoría, sin filtrar por expiry_date.
  6. 6. Nada cruzó la alerta con el movimientoLa alerta de alerts() y el descuento de applyStockDelta()/decrementForPrescription() nunca se consultan entre sí.

1-2. La alerta funciona: el inventario sabe qué está por caducar

Cuando el equipo abre el panel de alertas de inventario, alerts() (services/clinical/src/inventory/inventory.service.ts:305-325) consulta expiry_date IS NOT NULL AND expiry_date <= (clinic_today() + (N || ' days')::interval) y devuelve, aparte del stock bajo, la lista de artículos expiringSoon. Hasta aquí el sistema hace exactamente lo que promete: avisa con anticipación configurable.

Agenda una demo gratis

3-6. La salida -manual o por receta- no consulta esa misma alerta

El registro de una salida manual pasa por createMovement() (services/clinical/src/inventory/inventory.service.ts:266-302), que antes de mover el saldo sólo llama a requireItem() -confirma que el artículo existe y pertenece a la clínica- y a assertFkInTenant() para el encounterId. El movimiento en sí lo aplica applyStockDelta() (línea 446); su rama para salidas (líneas 467-477) ejecuta UPDATE inventory_items SET stock_qty = stock_qty - $2 WHERE id = $1 AND stock_qty >= $2 -ninguna cláusula toca expiry_date-.

El descuento automático al recetar sigue el mismo patrón. PrescriptionsService.issue() (services/clinical/src/prescriptions/prescriptions.service.ts:70) arma la lista de medicamentos de la receta y, en la línea 147, llama a decrementForPrescription() dentro de la misma transacción. Ese método (services/clinical/src/inventory/inventory.service.ts:346-426) busca el artículo con WHERE active = true AND category = 'medicine' AND lower(name) = $1 y descuenta con el mismo condicional de saldo suficiente -de nuevo, sin ninguna referencia a la fecha de caducidad-.

Parada¿Consulta la fecha de caducidad?
1-2. Panel de alertas (alerts())Sí -filtra por expiry_date y lista "por caducar"
3. Registro de salida manual (createMovement())No -sólo confirma existencia y pertenencia
4. Aplicación del movimiento (applyStockDelta(), rama 'out')No -sólo valida stock_qty >= qty
5. Descuento automático al recetar (decrementForPrescription())No -mapea por nombre y categoría, sin filtrar por caducidad
6. Cruce entre la alerta y cualquiera de los dos descuentosNo existe en ningún punto del código

Puedes estimar la exposición de este hallazgo, con tus propios números, en la calculadora de exposición por medicamento caducado en inventario.

Preguntas frecuentes

¿Por qué OdontoWare no bloquea la salida si ya tiene la alerta?
El panel de alertas y el registro de movimientos son dos rutas de código independientes: una lee expiry_date para informar, la otra mueve stock_qty sin volver a leerla. El hallazgo es justo esa falta de cruce entre ambas.

¿Agregar la validación en un solo punto resuelve todo el hallazgo?
No: hay dos rutas de descuento independientes -createMovement()/applyStockDelta() para salidas manuales, y decrementForPrescription() para el descuento automático al recetar-. Ambas necesitarían la misma validación.

¿OdontoWare corrige esto automáticamente?
No, hoy no. Es un hallazgo verificado en el código actual, no una función ya resuelta.

Ve el recorrido completo en tu propia demo

Te mostramos en vivo el panel de alertas y el registro de movimientos de inventario.

Agenda una demo gratis
Agendar 30 min
Contacta a un especialista
Ventas y Proyectos (abre en una ventana nueva) Soporte Técnico (abre en una ventana nueva)