FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
Sistema completo para monitoreo y gestion de flotas de vehiculos con: - Backend FastAPI con PostgreSQL/TimescaleDB - Frontend React con TypeScript y TailwindCSS - App movil React Native con Expo - Soporte para dispositivos GPS, Meshtastic y celulares - Video streaming en vivo con MediaMTX - Geocercas, alertas, viajes y reportes - Autenticacion JWT y WebSockets en tiempo real Documentacion completa y guias de usuario incluidas.
This commit is contained in:
529
backend/app/services/reporte_service.py
Normal file
529
backend/app/services/reporte_service.py
Normal file
@@ -0,0 +1,529 @@
|
||||
"""
|
||||
Servicio para generación de reportes.
|
||||
|
||||
Genera reportes en PDF y Excel para diferentes tipos de datos.
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.alerta import Alerta
|
||||
from app.models.carga_combustible import CargaCombustible
|
||||
from app.models.mantenimiento import Mantenimiento
|
||||
from app.models.ubicacion import Ubicacion
|
||||
from app.models.vehiculo import Vehiculo
|
||||
from app.models.viaje import Viaje
|
||||
from app.schemas.reporte import (
|
||||
DashboardGrafico,
|
||||
DashboardResumen,
|
||||
ReporteRequest,
|
||||
ReporteResponse,
|
||||
)
|
||||
|
||||
|
||||
class ReporteService:
|
||||
"""Servicio para generación de reportes."""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
"""
|
||||
Inicializa el servicio.
|
||||
|
||||
Args:
|
||||
db: Sesión de base de datos async.
|
||||
"""
|
||||
self.db = db
|
||||
|
||||
async def obtener_dashboard_resumen(self) -> DashboardResumen:
|
||||
"""
|
||||
Obtiene el resumen para el dashboard principal.
|
||||
|
||||
Returns:
|
||||
Datos del dashboard.
|
||||
"""
|
||||
ahora = datetime.now(timezone.utc)
|
||||
inicio_hoy = ahora.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
# Contadores de vehículos
|
||||
result = await self.db.execute(
|
||||
select(func.count(Vehiculo.id)).where(Vehiculo.activo == True)
|
||||
)
|
||||
total_vehiculos = result.scalar()
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.count(Vehiculo.id))
|
||||
.where(Vehiculo.activo == True)
|
||||
.where(Vehiculo.en_servicio == True)
|
||||
)
|
||||
vehiculos_activos = result.scalar()
|
||||
|
||||
# Vehículos en movimiento (velocidad > 5 km/h, última ubicación < 5 min)
|
||||
tiempo_reciente = ahora - timedelta(minutes=5)
|
||||
result = await self.db.execute(
|
||||
select(func.count(Vehiculo.id))
|
||||
.where(Vehiculo.activo == True)
|
||||
.where(Vehiculo.ultima_velocidad > 5)
|
||||
.where(Vehiculo.ultima_ubicacion_tiempo >= tiempo_reciente)
|
||||
)
|
||||
vehiculos_en_movimiento = result.scalar()
|
||||
|
||||
# Vehículos detenidos (velocidad < 5, ubicación reciente)
|
||||
result = await self.db.execute(
|
||||
select(func.count(Vehiculo.id))
|
||||
.where(Vehiculo.activo == True)
|
||||
.where(Vehiculo.ultima_velocidad <= 5)
|
||||
.where(Vehiculo.ultima_ubicacion_tiempo >= tiempo_reciente)
|
||||
)
|
||||
vehiculos_detenidos = result.scalar()
|
||||
|
||||
# Sin señal (última ubicación > 30 min)
|
||||
tiempo_sin_señal = ahora - timedelta(minutes=30)
|
||||
result = await self.db.execute(
|
||||
select(func.count(Vehiculo.id))
|
||||
.where(Vehiculo.activo == True)
|
||||
.where(Vehiculo.en_servicio == True)
|
||||
.where(Vehiculo.ultima_ubicacion_tiempo < tiempo_sin_señal)
|
||||
)
|
||||
vehiculos_sin_señal = result.scalar()
|
||||
|
||||
# Conductores (simplificado)
|
||||
from app.models.conductor import Conductor
|
||||
result = await self.db.execute(
|
||||
select(func.count(Conductor.id)).where(Conductor.activo == True)
|
||||
)
|
||||
conductores_activos = result.scalar()
|
||||
|
||||
# Alertas
|
||||
result = await self.db.execute(
|
||||
select(func.count(Alerta.id)).where(Alerta.atendida == False)
|
||||
)
|
||||
alertas_pendientes = result.scalar()
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.count(Alerta.id))
|
||||
.where(Alerta.atendida == False)
|
||||
.where(Alerta.severidad == "critica")
|
||||
)
|
||||
alertas_criticas = result.scalar()
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.count(Alerta.id)).where(Alerta.creado_en >= inicio_hoy)
|
||||
)
|
||||
alertas_hoy = result.scalar()
|
||||
|
||||
# Viajes de hoy
|
||||
result = await self.db.execute(
|
||||
select(func.count(Viaje.id)).where(Viaje.inicio_tiempo >= inicio_hoy)
|
||||
)
|
||||
viajes_hoy = result.scalar()
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.coalesce(func.sum(Viaje.distancia_km), 0))
|
||||
.where(Viaje.inicio_tiempo >= inicio_hoy)
|
||||
)
|
||||
distancia_hoy = result.scalar() or 0
|
||||
|
||||
# Mantenimientos
|
||||
result = await self.db.execute(
|
||||
select(func.count(Mantenimiento.id))
|
||||
.where(Mantenimiento.estado == "vencido")
|
||||
)
|
||||
mantenimientos_vencidos = result.scalar()
|
||||
|
||||
proximos_7_dias = ahora + timedelta(days=7)
|
||||
result = await self.db.execute(
|
||||
select(func.count(Mantenimiento.id))
|
||||
.where(Mantenimiento.estado == "programado")
|
||||
.where(Mantenimiento.fecha_programada <= proximos_7_dias.date())
|
||||
)
|
||||
mantenimientos_proximos = result.scalar()
|
||||
|
||||
return DashboardResumen(
|
||||
total_vehiculos=total_vehiculos,
|
||||
vehiculos_activos=vehiculos_activos,
|
||||
vehiculos_en_movimiento=vehiculos_en_movimiento,
|
||||
vehiculos_detenidos=vehiculos_detenidos,
|
||||
vehiculos_sin_señal=vehiculos_sin_señal,
|
||||
total_conductores=conductores_activos,
|
||||
conductores_activos=conductores_activos,
|
||||
alertas_pendientes=alertas_pendientes,
|
||||
alertas_criticas=alertas_criticas,
|
||||
alertas_hoy=alertas_hoy,
|
||||
viajes_hoy=viajes_hoy,
|
||||
distancia_hoy_km=float(distancia_hoy),
|
||||
mantenimientos_vencidos=mantenimientos_vencidos,
|
||||
mantenimientos_proximos=mantenimientos_proximos,
|
||||
actualizado_en=ahora,
|
||||
)
|
||||
|
||||
async def obtener_dashboard_graficos(self) -> DashboardGrafico:
|
||||
"""
|
||||
Obtiene datos para gráficos del dashboard.
|
||||
|
||||
Returns:
|
||||
Datos para gráficos.
|
||||
"""
|
||||
ahora = datetime.now(timezone.utc)
|
||||
|
||||
# Distancia por día (últimos 7 días)
|
||||
distancia_diaria = []
|
||||
for i in range(6, -1, -1):
|
||||
fecha = ahora - timedelta(days=i)
|
||||
inicio_dia = fecha.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
fin_dia = inicio_dia + timedelta(days=1)
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.coalesce(func.sum(Viaje.distancia_km), 0))
|
||||
.where(Viaje.inicio_tiempo >= inicio_dia)
|
||||
.where(Viaje.inicio_tiempo < fin_dia)
|
||||
)
|
||||
km = result.scalar() or 0
|
||||
|
||||
distancia_diaria.append({
|
||||
"fecha": inicio_dia.strftime("%Y-%m-%d"),
|
||||
"km": float(km),
|
||||
})
|
||||
|
||||
# Viajes por día
|
||||
viajes_diarios = []
|
||||
for i in range(6, -1, -1):
|
||||
fecha = ahora - timedelta(days=i)
|
||||
inicio_dia = fecha.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
fin_dia = inicio_dia + timedelta(days=1)
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.count(Viaje.id))
|
||||
.where(Viaje.inicio_tiempo >= inicio_dia)
|
||||
.where(Viaje.inicio_tiempo < fin_dia)
|
||||
)
|
||||
cantidad = result.scalar() or 0
|
||||
|
||||
viajes_diarios.append({
|
||||
"fecha": inicio_dia.strftime("%Y-%m-%d"),
|
||||
"cantidad": cantidad,
|
||||
})
|
||||
|
||||
# Alertas por tipo (últimos 7 días)
|
||||
inicio_semana = ahora - timedelta(days=7)
|
||||
from app.models.tipo_alerta import TipoAlerta
|
||||
result = await self.db.execute(
|
||||
select(TipoAlerta.nombre, func.count(Alerta.id))
|
||||
.join(TipoAlerta, Alerta.tipo_alerta_id == TipoAlerta.id)
|
||||
.where(Alerta.creado_en >= inicio_semana)
|
||||
.group_by(TipoAlerta.nombre)
|
||||
.order_by(func.count(Alerta.id).desc())
|
||||
.limit(5)
|
||||
)
|
||||
alertas_por_tipo = [
|
||||
{"tipo": row[0], "cantidad": row[1]}
|
||||
for row in result.all()
|
||||
]
|
||||
|
||||
# Consumo de combustible (últimos 30 días)
|
||||
consumo_combustible = []
|
||||
for i in range(29, -1, -1):
|
||||
fecha = ahora - timedelta(days=i)
|
||||
inicio_dia = fecha.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
fin_dia = inicio_dia + timedelta(days=1)
|
||||
|
||||
result = await self.db.execute(
|
||||
select(func.coalesce(func.sum(CargaCombustible.litros), 0))
|
||||
.where(CargaCombustible.fecha >= inicio_dia)
|
||||
.where(CargaCombustible.fecha < fin_dia)
|
||||
)
|
||||
litros = result.scalar() or 0
|
||||
|
||||
consumo_combustible.append({
|
||||
"fecha": inicio_dia.strftime("%Y-%m-%d"),
|
||||
"litros": float(litros),
|
||||
})
|
||||
|
||||
return DashboardGrafico(
|
||||
distancia_diaria=distancia_diaria,
|
||||
viajes_diarios=viajes_diarios,
|
||||
alertas_por_tipo=alertas_por_tipo,
|
||||
consumo_combustible=consumo_combustible,
|
||||
)
|
||||
|
||||
async def generar_reporte(
|
||||
self,
|
||||
request: ReporteRequest,
|
||||
) -> ReporteResponse:
|
||||
"""
|
||||
Genera un reporte según los parámetros especificados.
|
||||
|
||||
Args:
|
||||
request: Parámetros del reporte.
|
||||
|
||||
Returns:
|
||||
Información del reporte generado.
|
||||
"""
|
||||
reporte_id = str(uuid.uuid4())
|
||||
|
||||
# Recopilar datos según el tipo de reporte
|
||||
datos = await self._recopilar_datos_reporte(request)
|
||||
|
||||
# Generar archivo según formato
|
||||
if request.formato == "pdf":
|
||||
archivo_url = await self._generar_pdf(reporte_id, request.tipo, datos)
|
||||
elif request.formato == "excel":
|
||||
archivo_url = await self._generar_excel(reporte_id, request.tipo, datos)
|
||||
else: # csv
|
||||
archivo_url = await self._generar_csv(reporte_id, request.tipo, datos)
|
||||
|
||||
return ReporteResponse(
|
||||
id=reporte_id,
|
||||
tipo=request.tipo,
|
||||
formato=request.formato,
|
||||
estado="completado",
|
||||
archivo_url=archivo_url,
|
||||
creado_en=datetime.now(timezone.utc),
|
||||
completado_en=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
async def _recopilar_datos_reporte(
|
||||
self,
|
||||
request: ReporteRequest,
|
||||
) -> Dict[str, Any]:
|
||||
"""Recopila los datos necesarios para el reporte."""
|
||||
datos = {
|
||||
"periodo_inicio": request.fecha_inicio,
|
||||
"periodo_fin": request.fecha_fin,
|
||||
}
|
||||
|
||||
if request.tipo == "viajes":
|
||||
datos["viajes"] = await self._obtener_datos_viajes(
|
||||
request.fecha_inicio,
|
||||
request.fecha_fin,
|
||||
request.vehiculos_ids,
|
||||
)
|
||||
elif request.tipo == "alertas":
|
||||
datos["alertas"] = await self._obtener_datos_alertas(
|
||||
request.fecha_inicio,
|
||||
request.fecha_fin,
|
||||
request.vehiculos_ids,
|
||||
)
|
||||
elif request.tipo == "combustible":
|
||||
datos["combustible"] = await self._obtener_datos_combustible(
|
||||
request.fecha_inicio,
|
||||
request.fecha_fin,
|
||||
request.vehiculos_ids,
|
||||
)
|
||||
elif request.tipo == "mantenimiento":
|
||||
datos["mantenimiento"] = await self._obtener_datos_mantenimiento(
|
||||
request.fecha_inicio,
|
||||
request.fecha_fin,
|
||||
request.vehiculos_ids,
|
||||
)
|
||||
|
||||
return datos
|
||||
|
||||
async def _obtener_datos_viajes(
|
||||
self,
|
||||
desde: datetime,
|
||||
hasta: datetime,
|
||||
vehiculos_ids: List[int] = None,
|
||||
) -> List[dict]:
|
||||
"""Obtiene datos de viajes para el reporte."""
|
||||
query = (
|
||||
select(Viaje)
|
||||
.where(Viaje.inicio_tiempo >= desde)
|
||||
.where(Viaje.inicio_tiempo <= hasta)
|
||||
.order_by(Viaje.inicio_tiempo)
|
||||
)
|
||||
|
||||
if vehiculos_ids:
|
||||
query = query.where(Viaje.vehiculo_id.in_(vehiculos_ids))
|
||||
|
||||
result = await self.db.execute(query)
|
||||
viajes = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": v.id,
|
||||
"vehiculo_id": v.vehiculo_id,
|
||||
"inicio": v.inicio_tiempo.isoformat(),
|
||||
"fin": v.fin_tiempo.isoformat() if v.fin_tiempo else None,
|
||||
"distancia_km": v.distancia_km,
|
||||
"duracion_segundos": v.duracion_segundos,
|
||||
"velocidad_promedio": v.velocidad_promedio,
|
||||
"velocidad_maxima": v.velocidad_maxima,
|
||||
"estado": v.estado,
|
||||
}
|
||||
for v in viajes
|
||||
]
|
||||
|
||||
async def _obtener_datos_alertas(
|
||||
self,
|
||||
desde: datetime,
|
||||
hasta: datetime,
|
||||
vehiculos_ids: List[int] = None,
|
||||
) -> List[dict]:
|
||||
"""Obtiene datos de alertas para el reporte."""
|
||||
query = (
|
||||
select(Alerta)
|
||||
.where(Alerta.creado_en >= desde)
|
||||
.where(Alerta.creado_en <= hasta)
|
||||
.order_by(Alerta.creado_en)
|
||||
)
|
||||
|
||||
if vehiculos_ids:
|
||||
query = query.where(Alerta.vehiculo_id.in_(vehiculos_ids))
|
||||
|
||||
result = await self.db.execute(query)
|
||||
alertas = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"vehiculo_id": a.vehiculo_id,
|
||||
"tipo_alerta_id": a.tipo_alerta_id,
|
||||
"severidad": a.severidad,
|
||||
"mensaje": a.mensaje,
|
||||
"creado_en": a.creado_en.isoformat(),
|
||||
"atendida": a.atendida,
|
||||
}
|
||||
for a in alertas
|
||||
]
|
||||
|
||||
async def _obtener_datos_combustible(
|
||||
self,
|
||||
desde: datetime,
|
||||
hasta: datetime,
|
||||
vehiculos_ids: List[int] = None,
|
||||
) -> List[dict]:
|
||||
"""Obtiene datos de combustible para el reporte."""
|
||||
query = (
|
||||
select(CargaCombustible)
|
||||
.where(CargaCombustible.fecha >= desde)
|
||||
.where(CargaCombustible.fecha <= hasta)
|
||||
.order_by(CargaCombustible.fecha)
|
||||
)
|
||||
|
||||
if vehiculos_ids:
|
||||
query = query.where(CargaCombustible.vehiculo_id.in_(vehiculos_ids))
|
||||
|
||||
result = await self.db.execute(query)
|
||||
cargas = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"vehiculo_id": c.vehiculo_id,
|
||||
"fecha": c.fecha.isoformat(),
|
||||
"litros": c.litros,
|
||||
"precio_litro": c.precio_litro,
|
||||
"total": c.total,
|
||||
"odometro": c.odometro,
|
||||
"estacion": c.estacion,
|
||||
}
|
||||
for c in cargas
|
||||
]
|
||||
|
||||
async def _obtener_datos_mantenimiento(
|
||||
self,
|
||||
desde: datetime,
|
||||
hasta: datetime,
|
||||
vehiculos_ids: List[int] = None,
|
||||
) -> List[dict]:
|
||||
"""Obtiene datos de mantenimiento para el reporte."""
|
||||
query = (
|
||||
select(Mantenimiento)
|
||||
.where(Mantenimiento.fecha_programada >= desde.date())
|
||||
.where(Mantenimiento.fecha_programada <= hasta.date())
|
||||
.order_by(Mantenimiento.fecha_programada)
|
||||
)
|
||||
|
||||
if vehiculos_ids:
|
||||
query = query.where(Mantenimiento.vehiculo_id.in_(vehiculos_ids))
|
||||
|
||||
result = await self.db.execute(query)
|
||||
mantenimientos = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": m.id,
|
||||
"vehiculo_id": m.vehiculo_id,
|
||||
"tipo_mantenimiento_id": m.tipo_mantenimiento_id,
|
||||
"estado": m.estado,
|
||||
"fecha_programada": m.fecha_programada.isoformat(),
|
||||
"fecha_realizada": m.fecha_realizada.isoformat() if m.fecha_realizada else None,
|
||||
"costo_real": m.costo_real,
|
||||
}
|
||||
for m in mantenimientos
|
||||
]
|
||||
|
||||
async def _generar_pdf(
|
||||
self,
|
||||
reporte_id: str,
|
||||
tipo: str,
|
||||
datos: Dict[str, Any],
|
||||
) -> str:
|
||||
"""Genera un reporte en PDF."""
|
||||
# Implementación simplificada
|
||||
# En producción se usaría WeasyPrint o similar
|
||||
archivo_path = f"{settings.REPORTS_DIR}/{reporte_id}.pdf"
|
||||
# TODO: Implementar generación de PDF con WeasyPrint
|
||||
return archivo_path
|
||||
|
||||
async def _generar_excel(
|
||||
self,
|
||||
reporte_id: str,
|
||||
tipo: str,
|
||||
datos: Dict[str, Any],
|
||||
) -> str:
|
||||
"""Genera un reporte en Excel."""
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = tipo.capitalize()
|
||||
|
||||
# Escribir datos según el tipo
|
||||
if tipo in datos:
|
||||
items = datos[tipo]
|
||||
if items:
|
||||
# Headers
|
||||
headers = list(items[0].keys())
|
||||
for col, header in enumerate(headers, 1):
|
||||
ws.cell(row=1, column=col, value=header)
|
||||
|
||||
# Data
|
||||
for row, item in enumerate(items, 2):
|
||||
for col, key in enumerate(headers, 1):
|
||||
ws.cell(row=row, column=col, value=item.get(key))
|
||||
|
||||
archivo_path = f"{settings.REPORTS_DIR}/{reporte_id}.xlsx"
|
||||
wb.save(archivo_path)
|
||||
return archivo_path
|
||||
|
||||
except ImportError:
|
||||
return ""
|
||||
|
||||
async def _generar_csv(
|
||||
self,
|
||||
reporte_id: str,
|
||||
tipo: str,
|
||||
datos: Dict[str, Any],
|
||||
) -> str:
|
||||
"""Genera un reporte en CSV."""
|
||||
import csv
|
||||
|
||||
archivo_path = f"{settings.REPORTS_DIR}/{reporte_id}.csv"
|
||||
|
||||
if tipo in datos:
|
||||
items = datos[tipo]
|
||||
if items:
|
||||
with open(archivo_path, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=items[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(items)
|
||||
|
||||
return archivo_path
|
||||
Reference in New Issue
Block a user