# Relación slotting WMS ↔ método de inventario contable

> **Estado:** investigación cerrada 2026-05-16. Documento vivo — actualizar cuando cambien las reglas WMS o el método contable de una empresa.
>
> **Audiencia:** quien configure `wms_regla_slottings` o decida el `metodo_inventario_id` de una empresa.

---

## Resumen ejecutivo

El **slotting físico** (estrategia de qué ubicación se toca al picking) y el **método de inventario contable** (cómo se valoriza el costo de venta) son ortogonales en teoría pero acopladas en la práctica. Si la elección física no coincide con la contable, el costo registrado en `pos_inventarios` puede diferir del costo real de las unidades físicamente despachadas.

**Default recomendado por método contable:**

| Método contable (par_empresas.metodo_inventario_id) | Estrategia slotting recomendada | Por qué |
|---|---|---|
| **1 — FIFO** | FIFO (`wms_regla_slottings.estrategia='FIFO'`) | Físico y contable consistentes — lo más viejo sale primero, costo contable también. |
| **2 — LIFO** | FIFO o FEFO (ver nota abajo) | Casi nunca se hace LIFO físico real. Se mantiene FIFO físico y el sistema reconcilia el costo. |
| **3 — PMP** *(default)* | FEFO o ABC | PMP promedia todos los costos → orden físico no afecta el costo. Libre para optimizar rotación física. |
| **4 — Identificación específica** | Por lote (FEFO si tiene vencimiento, FIFO si no) | El lote físico debe ser el lote contable. WMS debe trackear `lote` por ubicación. |

---

## Matriz completa: método contable × estrategia slotting × restricciones

### Notación

- **OK**: combinación coherente, sin trabajo extra.
- **OK con reconciliación**: el costo contable se calcula del catálogo (FIFO/LIFO), no de la ubicación tocada. El sistema funciona pero el costo de la unidad despachada físicamente puede no ser el mismo que el costo registrado.
- **NO**: incoherente, no usar.

| Contable \ Slotting | FIFO físico | LIFO físico | FEFO físico | ABC físico | MIXTA físico |
|---|---|---|---|---|---|
| **FIFO contable**  | OK ✅ | NO ❌ | OK con reconciliación ⚠️ | OK con reconciliación ⚠️ | OK con reconciliación ⚠️ |
| **LIFO contable**  | OK con reconciliación ⚠️ | (raro físico) | OK con reconciliación ⚠️ | OK con reconciliación ⚠️ | OK con reconciliación ⚠️ |
| **PMP contable**   | OK ✅ | (raro físico) | OK ✅ | OK ✅ | OK ✅ |
| **ID específica**  | NO ❌ (salvo que coincida) | NO ❌ | OK si por lote ✅ | NO ❌ | NO ❌ |

### Notas por celda

- **FIFO contable + FEFO físico**: si el artículo tiene vencimiento, FEFO es preferible (no querés despachar lo más viejo si vence pronto). El costo FIFO se aplica al lote más viejo *en libros*, mientras que físicamente sale el lote que vence antes. Diferencia: si el más viejo es justo el de mayor vencimiento, coincide; si no, hay reconciliación.

- **LIFO físico**: prácticamente nadie lo hace en almacén real, salvo "drive-in" donde sólo se accede al último ingreso. Si LIFO es la regla contable, FIFO físico es el default seguro — el `InventoryCostService::costearLIFO` calcula el costo a partir del historial, no de la unidad física tocada.

- **PMP**: caso más cómodo. El costo unitario se actualiza al recibir mercadería (promedio ponderado) y se mantiene constante hasta la próxima recepción. El orden físico es irrelevante — usar la estrategia que más optimice operación (rotación, distancia al despacho, ABC).

- **Identificación específica + FIFO/LIFO físico**: la venta exige `lote_id` específico (pasado por la línea de pos_inventario o pos_venta_detalle). WMS debe poder ubicar **ese lote** físicamente. Si la regla de slotting es "primero el viejo" pero el operador pide un lote distinto, hay conflicto. **Lo correcto** es slotting "por lote" (FEFO si tiene vencimiento; FIFO si no) — el operador siempre escanea el lote que coincide con la venta.

---

## ¿El slotting WMS debe espejar el método contable?

**No siempre**, pero conviene:

1. **Cuando hay vencimiento**: FEFO físico es casi siempre la mejor decisión, independientemente del método contable. Vender lo que vence antes evita pérdidas. La reconciliación de costo se hace en libros (FIFO/LIFO calcula el costo de "lo más viejo/nuevo en stock" sin importar qué unidad física se tocó).

2. **Cuando hay ABC**: si la empresa tiene clase ABC habilitada, el slotting ABC pone los artículos clase A cerca del picking — reduce tiempo de operación. Esto convive sin problema con FIFO/LIFO/PMP contable.

3. **Cuando contabilidad es ID específica**: WMS está **obligado** a respetar el lote. No hay flexibilidad.

4. **Caso PMP**: WMS libre. Optimizar por rotación, distancia, o lo que el operador prefiera.

---

## Verificación a hacer en `wms_ubicacion_stocks`

| Columna | ¿Existe? | Para qué se usa |
|---|---|---|
| `pos_articulo_id` | ✅ | Identifica el artículo |
| `lote` | ✅ | Identifica el lote físico (clave para identificación específica y FEFO) |
| `fecha_vencimiento` | ✅ | Driver de FEFO |
| `fecha_ingreso` | ✅ | Driver de FIFO/LIFO |
| `costo_unitario` | ✅ | Costo capturado al momento del put-away |
| `lote_key` / `venc_key` | ✅ (generated) | Permiten unique compuesto a pesar de NULL |

**El esquema actual soporta los 4 métodos contables.** El campo `lote` es el puente con identificación específica.

---

## Reglas operativas del sistema (cómo se ejecuta hoy)

1. **Compra (`PosCompraController` → `pos_inventarios` con `movimiento=1`)** → si el artículo tiene `requiere_gestion_wms=true`, el `PosInventarioObserver` crea una `wms_tarea` tipo `put_list`. El operario confirma la ubicación final. El costo unitario de esa unidad se guarda en `wms_ubicacion_stocks.costo_unitario` (promedio ponderado si la ubicación ya tenía stock del mismo artículo+lote).

2. **Venta (`PosVentaController`)** → tres pasos en orden:
   - `InventoryCostService::costear{FIFO|LIFO|Identificacion}` decide el **costo contable** que registra `pos_inventarios.c_unitario`.
   - Si el método contable es PMP, se usa `pos_bodega_articulos.c_unitario`.
   - Si el artículo tiene `requiere_gestion_wms=true`, el observer crea una `wms_tarea` tipo `pick_list` con sugerencias del `WmsSlottingService` (siguiendo `wms_regla_slottings.estrategia`). El operario confirma las ubicaciones físicas y completa la tarea — recién ahí se descuenta stock de `wms_ubicacion_stocks` y se crea `wms_movimientos`.

3. **Invariantes** que el sistema mantiene:
   - `pos_bodega_articulos.stock` es siempre actualizado primero por `PosInventario` (vía POS).
   - `wms_ubicacion_stocks.stock_unidades` se actualiza al completar la tarea WMS.
   - **Convergencia**: una vez completadas todas las tareas WMS asociadas a una venta/compra, debe cumplirse `SUM(wms_ubicacion_stocks.stock_unidades WHERE bodega=X AND articulo=Y) = pos_bodega_articulos.stock WHERE bodega=X AND articulo=Y`. Si hay tareas pendientes, puede haber diferencia transitoria.

---

## Recomendación de default por método (para autoconfiguración)

Cuando se crea una `wms_regla_slottings` nueva para una bodega, el `estrategia` debería heredar del `metodo_inventario_id` de la empresa de la bodega:

```php
// Pseudocódigo de heurística sugerida (no implementada todavía):
$metodoContable = par_empresas.metodo_inventario_id;
$tieneVencimiento = pos_articulos del catálogo de esa bodega tienen mayoría con fecha_vencimiento != null;

switch ($metodoContable) {
    case 1: // FIFO contable
        return $tieneVencimiento ? 'FEFO' : 'FIFO';
    case 2: // LIFO contable
        return 'FIFO'; // físico FIFO siempre, contable LIFO se reconcilia
    case 3: // PMP
        return $tieneVencimiento ? 'FEFO' : 'ABC';
    case 4: // Identificación específica
        return 'FEFO'; // por lote, prioridad al que vence antes
    default:
        return 'FIFO';
}
```

**Implementación pendiente** — el endpoint `POST /wmsReglaSlottings` hoy permite cualquier valor sin sugerir default. Una mejora futura sería autoconfigurar y permitir override manual.

---

## Cambios necesarios en el módulo para cubrir todos los métodos

Estado actual:

- ✅ `wms_ubicacion_stocks.lote` y `fecha_vencimiento` permiten identificación específica.
- ✅ `WmsSlottingService::sugerirPicking` soporta FIFO/FEFO/LIFO/ABC/MIXTA con split picking.
- ✅ Tarea WMS guarda el `lote` y `fecha_vencimiento` específicos del item seleccionado, lo que permite trazabilidad para ID específica.
- ❌ El endpoint de venta NO valida que el `lote_id` solicitado (cuando método=4) coincida con el `lote` físicamente disponible en `wms_ubicacion_stocks`. Hoy la venta acepta cualquier lote y el WMS sugiere por su cuenta — pueden divergir.
- ❌ No hay autoconfiguración de `estrategia` al crear una `wms_regla_slottings`. Es responsabilidad del usuario configurarla.
- ❌ No hay reporte de "costo contable vs costo físico tocado" para detectar diferencias entre los métodos. Si se quiere precisión total contable, agregar.

---

## Lista de invariantes a respetar

1. `wms_ubicacion_stocks.lote` debe ser igual al `lote` reportado en `pos_inventarios` cuando el método contable es ID específica.
2. La suma de `wms_ubicacion_stocks.stock_unidades` por (bodega, artículo) **converge** con `pos_bodega_articulos.stock` después de cerrar todas las tareas WMS asociadas.
3. `pos_inventarios.wms_movimiento_id` (cuando esté lleno) apunta al `wms_movimientos.id` que reflejó esa entrada/salida físicamente.
4. Una `wms_tarea_lineas.unidades_realizadas` nunca puede superar `unidades_solicitadas` (el controller `WmsTareaController::completarLinea` lo valida implícitamente al cerrar la línea cuando alcanza la cantidad).
5. `wms_movimientos` es **inmutable** una vez creado — sólo lectura via API. Se borra implícitamente cuando se borra una tarea (ON DELETE NULL en `wms_tarea_lineas.wms_movimiento_id`, pero el movimiento queda como audit log).

---

## Recursos del código

- `app/Services/InventoryCostService.php` — implementa costearFIFO, costearLIFO, costearIdentificacion.
- `app/Services/Wms/WmsSlottingService.php` — sugiere ubicaciones según `wms_regla_slottings.estrategia`.
- `app/Http/Controllers/Pos/PosVentaController.php` — flujo de venta, integra ambos.
- `app/Http/Controllers/Wms/WmsTareaController.php` — completarLinea descuenta stock + crea movimiento.
- `app/Observers/PosInventarioObserver.php` — disparador que crea tareas WMS desde POS.
