Files
ATLAS/backend/app/services/reporte_service.py
FlotillasGPS Developer 51d78bacf4 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.
2026-01-21 08:18:00 +00:00

530 lines
17 KiB
Python

"""
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