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
El arqueo de caja no cuadra
Causa: Ventas no registradas, movimientos faltantes o errores de registro.Solución:
- Revisa el resumen de caja:
GET /api/caja/{id}/resumen
- Verifica que todas las ventas estén asociadas a la caja correcta
- Revisa los movimientos manuales de ingresos/egresos
- Si hay diferencia, registra un ajuste manual con el concepto “Diferencia de arqueo”
Las utilidades salen negativas
Causa: Los costos están mal configurados o los gastos son muy altos.Solución:
- Verifica que todos los productos tengan costo registrado
- Revisa los gastos operativos (egresos de caja)
- 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:
- Verifica tus permisos en Configuración → Usuarios → Permisos de Caja
- Consulta el estado de la caja:
GET /api/caja/{id}
- 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