Modulo bajas de inventario
Descripción del módulo
Este módulo permite registrar bajas de inventario de productos químicos. Cuando se da de baja un producto, se actualiza automáticamente:
- La cantidad disponible del lote específico
- El saldo total disponible del producto químico (suma de todos los lotes)
- Se crea un registro en
BalanceHistorypara el historial de movimientos
Flujo completo para Frontend
Paso 1: Obtener productos químicos por categoría
Endpoint: [ POST ] /search-chemical-product
Body:
{
"category": "desinfectante" // Valores válidos: 'regulador', 'desinfectante', 'floculante'
}Response: Lista de productos químicos filtrados por categoría
Paso 2: Obtener lotes disponibles de un producto químico
Endpoint: [ GET ] /inventories/write-offs/lots/:chemicalProductId
Parámetros:
chemicalProductId: ID del producto químico (se puede usar elnamedel producto)
Response:
{
"_id": "producto_id",
"name": "Nombre del producto",
"category": "desinfectante",
"availableQuantity": 5000, // Saldo total disponible
"quantitiesByLot": [
{
"quantity": 2000,
"units": "litros",
"lot": "L1",
"expirationDate": "2025-12-31T00:00:00.000Z"
},
{
"quantity": 3000,
"units": "litros",
"lot": "L2",
"expirationDate": "2026-01-15T00:00:00.000Z"
}
]
}Nota importante: Los lotes con quantity: 0 deberían omitirse del response (TODO pendiente)
Paso 3: Crear baja de inventario
Endpoint: [ POST ] /inventories/write-offs
Body:
{
"category": "desinfectante",
"chemicalProduct": "nombre_del_producto", // Se usa el name, no el _id
"lot": ["L1"], // Array con el lote seleccionado (solo se permite un lote a la vez)
"quantity": 500, // Cantidad a dar de baja
"units": "litros",
"reason": "perdida", // Valores válidos: 'perdida', 'dano', 'vencimiento', 'otro'
"customReason": null // Solo requerido si reason === 'otro'
}Validaciones importantes:
- Si
reason === 'otro'→customReasones obligatorio y no puede estar vacío - Si
reason !== 'otro'→customReasondebe sernullo no enviarse quantitydebe ser mayor a 0quantityno puede ser mayor que la cantidad disponible del lote seleccionado- El lote seleccionado debe existir en el producto químico
Response (201):
{
"_id": "write_off_id",
"category": "desinfectante",
"chemicalProduct": "nombre_del_producto",
"lot": ["L1"],
"quantity": 500,
"units": "litros",
"reason": "perdida",
"customReason": null,
"modifiedBy": "user_id",
"createdAt": "2025-12-04T10:30:00.000Z",
"updatedAt": "2025-12-04T10:30:00.000Z"
}Endpoints disponibles
1. Crear baja de inventario
- Método:
POST - Ruta:
/inventories/write-offs - Autenticación: Requerida (JWT)
- Permisos:
INVENTORY_WRITE_OFF_CREATION - Validaciones: Ver sección de validaciones
2. Listar todas las bajas de inventario
- Método:
GET - Ruta:
/inventories/write-offs - Autenticación: Requerida (JWT)
- Permisos:
INVENTORY_WRITE_OFF_CREATION - Response:
{
"total": 10,
"inventoryWriteOffs": [
{
"_id": "write_off_id",
"category": "desinfectante",
"chemicalProduct": "nombre_producto",
"lot": ["L1"],
"quantity": 500,
"units": "litros",
"reason": "perdida",
"customReason": null,
"modifiedBy": {
"name": "Juan",
"lastName": "Pérez"
},
"createdAt": "2025-12-04T10:30:00.000Z",
"updatedAt": "2025-12-04T10:30:00.000Z"
}
]
}3. Obtener baja de inventario por ID
- Método:
GET - Ruta:
/inventories/write-offs/:id - Autenticación: Requerida (JWT)
- Permisos:
INVENTORY_WRITE_OFF_CREATION - Response:
{
"inventoryWriteOff": {
"_id": "write_off_id",
"category": "desinfectante",
"chemicalProduct": "nombre_producto",
"lot": ["L1"],
"quantity": 500,
"units": "litros",
"reason": "perdida",
"customReason": null,
"modifiedBy": {
"name": "Juan",
"lastName": "Pérez"
},
"createdAt": "2025-12-04T10:30:00.000Z",
"updatedAt": "2025-12-04T10:30:00.000Z"
}
}4. Obtener lotes de un producto químico
- Método:
GET - Ruta:
/inventories/write-offs/lots/:chemicalProductId - Autenticación: Requerida (JWT)
- Permisos:
INVENTORY_WRITE_OFF_CREATION - Parámetros:
chemicalProductId(puede ser el_ido elnamedel producto)
Lógica de negocio
Cuando se crea una baja de inventario, el sistema realiza automáticamente:
Validaciones:
- Verifica que la cantidad sea mayor a 0
- Verifica que el producto químico exista
- Verifica que el lote seleccionado exista
- Verifica que la cantidad a dar de baja no exceda la cantidad disponible del lote
Actualización del inventario:
- Actualiza la cantidad del lote específico:
nuevaCantidad = cantidadActual - cantidadBaja - Recalcula el saldo total del producto:
suma de todas las cantidades de todos los lotes - Actualiza
availableQuantitydel producto químico con el nuevo saldo total
- Actualiza la cantidad del lote específico:
Registro en historial:
- Crea un registro en
BalanceHistorycon:tipo: 'Baja de inventario'movimiento: '****'cantidadDosificada: cantidad dada de bajatotalBalance: nuevo saldo total del productolote: lote afectadofechaExpiracion: fecha de expiración del lote
- Crea un registro en
Estructura de datos
IInventoryWriteOff (Request Body)
{
category: ChemicalCategoryType; // 'regulador' | 'desinfectante' | 'floculante'
chemicalProduct: string; // Nombre del producto (name, no _id)
lot: string[]; // Array con el lote seleccionado (solo un elemento)
quantity: number; // Cantidad a dar de baja
units: ValidUnitsType; // Unidad de medida
reason: string; // 'perdida' | 'dano' | 'vencimiento' | 'otro'
customReason?: string | null; // Solo si reason === 'otro'
}Motivos válidos (enum)
'perdida': Producto perdido'dano': Producto dañado'vencimiento': Producto vencido'otro': Otro motivo (requierecustomReason)
Validaciones implementadas
En el Validador (express-validator):
category: RequeridochemicalProduct: Requeridolot: Requeridoquantity: Requeridounits: Requeridoreason: RequeridocustomReason:- Si
reason === 'otro'→ Requerido y no puede estar vacío - Si
reason !== 'otro'→ No debe enviarse o debe sernull
- Si
En el Controlador:
quantitydebe ser mayor a 0- El producto químico debe existir
- El lote seleccionado debe existir
- La cantidad a dar de baja no puede exceder la cantidad disponible del lote
Códigos de respuesta HTTP
201: Baja de inventario creada exitosamente200: Consulta exitosa (GET)400: Error de validación (cantidad inválida, lote no existe, etc.)404: Producto químico o baja de inventario no encontrado422: Error de validación de Mongoose (enum inválido, etc.)500: Error interno del servidor
Notas importantes para Frontend
Solo un lote a la vez: El sistema solo permite dar de baja de un lote por operación. El campo
lotes un array pero solo debe contener un elemento.Campo
customReason:- Solo se habilita cuando
reason === 'otro' - Debe validarse en frontend que no esté vacío si se selecciona "otro"
- No debe enviarse si
reason !== 'otro'
- Solo se habilita cuando
Validación de cantidad: El frontend debe validar que la cantidad no exceda la cantidad disponible del lote antes de enviar la petición.
Actualización de UI: Después de crear una baja exitosa, se debe:
- Actualizar la lista de lotes del producto
- Actualizar el saldo total disponible
- Mostrar mensaje de éxito
Manejo de errores: Los errores de validación retornan un objeto con
msgque contiene el mensaje de error específico.
Archivos relacionados
- Controller:
src/controllers/inventoryWriteOff/inventoryWriteOffController.ts - Routes:
src/routes/v1/inventoryWriteOff/inventoryWriteOff.ts - Validator:
src/validators/inventoryWriteOffValidator.ts - Model:
src/models/inventoryWriteOff/inventoryWriteOff.ts - Interface:
src/interfaces/IInventoryWriteOff/IInventoryWriteOff.ts - Service:
src/services/inventoryService.ts - Enum:
src/enums/InventoryWriteOff/enumsInventoryWriteOff.ts