Compare commits

7 Commits

Author SHA1 Message Date
f7fc802ec2 Se crea endpoint para procesar todos los pedimentos 2025-10-16 17:09:56 -05:00
50e35992db se quito stop 2025-10-16 09:47:20 -06:00
9a8827bb6f customs crear/update pedimento 2025-10-15 19:29:11 -06:00
6e0b7eaa91 Merge pull request 'feat: Mejorar endpoints de carga masiva de documentos' (#2) from feature/bulk-document-upload-nomenclatura into main
Reviewed-on: #2
2025-10-15 00:10:04 +00:00
fa0d49a6d5 feat: Mejorar endpoints de carga masiva de documentos
 Nuevas funcionalidades:
- Corregir nomenclatura en bulk-create de pedimentos usando nombres exactos de archivos
- Endpoint bulk-upload para cargar múltiples documentos a un pedimento existente
- Soporte completo para archivos RAR y ZIP con manejo robusto

🔧 Mejoras técnicas bulk-create:
- Subdirectorios usan nombre exacto del archivo sin extensión (ej: 24-01-3420-1234567/)
- Resolución del problema de validación de nomenclatura inválida
- Mensajes de error mejorados con archivo original específico
- Procesamiento optimizado de múltiples archivos ZIP/RAR simultáneos

🔧 Mejoras técnicas bulk-upload:
- Organización heredada del pedimento en lugar del usuario
- Validación de cuotas de almacenamiento por organización
- Manejo de errores por archivo individual
- Soporte para múltiples tipos de archivo

📦 Dependencias:
- Agregado rarfile==4.1 para soporte completo de archivos RAR

🚀 Endpoints listos para producción:
- POST /api/customs/pedimentos/bulk-create/ (crear pedimentos + documentos)
- POST /api/record/documents/bulk-upload/ (subir documentos a pedimento existente)
2025-10-14 14:05:19 -05:00
9700d81dea se modifico tasks y views de auditor 2025-10-12 07:52:31 -06:00
8c842a6212 Merge pull request 'feat: agregar endpoints de eliminación masiva' (#1) from feature/bulk-delete-endpoints into main
Reviewed-on: #1
2025-10-10 01:34:32 +00:00
12 changed files with 699 additions and 89 deletions

View File

@@ -51,28 +51,6 @@ def auditor_descargas(pedimento, servicio, related_name, variable, mensaje):
print(f"✗ No se encontró proceso de auditoría para pedimento {pedimento_id}.")
## Auditar pedimentos
@shared_task
def auditar_procesamiento_remesas(organizacion_id):
pedimentos = obtener_pedimentos(organizacion_id)
for pedimento in pedimentos:
if pedimento.remesas:
# Tipo 3: Remesa
if not pedimento.documents.filter(document_type=3).exists():
ProcesamientoPedimento.objects.get_or_create(
pedimento=pedimento,
servicio_id=5, # ID del servicio de remesas
organizacion=organizacion_id
)
else:
xml_data = extraer_coves(pedimento)
if xml_data:
for remesa in xml_data:
Cove.objects.get_or_create(
pedimento=pedimento,
numero_cove=remesa.get('remesaSA'),
organizacion=organizacion_id
)
@shared_task
def auditar_procesamiento_remesa_por_pedimento(pedimento_id):
@@ -259,4 +237,68 @@ def auditar_acuse(organizacion_id):
variable='acuse_descargado',
mensaje='acuse'
)
@shared_task
def auditar_cove_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=8,
related_name='coves',
variable='acuse_descargado',
mensaje='COVE'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_acuse_cove_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=9,
related_name='coves',
variable='acuse_cove_descargado',
mensaje='acuse de COVE'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_edocument_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=7,
related_name='documentos',
variable='edocument_descargado',
mensaje='EDocument'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_acuse_por_pedimento(pedimento_id):
try:
from api.customs.models import Pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
auditor_descargas(
pedimento,
servicio=6,
related_name='documentos',
variable='acuse_descargado',
mensaje='acuse'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}

View File

@@ -265,7 +265,6 @@ def procesar_remesas(organizacion_id):
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
@shared_task
def procesar_coves(organizacion_id):
pedimentos = Pedimento.objects.filter(
@@ -429,3 +428,5 @@ def documentos_con_errores(organizacion_id):
# Aquí puedes agregar lógica adicional para manejar documentos con errores
# como enviar notificaciones, registrar en un log, etc.
# documentos = Document.objects.all() --- IGNORE ---

View File

@@ -34,8 +34,15 @@ from .views_auditor import (
crear_partidas_organizacion,
crear_partidas_pedimento,
auditar_pedimentos_endpoint,
auditar_procesamiento_remesas_endpoint,
auditar_procesamiento_remesa_pedimento_endpoint
auditar_coves_endpoint,
auditar_acuse_cove_endpoint,
auditar_edocuments_endpoint,
auditar_acuse_endpoint,
auditar_cove_pedimento_endpoint,
auditar_acuse_cove_pedimento_endpoint,
auditar_edocument_pedimento_endpoint,
auditar_acuse_pedimento_endpoint,
auditor_procesar_pedimentos_organizacion
)
urlpatterns = [
@@ -43,6 +50,13 @@ urlpatterns = [
path('auditor/crear-partidas/organizacion/', crear_partidas_organizacion, name='crear-partidas-organizacion'),
path('auditor/crear-partidas/pedimento/', crear_partidas_pedimento, name='crear-partidas-pedimento'),
path('auditor/auditar-pedimentos/', auditar_pedimentos_endpoint, name='auditar-pedimentos'),
path('auditor/auditar-procesamiento-remesas/', auditar_procesamiento_remesas_endpoint, name='auditar-procesamiento-remesas'),
path('auditor/auditar-procesamiento-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-procesamiento-remesa-pedimento'),
path('auditor/auditar-coves/', auditar_coves_endpoint, name='auditar-coves'),
path('auditor/auditar-acuse-cove/', auditar_acuse_cove_endpoint, name='auditar-acuse-cove'),
path('auditor/auditar-edocuments/', auditar_edocuments_endpoint, name='auditar-edocuments'),
path('auditor/auditar-acuse/', auditar_acuse_endpoint, name='auditar-acuse'),
path('auditor/auditar-cove/pedimento/', auditar_cove_pedimento_endpoint, name='auditar-cove-pedimento'),
path('auditor/auditar-acuse-cove/pedimento/', auditar_acuse_cove_pedimento_endpoint, name='auditar-acuse-cove-pedimento'),
path('auditor/auditar-edocument/pedimento/', auditar_edocument_pedimento_endpoint, name='auditar-edocument-pedimento'),
path('auditor/auditar-acuse/pedimento/', auditar_acuse_pedimento_endpoint, name='auditar-acuse-pedimento'),
path('auditor/procesar-pedimentos/organizaciones/', auditor_procesar_pedimentos_organizacion, name='procesar-pedimentos-organizaciones'),
]

View File

@@ -177,24 +177,24 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
# Verificar si la respuesta fue exitosa
if response.status_code == 200:
print(f"Servicio FastAPI ejecutado exitosamente: {response.status_code}")
print(f"📄 Respuesta: {response.json()}")
print(f"Servicio FastAPI ejecutado exitosamente: {response.status_code}")
print(f"Respuesta: {response.json()}")
elif response.status_code == 201:
print(f"Recurso creado exitosamente en FastAPI: {response.status_code}")
print(f"📄 Respuesta: {response.json()}")
print(f"Recurso creado exitosamente en FastAPI: {response.status_code}")
print(f"Respuesta: {response.json()}")
else:
print(f"⚠️ Servicio FastAPI respondió con error: {response.status_code}")
print(f"📄 Respuesta: {response.text}")
print(f"Servicio FastAPI respondió con error: {response.status_code}")
print(f"Respuesta: {response.text}")
except requests.exceptions.ConnectionError as e:
print(f"No se pudo conectar al servicio FastAPI: {e}")
print(f"🔧 Verifica que el servicio FastAPI esté corriendo en {SERVICE_API_URL}")
print(f"No se pudo conectar al servicio FastAPI: {e}")
print(f"Verifica que el servicio FastAPI esté corriendo en {SERVICE_API_URL}")
except requests.exceptions.Timeout as e:
print(f"Timeout al conectar con el servicio FastAPI: {e}")
print(f"Timeout al conectar con el servicio FastAPI: {e}")
except requests.exceptions.RequestException as e:
print(f"🚨 Error de request al servicio FastAPI: {e}")
print(f"Error de request al servicio FastAPI: {e}")
except Exception as e:
print(f"💥 Error inesperado al llamar al servicio FastAPI: {e}")
print(f"Error inesperado al llamar al servicio FastAPI: {e}")
def perform_update(self, serializer):
"""

View File

@@ -8,11 +8,20 @@ from core.permissions import IsSuperUser, IsSameOrganizationDeveloper
from .tasks.auditoria import (
crear_partidas,
crear_partidas_por_pedimento,
auditar_procesamiento_remesas,
auditar_procesamiento_remesa_por_pedimento
auditar_procesamiento_remesa_por_pedimento,
auditar_coves,
auditar_acuse_cove,
auditar_edocuments,
auditar_acuse,
auditar_cove_por_pedimento,
auditar_acuse_cove_por_pedimento,
auditar_edocument_por_pedimento,
auditar_acuse_por_pedimento
)
from .tasks.internal_services import auditar_pedimentos
from .tasks.microservice_v2 import procesar_pedimentos_completos
from api.customs.models import Pedimento
from api.organization.models import Organizacion
@swagger_auto_schema(
@@ -230,7 +239,7 @@ def auditar_procesamiento_remesa_pedimento_endpoint(request):
@swagger_auto_schema(
method='post',
operation_description="Audita el procesamiento de remesas de una organización",
operation_description="Audita los COVEs de una organización",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
@@ -246,10 +255,10 @@ def auditar_procesamiento_remesa_pedimento_endpoint(request):
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
def auditar_procesamiento_remesas_endpoint(request):
def auditar_coves_endpoint(request):
"""
Inicia una tarea de auditoría para el procesamiento de remesas de una organización.
Verifica el estado y la integridad del procesamiento de remesas.
Inicia una tarea de auditoría para los COVEs de una organización.
Verifica la existencia y validez de los COVEs generados.
"""
organizacion_id = request.data.get('organizacion_id')
@@ -268,8 +277,8 @@ def auditar_procesamiento_remesas_endpoint(request):
)
# Ejecutar la tarea de auditoría
task = auditar_procesamiento_remesas.delay(organizacion_id)
message = f"Auditoría de procesamiento de remesas iniciada para la organización {organizacion_id}"
task = auditar_coves.delay(organizacion_id)
message = f"Auditoría de COVEs iniciada para la organización {organizacion_id}"
return Response({
'message': message,
@@ -277,3 +286,350 @@ def auditar_procesamiento_remesas_endpoint(request):
}, status=status.HTTP_200_OK)
@swagger_auto_schema(
method='post',
operation_description="Audita los acuses de COVE de una organización",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la organización')
},
required=['organizacion_id']
),
responses={
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
400: openapi.Response('Error en los parámetros'),
403: openapi.Response('No tiene permisos suficientes')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
def auditar_acuse_cove_endpoint(request):
"""
Inicia una tarea de auditoría para los acuses de COVE de una organización.
Verifica la recepción y validez de los acuses de COVE.
"""
organizacion_id = request.data.get('organizacion_id')
if not organizacion_id:
return Response(
{'error': 'Debe proporcionar organizacion_id'},
status=status.HTTP_400_BAD_REQUEST
)
# Validar permisos
user = request.user
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
return Response(
{'error': 'No tiene permisos para esta organización'},
status=status.HTTP_403_FORBIDDEN
)
# Ejecutar la tarea de auditoría
task = auditar_acuse_cove.delay(organizacion_id)
message = f"Auditoría de acuses de COVE iniciada para la organización {organizacion_id}"
return Response({
'message': message,
'task_id': task.id
}, status=status.HTTP_200_OK)
@swagger_auto_schema(
method='post',
operation_description="Audita los EDocuments de una organización",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la organización')
},
required=['organizacion_id']
),
responses={
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
400: openapi.Response('Error en los parámetros'),
403: openapi.Response('No tiene permisos suficientes')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
def auditar_edocuments_endpoint(request):
"""
Inicia una tarea de auditoría para los EDocuments de una organización.
Verifica la existencia y validez de los EDocuments generados.
"""
organizacion_id = request.data.get('organizacion_id')
if not organizacion_id:
return Response(
{'error': 'Debe proporcionar organizacion_id'},
status=status.HTTP_400_BAD_REQUEST
)
# Validar permisos
user = request.user
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
return Response(
{'error': 'No tiene permisos para esta organización'},
status=status.HTTP_403_FORBIDDEN
)
# Ejecutar la tarea de auditoría
task = auditar_edocuments.delay(organizacion_id)
message = f"Auditoría de EDocuments iniciada para la organización {organizacion_id}"
return Response({
'message': message,
'task_id': task.id
}, status=status.HTTP_200_OK)
@swagger_auto_schema(
method='post',
operation_description="Audita los acuses de una organización",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la organización')
},
required=['organizacion_id']
),
responses={
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
400: openapi.Response('Error en los parámetros'),
403: openapi.Response('No tiene permisos suficientes')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
def auditar_acuse_endpoint(request):
"""
Inicia una tarea de auditoría para los acuses de una organización.
Verifica la recepción y validez de los acuses.
"""
organizacion_id = request.data.get('organizacion_id')
if not organizacion_id:
return Response(
{'error': 'Debe proporcionar organizacion_id'},
status=status.HTTP_400_BAD_REQUEST
)
# Validar permisos
user = request.user
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
return Response(
{'error': 'No tiene permisos para esta organización'},
status=status.HTTP_403_FORBIDDEN
)
# Ejecutar la tarea de auditoría
task = auditar_acuse.delay(organizacion_id)
message = f"Auditoría de acuses iniciada para la organización {organizacion_id}"
return Response({
'message': message,
'task_id': task.id
}, status=status.HTTP_200_OK)
@swagger_auto_schema(
method='post',
operation_description="Audita el COVE de un pedimento específico",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'pedimento_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID del pedimento')
},
required=['pedimento_id']
),
responses={
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
400: openapi.Response('Error en los parámetros'),
403: openapi.Response('No tiene permisos suficientes'),
404: openapi.Response('Pedimento no encontrado')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def auditar_cove_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
user = request.user
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_cove_por_pedimento.delay(pedimento_id)
return Response({'message': f'Auditoría de COVE iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
@swagger_auto_schema(
method='post',
operation_description="Audita el acuse de COVE de un pedimento específico",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'pedimento_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID del pedimento')
},
required=['pedimento_id']
),
responses={
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
400: openapi.Response('Error en los parámetros'),
403: openapi.Response('No tiene permisos suficientes'),
404: openapi.Response('Pedimento no encontrado')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def auditar_acuse_cove_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
user = request.user
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_acuse_cove_por_pedimento.delay(pedimento_id)
return Response({'message': f'Auditoría de acuse de COVE iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
@swagger_auto_schema(
method='post',
operation_description="Audita el EDocument de un pedimento específico",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'pedimento_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID del pedimento')
},
required=['pedimento_id']
),
responses={
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
400: openapi.Response('Error en los parámetros'),
403: openapi.Response('No tiene permisos suficientes'),
404: openapi.Response('Pedimento no encontrado')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def auditar_edocument_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
user = request.user
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_edocument_por_pedimento.delay(pedimento_id)
return Response({'message': f'Auditoría de EDocument iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
@swagger_auto_schema(
method='post',
operation_description="Audita el acuse de un pedimento específico",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'pedimento_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID del pedimento')
},
required=['pedimento_id']
),
responses={
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
400: openapi.Response('Error en los parámetros'),
403: openapi.Response('No tiene permisos suficientes'),
404: openapi.Response('Pedimento no encontrado')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def auditar_acuse_pedimento_endpoint(request):
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
user = request.user
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_acuse_por_pedimento.delay(pedimento_id)
return Response({'message': f'Auditoría de acuse iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
### Procesamiento de pedimentos ###
@swagger_auto_schema(
method='post',
operation_description="Procesamiento de todos los pedimentos de todas las organizaciones",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={}
),
responses={
200: openapi.Response('Tarea de procesamiento iniciada correctamente'),
403: openapi.Response('No tiene permisos suficientes'),
404: openapi.Response('No se encontraron organizaciones')
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
def auditor_procesar_pedimentos_organizacion(request):
"""
Inicia una tarea de procesamiento para todos los pedimentos de todas las organizaciones.
Solo usuarios administradores pueden ejecutar esta función.
"""
# Validar permisos (solo superusuarios pueden procesar todas las organizaciones)
user = request.user
if not user.is_superuser:
return Response(
{'error': 'Solo los superusuarios pueden procesar todas las organizaciones'},
status=status.HTTP_403_FORBIDDEN
)
organizaciones = Organizacion.objects.all()
if not organizaciones.exists():
return Response(
{'error': 'No se encontraron organizaciones'},
status=status.HTTP_404_NOT_FOUND
)
# Lista para recopilar todos los task_ids y detalles
tasks_iniciadas = []
for organizacion in organizaciones:
organizacion_id = str(organizacion.id)
print(f"Procesando organización: {organizacion_id} - {organizacion.nombre}")
# Ejecutar la tarea de procesamiento
task = procesar_pedimentos_completos.delay(organizacion_id)
# Agregar información de la tarea a la lista
tasks_iniciadas.append({
'organizacion_id': organizacion_id,
'organizacion_nombre': organizacion.nombre,
'task_id': task.id
})
# Crear mensaje general y lista de task_ids
total_organizaciones = len(tasks_iniciadas)
task_ids = [task['task_id'] for task in tasks_iniciadas]
message = f"Procesamiento de pedimentos iniciado para {total_organizaciones} organización(es)"
return Response({
'message': message,
'total_organizaciones': total_organizaciones,
'task_ids': task_ids,
'tasks_detalle': tasks_iniciadas
}, status=status.HTTP_200_OK)
### Fin Procesamiento de pedimentos ###

View File

@@ -312,6 +312,211 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
response_status = status.HTTP_200_OK
return Response(response_data, status=response_status)
@action(detail=False, methods=['post'], url_path='bulk-upload', parser_classes=[MultiPartParser])
def bulk_upload(self, request):
"""
Endpoint para subir múltiples documentos a un pedimento específico.
FormData esperado:
- pedimento_id: UUID del pedimento (requerido)
- files: Lista de archivos a subir (requerido)
Nota: Se usa automáticamente el tipo de documento "Documento General"
Respuesta exitosa:
{
"message": "Documentos subidos exitosamente",
"uploaded_count": 5,
"uploaded_documents": [
{
"id": "uuid1",
"filename": "documento1.pdf",
"size": 1024000,
"extension": "pdf"
},
...
],
"space_used_mb": 25.6,
"failed_files": [],
"errors": []
}
Respuesta con errores:
{
"message": "Algunos documentos no pudieron ser subidos",
"uploaded_count": 3,
"uploaded_documents": [...],
"space_used_mb": 15.2,
"failed_files": ["archivo4.pdf", "archivo5.doc"],
"errors": ["Archivo demasiado grande: archivo4.pdf", "Tipo de archivo no soportado: archivo5.doc"]
}
"""
# Validar datos requeridos
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
return Response(
{"error": "Se requiere el campo 'pedimento_id'"},
status=status.HTTP_400_BAD_REQUEST
)
files = request.FILES.getlist('files')
if not files:
return Response(
{"error": "Se requiere al menos un archivo para subir"},
status=status.HTTP_400_BAD_REQUEST
)
# Validar usuario autenticado
if not request.user.is_authenticated:
return Response(
{"error": "Usuario no autenticado"},
status=status.HTTP_401_UNAUTHORIZED
)
# Obtener el pedimento primero para usar su organización
from api.customs.models import Pedimento
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
except Pedimento.DoesNotExist:
return Response(
{"error": "Pedimento no encontrado"},
status=status.HTTP_404_NOT_FOUND
)
# Usar la organización del pedimento
organizacion = pedimento.organizacion
# Validar que el usuario tenga permisos para esta organización
if not request.user.is_superuser:
if not hasattr(request.user, 'organizacion') or request.user.organizacion != organizacion:
return Response(
{"error": "No tienes permisos para subir documentos a este pedimento"},
status=status.HTTP_403_FORBIDDEN
)
# Usar tipo de documento por defecto siempre
document_type, created = DocumentType.objects.get_or_create(
nombre="Documento General",
defaults={'descripcion': "Documento general sin tipo específico"}
)
if created:
print(f"✅ DocumentType creado: {document_type.nombre} (ID: {document_type.id})")
else:
print(f"♻️ DocumentType existente: {document_type.nombre} (ID: {document_type.id})")
uploaded_documents = []
failed_files = []
errors = []
total_space_used = 0
try:
with transaction.atomic():
# Obtener uso de almacenamiento
uso = UsoAlmacenamiento.objects.select_for_update().get_or_create(
organizacion=organizacion,
defaults={'espacio_utilizado': 0}
)[0]
# Calcular límites
max_almacenamiento_bytes = organizacion.licencia.almacenamiento * 1024 ** 3
espacio_inicial = uso.espacio_utilizado
# Calcular el tamaño total de todos los archivos
total_files_size = sum(file.size for file in files)
nuevo_espacio_total = espacio_inicial + total_files_size
# Validar que hay espacio suficiente para todos los archivos
if nuevo_espacio_total > max_almacenamiento_bytes:
espacio_faltante = nuevo_espacio_total - max_almacenamiento_bytes
return Response({
"error": "Espacio de almacenamiento insuficiente para todos los archivos",
"detalle": {
"espacio_faltante_gb": round(espacio_faltante / (1024 ** 3), 2),
"espacio_utilizado_gb": round(espacio_inicial / (1024 ** 3), 2),
"limite_gb": organizacion.licencia.almacenamiento,
"archivos_gb": round(total_files_size / (1024 ** 3), 4),
"total_archivos": len(files)
},
"codigo": "bulk_storage_limit_exceeded"
}, status=status.HTTP_400_BAD_REQUEST)
# Procesar cada archivo
espacio_usado_temp = espacio_inicial
for file in files:
try:
# Validaciones por archivo
if not file.name:
failed_files.append("archivo_sin_nombre")
errors.append("Archivo sin nombre detectado")
continue
# Obtener extensión del archivo
extension = file.name.split('.')[-1].lower() if '.' in file.name else ''
# Crear el documento
document = Document.objects.create(
organizacion=organizacion,
pedimento_id=pedimento_id,
document_type=document_type,
archivo=file,
size=file.size,
extension=extension
)
# Actualizar espacio usado
espacio_usado_temp += file.size
total_space_used += file.size
uploaded_documents.append({
"id": str(document.id),
"filename": file.name,
"size": file.size,
"extension": extension,
"document_type": document_type.nombre
})
except Exception as e:
failed_files.append(file.name)
errors.append(f"Error al procesar {file.name}: {str(e)}")
continue
# Actualizar el uso de almacenamiento final
uso.espacio_utilizado = espacio_usado_temp
uso.save()
except Exception as e:
return Response(
{"error": f"Error durante el procesamiento masivo: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# Convertir bytes a MB para la respuesta
space_used_mb = round(total_space_used / (1024 * 1024), 2)
# Preparar respuesta
response_data = {
"uploaded_count": len(uploaded_documents),
"uploaded_documents": uploaded_documents,
"space_used_mb": space_used_mb,
"pedimento_id": str(pedimento_id),
"document_type": document_type.nombre
}
if failed_files:
response_data.update({
"message": "Algunos documentos no pudieron ser subidos",
"failed_files": failed_files,
"errors": errors
})
response_status = status.HTTP_207_MULTI_STATUS
else:
response_data["message"] = "Documentos subidos exitosamente"
response_status = status.HTTP_201_CREATED
return Response(response_data, status=response_status)
class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]

14
api/tasks/filters.py Normal file
View File

@@ -0,0 +1,14 @@
from django_filters import rest_framework as filters
from .models import Task
class TaskFilter(filters.FilterSet):
servicio = filters.NumberFilter(field_name='servicio__id')
pedimento_app = filters.CharFilter(field_name='pedimento__pedimento_app')
pedimento = filters.UUIDFilter(field_name='pedimento__id')
timestamp_gte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='gte')
timestamp_lte = filters.DateTimeFilter(field_name='timestamp', lookup_expr='lte')
status = filters.CharFilter(field_name='status')
class Meta:
model = Task
fields = ['servicio', 'pedimento_app', 'pedimento', 'timestamp_gte', 'timestamp_lte', 'status']

View File

@@ -9,3 +9,4 @@ class Task(models.Model):
message = models.TextField()
status = models.CharField(max_length=50)
servicio = models.ForeignKey('customs.Servicio', on_delete=models.CASCADE, null=True, blank=True)

View File

@@ -2,6 +2,8 @@ from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
pedimento_app = serializers.CharField(source='pedimento.pedimento_app', read_only=True)
class Meta:
model = Task
fields = '__all__'

View File

@@ -1,9 +1,12 @@
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.pagination import PageNumberPagination
from api.logger.mixins import LoggingMixin
from .models import Task
from .serializers import TaskSerializer
from .filters import TaskFilter
from rest_framework.permissions import IsAuthenticated
# Create your views here.
@@ -14,9 +17,19 @@ from core.permissions import (
IsSuperUser
)
class TaskPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class TaskViewSet(LoggingMixin,viewsets.ModelViewSet):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
queryset = Task.objects.all()
queryset = Task.objects.select_related('pedimento', 'servicio').all()
serializer_class = TaskSerializer
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_class = TaskFilter
pagination_class = TaskPagination
ordering_fields = ['timestamp']
ordering = ['-timestamp'] # ordenamiento por defecto, más reciente primero
my_tags = ['tasks']

View File

@@ -3,46 +3,7 @@ from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
# Ejecutar pedimento completo de 5:00 a 22:00 (cada hora)
'creacion-servicio-pedimento-completo': {
'task': 'api.customs.tasks.internal_services.crear_todos_los_servicios',
'schedule': crontab(minute=0, hour='5-22'),
},
# Ejecutar pedimento completo de 5:00 a 22:00 (cada hora)
'ejecutar-pedimentos-completos-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_pedimento_completo',
'schedule': crontab(minute=0, hour='5-22'),
},
# Ejecutar partidas de 5:00 a 22:00 (cada hora)
'ejecutar-partidas-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_partidas_pedimento',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar coves de 5:00 a 22:00 (cada hora)
'ejecutar-coves-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_coves',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar remesas de 5:00 a 22:00 (cada hora)
'ejecutar-remesas-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_remesas',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar acuse coves de 5:00 a 22:00 (cada hora)
'ejecutar-acuse-coves-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_acuseCoves',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar acuse de 5:00 a 22:00 (cada hora)
'ejecutar-acuse-dia': {
'task': 'api.customs.tasks.microservice.ejecutar_acuse',
'schedule': crontab(minute=0, hour='5-23'),
},
# Ejecutar edocs solo de 23:00 a 4:59 (cada hora en ese rango)
'ejecutar-edocs-noche': {
'task': 'api.customs.tasks.microservice.ejecutar_edocs',
'schedule': crontab(minute=42, hour='23,0,1,2,3,4'),
},
}
"""

View File

@@ -50,6 +50,7 @@ python-dotenv==1.1.0
python-multipart==0.0.12
pytz==2025.2
PyYAML==6.0.2
rarfile==4.1
redis==6.2.0
referencing==0.36.2
requests==2.32.4