feat: Mejorar endpoints de carga masiva de documentos
✨ Nuevas funcionalidades: - Corregir nomenclatura en bulk-create de pedimentos usando nombres exactos de archivos - Endpoint bulk-upload para cargar múltiples documentos a un pedimento existente - Soporte completo para archivos RAR y ZIP con manejo robusto 🔧 Mejoras técnicas bulk-create: - Subdirectorios usan nombre exacto del archivo sin extensión (ej: 24-01-3420-1234567/) - Resolución del problema de validación de nomenclatura inválida - Mensajes de error mejorados con archivo original específico - Procesamiento optimizado de múltiples archivos ZIP/RAR simultáneos 🔧 Mejoras técnicas bulk-upload: - Organización heredada del pedimento en lugar del usuario - Validación de cuotas de almacenamiento por organización - Manejo de errores por archivo individual - Soporte para múltiples tipos de archivo 📦 Dependencias: - Agregado rarfile==4.1 para soporte completo de archivos RAR 🚀 Endpoints listos para producción: - POST /api/customs/pedimentos/bulk-create/ (crear pedimentos + documentos) - POST /api/record/documents/bulk-upload/ (subir documentos a pedimento existente)
This commit is contained in:
@@ -312,6 +312,211 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
||||
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)]
|
||||
|
||||
Reference in New Issue
Block a user