Files
backend/api/reports/views.py

1420 lines
65 KiB
Python

import csv
import io
import uuid
import datetime
import zipfile
import openpyxl
from django.apps import apps
from django.db import models
from django.db.models import Count, Q
from django.http import HttpResponse
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status, viewsets
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from api.customs.models import Cove, EDocument, Partida, Pedimento
from api.organization.models import Organizacion
from api.record.models import Document
from core.permissions import (
get_org_context,
require_permission,
user_has_permission,
)
from .models import ReportDocument
from .serializers import ExportModelSerializer
from .services import datastage_export
from .tasks.report_datastage import generate_report_datastage
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**(filters or {})).values(*fields)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{model_name}.csv"'
writer = csv.DictWriter(response, fieldnames=fields)
writer.writeheader()
for row in queryset:
writer.writerow(row)
return response
def export_model_to_excel(request, model_name, fields, module='datastage', filters=None):
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**(filters or {})).values(*fields)
wb = openpyxl.Workbook()
ws = wb.active
ws.append(fields)
for row in queryset:
# Convertir cada valor a string para asegurar compatibilidad con Excel
row_values = []
for field in fields:
value = row[field]
# Si es UUID u otro objeto, convertirlo a string
if hasattr(value, '__str__'):
value = str(value)
row_values.append(value)
ws.append(row_values)
output = io.BytesIO()
wb.save(output)
output.seek(0)
response = HttpResponse(output.read(
), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
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']
def get_permissions(self):
if self.request.method == 'GET':
return [IsAuthenticated(), require_permission('reportes.view')()]
return [IsAuthenticated(), require_permission('reportes.export')()]
# La lógica de exportación vive en services/datastage_export.py (la usa la
# task Celery generate_report_datastage); estos delegados conservan la
# interfaz para los métodos legacy de esta clase.
MAX_RECORDS_PER_FILE = datastage_export.MAX_RECORDS_PER_FILE
def safe_excel_value(self, value):
return datastage_export.safe_excel_value(value)
def get(self, request, *args, **kwargs):
"""Retorna RFCs distintos de Registro501 para la organización activa del usuario."""
try:
Registro501 = apps.get_model('datastage', 'Registro501')
org = get_org_context(request.user)
if not org:
return Response({'error': 'Sin organización activa'}, status=status.HTTP_403_FORBIDDEN)
qs = Registro501.objects.filter(organizacion=org)
rfcs = (
qs.exclude(rfc__isnull=True)
.exclude(rfc='')
.values_list('rfc', flat=True)
.distinct()
.order_by('rfc')
)
return Response({'rfcs': list(rfcs)})
except LookupError:
return Response({'rfcs': []})
@swagger_auto_schema(request_body=ExportModelSerializer, responses={202: 'Reporte encolado (Celery)'})
def post(self, request, *args, **kwargs):
"""
Encola la generación asíncrona del reporte DataStage (Celery + SSE).
Responde 202 con report_id y task_id; el progreso se sigue por SSE
(/stream/tasks/{task_id}) y el archivo se descarga después vía
/reports/report-document-download/{report_id}/.
"""
modo = request.data.get('modo', 'simple')
export_format = request.data.get('format', 'csv')
global_filters = request.data.get('globalFilters', {})
# Validar payload antes de encolar (mismos errores que el flujo síncrono)
if modo == 'multiple':
models_data = request.data.get('models', [])
if not models_data:
return Response({'error': 'models are required for multiple export'}, status=status.HTTP_400_BAD_REQUEST)
else:
model_name = request.data.get('model')
fields = request.data.get('fields')
if not model_name or not fields:
return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST)
try:
apps.get_model('datastage', model_name)
except LookupError:
return Response({'error': f'Model {model_name} not found'}, status=status.HTTP_404_NOT_FOUND)
global_filters, err = self._resolve_org_filter(global_filters, request.user)
if err:
return err
# La org ya resuelta viaja en el payload: la task no tiene request.user
payload = {
'modo': modo,
'format': export_format,
'globalFilters': global_filters,
'organizacion_id': global_filters.get('organizacion'),
}
if modo == 'multiple':
payload['models'] = models_data
else:
payload['model'] = model_name
payload['fields'] = fields
report = ReportDocument.objects.create(
user=request.user,
filters=payload,
status='pending',
report_type='datastage',
)
task = generate_report_datastage.delay(report.id)
return Response({
'report_id': report.id,
'task_id': task.id,
'status': report.status,
'created_at': report.created_at,
'download_url': None,
}, status=status.HTTP_202_ACCEPTED)
def _resolve_org_filter(self, global_filters, user):
"""
Devuelve los global_filters asegurando que siempre haya una organización.
La org se obtiene de active_organization (superuser) o del campo organizacion (usuario normal).
Retorna (filters_dict, error_response_or_None).
"""
filters = dict(global_filters or {})
if not filters.get('organizacion'):
org = get_org_context(user)
if not org:
return None, Response(
{'error': 'Sin organización activa'},
status=status.HTTP_403_FORBIDDEN,
)
filters['organizacion'] = str(org.id)
return filters, None
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)
# 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
# Si no hay registros, saltar este modelo
if queryset.count() == 0:
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:
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_test_3(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage agrupados en la misma hoja de Excel, con particionado por límite de registros"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
# 1. Recopilar todos los datos de cada modelo
all_models_data = {} # Ahora será una lista por clave
model_field_mappings = {}
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
# Asegurar que tenemos los campos de relación
required_fields = ['seccion_aduanera', 'patente', 'pedimento']
for field in required_fields:
if field not in fields:
fields.append(field)
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none()
total_records = queryset.count()
if total_records == 0:
continue
# Determinar campos de relación disponibles en este modelo
relation_fields = []
for field_name in ['seccion_aduanera', 'patente', 'pedimento']:
if field_name in fields:
relation_fields.append(field_name)
if not relation_fields:
# Si no hay campos de relación, usar un identificador único
relation_fields = ['datastage_id'] if 'datastage_id' in fields else [fields[0]]
# Guardar mapeo de campos para este modelo
if model_name not in model_field_mappings:
model_field_mappings[model_name] = fields
# Procesar cada registro
for record in queryset:
# Crear clave de relación
key_parts = []
for rel_field in relation_fields:
if rel_field in record and record[rel_field] is not None:
key_parts.append(str(record[rel_field]))
if not key_parts:
# Si no hay campos de relación, usar un hash del registro
import hashlib
record_str = str(sorted(record.items()))
key = hashlib.md5(record_str.encode()).hexdigest()[:10]
else:
key = "_".join(key_parts)
# Agregar prefijo del modelo a los campos para evitar colisiones
prefixed_fields = {}
for field_name, value in record.items():
# Solo agregar prefijo si no es un campo de relación
if field_name in relation_fields:
prefixed_field_name = field_name
else:
prefixed_field_name = f"{model_name}_{field_name}"
prefixed_fields[prefixed_field_name] = self.safe_excel_value(value)
# 🔥 CORRECIÓN: Ahora almacenamos una LISTA de registros por clave
if key not in all_models_data:
all_models_data[key] = {
'relation_fields': {}, # Campos de relación compartidos
'model_records': {} # Diccionario de listas por modelo
}
# Guardar campos de relación (solo una vez, ya que son los mismos)
for rel_field in relation_fields:
if rel_field in record:
all_models_data[key]['relation_fields'][rel_field] = record[rel_field]
# 🔥 GUARDAR COMO LISTA: Crear lista si no existe
if model_name not in all_models_data[key]['model_records']:
all_models_data[key]['model_records'][model_name] = []
# Agregar este registro a la lista del modelo
all_models_data[key]['model_records'][model_name].append(prefixed_fields)
except LookupError:
continue
# Si no hay datos, retornar error
if not all_models_data:
return Response({'error': 'No se encontraron datos para exportar'}, status=status.HTTP_404_NOT_FOUND)
# 2. Crear estructura de filas combinadas
# Ahora necesitamos expandir las filas cuando hay múltiples registros con la misma clave
combined_rows = []
for key, data in all_models_data.items():
relation_fields = data['relation_fields']
model_records = data['model_records']
# 🔥 NUEVO: Calcular cuántas filas necesitamos para esta clave
# Encontrar el modelo con más registros para esta clave
max_records_per_key = 1
for model_name, records in model_records.items():
if len(records) > max_records_per_key:
max_records_per_key = len(records)
# 🔗 CREAR UNA FILA POR CADA COMBINACIÓN
for i in range(max_records_per_key):
row_data = {}
# Campos de relación (mismos para todas las filas con esta clave)
for rel_field, rel_value in relation_fields.items():
row_data[rel_field] = self.safe_excel_value(rel_value)
# Datos de cada modelo
for model_name, records in model_records.items():
# Si hay un registro en esta posición i
if i < len(records):
record = records[i]
for field_name, value in record.items():
row_data[field_name] = value
else:
# Si no hay más registros para este modelo, poner campos vacíos
for field_name in model_field_mappings.get(model_name, []):
if field_name in ['seccion_aduanera', 'patente', 'pedimento']:
# Los campos de relación ya están llenados
continue
prefixed_field_name = f"{model_name}_{field_name}"
row_data[prefixed_field_name] = ''
combined_rows.append(row_data)
# 3. Determinar todos los campos únicos para los encabezados
all_fields_set = set()
# Campos de relación primero
common_relation_fields = ['seccion_aduanera', 'patente', 'pedimento']
# Agregar todos los campos de todas las filas
for row in combined_rows:
all_fields_set.update(row.keys())
# Ordenar campos: relación primero, luego alfabéticamente
all_fields = []
for rel_field in common_relation_fields:
if rel_field in all_fields_set:
all_fields.append(rel_field)
all_fields_set.remove(rel_field)
# Agregar el resto de campos ordenados alfabéticamente
all_fields.extend(sorted(all_fields_set))
total_records = len(combined_rows)
# 4. Manejar particionado
from django.core.paginator import Paginator
paginator = Paginator(combined_rows, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Crear nuevo workbook para cada partición
current_wb = openpyxl.Workbook()
current_ws = current_wb.active
# Nombre de hoja limitado a 31 caracteres
sheet_name = f"Datastage_p{page_num}"
if len(sheet_name) > 31:
sheet_name = sheet_name[:31]
current_ws.title = sheet_name
# Escribir encabezados
current_ws.append(all_fields)
# Escribir datos de esta página
for row_data in page.object_list:
row_values = [row_data.get(field, '') for field in all_fields]
current_ws.append(row_values)
# Autoajustar anchos de columna
for column in current_ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
current_ws.column_dimensions[column_letter].width = adjusted_width
# Guardar archivo en ZIP
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{page_num}.xlsx", part_buffer.getvalue())
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_combinado.zip"'
return response
except Exception as e:
import traceback
import logging
logging.getLogger(__name__).error("Error en exportación combinada: %s", traceback.format_exc())
return Response({'error': f'Error en exportación combinada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_datastage_multiple_partitioned_excel_test_2(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage agrupados en la misma hoja de Excel, con particionado por límite de registros"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
# 1. Recopilar todos los datos de cada modelo por clave (aduana, patente, pedimento)
all_models_data = {}
model_field_mappings = {}
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
required_fields = ['seccion_aduanera', 'patente', 'pedimento']
for field in required_fields:
if field not in fields:
fields.append(field)
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none()
total_records = queryset.count()
if total_records == 0:
continue
# Determinar campos de relación disponibles en este modelo
relation_fields = []
for field_name in ['seccion_aduanera', 'patente', 'pedimento']:
if field_name in fields:
relation_fields.append(field_name)
if not relation_fields:
# Si no hay campos de relación, usar un identificador único
relation_fields = ['datastage_id'] if 'datastage_id' in fields else [fields[0]]
# Procesar cada registro
for record in queryset:
# Crear clave de relación
key_parts = []
for rel_field in relation_fields:
if rel_field in record and record[rel_field] is not None:
key_parts.append(str(record[rel_field]))
if not key_parts:
# Si no hay campos de relación, usar un hash del registro
import hashlib
record_str = str(sorted(record.items()))
key = hashlib.md5(record_str.encode()).hexdigest()[:10]
else:
key = "_".join(key_parts)
# Agregar prefijo del modelo a los campos para evitar colisiones
prefixed_fields = {}
for field_name, value in record.items():
prefixed_field_name = f"{model_name}_{field_name}"
prefixed_fields[prefixed_field_name] = self.safe_excel_value(value)
# Registrar mapeo de campos
if model_name not in model_field_mappings:
model_field_mappings[model_name] = []
if field_name not in model_field_mappings[model_name]:
model_field_mappings[model_name].append(field_name)
# Guardar datos bajo la clave
if key not in all_models_data:
all_models_data[key] = {
'relation_fields': {},
'model_data': {}
}
# Guardar campos de relación
for rel_field in relation_fields:
if rel_field in record:
all_models_data[key]['relation_fields'][rel_field] = record[rel_field]
# Guardar datos del modelo
all_models_data[key]['model_data'][model_name] = prefixed_fields
except LookupError:
continue
# Si no hay datos, retornar error
if not all_models_data:
return Response({'error': 'No se encontraron datos para exportar'}, status=status.HTTP_404_NOT_FOUND)
# 2. Determinar todos los campos únicos que necesitaremos
all_fields_set = set()
# Primero agregar campos de relación comunes
common_relation_fields = ['seccion_aduanera', 'patente', 'pedimento']
for key, data in all_models_data.items():
# Agregar campos de relación
for rel_field in common_relation_fields:
if rel_field in data['relation_fields']:
all_fields_set.add(rel_field)
# Agregar campos de todos los modelos para esta clave
for model_name, model_fields in data['model_data'].items():
for field_name in model_fields.keys():
all_fields_set.add(field_name)
# Convertir a lista ordenada (campos de relación primero)
all_fields = []
for rel_field in common_relation_fields:
if rel_field in all_fields_set:
all_fields.append(rel_field)
all_fields_set.remove(rel_field)
# Luego agregar el resto de campos ordenados alfabéticamente
all_fields.extend(sorted(all_fields_set))
# 3. Crear datos combinados por fila
combined_rows = []
for key, data in all_models_data.items():
row_data = {}
# Campos de relación
for rel_field in common_relation_fields:
if rel_field in data['relation_fields']:
row_data[rel_field] = self.safe_excel_value(data['relation_fields'][rel_field])
else:
row_data[rel_field] = ''
# Datos de cada modelo
for model_name, model_fields in data['model_data'].items():
for field_name, value in model_fields.items():
row_data[field_name] = value
# Rellenar campos faltantes con vacío
for field in all_fields:
if field not in row_data:
row_data[field] = ''
combined_rows.append(row_data)
total_records = len(combined_rows)
# 4. Manejar particionado
from django.core.paginator import Paginator
paginator = Paginator(combined_rows, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Crear nuevo workbook para cada partición
current_wb = openpyxl.Workbook()
current_ws = current_wb.active
# Nombre de hoja limitado a 31 caracteres
sheet_name = f"Datastage_p{page_num}"
if len(sheet_name) > 31:
sheet_name = sheet_name[:31]
current_ws.title = sheet_name
# Escribir encabezados
current_ws.append(all_fields)
# Escribir datos de esta página
for row_data in page.object_list:
row_values = [row_data.get(field, '') for field in all_fields]
current_ws.append(row_values)
# Autoajustar anchos de columna (opcional)
for column in current_ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50) # Máximo 50 caracteres
current_ws.column_dimensions[column_letter].width = adjusted_width
# Guardar archivo en ZIP
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{page_num}.xlsx", part_buffer.getvalue())
# Información de depuración
print(f"Creada partición {page_num} con {len(page.object_list)} registros combinados")
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_combinado.zip"'
return response
except Exception as e:
import traceback
import logging
logging.getLogger(__name__).error("Error en exportación combinada: %s", traceback.format_exc())
return Response({'error': f'Error en exportación combinada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_datastage_multiple_partitioned_excel_test(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage agrupados en la misma hoja de Excel, con particionado por límite de registros"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
file_counter = 1
current_wb = None
current_ws = None
current_record_count = 0
combined_fields = [] # Almacenar todos los campos únicos
combined_data = [] # Almacenar todos los datos
# 1. Primero recopilar todos los campos y datos
all_models_data = {}
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)
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none()
total_records = queryset.count()
if total_records == 0:
continue
# Almacenar los datos de este modelo
all_models_data[model_name] = {
'fields': fields,
'data': list(queryset),
'total_records': total_records
}
# Agregar campos únicos a la lista combinada
for field in fields:
if field not in combined_fields:
combined_fields.append(field)
except LookupError:
continue
# Si no hay datos, retornar error
if not all_models_data:
return Response({'error': 'No se encontraron datos para exportar'}, status=status.HTTP_404_NOT_FOUND)
# 2. Crear estructura de datos combinada
# Primero, preparar los datos combinados
for model_name, model_info in all_models_data.items():
fields = model_info['fields']
data = model_info['data']
for record in data:
combined_record = {}
# Para cada campo en la lista combinada
for combined_field in combined_fields:
if combined_field in fields:
# Si el campo existe en este modelo, usar su valor
value = record.get(combined_field)
combined_record[combined_field] = self.safe_excel_value(value)
else:
# Si no existe, poner vacío
combined_record[combined_field] = ''
# Agregar columna para identificar el modelo origen
combined_record['_modelo_origen'] = model_name
combined_data.append(combined_record)
# Agregar campo de modelo origen a la lista de campos si no está ya
if '_modelo_origen' not in combined_fields:
combined_fields.append('_modelo_origen')
total_combined_records = len(combined_data)
# 3. Manejar particionado
from django.core.paginator import Paginator
paginator = Paginator(combined_data, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Crear nuevo workbook para cada partición
current_wb = openpyxl.Workbook()
current_ws = current_wb.active
current_ws.title = f"Todos_Modelos_p{page_num}"[:31]
# Escribir encabezados
current_ws.append(combined_fields)
# Escribir datos de esta página
for record in page.object_list:
row_values = [record.get(field, '') for field in combined_fields]
current_ws.append(row_values)
# Guardar archivo en ZIP
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_combinado_part{page_num}.xlsx", part_buffer.getvalue())
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_combinado.zip"'
return response
except Exception as e:
import traceback
import logging
logging.getLogger(__name__).error("Error en exportación combinada: %s", traceback.format_exc())
return Response({'error': f'Error en exportación combinada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
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 inteligentemente"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
file_counter = 1
current_wb = None
current_file_records_count = 0
MAX_SHEETS_PER_FILE = 10 # Límite de hojas por archivo Excel
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)
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none()
total_records = queryset.count()
if total_records == 0:
continue
# Si el modelo necesita particionarse (más de MAX_RECORDS_PER_FILE)
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)
# Verificar si necesitamos crear nuevo archivo
# 1. Si no hay archivo actual
# 2. Si ya tenemos muchas hojas en este archivo
# 3. Si este archivo ya está "lleno" (muchos registros)
if (current_wb is None or
len(current_wb.sheetnames) >= MAX_SHEETS_PER_FILE or
current_file_records_count > self.MAX_RECORDS_PER_FILE * 3): # ~150K registros
if current_wb is not None:
# Guardar archivo actual en ZIP
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
file_counter += 1
# Crear nuevo workbook
current_wb = openpyxl.Workbook()
current_wb.remove(current_wb.active) # Remover hoja por defecto
current_file_records_count = 0
# Crear hoja para esta parte del modelo
sheet_name = f"{model_name[:25]}_p{page_num}"
ws = current_wb.create_sheet(title=sheet_name[:31])
ws.append(fields)
# Escribir datos
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
current_file_records_count += len(page.object_list)
else:
# Modelo pequeño (≤ MAX_RECORDS_PER_FILE)
# Verificar si necesitamos nuevo archivo
if (current_wb is None or
len(current_wb.sheetnames) >= MAX_SHEETS_PER_FILE or
current_file_records_count + total_records > self.MAX_RECORDS_PER_FILE * 3):
if current_wb is not None:
# Guardar archivo actual
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
file_counter += 1
# Crear nuevo workbook
current_wb = openpyxl.Workbook()
current_wb.remove(current_wb.active)
current_file_records_count = 0
# Crear hoja para este modelo
sheet_name = model_name[:31]
ws = current_wb.create_sheet(title=sheet_name)
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)
current_file_records_count += total_records
except LookupError:
continue
# Guardar el último workbook si existe
if current_wb is not None:
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
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 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 get_related_keys_from_filters(self, global_filters, models_data, user):
return datastage_export.get_related_keys_from_filters(global_filters, models_data)
def apply_global_filters_to_model(self, global_filters, model, user):
return datastage_export.apply_global_filters_to_model(global_filters, model)
def apply_related_filters(self, global_filters, model, related_keys, user):
return datastage_export.apply_related_filters(global_filters, model, related_keys)
def estimate_excel_file_size(self, num_records, num_columns):
"""Estima tamaño aproximado del archivo Excel"""
# Estimación aproximada: 100 bytes por celda
return num_records * num_columns * 100
def export_with_size_control(self, request, models_data, global_filters, related_keys):
"""Versión con control de tamaño de archivo"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
file_counter = 1
current_wb = None
current_file_size_estimate = 0
MAX_FILE_SIZE_ESTIMATE = 50 * 1024 * 1024 # 50MB estimado
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)
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none()
total_records = queryset.count()
if total_records == 0:
continue
# Calcular tamaño estimado para este modelo
model_size_estimate = self.estimate_excel_file_size(total_records, len(fields))
# Si el modelo es muy grande o no cabe en el archivo actual
needs_new_file = (
current_wb is None or
current_file_size_estimate + model_size_estimate > MAX_FILE_SIZE_ESTIMATE or
(total_records > self.MAX_RECORDS_PER_FILE and current_file_size_estimate > 0)
)
if needs_new_file and current_wb is not None:
# Guardar archivo actual
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
file_counter += 1
current_wb = None
current_file_size_estimate = 0
if current_wb is None:
current_wb = openpyxl.Workbook()
current_wb.remove(current_wb.active)
# Manejar modelos que exceden el límite por hoja
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)
# Crear hoja para esta parte
sheet_name = f"{model_name[:20]}_p{page_num}"[:31]
ws = current_wb.create_sheet(title=sheet_name)
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)
# Actualizar tamaño estimado
page_size = self.estimate_excel_file_size(len(page.object_list), len(fields))
current_file_size_estimate += page_size
else:
# Modelo pequeño, una hoja
sheet_name = model_name[:31]
ws = current_wb.create_sheet(title=sheet_name)
ws.append(fields)
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
current_file_size_estimate += model_size_estimate
except LookupError:
continue
# Guardar último archivo si existe
if current_wb is not None:
part_buffer = io.BytesIO()
current_wb.save(part_buffer)
part_buffer.seek(0)
zip_file.writestr(f"datastage_part{file_counter}.xlsx", part_buffer.getvalue())
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: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ExportModelView(APIView):
my_tags = ['Reportes']
def get_permissions(self):
if self.request.method == 'GET':
return [IsAuthenticated(), require_permission('reportes.view')()]
return [IsAuthenticated(), require_permission('reportes.export')()]
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter('model', openapi.IN_QUERY, description="Nombre del modelo (ejemplo: Registro500)",
type=openapi.TYPE_STRING, required=True)
],
responses={200: openapi.Response('Campos disponibles', schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'fields': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Items(type=openapi.TYPE_STRING))
}
))}
)
def get(self, request, *args, **kwargs):
"""
Devuelve los campos disponibles para el modelo solicitado.
Ejemplo: /api/reports/exportmodel/?model=Registro500
"""
model_name = request.query_params.get('model')
module = request.query_params.get('module', 'datastage')
if not model_name:
return Response({'error': 'model is required'}, status=status.HTTP_400_BAD_REQUEST)
try:
model = apps.get_model(module, model_name)
except LookupError:
return Response({'error': f'Model {model_name} not found in app {module}'}, status=status.HTTP_404_NOT_FOUND)
fields = [f.name for f in model._meta.fields]
return Response({'fields': fields})
@swagger_auto_schema(
request_body=ExportModelSerializer,
responses={200: 'Archivo generado (Excel o CSV)'}
)
def post(self, request, *args, **kwargs):
model_name = request.data.get('model')
fields = request.data.get('fields')
filters = request.data.get('filters', {})
org = get_org_context(request.user)
filters['organizacion__id'] = org.id if org else None
export_type = request.data.get('type', 'csv')
module = request.data.get('module', 'datastage')
if not model_name or not fields:
return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST)
if export_type == 'excel':
return export_model_to_excel(request, model_name, fields, module, filters)
else:
return export_model_to_csv(request, model_name, fields, module, filters)
# Resumen general para dashboard
@api_view(['GET'])
@permission_classes([IsAuthenticated, require_permission('reportes.view')])
def dashboard_summary(request):
filters = {}
user = request.user
pedimento_app = request.query_params.get('pedimento_app')
aduana = request.query_params.get('aduana')
patente = request.query_params.get('patente')
regimen = request.query_params.get('regimen')
agente_aduanal = request.query_params.get('agente_aduanal')
tipo_operacion = request.query_params.get('tipo_operacion')
fecha_pago_gte = request.query_params.get('fecha_pago__gte')
fecha_pago_lte = request.query_params.get('fecha_pago__lte')
contribuyente__rfc = request.query_params.get('contribuyente__rfc')
org = get_org_context(user)
if not org:
return Response({'error': 'Sin organización activa.'}, status=status.HTTP_403_FORBIDDEN)
filters['organizacion_id'] = org.id
# Importador: filtrar solo por sus RFC asignados
if user.is_importador:
rfcs = list(user.rfc.values_list('rfc', flat=True))
if rfcs:
filters['contribuyente__rfc__in'] = rfcs
if pedimento_app:
filters['pedimento_app'] = pedimento_app
if aduana:
filters['aduana'] = aduana
if patente:
filters['patente'] = patente
if regimen:
filters['regimen'] = regimen
if agente_aduanal:
filters['agente_aduanal'] = agente_aduanal
if tipo_operacion:
filters['tipo_operacion__tipo'] = tipo_operacion
if fecha_pago_gte:
filters['fecha_pago__gte'] = fecha_pago_gte
if fecha_pago_lte:
filters['fecha_pago__lte'] = fecha_pago_lte
if contribuyente__rfc:
filters['contribuyente__rfc'] = contribuyente__rfc
# Filtrar pedimentos
pedimentos_qs = Pedimento.objects.filter(**filters)
pedimentos_total = pedimentos_qs.count()
pedimentos_completos = pedimentos_qs.filter(existe_expediente=True).count()
pedimentos_pendientes = pedimentos_total - pedimentos_completos
# Usar los IDs de pedimentos filtrados para los demás modelos
pedimento_ids = list(pedimentos_qs.values_list('id', flat=True))
coves_total = Cove.objects.filter(pedimento_id__in=pedimento_ids).count()
coves_procesados = Cove.objects.filter(
pedimento_id__in=pedimento_ids, cove_descargado=True).count()
acuse_coves_procesados = Cove.objects.filter(
pedimento_id__in=pedimento_ids, acuse_cove_descargado=True).count()
acuse_coves_pendientes = coves_total - acuse_coves_procesados
coves_pendientes = coves_total - coves_procesados
edocs_total = EDocument.objects.filter(
pedimento_id__in=pedimento_ids).count()
edocs_descargados = EDocument.objects.filter(
pedimento_id__in=pedimento_ids, edocument_descargado=True).count()
acuse_descargados = EDocument.objects.filter(
pedimento_id__in=pedimento_ids, acuse_descargado=True).count()
edocs_pendientes = edocs_total - edocs_descargados
acuses_pendientes = edocs_total - acuse_descargados
remesas_total = Document.objects.filter(
document_type__id=3, pedimento_id__in=pedimento_ids).count()
documentos_descargados = Document.objects.filter(
pedimento_id__in=pedimento_ids).count()
partidas_total = Partida.objects.filter(
pedimento_id__in=pedimento_ids).count()
partidas_descargadas = Partida.objects.filter(
pedimento_id__in=pedimento_ids, descargado=True).count()
partidas_pendientes = partidas_total - partidas_descargadas
# Indicadores de cumplimiento
cumplimiento_pedimentos = (
pedimentos_completos / pedimentos_total * 100) if pedimentos_total else 0
cumplimiento_acuse_coves = (
acuse_coves_procesados / coves_total * 100) if coves_total else 0
cumplimiento_coves = (
coves_procesados / coves_total * 100) if coves_total else 0
cumplimiento_edocs = (edocs_descargados /
edocs_total * 100) if edocs_total else 0
cumplimiento_acuses = (acuse_descargados /
edocs_total * 100) if edocs_total else 0
cumplimiento_partidas = (partidas_descargadas /
partidas_total * 100) if partidas_total else 0
# Calcular cumplimiento total (promedio de todos los indicadores)
indicadores = [
cumplimiento_pedimentos,
cumplimiento_coves,
cumplimiento_acuse_coves,
cumplimiento_edocs,
cumplimiento_acuses,
cumplimiento_partidas
]
cumplimiento_total = sum(indicadores) / len(indicadores) if indicadores else 0
return Response({
"cumplimiento_total": round(cumplimiento_total, 2),
"pedimentos": {
"total": pedimentos_total,
"completos": pedimentos_completos,
"pendientes": pedimentos_pendientes,
"cumplimiento": round(cumplimiento_pedimentos, 2)
},
"coves": {
"total": coves_total,
"coves_procesados": coves_procesados,
"coves_pendientes": coves_pendientes,
"coves_cumplimiento": round(cumplimiento_coves, 2),
},
"acuse_coves": {
"total": coves_total,
"acuse_coves_procesados": acuse_coves_procesados,
"acuse_coves_pendientes": acuse_coves_pendientes,
"acuse_coves_cumplimiento": round(cumplimiento_acuse_coves, 2)
},
"edocuments": {
"total": edocs_total,
"edocs_descargados": edocs_descargados,
"edocs_pendientes": edocs_pendientes,
"edocs_cumplimiento": round(cumplimiento_edocs, 2),
},
"acuses":{
"total": edocs_total,
"acuse_descargados": acuse_descargados,
"acuses_pendientes": acuses_pendientes,
"acuses_cumplimiento": round(cumplimiento_acuses, 2)
},
"remesas": {
"total": remesas_total
},
"documentos": {
"descargados": documentos_descargados
},
"partidas": {
"total": partidas_total,
"partidas_descargadas": partidas_descargadas,
"partidas_pendientes": partidas_pendientes,
"cumplimiento": round(cumplimiento_partidas, 2)
}
})