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. StockLotya es la fuente operativa de cantidad viva por(warehouse, asset, lot).Stockfunciona 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()sobreStockLotpara evitar sobreventa. - El historial de movimientos existe en
StockMovement, pero hoy sin FK explícita aLot. StockTransferusaitemsen JSON y estados limitados (PENDING,COMPLETED,CANCELLED).
2.2 Gaps respecto del objetivo SMA-55
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.
- No existe
Transferencias robustas
- Falta
StockTransferLinenormalizada. - Falta soporte de estados de negocio
draft,dispatched,partially_received,received,cancelled. - El payload JSON limita trazabilidad por lote y recepción parcial.
- Falta
Trazabilidad por lote en ledger
StockMovementdebería referenciarlotpara trazabilidad completa audit-friendly.
Kits/combos
- No existen entidades
Kit/BundleyKitComponent. - Falta política estándar de descuento por componentes sin stock propio del kit lógico.
- No existen entidades
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 aUnitOfMeasure, obligatorio en modelo objetivo).- flags de negocio (
is_active, etc.).
Restricciones:
- Unicidad por tenant para identificadores de negocio (
organization + skucuando aplique). base_uom.categorycompatible 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 + Warehousepara 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_stockcuando 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 >= 0conCheckConstraint.- 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
StockLotdebe 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:
draftdispatchedpartially_receivedreceivedcancelled
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 > 00 <= 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, asumirasset.base_uom. - Convertir a base antes de validar disponibilidad y antes de persistir movimientos.
- Nunca persistir
StockLot.quantityen 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
Decimalcon 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
StockMovementreferencia 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:
- correr dentro de
transaction.atomic(), - bloquear filas objetivo con
select_for_update(), - validar invariantes de no-negatividad,
- registrar
StockMovementinmutable 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
StockLotcon estrategia FIFO/FEFO/LIFO. - Registra
SALE_OUTBOUNDpor 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
StockLotbajo 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:
- Iniciar transacción atómica.
- Resolver scope exacto de filas (
StockLot,StockTransfer, etc.). - Aplicar
select_for_update()en orden estable para evitar deadlocks. - Revalidar disponibilidad tras lock.
- Persistir cambios y ledger.
- Commit.
Orden de lock recomendado:
- primero documento de negocio (
StockTransfer,Sale) si aplica, - luego
StockLotpor(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 >= 0a 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_sma55o./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.categorycompatible conasset.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:
- Aplicación: validación previa y posterior al cálculo bajo lock.
- DB:
CheckConstraintpara 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 (
cleancorto 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_itemyKitService.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
StockTransfera estados objetivo, - agregar
lotenStockMovement(nullable temporal para migración progresiva), - harden de constraints críticos en DB.
Migración:
- data migration para transformar
StockTransfer.itemsJSON 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) conDecimal, - 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
KityKitComponent, - 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 < 0por 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/dispatchedrealiza 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
StockLottieneStockMovementasociado. - 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:
ReceivePurchaseServiceDispatchSaleServiceTransferDispatchServiceTransferReceiveServiceAdjustStockServiceReverseStockOperationService
- 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
Riesgo: migración de transferencias JSON a líneas
- Mitigación: dual-read temporal + script de reconciliación.
Riesgo: errores de redondeo en UoM
- Mitigación:
Decimal, políticas por categoría y tests de precisión.
- Mitigación:
Riesgo: deadlocks por locks inconsistentes
- Mitigación: orden de lock estable y transacciones cortas.
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.