2 Commits

3 changed files with 589 additions and 5 deletions

View File

@@ -38,7 +38,68 @@ from api.customs.serializers import (
from api.logger.mixins import LoggingMixin from api.logger.mixins import LoggingMixin
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin, ProcesosPorOrganizacionMixin from mixins.filtrado_organizacion import OrganizacionFiltradaMixin, ProcesosPorOrganizacionMixin
import requests import requests
import os
import re
import zipfile
import tempfile
import shutil
import subprocess
from datetime import datetime
from django.core.files.base import ContentFile
from django.db import transaction
from rest_framework.parsers import MultiPartParser, FormParser
from api.record.models import Document, DocumentType
# Importar rarfile de manera opcional
try:
import rarfile
RAR_SUPPORT = True
except ImportError:
RAR_SUPPORT = False
def extract_rar_to_dir(rar_path, dest_dir):
"""
Extrae un archivo RAR a `dest_dir` usando varios mecanismos de respaldo:
1) intentará usar la librería `rarfile` si está disponible,
2) intentará ejecutar la utilidad `unrar` si está instalada en el sistema,
3) intentará ejecutar `7z`/`7za` (p7zip) si está instalada.
Lanza Exception con mensaje explicativo si falla.
"""
# Intento con rarfile (Python)
if RAR_SUPPORT:
try:
# rarfile puede trabajar con rutas en disco mejor que con file-like
with rarfile.RarFile(rar_path) as rf:
rf.extractall(dest_dir)
return
except Exception as e:
# Si rarfile falla (por ejemplo RarCannotExec), seguimos con herramientas externas
# Hacer log para depuración
print(f"rarfile extraction failed, will try external tools: {e}")
# Intento con comandos externos
# Probar 'unrar' primero
external_cmds = [
['unrar', 'x', '-o+', rar_path, dest_dir],
['7z', 'x', rar_path, f'-o{dest_dir}', '-y'],
['7za', 'x', rar_path, f'-o{dest_dir}', '-y']
]
for cmd in external_cmds:
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return
except FileNotFoundError:
# El ejecutable no existe en PATH, intentar siguiente
continue
except subprocess.CalledProcessError as e:
# El comando falló en la extracción; intentar siguiente
print(f"External extractor failed ({cmd[0]}): {e}")
continue
raise Exception("No se encontró una herramienta válida para extraer RAR (rarfile sin backend, 'unrar' o '7z' no disponibles o extracción fallida). Instale 'unrar' o 'p7zip' y asegúrese de que estén en PATH, o configure rarfile con un backend.")
from .tasks.microservice_v2 import * from .tasks.microservice_v2 import *
from .tasks.auditoria import crear_partidas_por_pedimento from .tasks.auditoria import crear_partidas_por_pedimento
@@ -324,6 +385,380 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
@action(detail=False, methods=['post'], url_path='bulk-create', parser_classes=[MultiPartParser, FormParser])
def bulk_create(self, request):
"""
Endpoint para crear múltiples pedimentos de manera masiva desde archivos.
FormData esperado:
- contribuyente: string (nombre del contribuyente)
- archivos: files (pueden ser múltiples archivos: zip, rar o individuales)
Nomenclatura esperada de archivos: anio-aduana-patente-pedimento
- anio: 2 dígitos (ej: 24)
- aduana: 2 o 3 dígitos (ej: 01, 123)
- patente: 4 dígitos (ej: 3420)
- pedimento: 7 dígitos (ej: 1234567)
Ejemplo: 24-01-3420-1234567
Nota: Cada archivo ZIP/RAR se procesa independientemente en su propio subdirectorio.
Respuesta exitosa:
{
"message": "Pedimentos creados exitosamente",
"created_count": 5,
"created_pedimentos": [...],
"documents_created": 15,
"processed_files": 3,
"summary": "Procesados 3 archivo(s): 5 pedimento(s) creado(s), 15 documento(s) asociado(s)",
"failed_files": [],
"errors": []
}
"""
print(request.data)
# Validar datos requeridos
contribuyente = request.data.get('contribuyente')
archivos = request.FILES.getlist('archivos')
if not contribuyente:
return Response(
{"error": "Se requiere el campo 'contribuyente'"},
status=status.HTTP_400_BAD_REQUEST
)
if not archivos:
return Response(
{"error": "Se requiere al menos un archivo"},
status=status.HTTP_400_BAD_REQUEST
)
# Validar organización del usuario
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
# Regex para validar nomenclatura: anio-aduana-patente-pedimento
nomenclatura_pattern = re.compile(r'^(\d{2})-(\d{2,3})-(\d{4})-(\d{7})$')
created_pedimentos = []
failed_files = []
errors = []
documents_created = 0
temp_dir = None
# Obtener DocumentType ANTES de la transacción atómica
print("Intentando obtener o crear DocumentType...")
try:
# Primero intentar obtener si ya existe
try:
document_type = DocumentType.objects.get(nombre="Pedimento")
print(f"DocumentType obtenido existente: {document_type.nombre} (ID: {document_type.id})")
except DocumentType.DoesNotExist:
# Si no existe, crear uno nuevo
document_type = DocumentType.objects.create(
nombre="Pedimento",
descripcion="Documento de pedimento"
)
print(f"DocumentType creado nuevo: {document_type.nombre} (ID: {document_type.id})")
except Exception as e:
print(f"Error al obtener/crear DocumentType: {str(e)}")
# Como fallback, intentar obtener cualquier DocumentType existente
try:
document_type = DocumentType.objects.first()
if document_type:
print(f"Usando DocumentType existente como fallback: {document_type.nombre} (ID: {document_type.id})")
else:
print("No hay DocumentType disponible")
return Response(
{"error": "No se pudo configurar el tipo de documento y no hay tipos existentes"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except Exception as fallback_error:
print(f"Error en fallback: {str(fallback_error)}")
return Response(
{"error": f"Error crítico al configurar tipo de documento: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
try:
print("Iniciando transacción atómica...")
with transaction.atomic():
# Crear directorio temporal
temp_dir = tempfile.mkdtemp()
print(f"Directorio temporal creado: {temp_dir}")
# Procesar cada archivo enviado
for idx, archivo in enumerate(archivos):
archivo_name = archivo.name.lower()
print(f"Procesando archivo {idx + 1}/{len(archivos)}: {archivo_name}")
# Crear subdirectorio para cada archivo usando el nombre del archivo sin extensión
archivo_name_sin_extension = os.path.splitext(archivo.name)[0]
sub_dir = os.path.join(temp_dir, archivo_name_sin_extension)
os.makedirs(sub_dir, exist_ok=True)
print(f"Subdirectorio creado: {sub_dir}")
if archivo_name.endswith('.zip'):
# Manejar archivo ZIP
print("Es un archivo ZIP")
try:
with zipfile.ZipFile(archivo, 'r') as zip_ref:
zip_ref.extractall(sub_dir)
print("Archivo ZIP extraído exitosamente")
except zipfile.BadZipFile as e:
return Response(
{"error": f"Archivo ZIP corrupto o inválido: {archivo.name} - {str(e)}"},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
return Response(
{"error": f"Error al extraer ZIP {archivo.name}: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST
)
elif archivo_name.endswith('.rar'):
# Manejar archivo RAR: guardar el archivo en disco y usar helper con fallbacks
# Guardar el archivo subido en un path temporal dentro del sub_dir
archivo_temp_path = os.path.join(sub_dir, archivo.name)
with open(archivo_temp_path, 'wb') as f:
for chunk in archivo.chunks():
f.write(chunk)
try:
extract_rar_to_dir(archivo_temp_path, sub_dir)
print(f"Archivo RAR {archivo.name} extraído en {sub_dir}")
except Exception as e:
error_msg = str(e)
help_msg = "Instale 'unrar' o 'p7zip' (7z) y asegúrese de que estén en PATH, o instale y configure 'rarfile' con un backend."
return Response(
{"error": f"Error al extraer archivo RAR {archivo.name}: {error_msg}. {help_msg}"},
status=status.HTTP_400_BAD_REQUEST
)
# if not RAR_SUPPORT:
# return Response(
# {"error": "Soporte para archivos RAR no disponible. Instalar rarfile: pip install rarfile"},
# status=status.HTTP_400_BAD_REQUEST
# )
# try:
# with rarfile.RarFile(archivo, 'r') as rar_ref:
# rar_ref.extractall(sub_dir)
# print(f"Archivo RAR {archivo.name} extraído en sub_dir")
# except rarfile.Error as e:
# return Response(
# {"error": f"Error al extraer archivo RAR {archivo.name}: {str(e)}"},
# status=status.HTTP_400_BAD_REQUEST
# )
else:
# Asumir que es un archivo individual
# Crear el archivo en el subdirectorio
archivo_path = os.path.join(sub_dir, archivo.name)
with open(archivo_path, 'wb') as f:
for chunk in archivo.chunks():
f.write(chunk)
print(f"Archivo individual {archivo.name} guardado en sub_dir:", archivo_path)
# Recorrer todos los archivos extraídos o el directorio
print("Iniciando recorrido de archivos...")
for root, dirs, files in os.walk(temp_dir):
print(f"Revisando directorio: {root}")
print(f"Archivos encontrados: {files}")
for file_name in files:
print(f"Procesando archivo: {file_name}")
file_path = os.path.join(root, file_name)
# Obtener la ruta relativa para determinar la estructura de carpetas
relative_path = os.path.relpath(file_path, temp_dir)
print(f"Ruta relativa: {relative_path}")
# Determinar si el archivo está en una carpeta que sigue la nomenclatura
folder_name = None
if os.path.dirname(relative_path):
# El archivo está dentro de una carpeta
folder_parts = relative_path.split(os.sep)
folder_name = folder_parts[0] # Primera carpeta (nombre del archivo ZIP/RAR sin extensión)
else:
# El archivo está en la raíz, usar el nombre del archivo sin extensión
folder_name = os.path.splitext(file_name)[0]
print(f"Folder name para validación: {folder_name}")
# Validar nomenclatura
match = nomenclatura_pattern.match(folder_name)
if not match:
print(f"Nomenclatura inválida: {folder_name}")
# Determinar el archivo original basado en el subdirectorio
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Nomenclatura inválida: {folder_name}. Esperado: anio-aduana-patente-pedimento"
})
continue
print(f"Nomenclatura válida: {folder_name}")
anio, aduana, patente, pedimento_num = match.groups()
print(f"Extraído - Año: {anio}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento_num}")
# Crear fecha_pago basada en el año
try:
# Convertir año de 2 dígitos a 4 dígitos
anio_completo = 2000 + int(anio) if int(anio) < 50 else 1900 + int(anio)
fecha_pago = datetime(anio_completo, 1, 1).date()
print(f"Fecha de pago calculada: {fecha_pago}")
except ValueError:
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Año inválido: {anio}"
})
continue
# Generar pedimento_app
pedimento_app = f"{anio}-{aduana.zfill(2)}-{patente}-{pedimento_num}"
print(f"Pedimento_app generado: {pedimento_app}")
print(f"Buscando pedimento existente con pedimento_app: {pedimento_app} y organización ID: {organizacion.id}")
# Verificar si el pedimento ya existe
existing_pedimento = Pedimento.objects.filter(
pedimento_app=pedimento_app,
organizacion=organizacion
).first()
print(f"Pedimento existente: {existing_pedimento is not None}")
if not existing_pedimento:
print("📝 Pedimento no existe, creando nuevo...")
# Crear nuevo pedimento
try:
print("🔄 Iniciando creación de pedimento...")
# Obtener o crear el importador
print(f"🏢 Buscando/creando importador con RFC: {contribuyente}")
importador, created = Importador.objects.get_or_create(
rfc=contribuyente,
defaults={
'nombre': f"Importador {contribuyente}",
'organizacion': organizacion
}
)
if created:
print(f"✅ Importador creado: {importador.rfc} - {importador.nombre}")
else:
print(f"♻️ Importador existente: {importador.rfc} - {importador.nombre}")
pedimento = Pedimento.objects.create(
organizacion=organizacion,
contribuyente=importador,
pedimento=int(pedimento_num),
aduana=int(aduana),
patente=int(patente),
fecha_pago=fecha_pago,
pedimento_app=pedimento_app,
agente_aduanal=f"Agente {patente}", # Valor por defecto
clave_pedimento="A1" # Valor por defecto
)
print(f"✅ Pedimento creado exitosamente: ID {pedimento.id}, pedimento_app: {pedimento_app}")
created_pedimentos.append({
"id": str(pedimento.id),
"pedimento_app": pedimento_app,
"contribuyente": importador.rfc,
"contribuyente_nombre": importador.nombre
})
except Exception as e:
print(f"❌ Error al crear pedimento: {str(e)}")
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Error al crear pedimento: {str(e)}"
})
continue
else:
print(f"♻️ Usando pedimento existente: ID {existing_pedimento.id}")
# Usar pedimento existente
pedimento = existing_pedimento
print(f"🔄 Iniciando creación de documento para pedimento ID: {pedimento.id}")
# Crear documento asociado al pedimento
try:
print("📖 Leyendo archivo desde directorio temporal...")
# Leer el archivo desde el directorio temporal
with open(file_path, 'rb') as f:
file_content = f.read()
print(f"📄 Archivo leído: {len(file_content)} bytes")
# Crear ContentFile que Django puede manejar correctamente
django_file = ContentFile(file_content, name=file_name)
print(f"Creando documento para archivo: {file_name}")
# Crear documento - Django automáticamente guardará el archivo en media/documents/
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento.id,
document_type=document_type,
archivo=django_file,
size=len(file_content),
extension=os.path.splitext(file_name)[1].lower().lstrip('.')
)
print(f"Documento creado exitosamente: {document.id}")
documents_created += 1
print(f"📊 Total documentos creados hasta ahora: {documents_created}")
except Exception as e:
print(f"❌ Error al crear documento: {str(e)}")
archivo_original = folder_name + ('.zip' if any(f.endswith('.zip') for f in [a.name for a in archivos]) else '.rar')
failed_files.append({
"file": relative_path,
"archivo_original": archivo_original,
"error": f"Error al crear documento: {str(e)}"
})
continue
print(f"🏁 Procesamiento completado. Archivos procesados en este directorio.")
except Exception as e:
return Response(
{"error": f"Error durante el procesamiento: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
finally:
# Limpiar directorio temporal
if temp_dir and os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
# Preparar respuesta
response_data = {
"created_count": len(created_pedimentos),
"created_pedimentos": created_pedimentos,
"documents_created": documents_created,
"failed_files": failed_files,
"processed_files": len(archivos),
"summary": f"Procesados {len(archivos)} archivo(s): {len(created_pedimentos)} pedimento(s) creado(s), {documents_created} documento(s) asociado(s)"
}
if failed_files:
response_data.update({
"message": "Procesamiento completado con algunos errores",
"errors": [item["error"] for item in failed_files]
})
response_status = status.HTTP_207_MULTI_STATUS
else:
response_data["message"] = "Pedimentos creados exitosamente"
response_status = status.HTTP_201_CREATED
return Response(response_data, status=response_status)
my_tags = ['Pedimentos'] my_tags = ['Pedimentos']
class PartidaViewSet(viewsets.ModelViewSet): class PartidaViewSet(viewsets.ModelViewSet):

View File

@@ -4,12 +4,12 @@ from rest_framework.routers import DefaultRouter
# import necessary viewsets # import necessary viewsets
# from .views import YourViewSet # Import your viewsets here # 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 # Create a router and register your viewsets with it
router = DefaultRouter() router = DefaultRouter()
# Register your viewsets with the router here # Register your viewsets with the router he -fre
# Example: # Example:
# from .views import MyViewSet # from .views import MyViewSet
# router.register(r'myviewset', MyViewSet, basename='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('documents/descargar/<uuid:pk>/', ProtectedDocumentDownloadView.as_view(), name='descargar-documento'),
path('fuente/', GetFuenteView.as_view(), name='get-fuente'), path('fuente/', GetFuenteView.as_view(), name='get-fuente'),
path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'), 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)), path('', include(router.urls)),
] ]

View File

@@ -13,6 +13,7 @@ from rest_framework.exceptions import ValidationError
from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer
from .models import Document, Fuente, DocumentType from .models import Document, Fuente, DocumentType
from ..customs.models import Pedimento
from api.organization.models import UsoAlmacenamiento from api.organization.models import UsoAlmacenamiento
from io import BytesIO from io import BytesIO
import zipfile import zipfile
@@ -32,6 +33,9 @@ from core.permissions import (
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import os
from django.core.files.storage import default_storage
from mixins.filtrado_organizacion import DocumentosFiltradosMixin from mixins.filtrado_organizacion import DocumentosFiltradosMixin
class CustomPagination(PageNumberPagination): class CustomPagination(PageNumberPagination):
@@ -531,7 +535,6 @@ class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'): if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
raise Http404("Usuario no autenticado") raise Http404("Usuario no autenticado")
try: try:
doc = Document.objects.get(pk=pk) doc = Document.objects.get(pk=pk)
except Document.DoesNotExist: except Document.DoesNotExist:
@@ -618,3 +621,147 @@ class DocumentTypeView(APIView):
return Response({"detail": "No hay tipos de documento disponibles."}, status=404) return Response({"detail": "No hay tipos de documento disponibles."}, status=404)
serializer = self.serializer_class(queryset, many=True) 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