From 72c0d70a712bf9c9440de1efd6cbb147d5ccf752 Mon Sep 17 00:00:00 2001 From: Luis Date: Mon, 24 Nov 2025 08:25:59 -0700 Subject: [PATCH] Se habilita opcion de descarga de pedimentos individuales, masivos, por filtro. --- api/record/urls.py | 6 +- api/record/views.py | 151 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/api/record/urls.py b/api/record/urls.py index 339e8bd..e8781a8 100644 --- a/api/record/urls.py +++ b/api/record/urls.py @@ -4,12 +4,12 @@ from rest_framework.routers import DefaultRouter # import necessary viewsets # from .views import YourViewSet # Import your viewsets here -from .views import DocumentViewSet, ProtectedDocumentDownloadView, BulkDownloadZipView, GetFuenteView, DocumentTypeView +from .views import DocumentViewSet, ProtectedDocumentDownloadView, BulkDownloadZipView, GetFuenteView, DocumentTypeView, ExpedienteZipDownloadView, MultiPedimentoZipDownloadView # Create a router and register your viewsets with it router = DefaultRouter() -# Register your viewsets with the router here +# Register your viewsets with the router he -fre # Example: # from .views import MyViewSet # router.register(r'myviewset', MyViewSet, basename='myviewset') @@ -23,5 +23,7 @@ urlpatterns = [ path('documents/descargar//', ProtectedDocumentDownloadView.as_view(), name='descargar-documento'), path('fuente/', GetFuenteView.as_view(), name='get-fuente'), path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'), + path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'), + path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'), path('', include(router.urls)), ] \ No newline at end of file diff --git a/api/record/views.py b/api/record/views.py index 9e7d7a7..7c33d45 100644 --- a/api/record/views.py +++ b/api/record/views.py @@ -13,6 +13,7 @@ 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 @@ -32,6 +33,9 @@ from core.permissions import ( 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): @@ -530,7 +534,6 @@ class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin): 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) @@ -617,4 +620,148 @@ class DocumentTypeView(APIView): 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) \ No newline at end of file + 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 + + + + \ No newline at end of file