# Plan Estratégico ERP: Rentabilidad, Compras, Lotes y Dashboard CxC

## Actualización 2026-04-11 — Decisiones cerradas

- Fase 1 (CxC) ya implementada en backend/frontend; no entra en esta iteración.
- Alcance Fase 2: implementación end-to-end del módulo COMPRAS.
- Migración inmediata: `costo_unitario_venta`/`costo_total_venta` en `factura_det` y `deposito_id` en `compra_cab`.
- Proveedores desde backend (tabla `proveedores`), no desde flujo legacy de contactos.
- Compras a crédito con política configurable por empresa:
  - `empresa_config_erp.compras_credito_requiere_cuotas = false` (default): crédito por plazo (1 cuota automática).
  - `empresa_config_erp.compras_credito_requiere_cuotas = true`: cuotas manuales obligatorias con suma exacta.
- `PATCH` de compras con edición completa y recalculo de impacto (stock/movimientos/CxP).
- Anulación de compra con reversión completa y recálculo de `precio_costo` por última compra activa.
- `deposito_id` requerido por compra (cabecera).
- `COMPRAS` se agrega como módulo, sin asignación automática a planes en seed.

**Supuestos de esta actualización**
- El documento refleja decisiones aprobadas de diseño/implementación aunque el código aún pueda estar en ejecución parcial.
- Fase 3 y Fase 4 se mantienen como roadmap sin cambios funcionales en esta actualización de texto.

## Avance 2026-04-11 — Implementación en curso (Fase 2)

- Se implementó módulo backend `proveedores` con CRUD completo sobre `tabla proveedores` y flujo `persona -> proveedor`.
- Endpoints nuevos en backend:
  - `POST /proveedores`
  - `GET /proveedores`
  - `GET /proveedores/search?q=...`
  - `GET /proveedores/:id`
  - `PUT /proveedores/:id`
  - `PUT /proveedores/activar-desactivar/:id`
  - `DELETE /proveedores/:id`
- Se integró frontend de proveedores (en `Contactos > Proveedores`) con listado, búsqueda, alta, edición, activar/desactivar y soft-delete usando API del backend (sin flujo legacy Supabase).
- Se rediseñó `ComprasTemplate` con layout operativo tipo POS (cabecera compacta de datos de compra, grilla de ítems, pagos/cuotas y panel de historial).
- El flujo de compras mantiene las decisiones cerradas: `deposito_id` obligatorio, crédito por plazo o cuotas según config de empresa, edición completa y anulación con reversión.

## Actualización 2026-04-15 — Alineación Fase 3 + Fase 4

- Se incorporan endpoints dedicados de rentabilidad:
  - `GET /reportes/rentabilidad/productos`
  - `GET /reportes/rentabilidad/clientes`
  - `GET /reportes/rentabilidad/dashboard`
- Base de cálculo de rentabilidad: `factura_det.costo_unitario_venta` y `factura_det.costo_total_venta`.
- Frontend incorpora página `ReporteRentabilidad` (tabs Producto/Cliente) y widget de rentabilidad en dashboard.
- Inventario integra tab `Lotes` condicionado por módulo/permisos.
- Detalle de factura muestra desglose de lotes consumidos (`factura_det_lote`).
- Regla de crédito configurable por empresa aplicada en backend/frontend de Compras.

## Actualización 2026-04-15 — Compras con lotes por producto + numeración automática

- En Compras, los campos de lote se solicitan solo para ítems cuyo producto tenga `maneja_lote=true`.
- Cuando aplica lote y el usuario no completa fechas:
  - `fecha_fabricacion` default = fecha actual.
  - `fecha_vencimiento` default = fecha actual.
- Si `numero_lote` viene vacío, backend lo genera automáticamente con formato:
  - `LYYYYMMDD-NNN` (ej. `L20260415-001`).
- Reglas de generación implementadas:
  - generación solo cuando el campo está vacío;
  - secuencia diaria incremental;
  - validación de unicidad por empresa (se rechaza lote repetido).
- El detalle de compra persiste el lote autogenerado para mantener trazabilidad en compra, stock por lote y movimientos.

## Contexto

Un cliente solicita: rentabilidad por producto/cliente, lotes de compras con precios, estado de cuenta, y dashboard de cuentas a cobrar con semáforos. Diseñamos la solución como módulos configurables del ERP, escalable para cualquier tamaño de empresa.

**Decisiones clave:**
- **Prioridad**: Dashboard CxC primero (usa datos existentes)
- **Módulos independientes**: COMPRAS (base) y LOTES (opcional/futuro)
- **Costo en factura**: SIEMPRE guardar `costo_unitario_venta` en `factura_det` al momento de facturar
- **Sin lotes**: usa `productos.precio_costo` (último precio de compra)
- **Con lotes**: usa FIFO desde `lotes_producto`
- **Valuación**: FIFO cuando hay lotes
- **Estado de cuenta**: PDF descargable

---

## FASE 1: Dashboard CxC + Estado de Cuenta (sin cambios de schema)

### 1.1 Backend — Nuevos endpoints

**Archivo**: `src/cobros/cobros.service.ts`

**Nuevo método: `getCuentasCobrarDashboard(empresaId)`**
- Query `factura_cuotas` pendientes, calcular aging por buckets:
  - Al día (no vencido)
  - 1-30 días vencido
  - 31-60 días
  - 61-90 días
  - 90+ días
- Retorna: totales por bucket (monto + cantidad), clientes morosos, próximos 5 vencimientos (7 días)

**Nuevo método: `generateEstadoCuentaPdf(empresaId, clienteId)`**
- Recopila cuotas pendientes del cliente agrupadas por factura
- Genera PDF via msv-kude (mismo patrón que `generateReciboPdf`)
- Contenido: datos empresa, datos cliente, tabla de facturas pendientes (nro factura, fecha, cuota, vencimiento, monto, saldo, días vencido), totales

**Archivo**: `src/cobros/cobros.controller.ts`
```
GET /cobros/cuentas-cobrar/dashboard
GET /cobros/cuentas-cobrar/estado-cuenta/:clienteId/pdf
```

### 1.2 Frontend — Widget Dashboard

**Nuevo archivo**: `src/components/organismos/DashboardDesign/CuentasCobrarWidget.jsx`
- Semáforo: verde (al día), amarillo (1-30), rojo (31+)
- Barra de aging con 5 colores por bucket
- KPIs: total pendiente, total vencido, clientes morosos
- Lista: próximos 5 vencimientos
- Link "Ver detalle" → `/cuentas-cobrar`
- Condición: `hasModule('CUENTAS_COBRAR')`

**Modificar**: `src/components/templates/DashboardTemplateV2.jsx`
- Agregar `<CuentasCobrarWidget />` después de la sección principal

**Nuevo en API**: `src/api/cobros.service.js`
- `getCuentasCobrarDashboard()`
- `getEstadoCuentaPdf(clienteId)` (responseType: blob)

**Modificar**: `src/pages/CuentasCobrar.jsx`
- En el modal de detalle del cliente, agregar botón "Descargar Estado de Cuenta"
- Llama `getEstadoCuentaPdf()` y dispara descarga del blob

### 1.3 Archivos a tocar
| Archivo | Cambio |
|---------|--------|
| `cobros.service.ts` | +2 métodos |
| `cobros.controller.ts` | +2 endpoints |
| `cobros.service.js` (frontend) | +2 funciones API |
| `DashboardTemplateV2.jsx` | Integrar widget |
| `CuentasCobrarWidget.jsx` | **NUEVO** |
| `CuentasCobrar.jsx` | Botón estado de cuenta PDF |

---

## FASE 2: Módulo de Compras (SIN lotes)

### 2.1 Database — Cambios en Prisma schema

**Archivo**: `prisma/schema.prisma`

**Agregar campo a `factura_det`:**
```prisma
costo_unitario_venta  Decimal?  @db.Decimal(18, 4)
costo_total_venta     Decimal?  @db.Decimal(18, 4)
```

> Esto se agrega YA en Fase 2 para que al facturar siempre se capture el costo vigente.

### 2.2 Backend — Módulo Compras

**Nuevo directorio**: `src/compras/`
- `compras.module.ts`
- `compras.controller.ts`
- `compras.service.ts`
- `dto/create-compra.dto.ts`
- `dto/update-compra.dto.ts`

**Endpoints:**
```
POST   /compras                    → crear factura de compra
GET    /compras                    → listar (paginado, filtros: fecha, proveedor, estado)
GET    /compras/:id                → detalle
PATCH  /compras/:id                → actualizar
PATCH  /compras/:id/anular         → anular compra
GET    /compras/proveedores/search?q=... → búsqueda/listado de proveedores
```

**Lógica del `create`:**
1. Crear `compra_cab` (proveedor, fecha, moneda, totales)
2. Para cada ítem → crear `compra_det` (producto, cantidad, precio_unitario)
3. **Actualizar `productos.precio_costo`** con el precio_unitario de esta compra (último costo)
4. Incrementar `stock_deposito.cantidad_disponible`
5. Crear `movimientos_inventario` (tipo: COMPRA, costo_unitario)
6. Crear `compra_subtotales` y `compra_forma_pagos`
7. Si es crédito → crear `cuentas_pagar`

**Registrar** `ComprasModule` en `app.module.ts`.

### 2.3 Backend — Captura de costo al facturar

**Archivo**: `src/facturas/facturas.service.ts`

Modificar método `create` (y `createFacturaAutomatico`). Al crear cada `factura_det`:
```typescript
// Capturar costo vigente del producto
const producto = await tx.productos.findUnique({ where: { id: item.producto_id } });
const costoUnitario = producto.precio_costo || 0;
// Guardar en factura_det
costo_unitario_venta: costoUnitario,
costo_total_venta: costoUnitario * cantidad,
```

> Sin lotes: usa `productos.precio_costo`. Con lotes (Fase 4): usa FIFO.

### 2.4 Frontend — Página de Compras

**Nuevo archivo**: `src/pages/Compras.jsx`
**Nuevo archivo**: `src/components/templates/ComprasTemplate.jsx`

Funcionalidad:
- Tabla de facturas de compra (nro, proveedor, fecha, total, estado)
- Formulario "Nueva Compra": selector proveedor, items grid (buscar producto, cantidad, precio_unitario, IVA), forma de pago
- Al guardar: el backend actualiza precio_costo y stock automáticamente
- Detalle de compra con items

**Nuevo archivo**: `src/api/compras.service.js`
- `getCompras()`, `getCompra()`, `createCompra()`, `updateCompra()`, `anularCompra()`

**Modificar**: `src/routers/routes.jsx`
```jsx
<Route path="/compras" element={<ProtectedRoute modulo="COMPRAS"><Compras /></ProtectedRoute>} />
```

**Modificar**: `src/utils/dataEstatica.jsx`
- Agregar link "Compras" en sidebar con `modulo: "COMPRAS"`

### 2.5 Módulo
```sql
INSERT INTO modulos (codigo, descripcion, active) VALUES ('COMPRAS', 'Módulo de Compras', true);
```

### 2.6 Archivos a tocar
| Archivo | Cambio |
|---------|--------|
| `schema.prisma` | +2 campos en factura_det |
| `src/compras/` (directorio) | **NUEVO** módulo completo |
| `facturas.service.ts` | Captura costo al facturar |
| `app.module.ts` | Registrar ComprasModule |
| `Compras.jsx` | **NUEVO** página |
| `ComprasTemplate.jsx` | **NUEVO** template |
| `compras.service.js` | **NUEVO** API service |
| `routes.jsx` | +1 ruta |
| `dataEstatica.jsx` | +1 link sidebar |

---

## FASE 3: Reportes de Rentabilidad

### 3.1 Backend — Endpoints de rentabilidad

**Nuevo directorio o extensión**: `src/reportes/` (o agregar a facturas)

**Método: `getRentabilidadPorProducto(empresaId, fechaDesde, fechaHasta)`**
- Query `factura_det` JOIN `productos` WHERE `costo_unitario_venta IS NOT NULL`
- GROUP BY producto_id
- Retorna por producto: cantidad vendida, ingreso total, costo total, ganancia bruta, margen %

**Método: `getRentabilidadPorCliente(empresaId, fechaDesde, fechaHasta)`**
- Igual pero GROUP BY `factura_cab.cliente_id`
- Retorna por cliente: facturas, ingreso total, costo total, ganancia, margen %

**Método: `getRentabilidadDashboard(empresaId)`**
- Top 5 productos más rentables, top 5 menos rentables
- Margen general del mes, comparación vs mes anterior

**Endpoints:**
```
GET /reportes/rentabilidad/productos?fecha_desde=&fecha_hasta=
GET /reportes/rentabilidad/clientes?fecha_desde=&fecha_hasta=
GET /reportes/rentabilidad/dashboard
```

### 3.2 Frontend — Página de reportes

**Nuevo archivo**: `src/pages/ReporteRentabilidad.jsx`
- Tab "Por Producto": tabla con margen color-coded (verde >30%, amarillo 15-30%, rojo <15%)
- Tab "Por Cliente": tabla con resumen de rentabilidad por cliente
- Filtro: rango de fechas
- Exportar CSV

**Nuevo archivo**: `src/components/organismos/DashboardDesign/CardRentabilidad.jsx`
- Mini widget: margen general %, ingresos/costo/utilidad del período y acceso al detalle
- Condición: `hasModule('REPORTES') && hasModule('FACTURACION')`

**Modificar**: `DashboardTemplateV2.jsx` — agregar `<RentabilidadWidget />`

**Nuevo en API**: `src/api/reportes-rentabilidad.service.js`
- `getRentabilidadProductos()`, `getRentabilidadClientes()`, `getRentabilidadDashboard()`

**Modificar**: `routes.jsx` — ruta `/reportes/rentabilidad`

### 3.3 Archivos a tocar
| Archivo | Cambio |
|---------|--------|
| `src/reportes/` (backend) | **NUEVO** o extender existente |
| `ReporteRentabilidad.jsx` | **NUEVO** página |
| `CardRentabilidad.jsx` | **NUEVO** widget dashboard |
| `DashboardTemplateV2.jsx` | Integrar widget |
| `reportes-rentabilidad.service.js` | **NUEVO** |
| `routes.jsx` | +1 ruta |

---

## FASE 4: Módulo de Lotes (OPCIONAL — cuando el cliente lo active)

### 4.1 Database — Nuevas tablas

**Archivo**: `prisma/schema.prisma`

```prisma
model lotes_producto {
  id                 String    @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
  empresa_id         String    @db.Uuid
  producto_id        String    @db.Uuid
  compra_det_id      String?   @db.Uuid
  numero_lote        String    @db.VarChar(50)
  fecha_fabricacion   DateTime? @db.Date
  fecha_vencimiento  DateTime? @db.Date
  costo_unitario     Decimal   @db.Decimal(18, 4)
  moneda_id          String?   @db.Uuid
  cantidad_original  Decimal   @db.Decimal(15, 4)
  cantidad_restante  Decimal   @db.Decimal(15, 4)
  activo             Boolean   @default(true)
  created_at         DateTime? @default(now()) @db.Timestamp(6)
  updated_at         DateTime? @default(now()) @db.Timestamp(6)

  // Relaciones
  empresas          empresas    @relation(fields: [empresa_id], references: [id])
  productos         productos   @relation(fields: [producto_id], references: [id])
  compra_det        compra_det? @relation(fields: [compra_det_id], references: [id])
  moneda            moneda?     @relation(fields: [moneda_id], references: [id])
  stock_lote        stock_lote[]
  factura_det_lote  factura_det_lote[]

  @@index([empresa_id, producto_id])
  @@index([producto_id, activo, created_at])
}

model stock_lote {
  id                  String          @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
  lote_id             String          @db.Uuid
  deposito_id         String          @db.Uuid
  cantidad_disponible Decimal         @default(0) @db.Decimal(15, 4)
  created_at          DateTime?       @default(now()) @db.Timestamp(6)
  updated_at          DateTime?       @default(now()) @db.Timestamp(6)

  lotes_producto      lotes_producto  @relation(fields: [lote_id], references: [id])
  deposito            depositos       @relation(fields: [deposito_id], references: [id])

  @@unique([lote_id, deposito_id])
}

model factura_det_lote {
  id              String          @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
  factura_det_id  String          @db.Uuid
  lote_id         String          @db.Uuid
  cantidad        Decimal         @db.Decimal(15, 4)
  costo_unitario  Decimal         @db.Decimal(18, 4)
  created_at      DateTime?       @default(now()) @db.Timestamp(6)

  factura_det     factura_det     @relation(fields: [factura_det_id], references: [id])
  lotes_producto  lotes_producto  @relation(fields: [lote_id], references: [id])

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

**Tabla de configuración ERP por empresa:**
```prisma
model empresa_config_erp {
  id                      String   @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
  empresa_id              String   @unique @db.Uuid
  maneja_lotes            Boolean  @default(false)
  metodo_costeo           String   @default("FIFO") @db.VarChar(10)  // FIFO, PROMEDIO, ULTIMO
  alertar_lotes_vencidos  Boolean  @default(true)
  dias_alerta_vencimiento Int      @default(30)
  compras_credito_requiere_cuotas Boolean @default(false)
  mostrar_costo_en_pos    Boolean  @default(false)
  cxc_alerta_dias         Int      @default(7)
  created_at              DateTime? @default(now()) @db.Timestamp(6)
  updated_at              DateTime? @default(now()) @db.Timestamp(6)

  empresas                empresas @relation(fields: [empresa_id], references: [id])
}
```

### 4.2 Backend — FIFO Service

**Nuevo archivo**: `src/stock/fifo.service.ts`
```typescript
@Injectable()
export class FifoService {
  async deductFifo(
    tx: Prisma.TransactionClient,
    productoId: string,
    depositoId: string,
    cantidad: number,
  ): Promise<{
    lotes_consumidos: Array<{ lote_id: string; cantidad: number; costo_unitario: number }>;
    costo_promedio_ponderado: number;
  }>
}
```

Lógica:
1. Query `lotes_producto` WHERE producto_id AND cantidad_restante > 0 ORDER BY created_at ASC
2. Consumir de cada lote hasta satisfacer la cantidad vendida
3. Decrementar `lotes_producto.cantidad_restante` y `stock_lote.cantidad_disponible`
4. Crear registros en `factura_det_lote`
5. Calcular costo promedio ponderado para `factura_det.costo_unitario_venta`

### 4.3 Modificar factura creation

**Archivo**: `src/facturas/facturas.service.ts`

Cambiar la lógica de costo (condicional):
```typescript
const config = await getEmpresaConfig(empresaId);
if (config?.maneja_lotes) {
  // FIFO: consumir lotes y obtener costo ponderado
  const fifoResult = await fifoService.deductFifo(tx, productoId, depositoId, cantidad);
  costoUnitario = fifoResult.costo_promedio_ponderado;
} else {
  // Sin lotes: usar último precio de costo
  costoUnitario = producto.precio_costo || 0;
}
```

### 4.4 Modificar Compras para crear lotes

**Archivo**: `src/compras/compras.service.ts`

Al crear compra, si `maneja_lotes = true`:
- Crear `lotes_producto` por cada línea de compra
- Crear `stock_lote` vinculado al lote y depósito
- La UI de compras muestra campos adicionales: número de lote, fecha fabricación, fecha vencimiento

### 4.5 Frontend — UI de lotes

- Campo opcional de lote en formulario de compras (visible si `maneja_lotes`)
- Vista de lotes por producto en Inventario
- Dashboard: alerta de lotes próximos a vencer
- En factura: mostrar qué lotes se consumieron (para usuarios con permiso)

### 4.6 Nota de Crédito — Reversión de lotes

**Archivo**: `src/nota-creditos/` service
- Al crear NC por devolución: consultar `factura_det_lote` de la factura original
- Revertir: incrementar `lotes_producto.cantidad_restante` y `stock_lote.cantidad_disponible`

### 4.7 Módulo
```sql
INSERT INTO modulos (codigo, descripcion, active) VALUES ('LOTES', 'Control de Lotes e Inventario FIFO', true);
```

### 4.8 Archivos a tocar
| Archivo | Cambio |
|---------|--------|
| `schema.prisma` | +3 tablas nuevas + config ERP |
| `fifo.service.ts` | **NUEVO** |
| `facturas.service.ts` | Condicional FIFO vs precio_costo |
| `compras.service.ts` | Crear lotes al comprar |
| `ComprasTemplate.jsx` | Campos lote condicionales |
| Inventario (frontend) | Vista de lotes |
| `nota-creditos/` | Reversión de lotes |

---

## Resumen de Módulos Configurables

| Módulo | Código | Qué habilita | Depende de |
|--------|--------|--------------|------------|
| Dashboard CxC | `CUENTAS_COBRAR` | Widget semáforo + aging en dashboard | Nada (datos existentes) |
| Compras | `COMPRAS` | Registro de facturas proveedor, actualiza precio_costo, captura costo en factura | Nada |
| Rentabilidad | `RENTABILIDAD` | Reportes margen por producto/cliente, widget dashboard | `COMPRAS` (necesita costos) |
| Lotes | `LOTES` | FIFO, stock por lote, lotes en compras, alertas vencimiento | `COMPRAS` |

**Escalabilidad**: Un emprendedor usa solo COMPRAS. Una mediana activa COMPRAS + RENTABILIDAD. Una grande activa todo incluyendo LOTES.

---

## Verificación y Testing

### Fase 1
- Verificar endpoint `/cobros/cuentas-cobrar/dashboard` retorna buckets correctos
- Verificar PDF de estado de cuenta se genera y descarga
- Verificar widget aparece en dashboard con datos reales
- Verificar semáforos: cuota vencida = rojo, al día = verde

### Fase 2
- Create contado → verificar persistencia de compra y actualización de stock + `precio_costo`
- Create crédito con `cuotas[]` → verificar creación de `cuentas_pagar` por cuota
- Patch completo de compra → verificar reversión y reaplicación correcta de impacto
- Anulación con recálculo de costo → verificar reversión de stock y recálculo de `precio_costo`
- Captura de costo en factura → verificar `factura_det.costo_unitario_venta` y `factura_det.costo_total_venta`

### Fase 3
- Consultar rentabilidad por producto → verificar cálculo margen = (venta - costo) / venta
- Consultar rentabilidad por cliente → verificar agregación correcta
- Widget dashboard → verificar top 5 productos

### Fase 4
- Compra con lotes → verificar creación de `lotes_producto` y `stock_lote`
- Venta FIFO → verificar que consume lote más antiguo primero
- Venta que cruza lotes → verificar `factura_det_lote` con múltiples registros
- NC → verificar reversión de lotes
- Config `maneja_lotes = false` → verificar que usa precio_costo simple
