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. Un medicamento de la vitrina llega a su fecha de caducidadEl artículo sigue activo (
active = true) y con existencia positiva eninventory_items. - 2. El panel de alertas lo detecta correctamente
alerts()consultaexpiry_datey lo lista dentro de "por caducar". - 3. En paralelo, alguien registra una salida manual del mismo artículo
createMovement()sólo confirma existencia y pertenencia antes de mover el saldo. - 4. El descuento se aplica sin mirar la caducidad
applyStockDelta()valida únicamentestock_qty >= qty. - 5. O, en lugar de una salida manual, se emite una receta con ese medicamento
decrementForPrescription()resuelve por nombre y categoría, sin filtrar porexpiry_date. - 6. Nada cruzó la alerta con el movimientoLa alerta de
alerts()y el descuento deapplyStockDelta()/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.
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 descuentos | No 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