from django.shortcuts import render from django.http import FileResponse, Http404 from django.db import transaction from rest_framework.pagination import PageNumberPagination from rest_framework.views import APIView from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.parsers import MultiPartParser from rest_framework.response import Response from rest_framework import status from rest_framework.exceptions import ValidationError from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer from .models import Document, Fuente, DocumentType from ..customs.models import Pedimento from api.organization.models import UsoAlmacenamiento from io import BytesIO import zipfile from django.utils.text import slugify from django.http import HttpResponse from rest_framework.decorators import action from datetime import timedelta from django.utils import timezone from core.permissions import ( IsSameOrganization, IsSameOrganizationDeveloper, IsSameOrganizationAndAdmin, IsSuperUser ) import logging logger = logging.getLogger(__name__) import os from django.core.files.storage import default_storage from mixins.filtrado_organizacion import DocumentosFiltradosMixin class CustomPagination(PageNumberPagination): """ Paginación personalizada con parámetros flexibles - Si no se especifica page_size, devuelve todos los resultados (sin paginación) - Si se especifica page_size, usa paginación normal """ page_size = None # Por defecto 10000 por página page_size_query_param = 'page_size' max_page_size = 10000 # Límite máximo de seguridad page_query_param = 'page' # Usar la paginación estándar de DRF, pero con page_size=10000 por defecto y máximo 10000 # Create your views here. class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin): """ ViewSet for Document model. """ permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )] model = Document pagination_class = CustomPagination serializer_class = DocumentSerializer # Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado) filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento', 'created_at'] # filterset_fields = ['extension', 'size', 'pedimento', 'pedimento__pedimento'] # Puedes filtrar por pedimento usando: /api/record/documents/?pedimento= o /api/record/documents/?pedimento__pedimento= # Ejemplo: /api/record/documents/?pedimento_numero=12345678 my_tags = ['Documents'] def get_queryset(self): queryset = self.get_queryset_filtrado_por_organizacion() modulo_efc = self.request.query_params.get('modulo') if modulo_efc: if modulo_efc == 'expedientes-detalle-pedimentos': queryset = queryset.exclude(document_type_id__in=['1','2','3','4','5','6','7','8','9','10']) # Filtro personalizado por document_type # document_type = self.request.query_params.get('document_type') # if document_type: # # Puedes agregar lógica personalizada aquí si es necesario # if document_type == '1': # queryset = queryset.filter(document_type_id=document_type) # elif document_type == '2': # queryset = queryset.filter(document_type_id=document_type) # else: # queryset = queryset.filter(document_type_id=document_type) # else: # queryset = queryset.filter(document_type_id='11') fechaCreacion = self.request.query_params.get('created_at__date') if fechaCreacion: queryset = queryset.filter(created_at=fechaCreacion) buscarArchivo = self.request.query_params.get('archivo__icontains') if buscarArchivo: queryset = queryset.filter(archivo__icontains=buscarArchivo) pedimento_numero = self.request.query_params.get('pedimento_numero') if pedimento_numero: queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero) return queryset @transaction.atomic def perform_create(self, serializer): user = self.request.user if not user.is_authenticated or not hasattr(user, 'organizacion'): raise ValidationError({"error": "Usuario no autenticado o sin organización"}) archivo = self.request.FILES.get('archivo') if not archivo: raise ValidationError({"archivo": "Se requiere un archivo para subir"}) # Permitir que el superusuario especifique la organización organizacion = user.organizacion if self.request.user.is_superuser: organizacion = serializer.validated_data.get('organizacion', organizacion) uso = UsoAlmacenamiento.objects.select_for_update().get_or_create( organizacion=organizacion, defaults={'espacio_utilizado': 0} )[0] # Calcular límites max_almacenamiento_bytes = organizacion.licencia.almacenamiento * 1024 ** 3 nuevo_espacio_utilizado = uso.espacio_utilizado + archivo.size # Validación estricta con raise ValidationError if nuevo_espacio_utilizado > max_almacenamiento_bytes: espacio_faltante = nuevo_espacio_utilizado - max_almacenamiento_bytes raise ValidationError({ "error": "Espacio de almacenamiento insuficiente", "detalle": { "espacio_faltante_gb": round(espacio_faltante / (1024 ** 3), 2), "espacio_utilizado_gb": round(uso.espacio_utilizado / (1024 ** 3), 2), "limite_gb": organizacion.licencia.almacenamiento, "archivo_gb": round(archivo.size / (1024 ** 3), 4) }, "codigo": "storage_limit_exceeded" }, code=status.HTTP_400_BAD_REQUEST) # Guardar documento y actualizar espacio atómicamente documento = serializer.save( organizacion=organizacion, size=archivo.size, extension=archivo.name.split('.')[-1].lower() ) uso.espacio_utilizado = nuevo_espacio_utilizado uso.save() @transaction.atomic def perform_update(self, serializer): instance = self.get_object() new_file = self.request.FILES.get('archivo') if new_file: if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): raise ValidationError({"error": "Usuario no autenticado o sin organización"}) organizacion = self.request.user.organizacion uso = UsoAlmacenamiento.objects.select_for_update().get(organizacion=organizacion) diferencia = new_file.size - instance.size max_almacenamiento_bytes = organizacion.licencia.almacenamiento * 1024 ** 3 nuevo_espacio_utilizado = uso.espacio_utilizado + diferencia if nuevo_espacio_utilizado > max_almacenamiento_bytes: espacio_faltante = nuevo_espacio_utilizado - max_almacenamiento_bytes raise ValidationError({ "error": "Espacio insuficiente para actualizar el archivo", "detalle": { "espacio_faltante_bytes": espacio_faltante, "tamaño_nuevo_archivo": new_file.size, "tamaño_anterior_archivo": instance.size }, "codigo": "update_storage_limit_exceeded" }, code=status.HTTP_400_BAD_REQUEST) # Actualizar documento y espacio serializer.save(size=new_file.size) uso.espacio_utilizado = nuevo_espacio_utilizado uso.save() else: serializer.save() def perform_destroy(self, instance): # Restar el espacio al eliminar uso = UsoAlmacenamiento.objects.get(organizacion=instance.organizacion) uso.espacio_utilizado -= instance.size uso.save() instance.delete() @action(detail=False, methods=['post'], url_path='bulk-delete') def bulk_delete(self, request): """ Endpoint para eliminar múltiples documentos de manera masiva. Payload esperado: { "ids": ["uuid1", "uuid2", "uuid3", ...] } Respuesta exitosa: { "message": "Documentos eliminados exitosamente", "deleted_count": 3, "deleted_ids": ["uuid1", "uuid2", "uuid3"], "space_freed_mb": 25.6 } Respuesta con errores: { "message": "Algunos documentos no pudieron ser eliminados", "deleted_count": 2, "deleted_ids": ["uuid1", "uuid2"], "failed_ids": ["uuid3"], "errors": ["No se encontró el documento con ID uuid3"], "space_freed_mb": 15.2 } """ # Obtener los IDs del payload ids = request.data.get('ids', []) if not ids: return Response( {"error": "Se requiere una lista de IDs para eliminar"}, status=status.HTTP_400_BAD_REQUEST ) if not isinstance(ids, list): return Response( {"error": "El campo 'ids' debe ser una lista"}, status=status.HTTP_400_BAD_REQUEST ) # Obtener el queryset filtrado por organización queryset = self.get_queryset() # Filtrar solo los documentos que existen y pertenecen a la organización del usuario existing_documents = queryset.filter(id__in=ids) existing_ids = list(existing_documents.values_list('id', flat=True)) # Convertir UUIDs a strings para comparación existing_ids_str = [str(id) for id in existing_ids] requested_ids_str = [str(id) for id in ids] # Identificar IDs que no existen o no pertenecen a la organización failed_ids = [id for id in requested_ids_str if id not in existing_ids_str] deleted_count = 0 total_space_freed = 0 errors = [] if existing_documents.exists(): try: # Usar transacción atómica para consistencia with transaction.atomic(): # Calcular el espacio total a liberar total_space_freed = sum(doc.size for doc in existing_documents) # Obtener la organización del usuario para actualizar el uso de almacenamiento if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'): return Response( {"error": "Usuario no autenticado o sin organización"}, status=status.HTTP_400_BAD_REQUEST ) organizacion = request.user.organizacion # Si es superusuario, puede eliminar documentos de cualquier organización if request.user.is_superuser: # Para superusuario, actualizar el uso de cada organización afectada organizaciones_afectadas = {} for doc in existing_documents: if doc.organizacion.id not in organizaciones_afectadas: organizaciones_afectadas[doc.organizacion.id] = { 'organizacion': doc.organizacion, 'espacio_liberado': 0 } organizaciones_afectadas[doc.organizacion.id]['espacio_liberado'] += doc.size # Actualizar uso de almacenamiento para cada organización for org_data in organizaciones_afectadas.values(): try: uso = UsoAlmacenamiento.objects.select_for_update().get( organizacion=org_data['organizacion'] ) uso.espacio_utilizado -= org_data['espacio_liberado'] uso.save() except UsoAlmacenamiento.DoesNotExist: # Si no existe el registro, no hay nada que actualizar pass else: # Para usuarios normales, solo documentos de su organización try: uso = UsoAlmacenamiento.objects.select_for_update().get( organizacion=organizacion ) uso.espacio_utilizado -= total_space_freed uso.save() except UsoAlmacenamiento.DoesNotExist: # Si no existe el registro, no hay nada que actualizar pass # Eliminar los documentos deleted_count = existing_documents.count() existing_documents.delete() except Exception as e: return Response( {"error": f"Error al eliminar documentos: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # Agregar errores para IDs no encontrados if failed_ids: errors = [f"No se encontró el documento con ID {id} o no pertenece a su organización" for id in failed_ids] # Convertir bytes a MB para la respuesta space_freed_mb = round(total_space_freed / (1024 * 1024), 2) # Preparar respuesta response_data = { "deleted_count": deleted_count, "deleted_ids": existing_ids_str, "space_freed_mb": space_freed_mb } if failed_ids: response_data.update({ "message": "Algunos documentos no pudieron ser eliminados", "failed_ids": failed_ids, "errors": errors }) response_status = status.HTTP_207_MULTI_STATUS else: response_data["message"] = "Documentos eliminados exitosamente" response_status = status.HTTP_200_OK return Response(response_data, status=response_status) @action(detail=False, methods=['post'], url_path='bulk-upload', parser_classes=[MultiPartParser]) def bulk_upload(self, request): """ Endpoint para subir múltiples documentos a un pedimento específico. FormData esperado: - pedimento_id: UUID del pedimento (requerido) - files: Lista de archivos a subir (requerido) Nota: Se usa automáticamente el tipo de documento "Documento General" Respuesta exitosa: { "message": "Documentos subidos exitosamente", "uploaded_count": 5, "uploaded_documents": [ { "id": "uuid1", "filename": "documento1.pdf", "size": 1024000, "extension": "pdf" }, ... ], "space_used_mb": 25.6, "failed_files": [], "errors": [] } Respuesta con errores: { "message": "Algunos documentos no pudieron ser subidos", "uploaded_count": 3, "uploaded_documents": [...], "space_used_mb": 15.2, "failed_files": ["archivo4.pdf", "archivo5.doc"], "errors": ["Archivo demasiado grande: archivo4.pdf", "Tipo de archivo no soportado: archivo5.doc"] } """ # Validar datos requeridos pedimento_id = request.data.get('pedimento_id') if not pedimento_id: return Response( {"error": "Se requiere el campo 'pedimento_id'"}, status=status.HTTP_400_BAD_REQUEST ) files = request.FILES.getlist('files') if not files: return Response( {"error": "Se requiere al menos un archivo para subir"}, status=status.HTTP_400_BAD_REQUEST ) # Validar usuario autenticado if not request.user.is_authenticated: return Response( {"error": "Usuario no autenticado"}, status=status.HTTP_401_UNAUTHORIZED ) # Obtener el pedimento primero para usar su organización from api.customs.models import Pedimento try: pedimento = Pedimento.objects.get(id=pedimento_id) except Pedimento.DoesNotExist: return Response( {"error": "Pedimento no encontrado"}, status=status.HTTP_404_NOT_FOUND ) # Usar la organización del pedimento organizacion = pedimento.organizacion # Validar que el usuario tenga permisos para esta organización if not request.user.is_superuser: if not hasattr(request.user, 'organizacion') or request.user.organizacion != organizacion: return Response( {"error": "No tienes permisos para subir documentos a este pedimento"}, status=status.HTTP_403_FORBIDDEN ) # Usar tipo de documento por defecto siempre document_type, created = DocumentType.objects.get_or_create( nombre="Documento General", defaults={'descripcion': "Documento general sin tipo específico"} ) if created: print(f"✅ DocumentType creado: {document_type.nombre} (ID: {document_type.id})") else: print(f"♻️ DocumentType existente: {document_type.nombre} (ID: {document_type.id})") uploaded_documents = [] failed_files = [] errors = [] total_space_used = 0 try: with transaction.atomic(): # Obtener uso de almacenamiento uso = UsoAlmacenamiento.objects.select_for_update().get_or_create( organizacion=organizacion, defaults={'espacio_utilizado': 0} )[0] # Calcular límites max_almacenamiento_bytes = organizacion.licencia.almacenamiento * 1024 ** 3 espacio_inicial = uso.espacio_utilizado # Calcular el tamaño total de todos los archivos total_files_size = sum(file.size for file in files) nuevo_espacio_total = espacio_inicial + total_files_size # Validar que hay espacio suficiente para todos los archivos if nuevo_espacio_total > max_almacenamiento_bytes: espacio_faltante = nuevo_espacio_total - max_almacenamiento_bytes return Response({ "error": "Espacio de almacenamiento insuficiente para todos los archivos", "detalle": { "espacio_faltante_gb": round(espacio_faltante / (1024 ** 3), 2), "espacio_utilizado_gb": round(espacio_inicial / (1024 ** 3), 2), "limite_gb": organizacion.licencia.almacenamiento, "archivos_gb": round(total_files_size / (1024 ** 3), 4), "total_archivos": len(files) }, "codigo": "bulk_storage_limit_exceeded" }, status=status.HTTP_400_BAD_REQUEST) # Procesar cada archivo espacio_usado_temp = espacio_inicial for file in files: try: # Validaciones por archivo if not file.name: failed_files.append("archivo_sin_nombre") errors.append("Archivo sin nombre detectado") continue # Obtener extensión del archivo extension = file.name.split('.')[-1].lower() if '.' in file.name else '' # Crear el documento document = Document.objects.create( organizacion=organizacion, pedimento_id=pedimento_id, document_type=document_type, archivo=file, size=file.size, extension=extension ) # Actualizar espacio usado espacio_usado_temp += file.size total_space_used += file.size uploaded_documents.append({ "id": str(document.id), "filename": file.name, "size": file.size, "extension": extension, "document_type": document_type.nombre }) except Exception as e: failed_files.append(file.name) errors.append(f"Error al procesar {file.name}: {str(e)}") continue # Actualizar el uso de almacenamiento final uso.espacio_utilizado = espacio_usado_temp uso.save() except Exception as e: return Response( {"error": f"Error durante el procesamiento masivo: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # Convertir bytes a MB para la respuesta space_used_mb = round(total_space_used / (1024 * 1024), 2) # Preparar respuesta response_data = { "uploaded_count": len(uploaded_documents), "uploaded_documents": uploaded_documents, "space_used_mb": space_used_mb, "pedimento_id": str(pedimento_id), "document_type": document_type.nombre } if failed_files: response_data.update({ "message": "Algunos documentos no pudieron ser subidos", "failed_files": failed_files, "errors": errors }) response_status = status.HTTP_207_MULTI_STATUS else: response_data["message"] = "Documentos subidos exitosamente" response_status = status.HTTP_201_CREATED return Response(response_data, status=response_status) class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin): permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] serializer_class = DocumentSerializer model = Document my_tags = ['Documents'] def get_queryset(self): return self.get_queryset_filtrado_por_organizacion() def get(self, request, pk): if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'): raise Http404("Usuario no autenticado") try: doc = Document.objects.get(pk=pk) except Document.DoesNotExist: raise Http404("Documento no encontrado") # Verifica que el usuario pertenece a la organización del documento if self.request.user.is_superuser: return FileResponse(doc.archivo.open('rb')) if doc.organizacion != request.user.organizacion: raise Http404("No autorizado") return FileResponse(doc.archivo.open('rb')) class BulkDownloadZipView(APIView): permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] my_tags = ['Documents'] def post(self, request): if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'): return Response({"error": "Usuario no autenticado o sin organización"}, status=401) pks = request.data.get('document_ids', []) pedimento_nombre = request.data.get('pedimento_nombre', 'documentos') if not isinstance(pks, list) or not pks: return Response({"error": "Debe proporcionar una lista de IDs de documentos en 'document_ids'."}, status=400) if self.request.user.is_superuser: docs = Document.objects.filter(pk__in=pks) else: docs = Document.objects.filter(pk__in=pks, organizacion=request.user.organizacion) if docs.count() != len(pks): return Response({"error": "Uno o más documentos no existen o no pertenecen a su organización."}, status=404) buffer = BytesIO() with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: for doc in docs: # Usar solo el nombre del archivo sin descripcion file_name = slugify(doc.archivo.name.rsplit('/', 1)[-1].rsplit('.', 1)[0]) ext = doc.archivo.name.split('.')[-1] zip_name = f"{file_name}.{ext}" doc.archivo.open('rb') zip_file.writestr(zip_name, doc.archivo.read()) doc.archivo.close() buffer.seek(0) safe_name = slugify(pedimento_nombre) response = HttpResponse(buffer, content_type='application/zip') response['Content-Disposition'] = f'attachment; filename={safe_name or "documentos"}.zip' return response class GetFuenteView(APIView): permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] serializer_class = FuenteSerializer my_tags = ['Fuente Documentos'] def get_queryset(self): if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): return Fuente.objects.none() return Fuente.objects.all() def get(self, request): queryset = self.get_queryset() serializer = self.serializer_class(queryset, many=True) return Response(serializer.data, status=200) class DocumentTypeView(APIView): permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] serializer_class = DocumentTypeSerializer my_tags = ['Tipo de Documentos'] def get_queryset(self): if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): return DocumentType.objects.none() return DocumentType.objects.all() def get(self, request): queryset = self.get_queryset() if not queryset.exists(): return Response({"detail": "No hay tipos de documento disponibles."}, status=404) serializer = self.serializer_class(queryset, many=True) return Response(serializer.data, status=200) class ExpedienteZipDownloadView(APIView, DocumentosFiltradosMixin): permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] my_tags = ['Documents'] def post(self, request): """ Descarga todos los documentos de un pedimento (o filtrados) en un ZIP. Body: { "pedimento_id": "" } """ pedimento_id = request.data.get('pedimento_id') if not pedimento_id: return Response({"error": "Falta pedimento_id"}, status=status.HTTP_400_BAD_REQUEST) # Validar que el pedimento existe try: pedimento = Pedimento.objects.get(pk=pedimento_id) except Pedimento.DoesNotExist: raise Http404("Pedimento no encontrado") # Filtrar documentos del pedimento (y de la org del usuario) base_qs = Document.objects.filter(pedimento=pedimento) if not request.user.is_superuser: if not hasattr(request.user, 'organizacion') or request.user.organizacion != pedimento.organizacion: return Response({"error": "No autorizado"}, status=status.HTTP_403_FORBIDDEN) base_qs = base_qs.filter(organizacion=request.user.organizacion) docs = base_qs.select_related('pedimento') if not docs.exists(): return Response({"error": "No hay documentos para este pedimento"}, status=status.HTTP_404_NOT_FOUND) # 1. Crear un único buffer y ZIP para todos los archivos buffer = BytesIO() missing_files = [] # opcional: para informar después files_found = [] with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: for doc in docs: # 2. Validaciones if not doc.archivo.name: logger.warning("Documento %s no tiene archivo asociado", doc.id) missing_files.append(f"{doc.id} (sin archivo)") continue if not default_storage.exists(doc.archivo.name): logger.warning("Archivo no encontrado en disco: %s", doc.archivo.path) missing_files.append(f"{doc.id} ({doc.archivo.name})") continue files_found.append(f"{doc.id} ({doc.archivo.name})") # 3. Nombre seguro para dentro del ZIP file_name = slugify(doc.archivo.name.rsplit('/', 1)[-1].rsplit('.', 1)[0]) ext = doc.archivo.name.split('.')[-1] name_inside_zip = f"{file_name}.{ext}" # 4. Escribir el archivo dentro del ZIP with doc.archivo.open('rb') as f: zip_file.writestr(name_inside_zip, f.read()) # 5. Preparar respuesta buffer.seek(0) zip_name = slugify(f"expediente_{pedimento.pedimento_app}") response = HttpResponse(buffer, content_type='application/zip') response['Content-Disposition'] = f'attachment; filename={zip_name or "documentos"}.zip' if not files_found: return Response({"error": f"No hay documentos para este pedimento: {pedimento.pedimento_app}"}, status=status.HTTP_404_NOT_FOUND) # (Opcional) cabecera personalizada si faltaron archivos # if missing_files: # response['X-Missing-Files'] = ', '.join(missing_files) # return Response({"error": f"No hay documentos para este pedimento: {pedimento.pedimento_app}"}, status=status.HTTP_404_NOT_FOUND) return response class MultiPedimentoZipDownloadView(APIView): permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper)] my_tags = ['Documents'] def post(self, request): """ Descarga todos los documentos de VARIOS pedimentos en un solo ZIP. Body: { "pedimento_ids": ["uuid1", "uuid2", ...] } """ pedimento_ids = request.data.get('pedimento_ids', []) if not isinstance(pedimento_ids, list) or not pedimento_ids: return Response({"error": "Se requiere una lista de pedimento_ids"}, status=status.HTTP_400_BAD_REQUEST) # Filtrar pedimentos visibles para el usuario base_qs = Pedimento.objects.filter(id__in=pedimento_ids) if not request.user.is_superuser: if not hasattr(request.user, 'organizacion'): return Response({"error": "No autorizado"}, status=status.HTTP_403_FORBIDDEN) base_qs = base_qs.filter(organizacion=request.user.organizacion) pedimentos = base_qs.select_related('organizacion') if not pedimentos.exists(): return Response({"error": "Ningún pedimento encontrado o autorizado"}, status=status.HTTP_404_NOT_FOUND) # Obtener todos los documentos de esos pedimentos docs = Document.objects.filter(pedimento__in=pedimentos) if not docs.exists(): return Response({"error": "No hay documentos para estos pedimentos"}, status=status.HTTP_404_NOT_FOUND) # Crear ZIP único buffer = BytesIO() missing_files = [] summary = {} with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: for doc in docs: ped_key = doc.pedimento.pedimento_app if not doc.archivo.name or not default_storage.exists(doc.archivo.name): missing_files.append(f"{doc.id} ({doc.archivo.name or 'sin archivo'})") logger.warning("Archivo faltante: %s", doc.id) continue summary[ped_key] = summary.get(ped_key, 0) + 1 # Nombre seguro: pedimento_app + nombre del archivo file_name = slugify(doc.archivo.name.rsplit('/', 1)[-1].rsplit('.', 1)[0]) ext = doc.archivo.name.split('.')[-1] name_inside_zip = f"{doc.pedimento.pedimento_app}/{file_name}.{ext}" with doc.archivo.open('rb') as f: zip_file.writestr(name_inside_zip, f.read()) buffer.seek(0) zip_name = slugify(f"expedientes_{len(summary)}_pedimentos") response = HttpResponse(buffer, content_type='application/zip') response['Content-Disposition'] = f'attachment; filename={zip_name}.zip' response['X-Zip-Filename'] = f"{zip_name}.zip" response['Access-Control-Expose-Headers'] = 'X-Zip-Filename' if missing_files: response['X-Missing-Files'] = ', '.join(missing_files) return response class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin): """ ViewSet for Document model. """ permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )] model = Document pagination_class = CustomPagination serializer_class = DocumentSerializer # Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado) # filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento'] filterset_fields = ['extension', 'size', 'pedimento', 'pedimento__pedimento','fuente'] # Puedes filtrar por pedimento usando: /api/record/documents/?pedimento= o /api/record/documents/?pedimento__pedimento= # Ejemplo: /api/record/documents/?pedimento_numero=12345678 my_tags = ['Documents'] def get_queryset(self): queryset = self.get_queryset_filtrado_por_organizacion() # Tipos de documento permitidos (fijos en código, Pedimento completo y remesas) TIPOS_PERMITIDOS = ['2', '3'] # <-- Ajusta aquí tus tipos tipo_documento = self.request.query_params.get('document_type') if tipo_documento: queryset = queryset.filter(document_type_id=tipo_documento) else: # Filtrar por tipos permitidos queryset = queryset.filter(document_type_id__in=TIPOS_PERMITIDOS) buscar_archivo = self.request.query_params.get('archivo__icontains') if buscar_archivo: queryset = queryset.filter(archivo__icontains=buscar_archivo) created_at__date = self.request.query_params.get('created_at__date') if created_at__date: queryset = queryset.filter(created_at=created_at__date) # Filtro adicional por pedimento_numero si se proporciona pedimento_numero = self.request.query_params.get('pedimento_numero') if pedimento_numero: queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero) return queryset