Caso de estudio · Proyecto de equipo · UTN FRM 2025

OptiStock
Gestión de inventarios

Un sistema web que calcula cuánto y cuándo reponer cada artículo con modelos de investigación operativa, y genera las órdenes de compra solo.

2modelos de inventario
10tablas relacionadas
6módulos de gestión
25acciones de API propias
5personas en el equipo
Fase 1 · Relevamiento

El problema

OptiStock es el trabajo de la materia Investigación Operativa de Ingeniería en Sistemas. El desafío: llevar los modelos de inventario del papel a un sistema que funcione de punta a punta.

Situación de partida

Un comercio compra a varios proveedores y vende a clientes. Cada artículo tiene su demanda, y cada proveedor su precio, demora y costo de pedido.

Dolores a resolver

  • Decidir a ojo cuánto pedir
  • Enterarse tarde de que falta stock
  • Pedidos duplicados del mismo artículo
  • Sin comparar el costo entre proveedores

Objetivo

Que cada artículo tenga su política de reposición calculada y que el sistema dispare las compras, sin depender de que alguien se acuerde.

Usuarios

Encargado de inventarioVentasTarea programada

Son actores lógicos: el sistema no tiene login ni roles.

✦Resultado en una frase

Una SPA en React y una API en Django, con 10 tablas y 6 módulos, donde una venta puede disparar sola una orden de compra y una tarea diaria revisa los artículos de intervalo fijo.

◎Mi rol: frontend

Hice las pantallas de Proveedores, Ventas y Clientes, el encabezado, la página principal y buena parte de los estilos. Incluye paginación, filtros, baja lógica y la gestión del proveedor predeterminado por artículo, conectadas con la API.

⚑El equipo

Frontend: Pablo Chaparro y Andrea Durán.
Backend: Brenda Mollaret, Leonardo Expósito y Nicolás López.

Fase 2 · Análisis

Requisitos

Cada uno se puede verificar en el sistema.

IDRequisitoTipoPrioridad
RF-01Alta, edición, consulta y baja lógica de artículos y familias.FuncionalAlta
RF-02Gestionar proveedores y asociarlos a artículos con precio, demora y costo de pedido.FuncionalAlta
RF-03Elegir un proveedor predeterminado por artículo y su modelo de inventario.FuncionalAlta
RF-04Calcular lote óptimo, punto de pedido, stock de seguridad y costo de gestión.FuncionalAlta
RF-05Registrar ventas validando stock y artículos dados de baja.FuncionalAlta
RF-06Crear órdenes de compra manuales sin artículos duplicados ni órdenes activas previas.FuncionalAlta
RF-07Llevar el ciclo Pendiente → Enviada → Finalizada, o Cancelada, y sumar stock al finalizar.FuncionalAlta
RF-08Generar una orden automática cuando una venta deja el stock en el punto de pedido.FuncionalAlta
RF-09Revisar por tarea programada los artículos de intervalo fijo.FuncionalAlta
RF-10Listar artículos faltantes y a reponer.FuncionalMedia
RF-11Filtrar órdenes por estado y por origen manual o automático.FuncionalMedia
RF-12Impedir bajas que dejen datos inconsistentes, como un artículo con stock.FuncionalMedia
RNF-01Consistencia: ventas y órdenes se guardan en transacciones atómicas.No funcionalAlta
RNF-02Rendimiento: listados paginados de a 10 registros.No funcionalMedia
RNF-03Usabilidad: formularios validados y avisos claros de error y éxito.No funcionalMedia
RNF-04Adaptable: interfaz usable en distintos tamaños de pantalla.No funcionalMedia
RNF-05Configuración: conexión a la base de datos por variables de entorno.No funcionalMedia
Fase 2 · Análisis

Alcance

Lo importante era el modelo de inventario. Lo demás se dejó afuera a propósito.

✓Dentro del alcance

  • Artículos, familias, proveedores y clientes
  • Modelos de lote fijo e intervalo fijo
  • Órdenes de compra manuales y automáticas
  • Ventas con control de stock
  • Listados de faltantes y de artículos a reponer
  • Baja lógica con reglas de negocio

✕Fuera del alcance

  • Login, usuarios y roles
  • Tests automatizados
  • Varios depósitos
  • Facturación
  • Reportes de demanda

Evolución natural: autenticación, tests de los cálculos y estimar la demanda a partir de las ventas registradas.

Fase 2 · Análisis

Casos de uso

Filtrá por actor. Como no hay permisos, cada caso se ata al endpoint que lo resuelve.

UC‑01 · Gestionar artículos

Alta, edición y baja lógica, con familia y datos de demanda.

/api/articulos/

UC‑02 · Gestionar familias

Agrupa artículos por categoría.

/api/familias/

UC‑03 · Gestionar proveedores

Datos de contacto, búsqueda y baja lógica.

/api/proveedores/

UC‑04 · Asociar artículos

Define precio, demora, costo y modelo por proveedor.

…/actualizar_articulos/

UC‑05 · Elegir predeterminado

Marca el proveedor y recalcula la política.

…/set-predeterminado/{id}/

UC‑06 · Ver faltantes

Artículos en o por debajo del stock de seguridad.

…/productos_faltantes/

UC‑07 · Ver a reponer

Bajo el punto de pedido y sin orden pendiente.

…/productos_a_reponer/

UC‑08 · Ajustar stock

Corrige el stock a mano.

…/ajustar-stock/

UC‑09 · Crear orden de compra

Toma el precio del proveedor y valida el stock proyectado.

POST /api/ordenes/

UC‑10 · Avanzar una orden

Envía, finaliza o cancela. Al finalizar suma stock.

PATCH /api/ordenes/{id}/

UC‑11 · Registrar venta

Elige cliente y artículos; descuenta stock.

POST /api/ventas/

UC‑12 · Gestionar clientes

Alta, búsqueda por nombre o CUIT y baja lógica.

/api/clientes/

UC‑13 · Disparar orden por punto de pedido

La venta deja el stock en R y se pide el lote Q.

generar_oc_para_articulo()

UC‑14 · Revisar intervalo fijo

Corre cada día y crea las órdenes que correspondan.

revision_intervalo_fijo()
Fase 3 · Diseño

Arquitectura

Cliente y API separados, conectados por REST. Tocá cada bloque para ver qué hace.

USUARIOS CLIENTE · SPA API · DJANGO DATOS EncargadoVentas Pages + ComponentsReact 19 · Bootstrap · MUI Services + Typesfetch · TypeScript ViewSets · RESTDjango REST Framework SchedulerAPScheduler · cron diario Serializers + ModelsReglas · SciPy · ORM MySQL10 tablas
Pasá el mouse o tocá un bloque. Las líneas doradas muestran el camino de una petición: pantalla → servicio → API → base de datos.
Fase 3 · Diseño

Diagrama de clases

Modelo de dominio. Los rombos marcan composición y las flechas, asociación. Las letras son las variables de las fórmulas.

Arrastrá para moverte · Ctrl+rueda para zoom
Compras Abastecimiento Catálogo Ventas estado*1 detalles1* *1 1* *1 modelo*1 *0..1 *1 *1 1* *1 EstadoOrdenCompra«catálogo»EstadoOrdenCompraPENDIENTE | ENVIADAFINALIZADA | CANCELADA OrdenCompraOrdenCompra+nroOrdenCompra: int+costoTotalOrdenCompra: float+esAutomatica: bool+motivoGeneracion: Motivo+save(): fija entrega al finalizar OrdenCompraDetalleOrdenCompraDetalle+subtotal: float+cantidad: int Motivo«enum»MotivoMANUALPUNTO_PEDIDOPERIODO_FIJO ProveedorProveedor+nombreProveedor: string+emailProveedor: string?+fechaBajaProveedor: Date? ProveedorArticuloProveedorArticulo+precioProveedorArticulo: float+tiempoEntrega: int «L»+costoPedido: float «S»+tiempoEntreRevisiones: int «T»+es_predeterminado: bool ModeloInventario«enum»ModeloInventarioLOTE_FIJOINTERVALO_FIJO FamiliaArticuloFamiliaArticulo+nombreFamiliaArticulo: string+fechaBaja: Date? ArticuloArticulo+stockArticuloActual: int+stockArticuloSeguridad: int «SS»+loteOptimoArticulo: int «Q»+puntoPedidoArticulo: int «R»+demandaAnual: float «D»+costoAlmacenamiento: float «H»+desviacion: float «σ»+nivelDeServicio: float+costoGestionInventario: float+calcular_datos_inventario() ClienteCliente+nombreCliente: string+cuitCliente: string+fechaBajaCliente: Date? VentaVenta+fechaVenta: Date+totalVenta: float VentaDetalleVentaDetalle+cantidad: int+precioUnitario: float «congelado»+total: float
ComprasAbastecimientoCatálogoVentas◆ composición · → asociación
Fase 3 · Diseño

Los modelos de inventario

El corazón del sistema. Cada artículo usa el modelo de su proveedor predeterminado.

QLote fijo (Q, R)

Se evalúa en cada venta. Si el stock llega al punto de pedido, se genera una orden por el lote óptimo.

Q = √(2·D·S / H)
R = d·L + z·σ
SS = z·σ
CGI = (D/Q)·S + (Q/2)·H + D·C

TIntervalo fijo (T)

Una tarea diaria recorre los artículos de este modelo y pide lo necesario para cubrir el período de revisión y la entrega.

Cubre T + L días de demanda,
suma el margen de seguridad
y resta el stock actual.

ƒProbá el lote fijo

Mismas fórmulas que calcular_datos_inventario(), con z redondeado a dos decimales.

–lote óptimo Q
–punto de pedido R
–stock de seguridad
Fase 3 · Diseño

Diagramas de proceso

Los dos caminos por los que nace una orden automática.

Fase 3 · Diseño

Estructura de código

Por capas en el cliente y por entidad en el servidor.

frontend/src
Pages/ // una por rutaArticulos · Ordenes · Proveedores · Ventas · Clientes
Components/ // tablas, modales, botonesVentaTable · ProveedorModal · PaginationButtons…
services/ // acceso a la API, uno por entidadArticuloService · OrdenCompraService…
types/ · enums/ // contrato con la API
Routes/ // AppRoutes
backend/OptiStock
Articulo/ // una app por entidadmodels · serializers · views · urls · migrations
OrdenCompra/utils/generacion_oc.py // orden automática
Proveedor/ · ProveedorArticulo/predeterminado y recálculo
Venta/ · VentaDetalle/ · Cliente/
scheduler.py // revisión programada

Un servicio por entidad

Las pantallas piden datos a un servicio y no arman URLs. Cambiar un endpoint toca un solo archivo.

Reglas en el servidor

Stock, estados y duplicados se validan en la API, aunque la interfaz también valide sus formularios.

Contrato tipado

Interfaces y enums de TypeScript reflejan los modelos, como los estados de una orden.

Fase 3 · Diseño

Decisiones técnicas

Qué se eligió, por qué y qué se resignó.

Contexto

Demora, costo de pedido y tiempo entre revisiones dependen de a quién se le compra.

Decisión

Guardarlos en la relación y calcular con el proveedor predeterminado. tiempoEntreRevisiones se movió de Articulo a esta tabla.

Consecuencias

Cambiar de proveedor cambia la política. Costo: hay que recalcular al cambiar el predeterminado y no hay política sin él.

Contexto

El lote fijo reacciona al stock; el intervalo fijo mira el calendario.

Decisión

Evaluar el punto de pedido dentro de la venta y el intervalo fijo en una tarea programada.

Consecuencias

Cada modelo se comporta como en la teoría. Costo: la lógica de creación de órdenes quedó en dos lugares.

Contexto

Hacía falta una tarea diaria en un trabajo académico, sin sumar infraestructura.

Decisión

BackgroundScheduler iniciado desde wsgi.py, con endpoint para forzarlo y cambiar el horario.

Consecuencias

Sin broker ni workers aparte. Costo: con varios workers de Gunicorn correría una vez por proceso; escalar pide Celery o un cron externo.

Contexto

Crear una venta toca stock, detalle y órdenes en una sola operación.

Decisión

Concentrar validaciones y transacciones en create y update.

Consecuencias

Atomicidad simple y vistas chicas. Costo: serializers largos y más difíciles de testear que una capa de servicios.

Contexto

Las ventas y órdenes pasadas no pueden perder sus referencias.

Decisión

Un campo fechaBaja por entidad y reglas previas, como no dar de baja con stock u órdenes activas.

Consecuencias

Se conserva la historia. Costo: cada consulta debe recordar filtrar los registros dados de baja.

Contexto

Cinco personas trabajando en paralelo sobre el mismo backend.

Decisión

Diez apps, cada una con modelo, serializer y vista.

Consecuencias

Menos conflictos y responsabilidades claras. Costo: mucho código repetido y dependencias cruzadas entre apps.

Contexto

El nivel de servicio se carga como probabilidad y las fórmulas necesitan z.

Decisión

Usar norm.ppf en lugar de una tabla manual.

Consecuencias

Admite cualquier nivel de servicio. Costo: una dependencia pesada para un solo cálculo.

Fase 4 · Implementación

Tecnologías

Frontend

React 19TypeScript 5.7Vite 6React Router 7

UI y formularios

React BootstrapMaterial UIFormikYupToastify

Backend

PythonDjango 5.2DRF 3.16Gunicorn

Cálculo y tareas

SciPyAPScheduler

Datos

MySQLPyMySQLdj‑database‑urlDocker

Trabajo en equipo

GitGitHubESLintPostman
Fase 4 · Implementación

Módulos del sistema

Capturas de la app real con la API simulada y datos inventados. Tocá una imagen para ampliarla.

Pantalla de Inicio

Inicio

Presenta el sistema y al equipo.

/
Pantalla de Artículos

Artículos

Maestro con stock, familia y acceso a proveedores y cálculos.

/api/articulos/
Pantalla de Faltantes y a reponer

Faltantes y a reponer

Filtra lo que está en riesgo de quiebre.

…/productos_faltantes/
Pantalla de Familias

Familias

Agrupa el catálogo.

/api/familias/
Pantalla de Proveedores

Proveedores

Búsqueda, bajas y artículos asociados con su modelo de inventario.

/api/proveedores/
Pantalla de Órdenes de compra

Órdenes de compra

Acciones según el estado: enviar, cancelar o finalizar.

/api/ordenes/
Pantalla de Órdenes automáticas

Órdenes automáticas

Separa lo que generó el sistema de lo manual.

/api/ordenes/automaticas/
Pantalla de Ventas

Ventas

Registro con detalle y control de stock.

/api/ventas/
Pantalla de Clientes

Clientes

Búsqueda por nombre o CUIT y baja lógica.

/api/clientes/
Fase 4 · Implementación

Problemas y soluciones

Salidos del historial de commits. Filtrá por área.

Problema

tiempoEntreRevisiones estaba en Articulo, pero depende del proveedor elegido.

→

Solución

Se movió a ProveedorArticulo con migraciones, y el frontend sumó el dato en la pantalla de proveedor.

Problema

El subtotal de las órdenes automáticas quedaba mal calculado.

→

Solución

Se calcula el subtotal como precio del proveedor por cantidad.

Problema

Una orden para un artículo sin punto de pedido fallaba al proyectar el stock.

→

Solución

Se valida el dato antes y se devuelve un mensaje claro.

Problema

La revisión de intervalo fijo se caía con campos vacíos.

→

Solución

Cada dato faltante se controla, y ese artículo se saltea sin frenar a los demás.

Problema

La baja de un artículo no leía bien el estado de sus órdenes.

→

Solución

Se compara con los estados del enum: Pendiente y Enviada.

Problema

Se podía quitar un artículo de un proveedor con una orden pendiente.

→

Solución

Se agregó esa restricción antes de eliminar la asociación.

Problema

Probar la revisión implicaba esperar a las 8:00.

→

Solución

Endpoint para forzarla y otro para cambiar el horario.

Problema

El modal de proveedor no acompañaba los datos nuevos del modelo.

→

Solución

Rediseño del modal con modelo de inventario y tiempo entre revisiones.

Problema

El alta de venta fallaba al integrarse con la API.

→

Solución

Se corrigió el envío del alta y se probaron los ABM contra el backend.

Problema

La configuración estaba pensada solo para desarrollo local.

→

Solución

Conexión a MySQL por variables de entorno con dj-database-url, y Gunicorn para servir la app.

Deuda técnica conocida

Lo que falta resolver, dicho sin vueltas.

Sin autenticación

La API está abierta y no hay usuarios. Es lo primero antes de exponerla.

Sin tests automatizados

Los cálculos y las reglas se probaron a mano.

URL de la API fija

El frontend apunta a localhost en cada servicio. Debería salir de una variable de entorno.

Intervalo fijo por revisar

La fórmula y la condición de revisión de scheduler.py no coinciden del todo con el modelo teórico.

Pantalla de inventario

La ruta /inventario quedó como marcador, sin contenido.

Cómo se trabajó

Evolución del proyecto

Abril 2025 · Base

Apps de Django, modelos y primeras pantallas.

Mayo 2025 · Módulos

ABM de artículos, proveedores y clientes, ventas y primeras órdenes de compra.

Junio 2025 · Automatización

Modelos de inventario, scheduler con horario configurable, órdenes automáticas y documentación.

Noviembre 2025 a marzo 2026 · Nube

Preparación para desplegar: MySQL por variables de entorno y ajustes de CORS y hosts.

Hablemos

¿Charlamos sobre el proyecto?

Desarrollo Full Stack