|
|
|
|
@@ -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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|