fix: se agrega funcionalidad de poder seleccionar resgistros en la vistas de partidas, coves, pedimento, edoc. Tambien se habilito la funcionalidad de poder eliminar los registros seleccionados.
This commit is contained in:
@@ -23,6 +23,7 @@ from django.http import HttpResponse
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from core.permissions import (
|
from core.permissions import (
|
||||||
IsSameOrganization,
|
IsSameOrganization,
|
||||||
@@ -461,6 +462,602 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
|||||||
|
|
||||||
return Response(response_data, status=response_status)
|
return Response(response_data, status=response_status)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path='bulk-delete-partidas-vu')
|
||||||
|
def bulk_delete_partidas_vu(self, request):
|
||||||
|
"""
|
||||||
|
Endpoint para eliminar múltiples archivos xlm de partidas de vu 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_vu = request.data.get('ids', [])
|
||||||
|
|
||||||
|
if not ids_vu:
|
||||||
|
return Response(
|
||||||
|
{"error": "Se requiere una lista de IDs para eliminar"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(ids_vu, 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()
|
||||||
|
|
||||||
|
from ..customs.models import Partida
|
||||||
|
|
||||||
|
partidas = Partida.objects.filter(id__in=ids_vu)
|
||||||
|
if not partidas.exists():
|
||||||
|
return Response(
|
||||||
|
{"error": "No se encontraron Partidas"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
ids = []
|
||||||
|
for partida in partidas:
|
||||||
|
|
||||||
|
pedimento_partida = partida.pedimento
|
||||||
|
pedimento_app = pedimento_partida.pedimento_app
|
||||||
|
pedimento_id= pedimento_partida.id
|
||||||
|
|
||||||
|
numero_partida = partida.numero_partida
|
||||||
|
|
||||||
|
documents = Document.objects.filter(
|
||||||
|
archivo__startswith=f'documents/vu_PT_{pedimento_app}_{numero_partida}',
|
||||||
|
pedimento_id=pedimento_id
|
||||||
|
).values_list('id', flat=True) # <-- solo los IDs
|
||||||
|
|
||||||
|
if documents.exists():
|
||||||
|
# agregar los IDs a la lista
|
||||||
|
ids.extend(documents)
|
||||||
|
|
||||||
|
|
||||||
|
if len(ids) <= 0:
|
||||||
|
return Response(
|
||||||
|
{"error": "No se encontraron docuemntos para eliminar"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
for doc in existing_documents:
|
||||||
|
archivos_eliminados = 0
|
||||||
|
try:
|
||||||
|
# Eliminar archivo físico
|
||||||
|
if doc.archivo and doc.archivo.storage.exists(doc.archivo.name):
|
||||||
|
doc.archivo.delete(save=False) # save=False para no intentar guardar el modelo
|
||||||
|
|
||||||
|
# Eliminar registro de la base de datos
|
||||||
|
doc.delete()
|
||||||
|
archivos_eliminados += 1
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"No se pudo eliminar el documento {doc.id}: {str(e)}")
|
||||||
|
failed_ids.append(str(doc.id))
|
||||||
|
|
||||||
|
# deleted_count = existing_documents.count()
|
||||||
|
deleted_count = archivos_eliminados
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path='bulk-delete-coves-vu')
|
||||||
|
def bulk_delete_coves_vu(self, request):
|
||||||
|
"""
|
||||||
|
Endpoint para eliminar múltiples archivos xlm de coves de vu 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_vu = request.data.get('ids', [])
|
||||||
|
|
||||||
|
if not ids_vu:
|
||||||
|
return Response(
|
||||||
|
{"error": "Se requiere una lista de IDs para eliminar"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(ids_vu, 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()
|
||||||
|
|
||||||
|
from ..customs.models import Cove
|
||||||
|
|
||||||
|
coves = Cove.objects.filter(id__in=ids_vu)
|
||||||
|
if not coves.exists():
|
||||||
|
return Response(
|
||||||
|
{"error": "No se encontraron COVEs"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
ids = []
|
||||||
|
for cove in coves:
|
||||||
|
|
||||||
|
pedimento_cove = cove.pedimento
|
||||||
|
pedimento_app = pedimento_cove.pedimento_app
|
||||||
|
pedimento_id=pedimento_cove.id
|
||||||
|
|
||||||
|
numero_cove = cove.numero_cove
|
||||||
|
|
||||||
|
documents = Document.objects.filter(
|
||||||
|
Q(archivo__startswith=f'documents/vu_COVE_{pedimento_app}_{numero_cove}') |
|
||||||
|
Q(archivo__startswith=f'documents/vu_AC_COVE_{pedimento_app}_{numero_cove}'),
|
||||||
|
pedimento_id=pedimento_id
|
||||||
|
).values_list('id', flat=True) # <-- solo los IDs
|
||||||
|
|
||||||
|
if documents.exists():
|
||||||
|
# agregar los IDs a la lista
|
||||||
|
ids.extend(documents)
|
||||||
|
|
||||||
|
|
||||||
|
if len(ids) <= 0:
|
||||||
|
return Response(
|
||||||
|
{"error": "No se encontraron docuemntos para eliminar"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
for doc in existing_documents:
|
||||||
|
archivos_eliminados = 0
|
||||||
|
try:
|
||||||
|
# Eliminar archivo físico
|
||||||
|
if doc.archivo and doc.archivo.storage.exists(doc.archivo.name):
|
||||||
|
doc.archivo.delete(save=False) # save=False para no intentar guardar el modelo
|
||||||
|
|
||||||
|
# Eliminar registro de la base de datos
|
||||||
|
doc.delete()
|
||||||
|
archivos_eliminados += 1
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"No se pudo eliminar el documento {doc.id}: {str(e)}")
|
||||||
|
failed_ids.append(str(doc.id))
|
||||||
|
|
||||||
|
# deleted_count = existing_documents.count()
|
||||||
|
deleted_count = archivos_eliminados
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path='bulk-delete-edocs-vu')
|
||||||
|
def bulk_delete_edocs_vu(self, request):
|
||||||
|
"""
|
||||||
|
Endpoint para eliminar múltiples archivos xlm de edocs de vu 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_vu = request.data.get('ids', [])
|
||||||
|
|
||||||
|
if not ids_vu:
|
||||||
|
return Response(
|
||||||
|
{"error": "Se requiere una lista de IDs para eliminar"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(ids_vu, 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()
|
||||||
|
|
||||||
|
from ..customs.models import EDocument
|
||||||
|
|
||||||
|
edocs = EDocument.objects.filter(id__in=ids_vu)
|
||||||
|
if not edocs.exists():
|
||||||
|
return Response(
|
||||||
|
{"error": "No se encontraron COVEs"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
ids = []
|
||||||
|
for edoc in edocs:
|
||||||
|
|
||||||
|
pedimento_edoc = edoc.pedimento
|
||||||
|
pedimento_id = pedimento_edoc.id
|
||||||
|
pedimento_app = pedimento_edoc.pedimento_app
|
||||||
|
|
||||||
|
numero_edocument = edoc.numero_edocument
|
||||||
|
|
||||||
|
documents = Document.objects.filter(
|
||||||
|
Q(archivo__startswith=f'documents/vu_ED_{pedimento_app}_{numero_edocument}') |
|
||||||
|
Q(archivo__startswith=f'documents/vu_AC_{pedimento_app}_{numero_edocument}'),
|
||||||
|
pedimento_id=pedimento_id
|
||||||
|
).values_list('id', flat=True) # <-- solo los IDs
|
||||||
|
|
||||||
|
if documents.exists():
|
||||||
|
# agregar los IDs a la lista
|
||||||
|
ids.extend(documents)
|
||||||
|
|
||||||
|
|
||||||
|
if len(ids) <= 0:
|
||||||
|
return Response(
|
||||||
|
{"error": "No se encontraron docuemntos para eliminar"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
archivos_eliminados = 0
|
||||||
|
for doc in existing_documents:
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Eliminar archivo físico
|
||||||
|
if doc.archivo and doc.archivo.storage.exists(doc.archivo.name):
|
||||||
|
doc.archivo.delete(save=False) # save=False para no intentar guardar el modelo
|
||||||
|
|
||||||
|
# Eliminar registro de la base de datos
|
||||||
|
doc.delete()
|
||||||
|
archivos_eliminados += 1
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"No se pudo eliminar el documento {doc.id}: {str(e)}")
|
||||||
|
failed_ids.append(str(doc.id))
|
||||||
|
|
||||||
|
# deleted_count = existing_documents.count()
|
||||||
|
deleted_count = archivos_eliminados
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
@action(detail=False, methods=['post'], url_path='bulk-upload', parser_classes=[MultiPartParser])
|
@action(detail=False, methods=['post'], url_path='bulk-upload', parser_classes=[MultiPartParser])
|
||||||
def bulk_upload(self, request):
|
def bulk_upload(self, request):
|
||||||
"""
|
"""
|
||||||
@@ -666,6 +1263,295 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
|||||||
|
|
||||||
return Response(response_data, status=response_status)
|
return Response(response_data, status=response_status)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path='bulk-upload-vu', parser_classes=[MultiPartParser])
|
||||||
|
def bulk_upload_vu(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
|
||||||
|
)
|
||||||
|
|
||||||
|
tab_seccion = request.data.get('tab_seccion')
|
||||||
|
if not tab_seccion:
|
||||||
|
return Response(
|
||||||
|
{"error": "Se requiere el campo 'tab_seccion'"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
numero_documento = request.data.get('numero')
|
||||||
|
if not numero_documento:
|
||||||
|
return Response(
|
||||||
|
{"error": "Se requiere el campo 'numero'"},
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
nuevo_nombre = file.name
|
||||||
|
|
||||||
|
# Validaciones por archivo
|
||||||
|
if not file.name:
|
||||||
|
failed_files.append("archivo_sin_nombre")
|
||||||
|
errors.append("Archivo sin nombre detectado")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# secciones = file.name.split('.')[-1].lower() if '.' in file.name else ''
|
||||||
|
|
||||||
|
filename = file.name
|
||||||
|
if '.' in filename:
|
||||||
|
base = '.'.join(filename.split('.')[:-1]) # todo excepto la última parte
|
||||||
|
secciones = filename.split('.')[-1] # la última “extensión” / flag
|
||||||
|
else:
|
||||||
|
base = filename
|
||||||
|
secciones = ""
|
||||||
|
|
||||||
|
file.name = base
|
||||||
|
|
||||||
|
# Obtener extensión del archivo
|
||||||
|
extension = file.name.split('.')[-1].lower() if '.' in file.name else ''
|
||||||
|
|
||||||
|
if tab_seccion == 'partida':
|
||||||
|
|
||||||
|
# Construir nombre nuevo
|
||||||
|
nuevo_nombre = f"vu_PT_{pedimento.pedimento_app}_{numero_documento}.{extension}"
|
||||||
|
|
||||||
|
# Usar tipo de documento por defecto siempre
|
||||||
|
document_type, created = DocumentType.objects.get_or_create(
|
||||||
|
nombre="Pedimento Partida",
|
||||||
|
defaults={'descripcion': "Tag para saber que el archivo guarda una partida"}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif tab_seccion == 'cove':
|
||||||
|
|
||||||
|
if secciones == 'general':
|
||||||
|
nuevo_nombre = f"vu_COVE_{pedimento.pedimento_app}_{numero_documento}.{extension}"
|
||||||
|
# Usar tipo de documento por defecto siempre
|
||||||
|
document_type, created = DocumentType.objects.get_or_create(
|
||||||
|
nombre="Cove",
|
||||||
|
defaults={'descripcion': "Tag para saber que el archivo guarda un cove"}
|
||||||
|
)
|
||||||
|
elif secciones == 'acuse':
|
||||||
|
nuevo_nombre = f"vu_AC_COVE_{pedimento.pedimento_app}_{numero_documento}.{extension}"
|
||||||
|
# Usar tipo de documento por defecto siempre
|
||||||
|
document_type, created = DocumentType.objects.get_or_create(
|
||||||
|
nombre="Acuse Cove",
|
||||||
|
defaults={'descripcion': "Tag para saber que el archivo guarda un acuse de cove"}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif tab_seccion == 'edoc':
|
||||||
|
|
||||||
|
if secciones == 'general':
|
||||||
|
nuevo_nombre = f"vu_ED_{pedimento.pedimento_app}_{numero_documento}.{extension}"
|
||||||
|
# Usar tipo de documento por defecto siempre
|
||||||
|
document_type, created = DocumentType.objects.get_or_create(
|
||||||
|
nombre="Pedimento EDocument",
|
||||||
|
defaults={'descripcion': "Tag para saber que el documento es un EDocument"}
|
||||||
|
)
|
||||||
|
elif secciones == 'acuse':
|
||||||
|
nuevo_nombre = f"vu_AC_{pedimento.pedimento_app}_{numero_documento}.{extension}"
|
||||||
|
# Usar tipo de documento por defecto siempre
|
||||||
|
document_type, created = DocumentType.objects.get_or_create(
|
||||||
|
nombre="Pedimento Acuse",
|
||||||
|
defaults={'descripcion': "Tag para saber que el documento es un Acuse"}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 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"}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
failed_files.append("archivo_sin_seccion")
|
||||||
|
errors.append("Archivo sin seccion")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Renombrar archivo
|
||||||
|
file.name = nuevo_nombre
|
||||||
|
|
||||||
|
# Crear el documento
|
||||||
|
document = Document.objects.create(
|
||||||
|
organizacion=organizacion,
|
||||||
|
pedimento_id=pedimento_id,
|
||||||
|
document_type=document_type,
|
||||||
|
archivo=file,
|
||||||
|
size=file.size,
|
||||||
|
fuente_id=7,
|
||||||
|
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):
|
class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
|
||||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||||
serializer_class = DocumentSerializer
|
serializer_class = DocumentSerializer
|
||||||
@@ -925,15 +1811,36 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = self.get_queryset_filtrado_por_organizacion()
|
queryset = self.get_queryset_filtrado_por_organizacion()
|
||||||
|
pedimento_id = self.request.query_params.get('pedimento')
|
||||||
|
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
|
||||||
# Tipos de documento permitidos (fijos en código, Pedimento completo y remesas)
|
# Tipos de documento permitidos (fijos en código, Pedimento completo y remesas)
|
||||||
TIPOS_PERMITIDOS = ['2', '3'] # <-- Ajusta aquí tus tipos
|
TIPOS_PERMITIDOS = ['2', '3'] # <-- Ajusta aquí tus tipos
|
||||||
tipo_documento = self.request.query_params.get('document_type')
|
tipo_documento = self.request.query_params.get('document_type')
|
||||||
if tipo_documento:
|
if tipo_documento:
|
||||||
queryset = queryset.filter(document_type_id=tipo_documento)
|
if tipo_documento == '2':
|
||||||
|
queryset = queryset.filter(archivo__startswith=f'documents/vu_PC_{pedimento.pedimento_app}.xml')
|
||||||
|
elif tipo_documento == '3':
|
||||||
|
queryset = queryset.filter(archivo__startswith=f'documents/vu_RM_{pedimento.pedimento_app}.xml')
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(archivo__startswith=f'documents/NOTFOUND_{pedimento.pedimento_app}.xml')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Filtrar por tipos permitidos
|
# Filtrar por tipos permitidos
|
||||||
queryset = queryset.filter(document_type_id__in=TIPOS_PERMITIDOS)
|
# queryset = queryset.filter(document_type_id__in=TIPOS_PERMITIDOS)
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(archivo__startswith=f'documents/vu_PC_{pedimento.pedimento_app}.xml') |
|
||||||
|
Q(archivo__startswith=f'documents/vu_RM_{pedimento.pedimento_app}.xml')
|
||||||
|
)
|
||||||
|
|
||||||
buscar_archivo = self.request.query_params.get('archivo__icontains')
|
buscar_archivo = self.request.query_params.get('archivo__icontains')
|
||||||
if buscar_archivo:
|
if buscar_archivo:
|
||||||
|
|||||||
Reference in New Issue
Block a user