diff --git a/api/record/views.py b/api/record/views.py index d312509..a684f17 100644 --- a/api/record/views.py +++ b/api/record/views.py @@ -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: