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 datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
|
||||
from core.permissions import (
|
||||
IsSameOrganization,
|
||||
@@ -461,6 +462,602 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
||||
|
||||
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])
|
||||
def bulk_upload(self, request):
|
||||
"""
|
||||
@@ -666,6 +1263,295 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
||||
|
||||
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):
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
serializer_class = DocumentSerializer
|
||||
@@ -925,15 +1811,36 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
||||
|
||||
def get_queryset(self):
|
||||
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_PERMITIDOS = ['2', '3'] # <-- Ajusta aquí tus tipos
|
||||
tipo_documento = self.request.query_params.get('document_type')
|
||||
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:
|
||||
# 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')
|
||||
if buscar_archivo:
|
||||
|
||||
Reference in New Issue
Block a user