Skip to content

SMA-55 — Plan de desarrollo integral: unidades de medida y equivalencias

1) Objetivo y principios de diseño

Este documento define el diseño objetivo para inventario en un ERP real con foco en:

  • exactitud de stock bajo concurrencia,
  • trazabilidad completa por lote y movimiento,
  • auditabilidad (ledger inmutable de movimientos),
  • extensibilidad para UoM equivalentes, kits, transferencias parciales y reversión.

Contexto asumido del sistema:

  • Multi-tenant con aislamiento por esquema.
  • Operaciones de stock con alta probabilidad de concurrencia (ventas, compras, transferencias, ajustes).
  • Necesidad de operar en unidades de negocio (caja, pack, kg) sin perder consistencia técnica.

Regla de oro:

El inventario operativo siempre se persiste en unidad base del asset. Cualquier unidad alternativa se convierte antes de impactar StockLot.


2) Estado actual validado y gaps detectados

2.1 Estado actual validado (código vigente)

En el estado actual del backend:

  • Existen Asset, Lot, Stock, StockLot, StockMovement, StockTransfer.
  • StockLot ya es la fuente operativa de cantidad viva por (warehouse, asset, lot).
  • Stock funciona como configuración/política (mínimo/máximo, override de precio), no como fuente física.
  • Varias rutas de venta/renta usan select_for_update() sobre StockLot para evitar sobreventa.
  • El historial de movimientos existe en StockMovement, pero hoy sin FK explícita a Lot.
  • StockTransfer usa items en JSON y estados limitados (PENDING, COMPLETED, CANCELLED).

2.2 Gaps respecto del objetivo SMA-55

  1. Unidades de medida y equivalencias

    • No existe UnitOfMeasure.
    • No existe AssetUnitConversion (equivalencias por asset).
    • No hay conversión explícita a unidad base en todas las entradas/salidas.
  2. Transferencias robustas

    • Falta StockTransferLine normalizada.
    • Falta soporte de estados de negocio draft, dispatched, partially_received, received, cancelled.
    • El payload JSON limita trazabilidad por lote y recepción parcial.
  3. Trazabilidad por lote en ledger

    • StockMovement debería referenciar lot para trazabilidad completa audit-friendly.
  4. Kits/combos

    • No existen entidades Kit/Bundle y KitComponent.
    • Falta política estándar de descuento por componentes sin stock propio del kit lógico.
  5. Restricciones de base de datos

    • Recomendado endurecer constraints explícitas para no-negatividad, consistencia de conversiones y unicidad semántica.

3) Modelo objetivo (declaración explícita de entidades)

3.1 Catálogo / identidad

Asset

Responsabilidad:

  • Representa el producto (identidad comercial y operativa).
  • Define la unidad base de stock.
  • No almacena cantidad viva.

Campos clave propuestos:

  • organization (tenant).
  • name, sku, metadata.
  • base_uom (FK a UnitOfMeasure, obligatorio en modelo objetivo).
  • flags de negocio (is_active, etc.).

Restricciones:

  • Unicidad por tenant para identificadores de negocio (organization + sku cuando aplique).
  • base_uom.category compatible con naturaleza del asset (ej.: no peso para activos discretos si negocio no lo permite).

3.2 Origen de stock

Lot

Responsabilidad:

  • Origen del stock (compra, producción, devolución, transferencia de entrada materializada como lote receptor, etc.).
  • Entidad inmutable en identidad (asset + lot), con cantidad histórica opcional.
  • No “vive” en warehouse; el warehouse vive en StockLot.

Campos clave:

  • asset, lot, type, expiry_date, acquisition_price, supplier, currency.
  • opcional recomendado: original_quantity (histórico, no operativo).

Restricciones:

  • unique(asset, lot).
  • checks de coherencia temporal (si aplica negocio para expiración).

3.3 Política de stock

Stock

Responsabilidad:

  • Contexto Asset + Warehouse para políticas (mínimo, máximo, punto de reposición, flags).
  • No es fuente de verdad de cantidad.

Campos clave:

  • warehouse, asset (únicos en combinación).
  • min_stock, max_stock, reorder_point.
  • banderas de replenishment y comportamiento.

Restricciones:

  • unique(warehouse, asset).
  • max_stock >= min_stock cuando ambos existen.

3.4 Estado real del inventario

StockLot

Responsabilidad:

  • Fuente operativa de verdad por Lot + Asset + Warehouse.
  • Guarda cantidad viva para despacho/consumo/transferencia.

Campos:

  • warehouse, asset, lot, quantity.

Restricciones críticas:

  • unique(warehouse, asset, lot).
  • quantity >= 0 con CheckConstraint.
  • Validación de integridad: lot.asset_id == asset_id.

Índices recomendados:

  • (warehouse, asset, quantity) para consumo FIFO/FEFO/LIFO.
  • (warehouse, asset, lot) para upsert y trazabilidad.

3.5 Movimientos

StockMovement (ledger histórico)

Responsabilidad:

  • Historial inmutable (fuente histórica de verdad).
  • Cada impacto en StockLot debe generar al menos un movimiento.
  • Cantidad siempre en unidad base.

Campos clave:

  • organization, warehouse, asset, lot (nullable para casos legacy puntuales, idealmente no nullable en operaciones nuevas).
  • quantity_base (entero o decimal según dominio), movement_type, reason_code, reference_type, reference_id.
  • created_by, created_at.

Restricciones:

  • Inmutabilidad por política de aplicación (no updates ni deletes lógicos directos).
  • quantity_base > 0.
  • catálogo cerrado de movement_type.

Recomendación:

  • Adoptar patrón de “append-only ledger”.
  • Reversiones generan movimientos compensatorios; nunca reescritura del pasado.

3.6 Transferencias

StockTransfer

Documento de negocio (cabecera):

  • organization, source_warehouse, destination_warehouse, estado, fechas, usuario.

Estados objetivo:

  • draft
  • dispatched
  • partially_received
  • received
  • cancelled

StockTransferLine

Detalle normalizado por asset/lote:

  • transfer, asset, lot (nullable según fase), quantity_planned_base, quantity_received_base.

Restricciones:

  • unicidad por combinación semántica (ej.: (transfer, asset, lot) cuando lot existe; y (transfer, asset) en modo sin lote explícito).
  • checks:
    • quantity_planned_base > 0
    • 0 <= quantity_received_base <= quantity_planned_base

Beneficio:

  • habilita recepción parcial, idempotencia por línea y trazabilidad por lote.

3.7 Unidades de medida y equivalencias (énfasis principal)

UnitOfMeasure

Catálogo global por tenant (o global compartido con visibilidad por tenant):

  • code, name, category (COUNT, WEIGHT, VOLUME, LENGTH, etc.), is_active.

Reglas:

  • Categorías separadas para impedir conversiones inválidas (kg ↔ unidad).
  • Debe existir al menos una base por categoría si se usa sistema de categorías por tenant.

AssetUnitConversion (o AssetUoM)

Equivalencias por asset:

  • asset, uom, factor_to_base.

Semántica:

  • quantity_base = quantity_input * factor_to_base.
  • factor_to_base > 0.
  • uom.category == asset.base_uom.category.

Restricciones:

  • unique(asset, uom).
  • factor_to_base > 0.

Reglas operativas:

  • Toda mutation/comando que recibe cantidad debe aceptar opcionalmente uom_id.
  • Si no viene uom_id, asumir asset.base_uom.
  • Convertir a base antes de validar disponibilidad y antes de persistir movimientos.
  • Nunca persistir StockLot.quantity en unidad no base.

Precisiones numéricas:

  • Para unidades discretas (COUNT), definir política de redondeo estricta (quantize + validación entero exacto si corresponde).
  • Para categorías continuas (peso/volumen), usar Decimal con precisión definida.

3.8 Kits / Combos

Kit (o Bundle)

  • Catálogo de composición comercial.
  • Sin stock propio para el modo “kit lógico”.

KitComponent

  • kit, asset, quantity_base_per_kit.

Reglas:

  • Venta de kit lógico descuenta componentes en StockLot.
  • La trazabilidad en StockMovement referencia el documento de venta + componente.

Diferencia clave:

  • Kit lógico: no se almacena stock del kit; se descuenta de componentes en tiempo de operación.
  • Kit físico (futuro): se podría pre-ensamblar como asset independiente con propio lote/stock y consumo de BOM al ensamblar.

Recomendación fase MVP:

  • implementar solo kit lógico con descuento por componentes y sin reserva avanzada inter-ítems.

4) Operaciones que modifican StockLot (definición estándar)

Todas las operaciones deben cumplir:

  1. correr dentro de transaction.atomic(),
  2. bloquear filas objetivo con select_for_update(),
  3. validar invariantes de no-negatividad,
  4. registrar StockMovement inmutable en unidad base.

4.1 Recepción de compra

  • Incrementa StockLot (crea lote si no existe).
  • Registra PURCHASE_INBOUND.
  • Si aplica UoM alterna, convierte primero a base.

4.2 Venta / consumo

  • Decrementa StockLot con estrategia FIFO/FEFO/LIFO.
  • Registra SALE_OUTBOUND por lote consumido.
  • Para kit lógico: consume cada componente con su factor.

4.3 Transferencia (salida y entrada)

  • Salida en origen: decrementa lotes y registra TRANSFER_OUTBOUND.
  • Entrada en destino: incrementa lotes transferidos y registra TRANSFER_INBOUND.
  • Soporta recepción parcial por línea.

4.4 Ajustes

  • Ajuste positivo: ADJUSTMENT_INBOUND.
  • Ajuste negativo: ADJUSTMENT_OUTBOUND.
  • Requiere motivo (reason_code) obligatorio.

4.5 Reversión

  • Nunca borra ni edita movimientos previos.
  • Crea movimientos compensatorios y reconstituye StockLot bajo lock.
  • Debe mantener referencia al movimiento/documento original.

5) Concurrencia y consistencia

5.1 Estrategia anti race condition

Patrón obligatorio por comando de stock:

  1. Iniciar transacción atómica.
  2. Resolver scope exacto de filas (StockLot, StockTransfer, etc.).
  3. Aplicar select_for_update() en orden estable para evitar deadlocks.
  4. Revalidar disponibilidad tras lock.
  5. Persistir cambios y ledger.
  6. Commit.

Orden de lock recomendado:

  • primero documento de negocio (StockTransfer, Sale) si aplica,
  • luego StockLot por (warehouse_id, asset_id, lot_id) ordenado.

Etapa 5: Endurecimiento de Concurrencia

  • [x] Agregar select_for_update() en CreateStockTransfer (StockLot antes de FIFO)
  • [x] Agregar select_for_update() en CompleteStockTransfer (transfer + StockLot)
  • [x] Agregar select_for_update() en CancelStockTransfer (transfer + StockLot)
  • [x] Agregar select_for_update() en ReceivePurchaseOrder (PO + StockLot)
  • [x] Agregar select_for_update() en AddStock/process_add_stock (StockLot)
  • [x] Agregar select_for_update() en DispatchStockMutation (StockLot)
  • [x] Agregar select_for_update() en AdjustStockLotMutation (StockLot)
  • [x] CheckConstraint StockLot.quantity >= 0 a nivel BD (Etapa 1)
  • [ ] Tests de concurrencia (threading/concurrent requests)

Etapa 6: Tests críticos

  • [x] Tests de stock: venta con stock exacto, venta con insuficiente, backorder (assets/tests/test_sma55.StockSaleTests)
  • [x] Tests de transferencias: creación, completado, cancelación (assets/tests/test_sma55.TransferTests)
  • [x] Tests de equivalencias: UoM convert_to_base, convert_from_base (assets/tests/test_sma55.UoMTests)
  • [x] Tests de kits: venta de kit descuenta componentes correctamente (assets/tests/test_sma55.KitTests)
  • [x] Tests de integridad: CheckConstraint StockLot.quantity >= 0 (assets/tests/test_sma55.IntegrityTests)
  • [x] Tests de marca: asset con/sin brand, filtro por brand (assets/tests/test_sma55.BrandTests)
  • [ ] Ejecutar: cd backend && python manage.py test assets.tests.test_sma55 o ./backend/scripts/run_sma55_tests.sh (requiere deps + PostgreSQL)

5.2 Constraints de base de datos

Críticas:

  • StockLot.quantity >= 0.
  • unique(warehouse, asset, lot).
  • AssetUnitConversion.factor_to_base > 0.
  • uom.category compatible con asset.base_uom.category (validación app + check parcial donde sea factible).
  • Transfer lines con received <= planned.

Defensivas:

  • constraints condicionales para unicidad de líneas con/ sin lote.
  • índices que acompañen los filtros de lock.

5.3 Validación de stock negativo

Doble barrera:

  1. Aplicación: validación previa y posterior al cálculo bajo lock.
  2. DB: CheckConstraint para bloquear persistencia inválida en cualquier ruta.

6) Reglas de negocio: modelo vs servicio de dominio

6.1 Reglas en modelos (declarativas)

En modelos deben vivir:

  • constraints estructurales (UniqueConstraint, CheckConstraint),
  • validaciones simples de consistencia intrafila (clean corto y determinista),
  • metadatos de relación e índices.

Ejemplos:

  • StockLot.quantity >= 0.
  • Stock.max_stock >= min_stock.
  • AssetUnitConversion.factor_to_base > 0.

6.2 Reglas en servicios de dominio (imperativas)

En servicios deben vivir:

  • orquestación transaccional multi-entidad,
  • locks y estrategia de concurrencia,
  • cálculos de consumo por lotes (FIFO/FEFO/LIFO),
  • conversión de UoM antes de tocar stock,
  • compensaciones/reversiones e idempotencia.

Ejemplos:

  • StockTransferService.dispatch/receive/cancel.
  • StockAdjustmentService.apply.
  • SalesStockService.consume_item y KitService.consume_components.

6.3 Por qué no mezclar lógica pesada en modelos

  • Los modelos no tienen contexto transaccional completo.
  • La lógica distribuida en save() dificulta testeo y observabilidad.
  • Incrementa riesgo de side effects silenciosos y race conditions no controladas.

7) Plan de desarrollo por etapas (accionable)

Etapa 1 — Validación del modelo actual

Objetivo:

  • levantar baseline real del estado actual y rutas que tocan stock.

Entregables:

  • matriz “modelo actual vs modelo objetivo”,
  • inventario de operaciones que alteran StockLot,
  • auditoría de locks existentes y faltantes.

Criterios de aceptación:

  • documento de brechas firmado por equipo técnico,
  • lista priorizada por riesgo (consistencia > feature).

Etapa 2 — Cierre de gaps estructurales

Objetivo:

  • incorporar entidades faltantes para trazabilidad y transferencia robusta.

Cambios:

  • agregar StockTransferLine,
  • extender StockTransfer a estados objetivo,
  • agregar lot en StockMovement (nullable temporal para migración progresiva),
  • harden de constraints críticos en DB.

Migración:

  • data migration para transformar StockTransfer.items JSON a líneas.
  • compatibilidad temporal lectura dual (items + lines) hasta apagar legacy.

Criterios de aceptación:

  • transferencias nuevas operan solo por líneas,
  • recepción parcial y estado intermedio disponibles.

Etapa 3 — Unidades y equivalencias

Objetivo:

  • habilitar operación comercial en UoM alternativas sin romper consistencia de stock.

Cambios:

  • crear UnitOfMeasure,
  • crear AssetUnitConversion,
  • agregar Asset.base_uom,
  • servicio de conversión central (UoMService) con Decimal,
  • adaptar mutaciones/servicios de compra, venta, ajuste, transferencia para aceptar uom_id.

Regla de implementación:

  • conversión a base al inicio de la operación; ledger y stock solo en base.

Criterios de aceptación:

  • casos como “2 cajas x 12 = 24 unidades” válidos y auditables,
  • bloqueo de conversiones cruzadas por categoría.

Etapa 4 — Soporte de kits lógicos

Objetivo:

  • vender kits sin stock propio, descontando componentes.

Estado:

  • [x] Implementado en modelos (Kit, KitComponent), migraciones y flujo de ventas.

Cambios:

  • agregar Kit y KitComponent,
  • adaptar creación/actualización/cancelación de ventas para kit lógico,
  • registrar movimientos por componente con referencia al ítem de venta.

Criterios de aceptación:

  • venta de kit descuenta correctamente componentes y lotes,
  • reversión de venta restaura componentes consistentemente.

Etapa 5 — Endurecimiento de concurrencia

Objetivo:

  • eliminar ventanas de race condition en rutas críticas.

Cambios:

  • estandarizar transaction.atomic + select_for_update,
  • lock explícito de cabecera de transferencia y líneas antes de mutar stock,
  • orden estable de locks para minimizar deadlocks,
  • validaciones post-lock obligatorias.

Criterios de aceptación:

  • pruebas concurrentes sin stock negativo ni doble consumo,
  • transferencias idempotentes ante reintentos de cliente.

Etapa 6 — Tests críticos (mínimo obligatorio)

6.1 Stock base y no-negatividad

  • no permite StockLot.quantity < 0 por app ni DB.
  • deducción concurrente sobre mismo lote no sobrevende.

6.2 Transferencias

  • dispatch decrementa origen y genera ledger.
  • receive parcial completa línea y estado partially_received.
  • receive total cierra a received.
  • cancel en draft/dispatched realiza compensación correcta.

6.3 UoM y equivalencias

  • conversiones exactas por factor.
  • rechazo por categoría incompatible.
  • precisión/rounding según tipo de unidad.

6.4 Kits

  • venta de kit descuenta componentes correctos.
  • falta de un componente bloquea operación completa (atomicidad).
  • reversión recompone stock y ledger.

6.5 Auditoría

  • toda mutación de StockLot tiene StockMovement asociado.
  • movimientos inmutables: reversión por compensación, no por edición.

8) Arquitectura recomendada (ERP-ready)

Patrones propuestos:

  • Command services por operación de negocio:
    • ReceivePurchaseService
    • DispatchSaleService
    • TransferDispatchService
    • TransferReceiveService
    • AdjustStockService
    • ReverseStockOperationService
  • UoMService único para conversiones.
  • Ledger-first discipline: toda mutación física acompañada de movimiento.
  • Idempotency keys para endpoints de alta concurrencia (futuro inmediato).

Antipatrones a evitar:

  • lógica de negocio pesada en model.save(),
  • mutar stock desde múltiples lugares sin servicio común,
  • conversiones ad hoc fuera de servicio central.

9) Riesgos y mitigaciones

  1. Riesgo: migración de transferencias JSON a líneas

    • Mitigación: dual-read temporal + script de reconciliación.
  2. Riesgo: errores de redondeo en UoM

    • Mitigación: Decimal, políticas por categoría y tests de precisión.
  3. Riesgo: deadlocks por locks inconsistentes

    • Mitigación: orden de lock estable y transacciones cortas.
  4. Riesgo: divergencia entre stock y ledger

    • Mitigación: contrato de servicio obligatorio + test de auditoría transversal.

10) Resultado esperado

Al finalizar las etapas:

  • el sistema opera comercialmente en UoM equivalentes sin guardar stock fuera de unidad base,
  • soporta kits lógicos con descuento por componentes,
  • transferencias tienen granularidad por línea/lote y estados reales,
  • el stock queda protegido ante concurrencia y con trazabilidad completa para auditoría ERP.