# PLAN FUNCIONAL Y TÉCNICO — Proveedor ↔ Marca, Vencimientos y Canjes

**Fecha:** 2026-06-12
**Stack:** NestJS + PostgreSQL + Prisma ORM + React + MUI
**Multiempresa:** `empresa_id` en todas las tablas
**Alcance:** habilitar reportes de vencimientos orientados a proveedor, formalizar canjes/devoluciones, acuerdos comerciales por marca, FEFO reforzado en POS

---

## 0. CONTEXTO Y DIAGNÓSTICO

### 0.1 Lo que YA existe (no tocar, reutilizar)

**Modelos Prisma:**
- `marcas` — entidad completa (`id`, `empresa_id`, `codigo`, `descripcion`, `active`).
- `proveedores` — entidad completa con vínculo a `personas`, multi-país.
- `productos` — ya tiene `marca_id` (FK) y `maneja_lote` (boolean).
- `lotes_producto` — completa: `numero_lote`, `fecha_fabricacion`, `fecha_vencimiento`, `cantidad_inicial`, `cantidad_disponible`, `costo_unitario`, vinculo a `compra_cab` + `compra_det`. Índice dedicado en `fecha_vencimiento`.
- `stock_lote` — stock por lote y depósito (UK `lote_deposito`).
- `compra_det` — ya captura `lote` (string) y `fecha_vencimiento` por línea.
- `factura_det_lote` — trazabilidad de lote consumido en cada venta.
- `movimientos_inventario` — kardex (pero **sin** `lote_id` directo — gap).
- `recepcion_compra_*` — recepción separada de compra (pero **sin** lote/vencimiento — gap).
- `nota_credito_cab/det` — existe (presumiblemente para clientes; canjes a proveedor no formalizados).

**Módulos NestJS:**
- `marcas/`, `proveedores/`, `productos/`, `compras/`, `recepciones-compra/`, `lotes/` (con `fifo.service.ts`), `stock/`, `ordenes-compra/`, `compra-asistida/`, `pagos-proveedor/`.

**Endpoints clave existentes:**
- `GET /lotes?producto=&deposito=&estado=vigente|vencido|agotado&vence_desde=&vence_hasta=`
- `GET /lotes/dashboard` — KPIs vencidos / próximos a vencer / stock por lote
- `GET /stock/reporte/niveles`
- `POST /compras` — crea cabecera + detalle (con lote y vencimiento)

**Frontend (pantallas existentes):**
- `ReporteLotesProductos.jsx` — filtros por producto, depósito, vencimiento, stock, costo.
- `ReporteMovimientosStock.jsx` — kardex.
- `ReporteNivelesStock.jsx` — mín/máx.
- `inventario/LotesTab.jsx`, `inventario/MarcasTab.jsx`.
- `ProveedoresDesign/` — `ProveedorFormDialog`, `ProveedorSelector`, `ProveedoresListConfig`.
- `NuevaCompraAsistidaPage.jsx` — captura lote/vencimiento.

### 0.2 Gaps que este plan cierra

| # | Gap | Impacto |
|---|---|---|
| G1 | No existe relación `proveedor ↔ marca` (N:M) | Imposible reportar "vencidos de las marcas que provee X" |
| G2 | `movimientos_inventario` sin `lote_id` | Kardex no permite seguir un lote movimiento por movimiento |
| G3 | `recepcion_compra_det` sin lote/vencimiento | Si se separa recepción de factura, se pierde el dato al recibir |
| G4 | Sin tabla/flujo formal de **canjes y devoluciones a proveedor** | Hoy se hace por NC genérica sin trazabilidad de lote |
| G5 | Sin acuerdos comerciales por marca (% canje permitido, vigencia, bonificaciones) | Imposible saber si un canje cumple el acuerdo |
| G6 | Sin reportes orientados al **proveedor** (vencidos/próximos por sus marcas) | Caso de uso central del usuario |
| G7 | FEFO existe (`fifo.service.ts`) pero POS no lo aplica/sugiere visiblemente | Vendedores eligen lote a mano |
| G8 | Sin métricas de performance por marca (rotación, margen, % devoluciones) | Decisiones de surtido a ciegas |

### 0.3 Filosofía del plan

- **Extender, no reescribir.** Las tablas de lotes/stock son sólidas — sumamos campos y tablas relacionales nuevas.
- **Compatible con empresas que NO usan lotes.** Toda funcionalidad nueva respeta `productos.maneja_lote = false` y degrada con elegancia.
- **Submódulos bajo el módulo de permisos existente `INVENTARIO`** (que ya contiene LOTES, STOCK, KARDEX) + nuevo módulo `PROVEEDORES_ACUERDOS` para la parte comercial.

---

## 1. SUBMÓDULOS NUEVOS (resumen)

| # | Submódulo | Código permiso | Propósito |
|---|---|---|---|
| 1 | Catálogo proveedor-marca | `INV_PRM_*` | Relación N:M con distribuidor oficial, vigencia |
| 2 | Acuerdos comerciales por marca | `PRV_ACR_*` | % canje, bonificaciones, surtido mínimo |
| 3 | Canjes y devoluciones a proveedor | `INV_CDV_*` | Flujo formal con trazabilidad de lote |
| 4 | Reportes orientados a proveedor | `INV_RPR_*` | Vencidos/próximos por proveedor, exportables |
| 5 | Extensión movimientos con lote | (extensión) | Kardex completo de lote |
| 6 | Recepción de compra con lote | (extensión) | Capturar lote al recibir, no solo al facturar |
| 7 | FEFO en POS | `INV_FEFO_*` | Sugerencia automática + bloqueo opcional |
| 8 | Performance por marca | `INV_RPM_*` | Rotación, margen, % devolución |

---

## 2. SUBMÓDULO 1 — Catálogo proveedor-marca (`INV_PRM_*`)

### 2.1 Modelo de datos

```prisma
model proveedor_marca {
  id                       String   @id @default(uuid())
  empresa_id               String
  proveedor_id             String
  marca_id                 String
  es_distribuidor_oficial  Boolean  @default(false)
  es_proveedor_preferente  Boolean  @default(false)  // uno solo por marca (parcial-unique)
  vigente_desde            DateTime
  vigente_hasta            DateTime?
  observacion              String?
  activo                   Boolean  @default(true)
  usuario_id               String
  created_at               DateTime @default(now())
  updated_at               DateTime @updatedAt

  proveedor                proveedores @relation(...)
  marca                    marcas      @relation(...)
  acuerdos                 proveedor_marca_acuerdo[]  // submódulo 2

  @@unique([empresa_id, proveedor_id, marca_id, vigente_desde])
  @@index([empresa_id, proveedor_id])
  @@index([empresa_id, marca_id])
}
```

**Unicidad de `es_proveedor_preferente`**: agregar índice parcial vía migración SQL (Prisma no soporta partial unique en schema):

```sql
CREATE UNIQUE INDEX uk_proveedor_preferente_por_marca
  ON proveedor_marca(empresa_id, marca_id)
  WHERE es_proveedor_preferente = true AND activo = true;
```

### 2.2 Reglas

- Un proveedor puede tener N marcas; una marca puede tener N proveedores.
- Solo **un** proveedor preferente por marca (índice parcial garantiza). Al setear uno nuevo, el anterior se desactiva.
- `es_distribuidor_oficial` es informativo (puede haber varios).
- La relación no es obligatoria: marcas sin proveedor declarado siguen funcionando (no romper compras existentes).
- **Inferencia histórica**: para backfill inicial, ejecutar query que arme `proveedor_marca` a partir del histórico de `compra_cab + compra_det → producto.marca_id`. Cada par (proveedor, marca) que aparezca al menos una vez en compras se siembra como activo (sin vigencia, marcado como `inferido = true` opcional para revisión manual).

### 2.3 Permisos

```
INV_PRM_PROVEEDOR_MARCA_VER
INV_PRM_PROVEEDOR_MARCA_VINCULAR
INV_PRM_PROVEEDOR_MARCA_DESVINCULAR
INV_PRM_PROVEEDOR_MARCA_EXPORTAR
```

### 2.4 Endpoints

```
GET    /proveedor-marca?proveedorId=&marcaId=&activo=
POST   /proveedor-marca
PATCH  /proveedor-marca/:id              (editar vigencia, flags)
DELETE /proveedor-marca/:id              (soft delete con motivo)
GET    /proveedor-marca/proveedor/:id    (todas las marcas que provee)
GET    /proveedor-marca/marca/:id        (todos los proveedores de la marca)
POST   /proveedor-marca/backfill         (admin: inferir desde histórico)
```

### 2.5 Pantallas frontend

**Extensión: `ProveedorFormDialog.jsx`**
- Nueva pestaña "Marcas que provee".
- Lista con: marca, preferente (toggle), distribuidor oficial (toggle), vigencia, acciones.
- Botón "Vincular marca" → autocomplete de marcas activas.
- Si el toggle "preferente" se activa y ya hay otro preferente para esa marca → confirmar reemplazo.

**Extensión: `inventario/MarcasTab.jsx`**
- Columna "Proveedor preferente" + columna "# proveedores que la proveen".
- Drawer/modal "Ver proveedores" desde fila.

**Nueva: pantalla `ProveedorMarcasCatalogo.jsx` (opcional)**
- Matriz proveedor × marca (vista admin) con filtros y export.

### 2.6 Integraciones

- **Productos**: al crear producto, sugerir proveedor preferente según marca (auto-fill en `proveedor_preferente_id` nuevo campo a sumar en `productos`).
- **Compra asistida**: al elegir proveedor, filtrar productos por marcas que provee (acelera carga).
- **Órdenes de compra automáticas** (futuro): generar OC por marca al proveedor preferente.

---

## 3. SUBMÓDULO 2 — Acuerdos comerciales por marca (`PRV_ACR_*`)

### 3.1 Modelo de datos

```prisma
model proveedor_marca_acuerdo {
  id                          String   @id @default(uuid())
  empresa_id                  String
  proveedor_marca_id          String   // FK a relación proveedor-marca
  tipo                        AcuerdoTipo
  vigente_desde               DateTime
  vigente_hasta               DateTime?

  // Canje de vencidos
  canje_acepta                Boolean  @default(false)
  canje_pct_max_anual         Decimal? @db.Decimal(5,2)  // ej. 3.00 = 3% del comprado/año
  canje_dias_antes_vto        Int?     // se acepta canje N días antes del vencimiento

  // Bonificaciones / rebates
  rebate_pct                  Decimal? @db.Decimal(5,2)  // % sobre compra
  rebate_periodicidad         RebatePeriodicidad?       // MENSUAL | TRIMESTRAL | ANUAL
  rebate_meta_compra_min      Decimal? @db.Decimal(19,4)

  // Volumen / surtido
  compra_minima_periodo       Decimal? @db.Decimal(19,4)
  surtido_minimo_skus         Int?     // mínimo de SKUs de la marca a mantener

  // Logística
  plazo_entrega_dias          Int?
  condicion_pago_default      String?
  descuento_pronto_pago_pct   Decimal? @db.Decimal(5,2)

  documento_url               String?   // contrato firmado
  observacion                 String?
  activo                      Boolean  @default(true)
  usuario_id                  String
  created_at                  DateTime @default(now())

  proveedor_marca             proveedor_marca @relation(...)

  @@index([empresa_id, proveedor_marca_id])
}

enum AcuerdoTipo {
  GENERAL
  CANJE
  REBATE
  SURTIDO
}

enum RebatePeriodicidad {
  MENSUAL
  TRIMESTRAL
  SEMESTRAL
  ANUAL
}
```

### 3.2 Reglas

- Un par proveedor-marca puede tener múltiples acuerdos (uno por tipo o varios consecutivos por vigencia).
- **Validación al canjear**: si hay acuerdo con `canje_pct_max_anual`, calcular % canjeado en el período y bloquear si excede.
- **Acumulado de rebate**: tabla auxiliar `proveedor_marca_rebate_acumulado` (período, monto_compra, monto_rebate_devengado) liquidable.
- **Alerta de surtido**: cron diario que valida que cada marca con acuerdo `SURTIDO` cumple el mínimo de SKUs con stock.
- **Alerta de compra mínima**: si llegando al fin del período no se alcanza, notificar al comprador.

### 3.3 Permisos

```
PRV_ACR_ACUERDO_VER
PRV_ACR_ACUERDO_CREAR
PRV_ACR_ACUERDO_EDITAR
PRV_ACR_ACUERDO_ANULAR
PRV_ACR_LIQUIDAR_REBATE
PRV_ACR_REPORTE
```

### 3.4 Endpoints

```
GET    /acuerdos-proveedor-marca?proveedorMarcaId=&tipo=&vigente=true
POST   /acuerdos-proveedor-marca
PATCH  /acuerdos-proveedor-marca/:id
PATCH  /acuerdos-proveedor-marca/:id/anular
GET    /acuerdos-proveedor-marca/rebate-acumulado?proveedorId=&desde=&hasta=
POST   /acuerdos-proveedor-marca/rebate/liquidar    (genera NC de proveedor)
GET    /acuerdos-proveedor-marca/cumplimiento-surtido/:proveedorId
GET    /acuerdos-proveedor-marca/cumplimiento-compra-minima/:proveedorId
```

### 3.5 Pantallas

**Extensión: `ProveedorFormDialog.jsx`**
- Dentro de la pestaña "Marcas que provee", expandir cada fila para ver/editar acuerdos.
- Submodal `AcuerdoMarcaModal.jsx` para CRUD de acuerdo (con tabs por tipo).

**Nueva: `proveedores/AcuerdosDashboard.jsx`**
- KPI: rebate devengado por período, cumplimiento de compra mínima, cumplimiento de surtido.
- Filtros por proveedor, marca, tipo.

### 3.6 Integraciones

- **Canje** (submódulo 3) consulta `canje_pct_max_anual` antes de aprobar.
- **Reportes de compra** filtrados por marca/proveedor.
- **Pagos a proveedor**: liquidación de rebate puede generar nota de crédito de proveedor automática (vincula con `pagos-proveedor`).

---

## 4. SUBMÓDULO 3 — Canjes y devoluciones a proveedor (`INV_CDV_*`)

### 4.1 Modelo de datos

```prisma
model canje_devolucion_proveedor_cab {
  id                       String   @id @default(uuid())
  empresa_id               String
  proveedor_id             String
  deposito_origen_id       String   // de qué depósito sale
  tipo                     CanjeDevTipo  // CANJE | DEVOLUCION
  motivo                   CanjeDevMotivo
  numero                   String   // correlativo interno
  fecha                    DateTime
  estado                   CanjeDevEstado
  acuerdo_id               String?  // si está bajo un acuerdo formal
  observacion              String?
  documento_remision_url   String?  // remisión interna firmada
  usuario_id               String
  autorizado_por_id        String?
  total_cantidad           Decimal  @db.Decimal(19,4)
  total_costo              Decimal  @db.Decimal(19,4)
  // Resolución
  nota_credito_proveedor_id String? // si proveedor emite NC a nuestro favor
  reposicion_compra_id      String? // si proveedor repone con nueva compra
  fecha_resolucion          DateTime?
  created_at                DateTime @default(now())

  detalle                   canje_devolucion_proveedor_det[]

  @@index([empresa_id, proveedor_id, fecha])
  @@index([empresa_id, estado])
}

model canje_devolucion_proveedor_det {
  id                  String   @id @default(uuid())
  canje_dev_id        String
  producto_id         String
  lote_id             String?  // si maneja_lote
  cantidad            Decimal  @db.Decimal(19,4)
  costo_unitario      Decimal  @db.Decimal(19,4)
  costo_total         Decimal  @db.Decimal(19,4)
  motivo_linea        String?  // override del motivo cabecera para esta línea

  @@index([canje_dev_id])
}

enum CanjeDevTipo {
  CANJE        // proveedor reemplaza con producto nuevo
  DEVOLUCION   // proveedor reintegra dinero (NC)
}

enum CanjeDevMotivo {
  VENCIDO
  PROXIMO_VENCER
  DEFECTUOSO
  ERROR_PEDIDO
  GARANTIA
  PRODUCTO_DISCONTINUADO
  OTRO
}

enum CanjeDevEstado {
  BORRADOR             // armando
  PENDIENTE_RETIRO     // listo, esperando que proveedor retire
  RETIRADO             // proveedor lo retiró
  RESUELTO_CON_NC      // proveedor emitió NC
  RESUELTO_CON_REPOSICION
  ANULADO
}
```

### 4.2 Reglas

- Al pasar a `RETIRADO`, **descontar stock** del lote (movimiento_inventario tipo `SALIDA_CANJE` o `SALIDA_DEVOLUCION` con `lote_id`).
- Al pasar a `RESUELTO_CON_NC`, vincular NC del proveedor (existente en `pagos-proveedor`).
- Al pasar a `RESUELTO_CON_REPOSICION`, vincular nueva compra (existente flujo de compras).
- Validación contra acuerdo:
  - Si proveedor-marca tiene `canje_acepta = false`, bloquear creación.
  - Si excede `canje_pct_max_anual`, advertir (no bloquear) o bloquear según política.
  - Si motivo es `PROXIMO_VENCER` y no está dentro de `canje_dias_antes_vto`, advertir.
- **Multi-proveedor en un mismo lote**: si un producto fue comprado a varios proveedores, sugerir canje al proveedor del lote (via `lotes_producto.compra_id → compra_cab.proveedor_id`).
- Imprimir **comprobante de canje/devolución** (PDF) para retiro físico.

### 4.3 Permisos

```
INV_CDV_CANJE_VER
INV_CDV_CANJE_REGISTRAR
INV_CDV_CANJE_AUTORIZAR
INV_CDV_CANJE_RETIRAR        // marcar como retirado
INV_CDV_CANJE_RESOLVER       // vincular NC o reposición
INV_CDV_CANJE_ANULAR
INV_CDV_REPORTE
```

### 4.4 Endpoints

```
GET    /canjes-devoluciones?proveedorId=&estado=&desde=&hasta=
POST   /canjes-devoluciones                       (crear borrador)
PATCH  /canjes-devoluciones/:id                   (editar borrador)
PATCH  /canjes-devoluciones/:id/confirmar         (pasa a PENDIENTE_RETIRO)
PATCH  /canjes-devoluciones/:id/retirar           (pasa a RETIRADO, descuenta stock)
PATCH  /canjes-devoluciones/:id/resolver          (NC o reposición)
PATCH  /canjes-devoluciones/:id/anular
GET    /canjes-devoluciones/:id/pdf               (comprobante)
GET    /canjes-devoluciones/sugerencias/:proveedorId  (productos vencidos/próximos del proveedor)
```

### 4.5 Pantallas

**Nueva: `inventario/CanjesDevolucionesTab.jsx`**
- Listado con filtros (proveedor, estado, motivo, fechas).
- Acciones por fila: confirmar, retirar, resolver, anular, imprimir.

**Nueva: `inventario/CanjeWizard.jsx`**
- Step 1: seleccionar proveedor → muestra acuerdo vigente si lo hay.
- Step 2: seleccionar productos/lotes — viene **pre-llenado** con sugerencia "vencidos del proveedor" + opción de agregar manual.
- Step 3: tipo (canje/devolución), motivo, observación, documento.
- Step 4: revisar totales + autorización si requerida → confirmar.

**Extensión: `ReporteLotesProductos.jsx`**
- Acción "Generar canje" sobre filas vencidas: pre-llena el wizard.

### 4.6 PDFs

- **Comprobante de canje/devolución** (nuevo en `msv-kude`): logo, datos proveedor, detalle producto+lote+cantidad+costo, motivo, firmas (entrega/recibe). Numerado por empresa.

### 4.7 Integraciones

- **Movimientos de inventario**: al retirar, crear movimiento con tipo `SALIDA_CANJE` o `SALIDA_DEVOLUCION` referenciando `canje_devolucion_proveedor_id` en `documento_id`.
- **Acuerdos**: lee `canje_pct_max_anual` para validar.
- **Pagos a proveedor**: link bidireccional con NC de proveedor cuando se resuelve.

---

## 5. SUBMÓDULO 4 — Reportes orientados a proveedor (`INV_RPR_*`)

### 5.1 Reportes nuevos

**5.1.1 Vencidos por proveedor (mensual)**

Query: para un proveedor X, listar todos los lotes vencidos en un rango cuya marca esté en `proveedor_marca` activa con ese proveedor.

```sql
SELECT lp.id, lp.numero_lote, lp.fecha_vencimiento,
       p.id AS producto_id, p.descripcion, m.descripcion AS marca,
       SUM(sl.cantidad_disponible) AS stock_actual,
       lp.costo_unitario,
       SUM(sl.cantidad_disponible) * lp.costo_unitario AS costo_total
FROM lotes_producto lp
JOIN productos p ON p.id = lp.producto_id
JOIN marcas m   ON m.id = p.marca_id
JOIN proveedor_marca pm ON pm.marca_id = m.id AND pm.activo = true
JOIN stock_lote sl ON sl.lote_id = lp.id
WHERE lp.empresa_id = $1
  AND pm.proveedor_id = $2
  AND lp.fecha_vencimiento BETWEEN $3 AND $4
  AND sl.cantidad_disponible > 0
GROUP BY lp.id, p.id, m.descripcion
ORDER BY lp.fecha_vencimiento;
```

**5.1.2 Próximos a vencer por proveedor (30/60/90 días)**

Idem anterior con `fecha_vencimiento BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '90 days'`, agrupado por bucket (0-30, 31-60, 61-90).

**5.1.3 Resumen por proveedor (dashboard ejecutivo)**

KPI agregados: # marcas que provee, valor stock total de sus marcas, # lotes vencidos no canjeados, % canjeado YTD vs límite del acuerdo.

**5.1.4 Stock total de marcas del proveedor**

Para auditoría — qué tengo de las marcas que él provee, por depósito.

### 5.2 Permisos

```
INV_RPR_REPORTE_VENCIDOS_PROVEEDOR_VER
INV_RPR_REPORTE_PROXIMOS_VENCER_VER
INV_RPR_REPORTE_STOCK_POR_PROVEEDOR_VER
INV_RPR_REPORTE_EXPORTAR
```

### 5.3 Endpoints

```
GET    /reportes/proveedor/:id/vencidos?desde=&hasta=&deposito=
GET    /reportes/proveedor/:id/proximos-vencer?dias=30|60|90&deposito=
GET    /reportes/proveedor/:id/stock-marcas?deposito=
GET    /reportes/proveedor/:id/resumen
POST   /reportes/proveedor/:id/exportar?formato=pdf|excel
POST   /reportes/proveedor/:id/enviar-email  (genera PDF y manda al contacto del proveedor)
```

### 5.4 Pantallas

**Nueva: `proveedores/ReporteVencidosProveedorPage.jsx`**
- Selector proveedor + rango + depósito.
- Tabla con producto, marca, lote, vencimiento, cantidad, costo.
- Acción "Generar canje" pre-llena wizard.
- Export PDF/Excel + botón "Enviar al proveedor por email".

**Nueva: `proveedores/ProximosVencerProveedorPage.jsx`**
- Buckets 30/60/90 con totales por bucket.
- Misma estructura que el anterior.

**Extensión: `ReporteLotesProductos.jsx`**
- Filtro adicional "Por proveedor (de la marca)" — usa la nueva relación.

**Nueva: `proveedores/ProveedorDashboard.jsx`**
- KPI resumen con links a los reportes anteriores.

### 5.5 PDF

- **Informe de vencidos por proveedor** (`msv-kude`): layout cabecera con datos de empresa y proveedor, detalle, totales, opcional firma. Diseño limpio para email.

### 5.6 Automatización

- **Job mensual configurable** (cron): el día N de cada mes, generar y enviar por email a cada proveedor con relación activa el informe de vencidos del mes pasado.
- Configuración en `proveedor_marca` o nueva tabla `proveedor_reporte_config` (frecuencia, día, emails destino).

---

## 6. SUBMÓDULO 5 — Extensión movimientos con lote (extensión, sin permisos nuevos)

### 6.1 Cambio en `movimientos_inventario`

```prisma
model movimientos_inventario {
  // ... campos existentes
  lote_id   String?  // NUEVO — nullable para retrocompat
  // ...
}
```

### 6.2 Reglas

- Todo movimiento que afecta un producto con `maneja_lote = true` **debe** llevar `lote_id` (validación en service).
- Backfill histórico: imposible recuperar lote para movimientos viejos. Marcar `lote_id = null` y agregar columna `legacy = true` opcional.
- Endpoints existentes de `/stock/ajustar` y `/stock/transferir` se extienden con `lote_id` obligatorio cuando aplica.
- Kardex (`ReporteMovimientosStock.jsx`) gana nueva columna "Lote".

### 6.3 Integraciones

- **Canjes/devoluciones** (submódulo 3): mueve stock con lote.
- **FEFO en POS** (submódulo 7): al vender, el lote seleccionado va al movimiento.
- **Trazabilidad de lote**: nueva pantalla "Timeline del lote" que muestra todos los movimientos del lote desde ingreso hasta agotamiento o vencimiento.

### 6.4 Pantallas

**Extensión: `ReporteMovimientosStock.jsx`**
- Columna "Lote" + filtro por lote.

**Nueva: `inventario/LoteTimelineDrawer.jsx`**
- Desde `LotesTab` o `ReporteLotesProductos`, click en lote abre drawer con timeline: compra origen → recepción → movimientos → ventas → canje/devolución (si aplica).

---

## 7. SUBMÓDULO 6 — Recepción de compra con lote (extensión)

### 7.1 Cambio en `recepcion_compra_det`

```prisma
model recepcion_compra_det {
  // ... existentes
  numero_lote        String?
  fecha_vencimiento  DateTime?
  // ...
}
```

### 7.2 Reglas

- En recepción separada de factura (común en empresas grandes), la **recepción** crea los `lotes_producto` (no la factura).
- Si después la factura llega con datos diferentes de lote, abrir flujo de reconciliación (manual con permiso).
- Si recepción y factura son simultáneas (flujo actual mayoritario), no cambia nada.

### 7.3 Pantallas

**Extensión: pantalla de recepción de compra (existente)**
- Capturar lote y vencimiento por línea cuando el producto `maneja_lote`.

---

## 8. SUBMÓDULO 7 — FEFO en POS (`INV_FEFO_*`)

### 8.1 Estado actual

`fifo.service.ts` existe en backend pero no está claro si el POS lo invoca al vender, ni si el vendedor lo ve.

### 8.2 Reglas

- Al agregar producto con `maneja_lote = true` al carrito de venta:
  - Backend retorna sugerencia FEFO (lote con vencimiento más cercano).
  - POS muestra chip "Lote sugerido: X — vence YYYY-MM-DD".
  - Vendedor puede **aceptar** (default) o **cambiar** (con permiso `INV_FEFO_FEFO_OVERRIDE`).
  - Si lote sugerido está **vencido** → bloquear venta (con permiso `INV_FEFO_VENDER_VENCIDO` permite override con confirmación).
  - Si lote sugerido vence en menos de N días (configurable por empresa, ej. 30) → warning visible.
- Al cerrar la venta, `factura_det_lote` se llena con los lotes consumidos (ya existe).

### 8.3 Permisos

```
INV_FEFO_FEFO_SUGERIR        (default ON para vendedores)
INV_FEFO_FEFO_OVERRIDE       (cambiar manual el lote)
INV_FEFO_VENDER_VENCIDO      (override de bloqueo)
INV_FEFO_VENDER_PROXIMO_VENCER (override de warning, opcional)
```

### 8.4 Endpoints

```
GET    /pos/sugerir-lote?productoId=&depositoId=&cantidad=
                       → { sugerido: { lote_id, numero, fecha_vencimiento }, opciones: [...] }
```

### 8.5 Pantallas

**Extensión: POS (línea de venta)**
- Chip "Lote: XXX (vence dd/mm/yyyy)" en cada línea de producto con lote.
- Click en chip → modal "Cambiar lote" con tabla de lotes disponibles ordenados por vencimiento.
- Banner rojo si producto vencido en carrito; banner amarillo si próximo a vencer.

**Extensión: `inventario/Configuracion`**
- Setting "Umbral 'próximo a vencer'" en días (default 30).
- Setting "Bloquear venta de vencidos" (boolean, default true).

---

## 9. SUBMÓDULO 8 — Performance por marca (`INV_RPM_*`)

### 9.1 Reportes

**9.1.1 Rotación de marca**: ventas / stock promedio por período. KPI gerencial.

**9.1.2 Margen por marca**: precio venta - costo (con valuación por método de la empresa) por marca.

**9.1.3 % devolución/canje por marca**: relación entre canjeado/devuelto y comprado en un período.

**9.1.4 Cumplimiento de surtido**: # SKUs activos con stock vs surtido total declarado de la marca.

### 9.2 Permisos

```
INV_RPM_REPORTE_ROTACION_VER
INV_RPM_REPORTE_MARGEN_VER
INV_RPM_REPORTE_DEVOLUCIONES_VER
INV_RPM_REPORTE_EXPORTAR
```

### 9.3 Endpoints

```
GET    /reportes/marca/:id/rotacion?desde=&hasta=
GET    /reportes/marca/:id/margen?desde=&hasta=
GET    /reportes/marca/:id/devoluciones?desde=&hasta=
GET    /reportes/marcas/ranking-rotacion?desde=&hasta=
GET    /reportes/marcas/ranking-margen?desde=&hasta=
```

### 9.4 Pantallas

**Nueva: `inventario/PerformanceMarcasPage.jsx`**
- Tabs por reporte. Filtros por rango y categoría.
- Tabla + gráficos básicos.
- Export PDF/Excel.

---

## 10. PERMISOS — Resumen consolidado para `seguridad.seed-data.ts`

Bajo módulo `INVENTARIO` (extender):

```typescript
{ codigo: "INV_PRM", nombre: "Proveedor-Marca", privilegios: [
  "INV_PRM_PROVEEDOR_MARCA_VER", "INV_PRM_PROVEEDOR_MARCA_VINCULAR",
  "INV_PRM_PROVEEDOR_MARCA_DESVINCULAR", "INV_PRM_PROVEEDOR_MARCA_EXPORTAR"
]},
{ codigo: "INV_CDV", nombre: "Canjes y devoluciones a proveedor", privilegios: [
  "INV_CDV_CANJE_VER", "INV_CDV_CANJE_REGISTRAR", "INV_CDV_CANJE_AUTORIZAR",
  "INV_CDV_CANJE_RETIRAR", "INV_CDV_CANJE_RESOLVER", "INV_CDV_CANJE_ANULAR",
  "INV_CDV_REPORTE"
]},
{ codigo: "INV_RPR", nombre: "Reportes por proveedor", privilegios: [
  "INV_RPR_REPORTE_VENCIDOS_PROVEEDOR_VER",
  "INV_RPR_REPORTE_PROXIMOS_VENCER_VER",
  "INV_RPR_REPORTE_STOCK_POR_PROVEEDOR_VER",
  "INV_RPR_REPORTE_EXPORTAR"
]},
{ codigo: "INV_FEFO", nombre: "FEFO en POS", privilegios: [
  "INV_FEFO_FEFO_SUGERIR", "INV_FEFO_FEFO_OVERRIDE",
  "INV_FEFO_VENDER_VENCIDO", "INV_FEFO_VENDER_PROXIMO_VENCER"
]},
{ codigo: "INV_RPM", nombre: "Performance por marca", privilegios: [
  "INV_RPM_REPORTE_ROTACION_VER", "INV_RPM_REPORTE_MARGEN_VER",
  "INV_RPM_REPORTE_DEVOLUCIONES_VER", "INV_RPM_REPORTE_EXPORTAR"
]},
```

Nuevo módulo `PROVEEDORES_ACUERDOS`:

```typescript
{ codigo: "PRV_ACR", nombre: "Acuerdos comerciales", privilegios: [
  "PRV_ACR_ACUERDO_VER", "PRV_ACR_ACUERDO_CREAR",
  "PRV_ACR_ACUERDO_EDITAR", "PRV_ACR_ACUERDO_ANULAR",
  "PRV_ACR_LIQUIDAR_REBATE", "PRV_ACR_REPORTE"
]},
```

---

## 11. ROADMAP POR FASES

### **FASE A — Catálogo y reportes inmediatos (sprint 1, ~3 semanas)** 🔴 PRIORITARIO

Resuelve el caso de uso central del usuario.

| # | Item | Submódulo |
|---|---|---|
| A.1 | Tabla `proveedor_marca` + CRUD + UI en ProveedorFormDialog | 1 |
| A.2 | Script de backfill desde histórico de compras | 1 |
| A.3 | Reporte "Vencidos por proveedor" + PDF + Excel | 4 |
| A.4 | Reporte "Próximos a vencer por proveedor" (30/60/90) | 4 |
| A.5 | Botón "Enviar al proveedor por email" | 4 |
| A.6 | Seeds permisos `INV_PRM_*`, `INV_RPR_*` | — |

**Entregable de la fase**: cualquier proveedor recibe mensualmente (manual al principio) el listado de vencidos de sus marcas.

### **FASE B — Canjes formales (sprint 2, ~3 semanas)**

| # | Item | Submódulo |
|---|---|---|
| B.1 | Tablas `canje_devolucion_*` + endpoints + máquina de estados | 3 |
| B.2 | UI Wizard de canje (CanjeWizard.jsx) | 3 |
| B.3 | Listado `CanjesDevolucionesTab.jsx` | 3 |
| B.4 | PDF comprobante de canje/devolución en msv-kude | 3 |
| B.5 | Movimientos de inventario con `lote_id` + extensión kardex UI | 5 |
| B.6 | Vinculación a NC de proveedor / compra de reposición | 3 |

**Entregable de la fase**: flujo completo de canje desde detectar vencido hasta cerrar con NC o reposición.

### **FASE C — Acuerdos comerciales (sprint 3, ~3 semanas)**

| # | Item | Submódulo |
|---|---|---|
| C.1 | Tabla `proveedor_marca_acuerdo` + endpoints + UI | 2 |
| C.2 | Validación de canje vs acuerdo | 2, 3 |
| C.3 | Acumulado y liquidación de rebate | 2 |
| C.4 | Alertas surtido + compra mínima | 2 |
| C.5 | Recepción con lote/vencimiento | 6 |

### **FASE D — POS reforzado y automatización (sprint 4, ~3 semanas)**

| # | Item | Submódulo |
|---|---|---|
| D.1 | FEFO sugerido en POS + bloqueo vencidos | 7 |
| D.2 | Job mensual de envío de informe a proveedores | 4 |
| D.3 | Timeline de lote (LoteTimelineDrawer) | 5 |
| D.4 | Settings de empresa para umbrales | 7 |

### **FASE E — Analítica de marca (sprint 5, ~2 semanas)**

| # | Item | Submódulo |
|---|---|---|
| E.1 | Reporte rotación por marca | 8 |
| E.2 | Reporte margen por marca | 8 |
| E.3 | Reporte % devolución por marca | 8 |
| E.4 | Dashboard ejecutivo proveedor | 4 |

### **FASE F — Opcional / futuro**

- OC automática al proveedor preferente por marca con reposición sugerida.
- Integración con factura electrónica del proveedor (importar NC).
- ML: predicción de vencimientos basado en histórico (qué comprar menos).
- Portal proveedor: ve sus marcas, vencidos, canjes pendientes, acuerdos.

---

## 12. RIESGOS Y CONSIDERACIONES

### 12.1 Backfill de `proveedor_marca`

El script de inferencia debe:
1. Recorrer `compra_cab + compra_det → producto.marca_id → proveedor_id`.
2. Crear `proveedor_marca` único por par activo con `vigente_desde = MIN(fecha_compra)`.
3. Marcar como `inferido = true` para que el usuario revise y confirme/depure.
4. NO marcar como `preferente` ni `distribuidor_oficial` automáticamente.

### 12.2 Performance del reporte de vencidos por proveedor

Si hay millones de lotes, la query con JOIN a `proveedor_marca` puede ser pesada. Plan:
- Índice compuesto `(empresa_id, fecha_vencimiento)` en `lotes_producto` (ya existe parcial — verificar).
- Índice `(empresa_id, proveedor_id, marca_id, activo)` en `proveedor_marca`.
- Para empresas con > 100k lotes, considerar **vista materializada** `mv_vencidos_por_proveedor` con refresh diario.

### 12.3 Compatibilidad con productos sin `maneja_lote`

- Toda lógica nueva chequea `maneja_lote` antes de exigir lote.
- Canjes/devoluciones funcionan también sin lote (campo `lote_id` nullable en detalle).
- Reportes de vencidos solo aplican a productos que sí manejan lote — no romper a empresas que no usan.

### 12.4 Multi-proveedor para un mismo producto

Un producto de marca X puede comprarse a varios proveedores. El lote registra `compra_id → proveedor_id` de origen. **Importante**:
- El reporte "vencidos por proveedor" debe usar `lote.proveedor_origen` (de la compra), NO `proveedor_marca`.
- `proveedor_marca` define **quién provee la marca en general** (para reportes informativos).
- El **canje** se le ofrece al proveedor del lote por defecto, pero puede dirigirse a otro si hay acuerdo.

Definir esta semántica con stakeholder antes de empezar.

### 12.5 Email a proveedor

- Validar dirección destino (campo `email` en `proveedores` o nueva tabla `proveedor_contacto` con múltiples emails y tipos).
- Tracking del envío (tabla `proveedor_envio_log` con fecha, asunto, status, PDF adjunto).
- Botón "Reenviar" si rebota.

### 12.6 Auditoría

- Vinculaciones/desvinculaciones proveedor-marca: auditar usuario + fecha.
- Cambios en acuerdos comerciales: histórico completo con campo `proveedor_marca_acuerdo_historial`.
- Canjes/devoluciones: cada transición de estado a `AuditoriaService`.

### 12.7 UI Standards

Cumplir `docs/ui-standards.md`:
- Montos → `MonedaInput`.
- Estados (`CanjeDevEstado`, `AcuerdoTipo`) → `EstadoChip` con enum centralizado.
- Listas vacías → `EmptyState`.
- Cada pantalla nueva → `ScreenGuia` colapsable.
- Fechas → `src/utils/fecha.js`.

---

## 13. PRÓXIMOS PASOS PARA APROBACIÓN

1. **Validar semántica multi-proveedor** del punto 12.4: ¿el canje se ofrece al proveedor del lote o al proveedor preferente de la marca?
2. **Definir umbrales default** por empresa: días "próximo a vencer" (sugerido 30), bloquear venta vencidos (sí/no), día del mes para envío automático (sugerido 5).
3. **Confirmar política de canje fuera de acuerdo**: bloquear duro o solo advertir.
4. **Wireframes** de las 4 pantallas críticas: ReporteVencidosProveedor, CanjeWizard, ProveedorFormDialog (tab Marcas), PerformanceMarcas.
5. **Sprint planning** de Fase A.

---

**Fin del plan.**
