diff --git a/api/customs/views.py b/api/customs/views.py index 0e6a2b5..7fb8ed2 100644 --- a/api/customs/views.py +++ b/api/customs/views.py @@ -7,6 +7,7 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.exceptions import PermissionDenied +from rest_framework import status from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter, OrderingFilter from core.permissions import ( @@ -231,6 +232,98 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada ] } + @action(detail=False, methods=['post'], url_path='bulk-delete') + def bulk_delete(self, request): + """ + Endpoint para eliminar múltiples pedimentos de manera masiva. + + Payload esperado: + { + "ids": ["uuid1", "uuid2", "uuid3", ...] + } + + Respuesta exitosa: + { + "message": "Pedimentos eliminados exitosamente", + "deleted_count": 3, + "deleted_ids": ["uuid1", "uuid2", "uuid3"] + } + + Respuesta con errores: + { + "message": "Algunos pedimentos no pudieron ser eliminados", + "deleted_count": 2, + "deleted_ids": ["uuid1", "uuid2"], + "failed_ids": ["uuid3"], + "errors": ["No se encontró el pedimento con ID uuid3"] + } + """ + # Obtener los IDs del payload + ids = request.data.get('ids', []) + + if not ids: + return Response( + {"error": "Se requiere una lista de IDs para eliminar"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not isinstance(ids, list): + return Response( + {"error": "El campo 'ids' debe ser una lista"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Obtener el queryset filtrado por organización + queryset = self.get_queryset() + + # Filtrar solo los pedimentos que existen y pertenecen a la organización del usuario + existing_pedimentos = queryset.filter(id__in=ids) + existing_ids = list(existing_pedimentos.values_list('id', flat=True)) + + # Convertir UUIDs a strings para comparación + existing_ids_str = [str(id) for id in existing_ids] + requested_ids_str = [str(id) for id in ids] + + # Identificar IDs que no existen o no pertenecen a la organización + failed_ids = [id for id in requested_ids_str if id not in existing_ids_str] + + deleted_count = 0 + errors = [] + + if existing_pedimentos.exists(): + try: + # Eliminar los pedimentos encontrados + deleted_count = existing_pedimentos.count() + existing_pedimentos.delete() + except Exception as e: + return Response( + {"error": f"Error al eliminar pedimentos: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # Agregar errores para IDs no encontrados + if failed_ids: + errors = [f"No se encontró el pedimento con ID {id} o no pertenece a su organización" for id in failed_ids] + + # Preparar respuesta + response_data = { + "deleted_count": deleted_count, + "deleted_ids": existing_ids_str + } + + if failed_ids: + response_data.update({ + "message": "Algunos pedimentos no pudieron ser eliminados", + "failed_ids": failed_ids, + "errors": errors + }) + response_status = status.HTTP_207_MULTI_STATUS + else: + response_data["message"] = "Pedimentos eliminados exitosamente" + response_status = status.HTTP_200_OK + + return Response(response_data, status=response_status) + my_tags = ['Pedimentos'] class PartidaViewSet(viewsets.ModelViewSet): diff --git a/api/record/views.py b/api/record/views.py index c11432a..a3a775d 100644 --- a/api/record/views.py +++ b/api/record/views.py @@ -163,6 +163,155 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin): uso.espacio_utilizado -= instance.size uso.save() instance.delete() + + @action(detail=False, methods=['post'], url_path='bulk-delete') + def bulk_delete(self, request): + """ + Endpoint para eliminar múltiples documentos de manera masiva. + + Payload esperado: + { + "ids": ["uuid1", "uuid2", "uuid3", ...] + } + + Respuesta exitosa: + { + "message": "Documentos eliminados exitosamente", + "deleted_count": 3, + "deleted_ids": ["uuid1", "uuid2", "uuid3"], + "space_freed_mb": 25.6 + } + + Respuesta con errores: + { + "message": "Algunos documentos no pudieron ser eliminados", + "deleted_count": 2, + "deleted_ids": ["uuid1", "uuid2"], + "failed_ids": ["uuid3"], + "errors": ["No se encontró el documento con ID uuid3"], + "space_freed_mb": 15.2 + } + """ + # Obtener los IDs del payload + ids = request.data.get('ids', []) + + if not ids: + return Response( + {"error": "Se requiere una lista de IDs para eliminar"}, + status=status.HTTP_400_BAD_REQUEST + ) + + if not isinstance(ids, list): + return Response( + {"error": "El campo 'ids' debe ser una lista"}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Obtener el queryset filtrado por organización + queryset = self.get_queryset() + + # Filtrar solo los documentos que existen y pertenecen a la organización del usuario + existing_documents = queryset.filter(id__in=ids) + existing_ids = list(existing_documents.values_list('id', flat=True)) + + # Convertir UUIDs a strings para comparación + existing_ids_str = [str(id) for id in existing_ids] + requested_ids_str = [str(id) for id in ids] + + # Identificar IDs que no existen o no pertenecen a la organización + failed_ids = [id for id in requested_ids_str if id not in existing_ids_str] + + deleted_count = 0 + total_space_freed = 0 + errors = [] + + if existing_documents.exists(): + try: + # Usar transacción atómica para consistencia + with transaction.atomic(): + # Calcular el espacio total a liberar + total_space_freed = sum(doc.size for doc in existing_documents) + + # Obtener la organización del usuario para actualizar el uso de almacenamiento + 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 + + # Si es superusuario, puede eliminar documentos de cualquier organización + if request.user.is_superuser: + # Para superusuario, actualizar el uso de cada organización afectada + organizaciones_afectadas = {} + for doc in existing_documents: + if doc.organizacion.id not in organizaciones_afectadas: + organizaciones_afectadas[doc.organizacion.id] = { + 'organizacion': doc.organizacion, + 'espacio_liberado': 0 + } + organizaciones_afectadas[doc.organizacion.id]['espacio_liberado'] += doc.size + + # Actualizar uso de almacenamiento para cada organización + for org_data in organizaciones_afectadas.values(): + try: + uso = UsoAlmacenamiento.objects.select_for_update().get( + organizacion=org_data['organizacion'] + ) + uso.espacio_utilizado -= org_data['espacio_liberado'] + uso.save() + except UsoAlmacenamiento.DoesNotExist: + # Si no existe el registro, no hay nada que actualizar + pass + else: + # Para usuarios normales, solo documentos de su organización + try: + uso = UsoAlmacenamiento.objects.select_for_update().get( + organizacion=organizacion + ) + uso.espacio_utilizado -= total_space_freed + uso.save() + except UsoAlmacenamiento.DoesNotExist: + # Si no existe el registro, no hay nada que actualizar + pass + + # Eliminar los documentos + deleted_count = existing_documents.count() + existing_documents.delete() + + except Exception as e: + return Response( + {"error": f"Error al eliminar documentos: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + + # Agregar errores para IDs no encontrados + if failed_ids: + errors = [f"No se encontró el documento con ID {id} o no pertenece a su organización" for id in failed_ids] + + # Convertir bytes a MB para la respuesta + space_freed_mb = round(total_space_freed / (1024 * 1024), 2) + + # Preparar respuesta + response_data = { + "deleted_count": deleted_count, + "deleted_ids": existing_ids_str, + "space_freed_mb": space_freed_mb + } + + if failed_ids: + response_data.update({ + "message": "Algunos documentos no pudieron ser eliminados", + "failed_ids": failed_ids, + "errors": errors + }) + response_status = status.HTTP_207_MULTI_STATUS + else: + response_data["message"] = "Documentos eliminados exitosamente" + response_status = status.HTTP_200_OK + + return Response(response_data, status=response_status) class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin): permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]