# Próximo ajuste — La nota de remisión no consume lotes (FIFO)

Estado: **pendiente** (propuesta). Fecha de registro: 2026-08-29.
Alcance: módulo Nota de Remisión + Lotes/FIFO + Facturación (skip de ítems remisionados).
Prioridad: **media** — no hay datos dañados hoy (ver "Impacto real"), pero rompe la
consistencia stock↔lotes apenas se combinen remisiones con productos que manejan lote.

## Contexto

Desde que el módulo `LOTES` quedó operativo (2026-08-29, ver
`docs/plan-motor-precios-rentabilidad.md`), las salidas de stock por **venta** consumen lotes por
FIFO y registran ese consumo en `factura_det_lote`, lo que permite:

- costear la venta con el costo **real** de las unidades entregadas (no el último costo de compra), y
- **devolver** esas unidades al lote correcto al anular la factura o emitir una NC.

La **nota de remisión** también saca mercadería del depósito, pero quedó fuera de ese circuito.

## El gap

`nota-remision.service.ts` descuenta `stock_deposito` y registra el movimiento de inventario
(líneas ~757-786), pero **no toca `lotes_producto` ni `stock_lote`**. Las menciones a "lote" en ese
archivo son todas del `nro_lote` de **envío en lote a SIFEN** — un concepto distinto, sin relación
con los lotes de producto.

Y hay un agravante en el lado de facturación. `facturas.service.ts` saltea el consumo FIFO para los
ítems que vienen de una remisión:

```ts
// Un ítem que viene de una remisión NO descuenta stock (ya lo hizo la
// remisión). El chequeo es POR ÍTEM: permite mezclar en una misma factura
// ítems de remisión (no descuentan) con productos normales (sí descuentan).
const esItemDeRemision = (it: any) => (it?.aplicaciones_remision?.length ?? 0) > 0;
...
if (!esItemDeRemision(item) && lotesEnabled && ... ) { /* deductFifo */ }
```

El comentario dice *"ya consumieron stock/lotes al remisionar"* — es **cierto para stock, falso para
lotes**. Resultado con lotes activos:

| Paso | `stock_deposito` | Lote |
|---|---|---|
| Se emite la remisión | −1 | **sin cambios** ❌ |
| Se factura esa remisión | sin cambios (correcto) | **sin cambios** (skip) ❌ |

El lote queda **inflado de forma permanente** respecto al stock del depósito. A partir de ahí:

- El stock por lotes deja de cuadrar con el stock del depósito.
- Como el lote sigue figurando disponible, una venta posterior lo consume aunque esa mercadería ya
  salió físicamente por la remisión → se costea contra un lote que no existe.
- Si el lote se agota "en los papeles" antes que en la realidad, las ventas caen al fallback de
  `productos.precio_costo` (ver `FifoService.deductFifo` → `cantidad_sin_lote`), perdiendo la
  precisión de costo que motivó todo el módulo.

## Impacto real (al 2026-08-29)

- **Comercial Kety: 0 remisiones y 0 notas de crédito emitidas.** No hay datos afectados.
- Los productos con `maneja_lote` son 2 (de prueba). El riesgo se materializa recién cuando se
  combinen remisiones + productos con lote.

Por eso se documenta como ajuste pendiente en vez de resolverse sobre la marcha: el arreglo correcto
implica una tabla nueva, y no había urgencia que justificara hacerlo sin diseño.

## Por qué no alcanza un parche

Se evaluaron dos atajos y ambos tienen defectos:

1. **Consumir el lote recién al facturar la remisión** (quitar el skip de FIFO, manteniendo el skip
   de stock). Suena barato, pero si la remisión **nunca se factura** —o se factura parcialmente— el
   lote queda inflado igual. Además invierte el orden real de los hechos: la mercadería salió al
   remisionar, no al facturar.
2. **Bloquear remisiones de productos que manejan lote.** Consistente, pero limita la operación por
   una carencia técnica, no por una regla de negocio.

El problema de fondo es que **no hay dónde registrar qué lote consumió una remisión**: hoy esa
trazabilidad existe solo atada a `factura_det` (`factura_det_lote`).

## Diseño propuesto

### 1. Modelo de datos — nueva tabla

Espeja `factura_det_lote`, incluida la columna que lo hace idempotente (ver más abajo):

```prisma
model remision_det_lote {
  id                  String   @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
  remision_det_id     String   @db.Uuid
  lote_id             String   @db.Uuid
  cantidad            Decimal  @default(0) @db.Decimal(18, 4)
  /// Cuánto de este consumo ya se devolvió (anulación de la remisión).
  cantidad_restaurada Decimal  @default(0) @db.Decimal(18, 4)
  costo_unitario      Decimal  @default(0) @db.Decimal(18, 4)
  costo_total         Decimal  @default(0) @db.Decimal(18, 4)
  created_at          DateTime? @default(now()) @db.Timestamp(6)

  remision_det   nota_remision_det @relation(fields: [remision_det_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
  lotes_producto lotes_producto    @relation(fields: [lote_id], references: [id], onDelete: NoAction, onUpdate: NoAction)

  @@index([remision_det_id])
  @@index([lote_id])
}
```

### 2. Emisión de la remisión

En `nota-remision.service.ts`, junto al descuento de `stock_deposito`:

- Resolver `lotesEnabled` con el mismo criterio que el resto (`LotesService.isEnabledForEmpresa`
  sobre la sucursal del depósito).
- Si el producto tiene `maneja_lote`, llamar a `FifoService.deductFifo` y persistir el resultado en
  `remision_det_lote`.
- Respetar el fallback ya definido: `deductFifo` **no bloquea** si falta stock por lotes, devuelve
  `cantidad_sin_lote` (stock previo a activar lotes). La remisión debe seguir el mismo criterio que
  la venta y no frenarse por eso.

### 3. Anulación de la remisión

Reverso simétrico: devolver al lote lo consumido, usando `cantidad_restaurada` para que una segunda
anulación/reverso no restaure dos veces (mismo problema que ya se corrigió en
`restoreFacturaDetLotes`, ver §"Antecedente" abajo).

Conviene extraer un helper genérico en `LotesService` en vez de duplicar la lógica:
`restoreLotesDesdeConsumos(tx, { consumos, depositoId, cantidad })`, y que tanto
`restoreFacturaDetLotes` como el nuevo `restoreRemisionDetLotes` lo usen.

### 4. Facturación de la remisión

El skip actual **queda como está** (el ítem no debe consumir lote de nuevo), pero hay que corregir
el comentario, que hoy afirma algo falso. Además, el costo de la línea facturada debería tomarse del
consumo registrado en `remision_det_lote` para que la rentabilidad refleje el costo real de las
unidades entregadas, en vez de caer a `productos.precio_costo`.

### 5. Otras salidas de stock con el mismo hueco

Mismo diagnóstico, mismo patrón de solución (evaluar caso por caso si amerita):

- `inventario-fisico.service.ts` — ajusta `stock_deposito` sin tocar lotes.
- `recepciones-compra.service.ts` — suma stock sin crear lote.

## Checklist de implementación

- [ ] Migración: tabla `remision_det_lote` + relaciones en `nota_remision_det` y `lotes_producto`.
- [ ] `LotesService`: extraer `restoreLotesDesdeConsumos` y reusarlo en factura y remisión.
- [ ] `nota-remision.service.ts`: consumir FIFO al emitir y persistir en `remision_det_lote`.
- [ ] `nota-remision.service.ts`: devolver lotes al anular la remisión.
- [ ] `facturas.service.ts`: corregir el comentario del skip y tomar el costo desde
      `remision_det_lote` cuando el ítem venga de una remisión.
- [ ] Tests: emitir remisión con lote → verificar consumo; anular → verificar devolución; anular dos
      veces → verificar que no restaura de más.
- [ ] Probar con datos reales: remisión de un producto con lote, facturarla, y confirmar que
      `stock_deposito` y el stock por lotes quedan cuadrados en cada paso.

## Antecedente relacionado (ya resuelto)

Durante la auditoría de lotes del 2026-08-29 se encontraron y corrigieron, en el circuito de venta:

- **FIFO bloqueaba la venta** cuando no había lotes suficientes → ahora devuelve `cantidad_sin_lote`
  y el remanente se costea con `precio_costo`.
- **FIFO se aplicaba a productos sin `maneja_lote`** → ahora respeta esa bandera.
- **El vencimiento se forzaba a hoy** (backend y frontend) → todo lote sin vencimiento nacía vencido.
- **La cancelación SIFEN (ECAN/EINU) restauraba stock pero no lotes** — camino distinto al de la
  anulación directa de facturas.
- **`restoreFacturaDetLotes` no era idempotente** → columna `factura_det_lote.cantidad_restaurada`.

La **nota de crédito** sí restaura lotes correctamente y no requiere cambios.

## Documentos relacionados

- `docs/plan-motor-precios-rentabilidad.md` — motor de precios, costeo y auditoría de lotes.
- `docs/guias/guia-motor-precios-rentabilidad.md` — guía funcional.
