Se habilita opcion de descarga de pedimentos individuales, masivos, por filtro.

This commit is contained in:
2025-11-24 08:25:59 -07:00
parent 4b44c098c4
commit 72c0d70a71
2 changed files with 153 additions and 4 deletions

View File

@@ -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/<uuid:pk>/', 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)),
]

View File

@@ -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)
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": "<uuid>" }
"""
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