1420 lines
65 KiB
Python
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)
|
|
}
|
|
})
|