Skip to main content

Reportes Financieros

El módulo financiero proporciona reportes completos para el análisis de liquidez, rentabilidad y salud financiera de la empresa.

Módulos Financieros

Caja

Apertura, cierre, arqueo y movimientos diarios

Cuentas por Cobrar

Seguimiento de pagos pendientes de clientes

Cuentas por Pagar

Control de deudas con proveedores

Utilidades

Análisis de rentabilidad y márgenes

Reportes de Caja

Estructura del Sistema de Caja

Tabla: cajas Estados de caja:
  • Activa: Caja abierta con operaciones en curso
  • Inactiva: Caja cerrada
Permisos por usuario (tabla permisos_caja):
  • puede_abrir_caja: Realizar apertura
  • puede_cerrar_caja: Solicitar cierre
  • puede_autorizar_cierre: Aprobar cierre solicitado
  • puede_rechazar_cierre: Rechazar cierre
  • puede_registrar_movimientos: Ingresos/egresos manuales
  • puede_ver_reportes: Acceso a reportes
Los permisos de caja son por usuario, no por rol. Cada usuario tiene configuración independiente.

Apertura de Caja

Endpoint: POST /api/caja/{id}/abrir Datos requeridos:
{
  monto_inicial: 500.00,              // Efectivo inicial
  denominaciones: [                   // Billetes y monedas
    { valor: 200, cantidad: 1 },     // 1 billete de S/ 200
    { valor: 100, cantidad: 2 },     // 2 billetes de S/ 100
    { valor: 50, cantidad: 2 },      // 2 billetes de S/ 50
    { valor: 20, cantidad: 5 }       // 5 billetes de S/ 20
  ],
  observaciones: 'Apertura turno mañana'
}
Tablas afectadas:
  • cajas: Se actualiza estado = 'Activa', monto_inicial, id_usuario_apertura, fecha_apertura
  • apertura_caja_billetes: Se registran las denominaciones
// CajaSesionService.php
public function abrir(Caja $caja, array $datos, User $usuario): Caja
{
    // Validar que la caja esté inactiva
    if ($caja->estado === CajaEstadoEnum::Activa->value) {
        throw new \Exception('La caja ya está abierta');
    }
    
    // Actualizar caja
    $caja->update([
        'estado' => CajaEstadoEnum::Activa->value,
        'monto_inicial' => $datos['monto_inicial'],
        'id_usuario_apertura' => $usuario->id,
        'fecha_apertura' => now()
    ]);
    
    // Registrar denominaciones
    foreach ($datos['denominaciones'] as $denom) {
        AperturaCajaBillete::create([
            'id_caja' => $caja->id,
            'id_denominacion' => $denom['id'],
            'cantidad' => $denom['cantidad']
        ]);
    }
    
    return $caja;
}

Movimientos de Caja

Endpoint: POST /api/caja/{id}/movimientos Tipos de movimiento:
  • ingreso: Entrada de efectivo (cobros adicionales, anticipos)
  • egreso: Salida de efectivo (gastos, pagos)
Tabla: movimientos_caja
// Registrar movimiento
MovimientoCaja::create([
    'id_caja' => $cajaId,
    'id_empresa' => $empresaId,
    'id_usuario' => $usuarioId,
    'tipo' => 'egreso',
    'monto' => 150.00,
    'concepto' => 'Pago de luz',
    'observaciones' => 'Recibo 123456',
    'metodo_pago' => 'efectivo'
]);
Consultar movimientos:
// Obtener movimientos de una caja
const response = await fetch(`/api/caja/${cajaId}/movimientos`);
const movimientos = await response.json();

movimientos.forEach(mov => {
  console.log(`
    ${mov.tipo.toUpperCase()}: ${mov.concepto}
    Monto: ${mov.monto}
    Usuario: ${mov.usuario.name}
    Fecha: ${mov.created_at}
  `);
});

Arqueo de Caja

Endpoint: GET /api/caja/{id}/resumen El arqueo calcula el efectivo que debería haber en caja vs el efectivo real. Cálculo:
Efectivo Teórico = Monto Inicial 
                 + Ventas en Efectivo 
                 + Ingresos Manuales 
                 - Egresos Manuales

Diferencia = Efectivo Real (contado físicamente) - Efectivo Teórico
Tabla: arqueos_diarios
// CajaArqueoService.php
public function resumen(Caja $caja): array
{
    $montoInicial = $caja->monto_inicial;
    
    // Ventas en efectivo
    $ventasEfectivo = Venta::where('id_caja', $caja->id)
        ->where('metodo_pago', 'efectivo')
        ->where('estado', '1')
        ->sum('total');
    
    // Movimientos
    $ingresos = MovimientoCaja::where('id_caja', $caja->id)
        ->where('tipo', 'ingreso')
        ->sum('monto');
    
    $egresos = MovimientoCaja::where('id_caja', $caja->id)
        ->where('tipo', 'egreso')
        ->sum('monto');
    
    $efectivoTeorico = $montoInicial + $ventasEfectivo + $ingresos - $egresos;
    
    return [
        'monto_inicial' => $montoInicial,
        'ventas_efectivo' => $ventasEfectivo,
        'ingresos' => $ingresos,
        'egresos' => $egresos,
        'efectivo_teorico' => $efectivoTeorico,
        'efectivo_real' => $caja->efectivo_contado ?? $efectivoTeorico,
        'diferencia' => ($caja->efectivo_contado ?? $efectivoTeorico) - $efectivoTeorico
    ];
}

Ventas por Método de Pago

Endpoint: GET /api/caja/{id}/ventas-por-metodo Desglosa las ventas del día por método de pago.
// Resultado ejemplo
{
  ventas_por_metodo: [
    { metodo: 'Efectivo', total: 3500.00, count: 15 },
    { metodo: 'Tarjeta Visa', total: 2100.00, count: 8 },
    { metodo: 'Yape', total: 450.00, count: 5 },
    { metodo: 'Transferencia BCP', total: 1200.00, count: 3 }
  ],
  total_general: 7250.00,
  total_documentos: 31
}

Cuentas por Cobrar

Gestión de pagos pendientes de clientes (créditos de ventas).

Exportar a Excel

Endpoint: POST /api/cuentas-cobrar/export/excel Filtros:
{
  estado: 'P',              // P=Pendiente, C=Cancelado, V=Vencido
  fecha_desde: '2024-01-01',
  fecha_hasta: '2024-12-31',
  cliente: 'ACME Corp'      // Búsqueda por nombre o documento
}
Columnas del reporte:
  • Documento (Serie-Número de la venta)
  • Cliente
  • Número de Cuota
  • Fecha de Vencimiento
  • Monto
  • Pagado
  • Saldo
  • Estado (PENDIENTE/VENCIDO/PAGADO)
// CuentasPorCobrarExportController.php:44-117
public function exportExcel(Request $request)
{
    $query = DiaVenta::with(['venta.cliente'])
        ->whereHas('venta', function ($q) use ($user) {
            $q->where('id_empresa', $user->id_empresa)
              ->where('estado', '1');
        });
    
    // Aplicar filtros
    if ($request->filled('estado')) {
        $estado = $request->estado;
        if ($estado === 'P') $query->pendientes();
        elseif ($estado === 'C') $query->canceladas();
        elseif ($estado === 'V') $query->vencidas();
    }
    
    $cuotas = $query->orderBy('fecha_vencimiento', 'asc')->get();
    
    // ... generar Excel
}

Estados de Cuotas

Modelo: DiaVenta (tabla dias_ventas)
// Modelo DiaVenta.php

// Cuotas pendientes
public function scopePendientes($query)
{
    return $query->where('estado', 'P');
}

// Cuotas canceladas
public function scopeCanceladas($query)
{
    return $query->where('estado', 'C');
}

// Cuotas vencidas (pendientes + fecha pasada)
public function scopeVencidas($query)
{
    return $query->where('estado', 'P')
        ->where('fecha_vencimiento', '<', now());
}

// Antigüedad de la deuda
public function getDiasVencidosAttribute()
{
    if ($this->estado !== 'P') return 0;
    return now()->diffInDays($this->fecha_vencimiento, false);
}

Análisis de Cartera

const analizarCartera = async () => {
  const response = await fetch('/api/cuentas-cobrar');
  const cuotas = await response.json();
  
  const analisis = {
    total_por_cobrar: 0,
    vencidas: { count: 0, monto: 0 },
    por_vencer: { count: 0, monto: 0 },
    por_antigüedad: {
      '0-30': 0,    // Vencidas hace menos de 30 días
      '31-60': 0,   // Vencidas hace 31-60 días
      '61-90': 0,   // Vencidas hace 61-90 días
      '90+': 0      // Vencidas hace más de 90 días
    }
  };
  
  cuotas.forEach(c => {
    if (c.estado === 'P') {
      const saldo = c.monto_cuota - c.monto_pagado;
      analisis.total_por_cobrar += saldo;
      
      if (c.dias_vencidos > 0) {
        analisis.vencidas.count++;
        analisis.vencidas.monto += saldo;
        
        // Clasificar por antigüedad
        if (c.dias_vencidos <= 30) analisis.por_antigüedad['0-30'] += saldo;
        else if (c.dias_vencidos <= 60) analisis.por_antigüedad['31-60'] += saldo;
        else if (c.dias_vencidos <= 90) analisis.por_antigüedad['61-90'] += saldo;
        else analisis.por_antigüedad['90+'] += saldo;
      } else {
        analisis.por_vencer.count++;
        analisis.por_vencer.monto += saldo;
      }
    }
  });
  
  return analisis;
};

Cuentas por Pagar

Gestión de deudas con proveedores (créditos de compras). Endpoint: POST /api/cuentas-pagar/export/excel Estados:
  • 1 = Pendiente
  • 0 = Pagado
  • V = Vencido (pendiente + fecha pasada)
Diferencias con Cuentas por Cobrar:
  • Usa el modelo DiaCompra (tabla dias_compras)
  • Relacionado con compras en lugar de ventas
  • Estados son numéricos: '1' (pendiente) y '0' (pagado)
// CuentasPorPagarExportController.php:18-42
private function getCuotas(Request $request)
{
    $query = DiaCompra::with(['compra.proveedor'])
        ->whereHas('compra', function ($q) use ($user) {
            $q->where('id_empresa', $user->id_empresa)
              ->where('estado', '1');
        });
    
    if ($request->filled('estado')) {
        $estado = $request->estado;
        if ($estado === '1') $query->pendientes();
        elseif ($estado === '0') $query->pagadas();
        elseif ($estado === 'V') $query->vencidas();
    }
    
    return $query->orderBy('fecha', 'asc')->get();
}

Reporte de Utilidades

Análisis completo de rentabilidad con múltiples perspectivas. Endpoint: GET /api/utilidades?periodo=mes

KPIs Principales

Cálculos:
Ingresos = Σ Subtotal de ventas activas
Costo = Σ (Cantidad × Costo del producto) de todas las ventas
Gastos = Σ Egresos de caja
Utilidad = Ingresos - Costo - Gastos
Margen % = (Utilidad / Ingresos) × 100
// UtilidadesController.php:26-48
$ingresos = Venta::porEmpresa($empresaId)
    ->activas()
    ->whereBetween('fecha_emision', [$desde, $hasta])
    ->sum('subtotal');

$costo = DB::table('productos_ventas as pv')
    ->join('ventas as v', 'v.id_venta', '=', 'pv.id_venta')
    ->join('productos as p', 'p.id_producto', '=', 'pv.id_producto')
    ->where('v.id_empresa', $empresaId)
    ->where('v.estado', '!=', '2')
    ->whereBetween('v.fecha_emision', [$desde, $hasta])
    ->selectRaw('COALESCE(SUM(pv.cantidad * COALESCE(p.costo, 0)), 0) as total_costo')
    ->value('total_costo') ?? 0;

$gastos = MovimientoCaja::where('id_empresa', $empresaId)
    ->where('tipo', 'egreso')
    ->whereBetween('created_at', [$desde, $hasta])
    ->sum('monto');

$utilidad = $ingresos - $costo - $gastos;
$margen = $ingresos > 0 ? ($utilidad / $ingresos) * 100 : 0;

Rentabilidad por Producto

Top 50 productos más rentables del periodo. Métricas por producto:
  • Unidades vendidas
  • Ingreso total
  • Costo total
  • Utilidad total
  • Margen %
  • % de participación en ventas
// UtilidadesController.php:68-109
$rentabilidadProductos = DB::table('productos_ventas as pv')
    ->join('ventas as v', 'v.id_venta', '=', 'pv.id_venta')
    ->join('productos as p', 'p.id_producto', '=', 'pv.id_producto')
    ->leftJoin('categorias as c', 'c.id', '=', 'p.categoria_id')
    ->where('v.id_empresa', $empresaId)
    ->where('v.estado', '!=', '2')
    ->whereBetween('v.fecha_emision', [$desde, $hasta])
    ->groupBy('p.id_producto', 'p.nombre', 'p.costo', 'c.nombre')
    ->selectRaw('
        p.nombre as producto,
        COALESCE(c.nombre, "Sin categoría") as categoria,
        SUM(pv.cantidad) as unidades,
        SUM(pv.subtotal) as ingreso_total,
        COALESCE(SUM(pv.cantidad * p.costo), 0) as costo_total
    ')
    ->orderByRaw('SUM(pv.subtotal) - COALESCE(SUM(pv.cantidad * p.costo), 0) DESC')
    ->limit(50)
    ->get();

Margen por Categoría

Rentabilidad agrupada por categoría de productos.

Margen por Vendedor

Desempeño de cada vendedor:
  • Ingresos generados
  • Costos asociados
  • Descuentos otorgados
  • Utilidad neta
  • Margen %
  • Número de ventas
  • Ticket promedio

Utilidad en el Tiempo

Gráfico de evolución:
  • Agrupación diaria (periodos ≤ 31 días)
  • Agrupación mensual (periodos > 31 días)

Gastos Operativos

Desglose de egresos por concepto con % de participación.

Reportes de Bancos

Tablas:
  • bancos: Instituciones bancarias
  • cuentas_bancarias: Cuentas de la empresa
  • movimientos_bancarios: Transacciones
Tipos de movimiento:
  • ingreso: Depósito, transferencia recibida
  • egreso: Retiro, pago, transferencia enviada
// Modelo MovimientoBancario
public function scopeIngresos($query)
{
    return $query->where('tipo', 'ingreso');
}

public function scopeEgresos($query)
{
    return $query->where('tipo', 'egreso');
}

// Saldo de una cuenta
public function calcularSaldo(CuentaBancaria $cuenta, $hasta = null)
{
    $query = MovimientoBancario::where('id_cuenta', $cuenta->id);
    
    if ($hasta) {
        $query->where('fecha', '<=', $hasta);
    }
    
    $ingresos = (clone $query)->ingresos()->sum('monto');
    $egresos = (clone $query)->egresos()->sum('monto');
    
    return $cuenta->saldo_inicial + $ingresos - $egresos;
}

Solución de Problemas

Causa: Ventas no registradas, movimientos faltantes o errores de registro.Solución:
  1. Revisa el resumen de caja: GET /api/caja/{id}/resumen
  2. Verifica que todas las ventas estén asociadas a la caja correcta
  3. Revisa los movimientos manuales de ingresos/egresos
  4. Si hay diferencia, registra un ajuste manual con el concepto “Diferencia de arqueo”
Causa: Los costos están mal configurados o los gastos son muy altos.Solución:
  1. Verifica que todos los productos tengan costo registrado
  2. Revisa los gastos operativos (egresos de caja)
  3. Analiza el reporte de rentabilidad por producto para identificar productos con margen bajo
Causa: No tienes permisos o la caja ya está abierta.Solución:
  1. Verifica tus permisos en Configuración → Usuarios → Permisos de Caja
  2. Consulta el estado de la caja: GET /api/caja/{id}
  3. Si la caja está Activa, debes cerrarla primero (requiere permiso de cierre)

Próximos Pasos

Reportes de Ventas

Analiza tus ingresos

Reportes de Compras

Controla tus gastos

Exportar a Contabilidad

Formatos para sistemas contables

Dashboard

Vista general de indicadores

Build docs developers (and LLMs) love