# Conteo de Stock y Conciliación de Inventario en XPERT WMS

Relevado directamente del código de `XPERT_SAPB1` (`StockCountService`, `StockCountController`, `InventoryReconciliationController`, migración `028_stock_counts.sql`). Son dos funcionalidades distintas y complementarias, pensadas para dos preguntas diferentes:

- **Conteo de Stock (Nivel 1):** "¿lo que dice el sistema en este bin es lo que hay físicamente?" — ajusta stock real, con control y trazabilidad.
- **Conciliación de Inventario (Nivel 2):** "¿lo que dice el WMS coincide con lo que el cliente/empresa cree tener?" — solo reporta diferencias, no ajusta nada.

---

## 1. Conteo de Stock (ajuste de inventario ad-hoc)

### 1.1 Qué es y cuándo se usa

Es un conteo físico **puntual, disparado a demanda** — no es un inventario general programado, sino algo que se lanza sobre **una ubicación (bin) puntual**, por ejemplo ante sospecha de faltante, antes de un despacho grande, o para auditar una zona. Se dispara desde la pantalla de Stock o desde el detalle de un bin en el Mapa del Depósito ("Contar esta ubicación"), indicando un motivo obligatorio.

Si el bin tiene stock de **más de una empresa**, el sistema obliga a elegir cuál empresa contar — no se mezclan empresas en un mismo documento de ajuste.

### 1.2 El flujo completo (máquina de estados)

```
PENDING / COUNTING → (conteo) → NEEDS_RECOUNT → (reconteo) → PENDING_APPROVAL → APPROVED
                              ↘ (sin diferencia) → PENDING_APPROVAL ↗          ↘ REJECTED
```

**Paso 1 — Creación (`createCount`).** Al crear el conteo, el sistema saca una **foto (snapshot) del stock actual del bin** por ítem+lote+serie (`system_qty` en `stock_count_lines`) y genera una tarea `COUNT` en la cola de tareas del almacén. Si hay un operador `op_almacen` conectado en ese almacén, se le asigna automáticamente (queda en `COUNTING`); si no, queda `PENDING` hasta que alguien la tome.

**Paso 2 — Conteo ciego.** El operador cuenta desde el colector RF. Clave: **el operador nunca ve `system_qty`** — el endpoint que le sirve las líneas a contar (`GET /stock-counts/{id}/blind-lines`) expone solo qué contar, nunca cuánto "debería" haber. Esto evita que el operador ajuste su conteo para que "cierre" con el sistema.

**Paso 3 — Cálculo de variación y tolerancia.** Al enviar el conteo (`submit-count`), el servidor calcula la variación contra `system_qty` **recién en ese momento**, y la compara contra una **tolerancia configurable por empresa** (`company_settings.count_tolerance_pct`, default 5%):
- Si la variación % está **dentro** de la tolerancia → la línea pasa a `COUNTED` y el conteo completo va directo a `PENDING_APPROVAL`.
- Si la variación % **supera** la tolerancia → la línea pasa a `NEEDS_RECOUNT` y el conteo completo también.

**Paso 4 — Reconteo (si aplica).** Un supervisor asigna el reconteo (`assign-recount`) — el sistema intenta asignarlo a **un operador distinto** de quien contó la primera vez, si hay otro disponible. El reconteo se carga igual que el conteo (`submit-recount`, llena `recounted_qty`), pero **siempre termina en `PENDING_APPROVAL`**, aunque el segundo conteo confirme el primero — un reconteo nunca ajusta stock por sí solo, siempre necesita aprobación humana.

**Paso 5 — Aprobación (segregación de funciones).** Acá recién se toca el stock real. Reglas duras:
- **Quien aprueba no puede ser quien contó ni quien recontó** ese mismo documento (control cruzado obligatorio, no solo un permiso).
- Además hace falta el permiso `stock_count.approve` (rol típico: supervisor/calidad) — o `stock_count.reject` para rechazar.
- Toda línea con diferencia **exige un motivo** (`reason_code`): `DAMAGED` (dañada), `EXPIRED` (vencida), `SHRINKAGE` (faltante no justificado), `COUNT_ERROR` (error de conteo anterior), `OVERAGE` (sobrante) u `OTHER`. Sin motivo, la aprobación se rechaza a nivel de servidor (no es solo validación de UI).
- Las líneas **sin diferencia** no piden motivo, pasan directo a `ADJUSTED`.

**Al aprobar:**
1. Por cada línea con diferencia: actualiza (o borra, si quedó en 0) la posición de stock real, inserta un movimiento `ADJUSTMENT` en el kardex, y deja registro en auditoría (`AuditService`) con cantidad anterior, nueva, motivo y usuario.
2. Todo ocurre **en una transacción** — si algo falla, no queda stock a medio ajustar.
3. **Push-back a SAP (best-effort, fuera de la transacción local):** agrupa los ajustes netos por ítem y llama a SAP Business One vía Service Layer — `InventoryGenEntries` para lo que sobró, `InventoryGenExits` para lo que faltó (SAP no permite mezclar entradas y salidas en un mismo documento). Si SAP falla, **no revierte el ajuste local** — el stock del WMS ya quedó corregido, solo se registra el error en `sap_sync_log` para reintentar o resolver manualmente.

**Rechazo (`reject`):** un supervisor puede rechazar el conteo completo (con motivo), sin tocar stock. Cancela las tareas pendientes asociadas.

### 1.3 Puntos de control (por qué está diseñado así)

| Control | Cómo se aplica |
|---|---|
| El operador no puede "hacer trampa" | Conteo ciego — nunca ve la cantidad esperada |
| Diferencias grandes no pasan sin revisión | Tolerancia % configurable por empresa → dispara reconteo automático |
| Nadie ajusta su propio conteo | El aprobador no puede ser quien contó ni quien recontó (chequeo server-side, no solo de UI) |
| Todo ajuste queda justificado | `reason_code` obligatorio por línea con diferencia |
| Todo ajuste es trazable | Movimiento `ADJUSTMENT` en el kardex + entrada en `audit_log` con antes/después |
| El WMS no se traba si SAP falla | Push-back a SAP es best-effort y asíncrono respecto del ajuste local |

### 1.4 Dónde se ve en el sistema

- Menú **"🧮 Conteos de Inventario"**: listado de todos los documentos de conteo (`AJ-########`), filtrable por estado, con quién lo contó/recontó/aprobó y fecha.
- Detalle de cada conteo: tabla por línea con SKU, lote, `system_qty`, `counted_qty`, `recounted_qty`, variación y motivo — acá el supervisor aprueba/rechaza.
- App RF (`rf.html`): cola de tareas `COUNT` asignadas al operador, con las líneas ciegas para contar/recontar.
- Reporte exportable por conteo (`GET /stock-counts/{id}/report`) una vez aprobado o rechazado, para archivo/auditoría.

---

## 2. Conciliación de Inventario (Nivel 2)

### 2.1 Qué es y en qué se diferencia del Conteo

Es una **comparación de totales**, no un conteo físico: contrasta el stock que el WMS tiene registrado (ya con cualquier ajuste de conteos previos aplicado) contra un **archivo externo** que el cliente/empresa carga — típicamente un export de su propio SAP o de otro sistema — para responder "¿lo que cree tener la empresa coincide con lo que dice el WMS?".

**No persiste nada y no ajusta stock.** Es una foto puntual que se puede correr las veces que haga falta (cierre de mes, auditoría, chequeo puntual), sin depender de una consulta en vivo al Service Layer de SAP.

### 2.2 Cómo funciona

1. El usuario sube un **CSV** (`sku;expected_qty`) desde la pantalla "⚖️ Conciliación de Inventario".
2. El sistema trae los **totales actuales por SKU** de todo el stock del WMS para esa empresa (`StockRepository::getTotalsByCompany`).
3. Cruza ambos conjuntos por SKU y devuelve, por cada uno:
   - `system_qty` (lo que tiene el WMS) vs `expected_qty` (lo que vino en el archivo)
   - `variance` = system_qty − expected_qty
   - Un flag si el SKU **no está en el sistema** (`found_in_system: false`)
   - Un flag si el SKU **sí está en el sistema pero no vino en el archivo subido** (`missing_from_upload: true`) — para detectar qué le falta reportar a la empresa, no solo qué le sobra/falta en cantidad.
4. El resultado se muestra en una tabla con badge visual: **OK** (coincide exacto), **"No vino en el archivo"**, o **"Sin stock en el sistema"**.

### 2.3 Para qué sirve en la práctica

- Auditorías periódicas (cierre de mes) sin necesidad de contar físicamente cada bin.
- Detectar de un vistazo SKUs fantasma (el sistema tiene stock que el cliente no reconoce) o SKUs huérfanos (el cliente espera stock que el WMS no tiene).
- Punto de partida para decidir **dónde** vale la pena lanzar un Conteo de Stock (Nivel 1) real, en vez de contar todo el depósito a ciegas.

### 2.4 Diferencias clave frente al Conteo de Stock

| | Conteo de Stock (Nivel 1) | Conciliación de Inventario (Nivel 2) |
|---|---|---|
| Unidad de análisis | Una ubicación (bin) puntual | Todo el inventario de la empresa, por SKU |
| Origen del dato de comparación | Snapshot del propio WMS al crear el conteo | Archivo externo cargado por el usuario |
| ¿Es físico? | Sí — alguien cuenta con el colector RF | No — es un cruce de números |
| ¿Ajusta stock? | Sí, tras aprobación | No, nunca — solo reporta |
| ¿Requiere aprobación/segregación de funciones? | Sí, obligatoria | No aplica (no hay ajuste que aprobar) |
| ¿Impacta SAP? | Sí (push-back de ajuste neto) | No |
| ¿Queda persistido como documento? | Sí (`stock_counts` + `stock_count_lines`, con historial) | No — se recalcula cada vez que se sube un archivo |

En conjunto, forman un embudo natural: la **Conciliación (Nivel 2)** es rápida y de alto nivel para detectar *dónde* mirar, y el **Conteo de Stock (Nivel 1)** es el mecanismo controlado y auditable para *corregir* stock cuando hace falta.
