actualizacion de reportes

This commit is contained in:
Dulce
2025-11-25 13:17:41 -07:00
parent 4b44c098c4
commit a765026075
6 changed files with 1013 additions and 10 deletions

View File

@@ -48,7 +48,10 @@ from core.permissions import (
IsSuperUser
)
from .serializers import ExportModelSerializer
import uuid
import datetime
import zipfile
from django.db import models
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
model = apps.get_model(module, model_name)
@@ -86,11 +89,688 @@ def export_model_to_excel(request, model_name, fields, module='datastage', filte
response['Content-Disposition'] = f'attachment; filename="{model_name}.xlsx"'
return response
# class ControlPedimentoView(APIView):
# my_tags = ['Control-Pedimento']
# permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
# @swagger_auto_schema(request_body=ExportModelSerializer, responses={200: 'Archivo generado (Excel o CSV)'})
# def post(self, request, *args, **kwargs):
# """
# Endpoint específico para exportación de DataStage con soporte múltiple
# """
# # Verificar si es modo múltiple
# modo = request.data.get('modo', 'simple')
# if modo == 'multiple':
# return self.handle_multiple_export(request)
# else:
# return self.handle_simple_export(request)
class ExportDataStageView(APIView):
my_tags = ['Reportes-DataStage']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
# Constantes para partición
# MAX_RECORDS_PER_FILE = 100 # Límite seguro por archivo
MAX_RECORDS_PER_FILE = 50000 # Límite seguro por archivo
def safe_excel_value(self, value):
"""
Convierte cualquier valor a un formato seguro para Excel
"""
if value is None:
return ''
elif isinstance(value, (uuid.UUID,)):
return str(value)
elif hasattr(value, 'uuid'):
return str(value.uuid)
elif hasattr(value, 'id'):
return str(value.id)
elif isinstance(value, (datetime.datetime, datetime.date)):
return value.isoformat()
elif isinstance(value, (dict, list)):
return str(value)
else:
return str(value)
@swagger_auto_schema(request_body=ExportModelSerializer, responses={200: 'Archivo generado (Excel o CSV)'})
def post(self, request, *args, **kwargs):
"""
Endpoint específico para exportación de DataStage con soporte múltiple
"""
# Verificar si es modo múltiple
modo = request.data.get('modo', 'simple')
if modo == 'multiple':
return self.handle_multiple_export(request)
else:
return self.handle_simple_export(request)
def handle_simple_export(self, request):
"""Maneja exportación simple de DataStage (un solo modelo)"""
model_name = request.data.get('model')
fields = request.data.get('fields')
global_filters = request.data.get('globalFilters', {})
export_type = request.data.get('format', 'csv')
module = 'datastage'
if not model_name or not fields:
return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST)
try:
model = apps.get_model(module, model_name)
filters = self.apply_global_filters_to_model(global_filters, model, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if export_type == 'excel':
# Verificar si necesita partición
if total_records > self.MAX_RECORDS_PER_FILE:
return self.export_single_model_partitioned(request, model_name, fields, filters, total_records)
else:
return export_model_to_excel(request, model_name, fields, module, filters)
else:
if total_records > self.MAX_RECORDS_PER_FILE:
return self.export_single_model_csv_partitioned(request, model_name, fields, filters, total_records)
else:
return export_model_to_csv(request, model_name, fields, module, filters)
except LookupError:
return Response({'error': f'Model {model_name} not found'}, status=status.HTTP_404_NOT_FOUND)
def handle_multiple_export(self, request):
"""Maneja exportación múltiple de DataStage (varios modelos)"""
models_data = request.data.get('models', [])
export_type = request.data.get('format', 'csv')
global_filters = request.data.get('globalFilters', {})
if not models_data:
return Response({'error': 'models are required for multiple export'}, status=status.HTTP_400_BAD_REQUEST)
related_keys = self.get_related_keys_from_filters(global_filters, models_data, request.user)
total_estimated_records = self.estimate_total_records(models_data, global_filters, related_keys, request.user)
if total_estimated_records > self.MAX_RECORDS_PER_FILE:
if export_type == 'excel':
return self.export_datastage_multiple_partitioned_excel(request, models_data, global_filters, related_keys)
else:
return self.export_datastage_multiple_partitioned_csv(request, models_data, global_filters, related_keys)
else:
if export_type == 'excel':
return self.export_datastage_multiple_to_excel(request, models_data, global_filters, related_keys)
else:
return self.export_datastage_multiple_to_csv(request, models_data, global_filters, related_keys)
def estimate_total_records(self, models_data, global_filters, related_keys, user):
"""Estima el total de registros para todos los modelos"""
total = 0
for model_data in models_data:
model_name = model_data.get('model')
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, user)
total += model.objects.filter(**filters).count()
except:
continue
return total
def export_datastage_multiple_to_excel(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage con filtrado relacionado (múltiples hojas)"""
wb = openpyxl.Workbook()
wb.remove(wb.active)
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
# 🔥 APLICAR FILTROS RELACIONADOS
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
print(f"🎯 FILTROS APLICADOS A {model_name}: {filters}")
# Si hay filtros, aplicarlos; si no, obtener todos los registros
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none() # No obtener nada si no hay filtros
print(f"📊 {model_name}: {queryset.count()} registros después de filtrado relacionado")
# Si no hay registros, saltar este modelo
if queryset.count() == 0:
print(f"🔶 {model_name}: Sin registros después de filtrado, saltando...")
continue
# Crear hoja (limitar nombre a 31 caracteres)
sheet_name = model_name[:31]
ws = wb.create_sheet(title=sheet_name)
# Escribir encabezados
ws.append(fields)
# Escribir datos
for row in queryset:
row_values = []
for field in fields:
value = row[field]
# 🔥 USAR safe_excel_value para convertir valores
row_values.append(self.safe_excel_value(value))
ws.append(row_values)
except LookupError:
print(f"Modelo {model_name} no encontrado")
continue
# Si no se crearon hojas, crear una vacía
if len(wb.sheetnames) == 0:
ws = wb.create_sheet(title="Sin datos")
ws.append(["No se encontraron datos para los modelos especificados"])
output = io.BytesIO()
wb.save(output)
output.seek(0)
response = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = 'attachment; filename="datastage_related_report.xlsx"'
return response
def export_datastage_multiple_partitioned_excel(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos Excel particionados"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
# Si hay filtros, aplicarlos; si no, obtener todos los registros
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none() # No obtener nada si no hay filtros
total_records = queryset.count()
if total_records == 0:
print(f"🔶 {model_name}: Sin registros, saltando...")
continue
if total_records > self.MAX_RECORDS_PER_FILE:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = f"Parte_{page_num}"[:31]
ws.append(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
# Guardar parte en ZIP
part_buffer = io.BytesIO()
wb.save(part_buffer)
part_buffer.seek(0)
filename = f"{model_name}_part{page_num}.xlsx"
zip_file.writestr(filename, part_buffer.getvalue())
else:
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Datos"[:31]
ws.append(fields)
# Escribir datos
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
part_buffer = io.BytesIO()
wb.save(part_buffer)
part_buffer.seek(0)
filename = f"{model_name}.xlsx"
zip_file.writestr(filename, part_buffer.getvalue())
except LookupError as e:
continue
except Exception as e:
continue
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error en exportación particionada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_datastage_multiple_to_csv(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos CSV en ZIP"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if total_records == 0:
continue
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
except LookupError:
continue
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
def export_datastage_multiple_partitioned_csv(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos CSV particionados en ZIP"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if total_records == 0:
continue
if total_records > self.MAX_RECORDS_PER_FILE:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}_part{page_num}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
else:
# Modelo pequeño, exportar completo
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
# Escribir encabezados
writer.writerow(fields)
# Escribir datos
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
except LookupError as e:
continue
except Exception as e:
continue
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
except Exception as e:
return Response({'error': f'Error en exportación CSV particionada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_single_model_partitioned(self, request, model_name, fields, filters, total_records):
"""Exporta un solo modelo particionado a ZIP"""
try:
zip_buffer = io.BytesIO()
module = 'datastage'
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**filters).values(*fields)
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Crear Excel para esta parte
wb = openpyxl.Workbook()
ws = wb.active
ws.title = f"Parte_{page_num}"[:31]
ws.append(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
part_buffer = io.BytesIO()
wb.save(part_buffer)
part_buffer.seek(0)
filename = f"{model_name}_part{page_num}.xlsx"
zip_file.writestr(filename, part_buffer.getvalue())
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename="{model_name}_particionado.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error exportando modelo: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_single_model_csv_partitioned(self, request, model_name, fields, filters, total_records):
"""Exporta un solo modelo CSV particionado a ZIP"""
try:
zip_buffer = io.BytesIO()
module = 'datastage'
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**filters).values(*fields)
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}_part{page_num}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename="{model_name}_particionado.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error exportando modelo CSV: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_related_keys_from_filters(self, global_filters, models_data, user):
"""
Obtiene patentes, pedimentos y datastages que cumplen EXACTAMENTE con TODOS los filtros globales
para usarlos como relación entre modelos
"""
related_keys = {
'patentes': set(),
'pedimentos': set(),
'datastage_ids': set()
}
# Si no hay filtros globales, retornar vacío (no hay relación)
if not any(global_filters.values()):
return {}
all_records_with_filters = []
# Buscar en TODOS los modelos que puedan tener los campos de filtro
for model_data in models_data:
model_name = model_data.get('model')
try:
model = apps.get_model('datastage', model_name)
model_fields = [f.name for f in model._meta.get_fields()]
# Construir filtros EXACTOS con TODOS los campos disponibles
filters = {}
has_any_filter = False
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
has_any_filter = True
if 'patente' in model_fields and global_filters.get('patente'):
filters['patente'] = global_filters['patente']
has_any_filter = True
if 'pedimento' in model_fields and global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
has_any_filter = True
if 'rfc' in model_fields and global_filters.get('rfc'):
filters['rfc'] = global_filters['rfc']
has_any_filter = True
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
has_any_filter = True
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
has_any_filter = True
if has_any_filter:
records = model.objects.filter(**filters).values('patente', 'pedimento', 'datastage_id')
record_count = records.count()
all_records_with_filters.extend(list(records))
except LookupError:
continue
if not all_records_with_filters:
return {'patentes': set(), 'pedimentos': set(), 'datastage_ids': set()}
for record in all_records_with_filters:
if record.get('patente'):
related_keys['patentes'].add(record['patente'])
if record.get('pedimento'):
related_keys['pedimentos'].add(record['pedimento'])
if record.get('datastage_id'):
related_keys['datastage_ids'].add(record['datastage_id'])
related_keys = {k: list(v) for k, v in related_keys.items() if v}
return related_keys
def apply_global_filters_to_model(self, global_filters, model, user):
"""
Aplica filtros globales específicamente para modelos DataStage (modo simple)
"""
filters = {}
model_fields = [f.name for f in model._meta.get_fields()]
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
if 'patente' in model_fields and global_filters.get('patente'):
filters['patente'] = global_filters['patente']
if 'pedimento' in model_fields and global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
if 'rfc' in model_fields and global_filters.get('rfc'):
filters['rfc'] = global_filters['rfc']
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
return filters
def apply_related_filters(self, global_filters, model, related_keys, user):
"""
Aplica filtros relacionados basados en campos comunes de manera ESTRICTA - VERSIÓN CORREGIDA
"""
filters = {}
model_fields = [f.name for f in model._meta.get_fields()]
print(f"🔍 APLICANDO FILTROS RELACIONADOS PARA: {model.__name__}")
print(f"🔍 CLAVES RELACIONADAS DISPONIBLES: {related_keys}")
# 🔥 ESTRATEGIA MEJORADA: Usar claves relacionadas SI HAY, sino aplicar filtros directos SOLO si existen
has_related_keys = any(related_keys.values())
if has_related_keys:
# 🔥 MODO RELACIONADO ESTRICTO: Usar SOLO las claves obtenidas
print("🎯 MODO RELACIONADO ACTIVO - FILTRANDO POR CLAVES COMUNES")
# Crear condiciones para las claves relacionadas
from django.db.models import Q
related_conditions = Q()
has_related_conditions = False
if related_keys.get('patentes') and 'patente' in model_fields:
filters['patente__in'] = related_keys['patentes']
print(f"✅ FILTRO PATENTE RELACIONADO: {len(related_keys['patentes'])} patentes")
has_related_conditions = True
if related_keys.get('pedimentos') and 'pedimento' in model_fields:
filters['pedimento__in'] = related_keys['pedimentos']
print(f"✅ FILTRO PEDIMENTO RELACIONADO: {len(related_keys['pedimentos'])} pedimentos")
has_related_conditions = True
if related_keys.get('datastage_ids') and 'datastage_id' in model_fields:
filters['datastage_id__in'] = related_keys['datastage_ids']
print(f"✅ FILTRO DATASTAGE RELACIONADO: {len(related_keys['datastage_ids'])} IDs")
has_related_conditions = True
# Si NO HAY condiciones relacionadas para este modelo (no tiene los campos)
if not has_related_conditions:
print(f"⚠️ {model.__name__} no tiene campos para aplicar filtros relacionados - RETORNANDO VACÍO")
return {} # Retornar filtro vacío hará que no se obtengan registros
else:
# 🔥 MODO DIRECTO: No hay claves relacionadas, aplicar filtros directos SOLO si existen
print("🎯 MODO DIRECTO - APLICANDO FILTROS INDIVIDUALES")
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
print(f"✅ FILTRO ORGANIZACIÓN DIRECTO: {global_filters['organizacion']}")
if 'patente' in model_fields and global_filters.get('patente'):
filters['patente'] = global_filters['patente']
print(f"✅ FILTRO PATENTE DIRECTO: {global_filters['patente']}")
if 'pedimento' in model_fields and global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
print(f"✅ FILTRO PEDIMENTO DIRECTO: {global_filters['pedimento']}")
if 'rfc' in model_fields and global_filters.get('rfc'):
filters['rfc'] = global_filters['rfc']
print(f"✅ FILTRO RFC DIRECTO: {global_filters['rfc']}")
# 🔥 APLICAR ORGANIZACIÓN SIEMPRE si existe (en ambos modos)
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
print(f"✅ FILTRO ORGANIZACIÓN: {global_filters['organizacion']}")
# 🔥 APLICAR FILTROS DE FECHA SIEMPRE (si el campo existe)
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
print(f"✅ FILTRO FECHA DESDE: {global_filters['fecha_pago_desde']}")
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
print(f"✅ FILTRO FECHA HASTA: {global_filters['fecha_pago_hasta']}")
print(f"🎯 FILTROS FINALES PARA {model.__name__}: {filters}")
return filters
class ExportModelView(APIView):
my_tags = ['Reportes']
permission_classes = [IsAuthenticated & (
IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
@swagger_auto_schema(
manual_parameters=[