Cuando nos enfrentamos al diseño de un sistema de ventas y control de stock, uno de los mayores desafíos no es solo registrar las entradas y salidas, sino definir cómo fluye el inventario. Conceptos como FIFO, LIFO y FEFO suelen mezclarse entre la teoría contable y la práctica logística, lo que puede llevarnos a cometer errores graves al intentar traducirlos a código SQL.
1. Clarificando conceptos: Valuación vs. Flujo Físico
Durante el desarrollo de un módulo de inventarios en DOSCLIC, me encontré con la necesidad de ordenar estos conceptos. Es muy común confundir los métodos de valuación de inventarios (que definen cómo se calcula el costo contable de lo que vendes, como FIFO, LIFO o el Promedio Ponderado) con los métodos de flujo físico (cómo se mueven realmente las mercancías en el almacén, como FEFO).
Para un sistema que maneja productos perecederos, medicamentos o lotes con fecha de vencimiento, la regla de oro es combinar el control de lotes con FEFO (First Expired, First Out). Aquí, el costo pasa a un segundo plano frente a la prioridad de evitar pérdidas por caducidad.
2. La trampa del ORDER BY y el error común en FEFO
Inicialmente, mi enfoque para resolver esto programáticamente fue directo. Pensé en mapear cada regla de negocio a una cláusula de ordenamiento en SQL de la siguiente manera:
- FIFO (PEPS):
ORDER BY fecha_entrada ASC - LIFO (UEPS):
ORDER BY fecha_entrada DESC - FEFO:
WHERE caducidad < fecha_actual ORDER BY fecha_caducidad ASC
WHERE caducidad < fecha_actual es un error conceptual grave. Esta consulta solo devolverá los lotes que ya están vencidos (y que, por lo tanto, no deberías vender), en lugar de priorizar los que están más próximos a vencer pero aún son aptos para el consumo.Para implementar un FEFO correcto, no debemos filtrar por productos ya caducados, sino ordenar de manera ascendente por la fecha de vencimiento de aquellos lotes que aún tienen stock disponible.
3. La consulta SQL correcta para el control de lotes
Para que el sistema funcione de manera robusta, la consulta debe garantizar que solo seleccionamos lotes con existencias reales y ordenarlos según la estrategia elegida. Aquí tienes la estructura recomendada para FEFO:
SELECT *
FROM lotes
WHERE producto_id = ?
AND cantidad_disponible > 0
ORDER BY fecha_caducidad ASC, fecha_entrada ASC;fecha_entrada ASC) actúa como un mecanismo de desempate si dos lotes tienen exactamente la misma fecha de caducidad.4. Más allá del SQL: El reto de la asignación parcial
Aunque un simple ORDER BY define la prioridad de salida, en el desarrollo real de software esto no es suficiente. El inventario físico rara vez se consume en bloques perfectos; nos enfrentamos constantemente al problema de la asignación parcial.
Imagina el siguiente escenario:
- Lote A (más antiguo): 5 unidades disponibles.
- Lote B (más reciente): 10 unidades disponibles.
Si un cliente realiza una compra de 8 unidades bajo la regla FIFO, no podemos simplemente restar las 8 unidades de una sola fila. La lógica de negocio en el backend debe realizar un consumo iterativo:
- Consultar los lotes disponibles ordenados por la regla correspondiente.
- Tomar las 5 unidades del Lote A (dejando su stock en 0).
- Calcular el remanente (3 unidades) y tomarlas del Lote B (dejando su stock en 7).
- Registrar las transacciones de salida correspondientes para mantener la trazabilidad.
Además, es crítico envolver este proceso en una transacción de base de datos y utilizar bloqueos de concurrencia (como FOR UPDATE en SQL) para evitar que dos ventas simultáneas intenten consumir el mismo lote al mismo tiempo, generando descuadres de stock.
La historia detrás de la nota
A veces pensamos que las reglas de negocio complejas requieren consultas SQL monstruosas, cuando en realidad la base es un ordenamiento limpio combinado con una lógica de backend robusta que controle la concurrencia y la distribución parcial.