diff --git a/api/reports/views.py b/api/reports/views.py index d395774..d21b5f3 100644 --- a/api/reports/views.py +++ b/api/reports/views.py @@ -194,7 +194,7 @@ class ExportDataStageView(APIView): if export_type == 'excel': # Siempre usar el m茅todo particionado inteligente para Excel - return self.export_datastage_multiple_partitioned_excel(request, models_data, global_filters, related_keys) + return self.export_datastage_multiple_partitioned_excel_agrupados(request, models_data, global_filters, related_keys) else: # Para CSV, podemos mantener la l贸gica actual o mejorarla total_estimated_records = self.estimate_total_records(models_data, global_filters, related_keys, request.user) @@ -278,6 +278,868 @@ class ExportDataStageView(APIView): ) response['Content-Disposition'] = 'attachment; filename="datastage_related_report.xlsx"' return response + + def export_datastage_multiple_partitioned_excel_agrupados(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() + + # 馃敟 PRECARGAR ORGANIZACIONES para mapeo r谩pido + from api.organization.models import Organizacion + organizaciones = Organizacion.objects.all() + org_mapping = {str(org.id): org.nombre for org in organizaciones} + + 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 + + # Normalizar nombres de campo entrantes: si se pas贸 "Organizacion" + # (cualquier capitalizaci贸n), usar el campo real de la BD `organizacion_id`. + normalized_fields = [] + for f in fields: + try: + key = f.strip() if isinstance(f, str) else f + except Exception: + key = f + + if isinstance(key, str) and key.lower() == 'organizacion': + if 'organizacion_id' not in normalized_fields: + normalized_fields.append('organizacion_id') + else: + if key not in normalized_fields: + normalized_fields.append(key) + + fields = normalized_fields + + # 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) + + # 馃敟 A帽adir organizacion_id a los campos si no est谩 y existe en el modelo + if 'organizacion_id' not in fields and 'organizacion_id' in [f.name for f in apps.get_model('datastage', model_name)._meta.get_fields()]: + fields.append('organizacion_id') + + 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) + + # 馃敟 PROCESAR CAMPO organizacion_id para convertirlo a nombre + processed_record = {} + for field_name, value in record.items(): + # Convertir organizacion_id a nombre + if field_name == 'organizacion_id' and value: + org_id_str = str(value) + # Usar el nombre de la organizaci贸n si est谩 en el mapeo + if org_id_str in org_mapping: + processed_value = org_mapping[org_id_str] + else: + # Si no se encuentra, intentar obtener de la base de datos + try: + org = Organizacion.objects.filter(id=value).first() + processed_value = org.nombre if org else str(value) + # Actualizar mapeo para futuras referencias + org_mapping[org_id_str] = processed_value + except: + processed_value = str(value) + else: + processed_value = value + + # Agregar prefijo del modelo a los campos para evitar colisiones + if field_name in relation_fields: + prefixed_field_name = field_name + else: + prefixed_field_name = f"{model_name}_{field_name}" + + # 馃敟 RENOMBRAR organizacion_id a organizacion_nombre + if field_name == 'organizacion_id': + prefixed_field_name = prefixed_field_name.replace('organizacion_id', 'organizacion_nombre') + + processed_record[prefixed_field_name] = self.safe_excel_value(processed_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(processed_record) + + 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', 'organizacion_id']: + # Los campos de relaci贸n ya est谩n llenados o transformados + continue + prefixed_field_name = f"{model_name}_{field_name}" + # 馃敟 RENOMBRAR organizacion_id a organizacion_nombre + if field_name == 'organizacion_id': + prefixed_field_name = prefixed_field_name.replace('organizacion_id', 'organizacion_nombre') + 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) + + # 馃敟 Mover organizacion_nombre cerca de los campos de relaci贸n + org_fields = [f for f in all_fields_set if 'organizacion' in f.lower()] + for org_field in sorted(org_fields): + all_fields.append(org_field) + all_fields_set.remove(org_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()) + + # Informaci贸n de depuraci贸n + print(f"Creada partici贸n {page_num} con {len(page.object_list)} registros combinados") + print(f"Total de claves 煤nicas: {len(all_models_data)}") + print(f"Total de filas expandidas: {total_records}") + + 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 + error_details = traceback.format_exc() + print(f"Error en exportaci贸n: {error_details}") + 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_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()) + + # Informaci贸n de depuraci贸n + print(f"Creada partici贸n {page_num} con {len(page.object_list)} registros combinados") + print(f"Total de claves 煤nicas: {len(all_models_data)}") + print(f"Total de filas expandidas: {total_records}") + + 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 + error_details = traceback.format_exc() + print(f"Error en exportaci贸n: {error_details}") + 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 + error_details = traceback.format_exc() + print(f"Error en exportaci贸n: {error_details}") + 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()) + + # Informaci贸n de depuraci贸n (opcional) + print(f"Creada partici贸n {page_num} con {len(page.object_list)} registros") + + 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 + error_details = traceback.format_exc() + print(f"Error en exportaci贸n: {error_details}") + 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"""