Compare commits

17 Commits

Author SHA1 Message Date
72c0d70a71 Se habilita opcion de descarga de pedimentos individuales, masivos, por filtro. 2025-11-24 08:25:59 -07:00
4b44c098c4 auditor 2025-10-22 19:35:39 -06:00
aaa1e79473 Documentacion 2025-10-22 19:17:11 -06:00
91ab38fc91 Documentacion 2025-10-22 19:08:08 -06:00
0e0572125a microservicios 2025-10-22 18:34:40 -06:00
73413fe3d9 microservicios 2025-10-22 18:32:46 -06:00
3b19520481 repo 2025-10-21 21:44:51 -06:00
474cb151ef Reportes 2025-10-21 18:47:15 -06:00
14c06cbf43 reportes 2025-10-20 21:14:31 -06:00
265f471ea6 Merge pull request 'Se crea endpoint para procesar todos los pedimentos' (#3) from feature/Agendar-todos into main
Reviewed-on: #3
2025-10-21 01:28:25 +00:00
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
25 changed files with 1798 additions and 312 deletions

View File

@@ -17,4 +17,7 @@ EMAIL_HOST_PASSWORD=N036p7y!
EMAIL_PORT=587
EMAIL_HOST=secure.emailsrvr.com
SERVICE_API_URL=http://localhost:8001/api/v1
SERVICE_API_URL=http://host.docker.internal:8001/api/v1
SERVICE_API_URL_V2=http://host.docker.internal:8001/api/v2
CELERY_BROKER_URL=redis://redis_backend_dev:6379/0
CELERY_RESULT_BACKEND=redis://redis_backend_dev:6379/0

View File

@@ -1,6 +1,5 @@
from django.core.management.base import BaseCommand
from api.customs.tasks.auditoria import (
auditar_procesamiento_remesas,
auditar_coves,
auditar_acuse_cove,
auditar_edocuments,
@@ -15,7 +14,6 @@ class Command(BaseCommand):
# Definir las tareas disponibles
TAREAS_DISPONIBLES = {
'remesas': (auditar_procesamiento_remesas, "Auditoría de remesas"),
'partidas': (crear_partidas, "Creación de partidas"),
'coves': (auditar_coves, "Auditoría de COVEs"),
'acuse-cove': (auditar_acuse_cove, "Auditoría de acuses de COVEs"),

View File

@@ -0,0 +1,44 @@
from django.core.management.base import BaseCommand
from api.organization.models import Organizacion
from api.customs.tasks import microservice_v2
class Command(BaseCommand):
help = 'Ejecuta tareas de microservicio por organización y procesamiento.'
def add_arguments(self, parser):
parser.add_argument(
'--organizacion_id',
type=str,
help='ID de la organización a procesar (opcional, si no se envía se procesan todas)'
)
parser.add_argument(
'--procesamiento',
type=str,
help='Tipo de procesamiento a ejecutar (opcional, si no se envía se ejecutan todos)'
)
parser.add_argument(
'--todos',
type=bool,
help='Ejecutar todos los procesos (opcional)'
)
def handle(self, *args, **options):
todos = options.get('todos', False)
organizacion_id = options.get('organizacion_id')
procesamiento = options.get('procesamiento')
if todos:
organizaciones = Organizacion.objects.all()
for org in organizaciones:
microservice_v2.ejecutar_todos_por_organizacion(org.id)
self.stdout.write(self.style.SUCCESS('Se ejecutaron todos los procesos para todas las organizaciones.'))
return
if organizacion_id:
if procesamiento:
microservice_v2.ejecutar_procesamiento_por_organizacion(organizacion_id, procesamiento)
self.stdout.write(self.style.SUCCESS(f'Se ejecutó el procesamiento {procesamiento} para la organización {organizacion_id}.'))
else:
microservice_v2.ejecutar_todos_por_organizacion(organizacion_id)
self.stdout.write(self.style.SUCCESS(f'Se ejecutaron todos los procesos para la organización {organizacion_id}.'))

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):
@@ -260,3 +238,67 @@ def auditar_acuse(organizacion_id):
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(
@@ -428,4 +427,35 @@ def documentos_con_errores(organizacion_id):
print(f"Documento con error: {doc.id} en organización {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 ---
def ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento):
if procesamiento == 'coves':
procesar_coves.delay(organizacion_id)
elif procesamiento == 'edocs':
procesar_edocs.delay(organizacion_id)
elif procesamiento == 'acuses':
procesar_acuses.delay(organizacion_id)
elif procesamiento == 'acuse_coves':
procesar_acuse_coves.delay(organizacion_id)
elif procesamiento == 'partidas':
procesar_partidas.delay(organizacion_id)
elif procesamiento == 'pedimentos_completos':
procesar_pedimentos_completos.delay(organizacion_id)
elif procesamiento == 'remesas':
procesar_remesas.delay(organizacion_id)
else:
# Procesamiento no reconocido
pass
def ejecutar_todos_por_organizacion(organizacion_id):
procesar_coves.delay(organizacion_id)
procesar_edocs.delay(organizacion_id)
procesar_acuses.delay(organizacion_id)
procesar_acuse_coves.delay(organizacion_id)
procesar_partidas.delay(organizacion_id)
procesar_pedimentos_completos.delay(organizacion_id)
procesar_remesas.delay(organizacion_id)

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

@@ -4,12 +4,12 @@ from rest_framework.routers import DefaultRouter
# import necessary viewsets
# from .views import YourViewSet # Import your viewsets here
from .views import DocumentViewSet, ProtectedDocumentDownloadView, BulkDownloadZipView, GetFuenteView, DocumentTypeView
from .views import DocumentViewSet, ProtectedDocumentDownloadView, BulkDownloadZipView, GetFuenteView, DocumentTypeView, ExpedienteZipDownloadView, MultiPedimentoZipDownloadView
# Create a router and register your viewsets with it
router = DefaultRouter()
# Register your viewsets with the router here
# Register your viewsets with the router he -fre
# Example:
# from .views import MyViewSet
# router.register(r'myviewset', MyViewSet, basename='myviewset')
@@ -23,5 +23,7 @@ urlpatterns = [
path('documents/descargar/<uuid:pk>/', ProtectedDocumentDownloadView.as_view(), name='descargar-documento'),
path('fuente/', GetFuenteView.as_view(), name='get-fuente'),
path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'),
path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'),
path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'),
path('', include(router.urls)),
]

View File

@@ -13,6 +13,7 @@ from rest_framework.exceptions import ValidationError
from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer
from .models import Document, Fuente, DocumentType
from ..customs.models import Pedimento
from api.organization.models import UsoAlmacenamiento
from io import BytesIO
import zipfile
@@ -32,6 +33,9 @@ from core.permissions import (
import logging
logger = logging.getLogger(__name__)
import os
from django.core.files.storage import default_storage
from mixins.filtrado_organizacion import DocumentosFiltradosMixin
class CustomPagination(PageNumberPagination):
@@ -313,6 +317,211 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
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)]
serializer_class = DocumentSerializer
@@ -326,7 +535,6 @@ class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
raise Http404("Usuario no autenticado")
try:
doc = Document.objects.get(pk=pk)
except Document.DoesNotExist:
@@ -413,3 +621,147 @@ class DocumentTypeView(APIView):
return Response({"detail": "No hay tipos de documento disponibles."}, status=404)
serializer = self.serializer_class(queryset, many=True)
return Response(serializer.data, status=200)
class ExpedienteZipDownloadView(APIView, DocumentosFiltradosMixin):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
my_tags = ['Documents']
def post(self, request):
"""
Descarga todos los documentos de un pedimento (o filtrados) en un ZIP.
Body: { "pedimento_id": "<uuid>" }
"""
pedimento_id = request.data.get('pedimento_id')
if not pedimento_id:
return Response({"error": "Falta pedimento_id"}, status=status.HTTP_400_BAD_REQUEST)
# Validar que el pedimento existe
try:
pedimento = Pedimento.objects.get(pk=pedimento_id)
except Pedimento.DoesNotExist:
raise Http404("Pedimento no encontrado")
# Filtrar documentos del pedimento (y de la org del usuario)
base_qs = Document.objects.filter(pedimento=pedimento)
if not request.user.is_superuser:
if not hasattr(request.user, 'organizacion') or request.user.organizacion != pedimento.organizacion:
return Response({"error": "No autorizado"}, status=status.HTTP_403_FORBIDDEN)
base_qs = base_qs.filter(organizacion=request.user.organizacion)
docs = base_qs.select_related('pedimento')
if not docs.exists():
return Response({"error": "No hay documentos para este pedimento"}, status=status.HTTP_404_NOT_FOUND)
# 1. Crear un único buffer y ZIP para todos los archivos
buffer = BytesIO()
missing_files = [] # opcional: para informar después
files_found = []
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for doc in docs:
# 2. Validaciones
if not doc.archivo.name:
logger.warning("Documento %s no tiene archivo asociado", doc.id)
missing_files.append(f"{doc.id} (sin archivo)")
continue
if not default_storage.exists(doc.archivo.name):
logger.warning("Archivo no encontrado en disco: %s", doc.archivo.path)
missing_files.append(f"{doc.id} ({doc.archivo.name})")
continue
files_found.append(f"{doc.id} ({doc.archivo.name})")
# 3. Nombre seguro para dentro del ZIP
file_name = slugify(doc.archivo.name.rsplit('/', 1)[-1].rsplit('.', 1)[0])
ext = doc.archivo.name.split('.')[-1]
name_inside_zip = f"{file_name}.{ext}"
# 4. Escribir el archivo dentro del ZIP
with doc.archivo.open('rb') as f:
zip_file.writestr(name_inside_zip, f.read())
# 5. Preparar respuesta
buffer.seek(0)
zip_name = slugify(f"expediente_{pedimento.pedimento_app}")
response = HttpResponse(buffer, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={zip_name or "documentos"}.zip'
if not files_found:
return Response({"error": f"No hay documentos para este pedimento: {pedimento.pedimento_app}"}, status=status.HTTP_404_NOT_FOUND)
# (Opcional) cabecera personalizada si faltaron archivos
# if missing_files:
# response['X-Missing-Files'] = ', '.join(missing_files)
# return Response({"error": f"No hay documentos para este pedimento: {pedimento.pedimento_app}"}, status=status.HTTP_404_NOT_FOUND)
return response
class MultiPedimentoZipDownloadView(APIView):
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper)]
my_tags = ['Documents']
def post(self, request):
"""
Descarga todos los documentos de VARIOS pedimentos en un solo ZIP.
Body: { "pedimento_ids": ["uuid1", "uuid2", ...] }
"""
pedimento_ids = request.data.get('pedimento_ids', [])
if not isinstance(pedimento_ids, list) or not pedimento_ids:
return Response({"error": "Se requiere una lista de pedimento_ids"}, status=status.HTTP_400_BAD_REQUEST)
# Filtrar pedimentos visibles para el usuario
base_qs = Pedimento.objects.filter(id__in=pedimento_ids)
if not request.user.is_superuser:
if not hasattr(request.user, 'organizacion'):
return Response({"error": "No autorizado"}, status=status.HTTP_403_FORBIDDEN)
base_qs = base_qs.filter(organizacion=request.user.organizacion)
pedimentos = base_qs.select_related('organizacion')
if not pedimentos.exists():
return Response({"error": "Ningún pedimento encontrado o autorizado"}, status=status.HTTP_404_NOT_FOUND)
# Obtener todos los documentos de esos pedimentos
docs = Document.objects.filter(pedimento__in=pedimentos)
if not docs.exists():
return Response({"error": "No hay documentos para estos pedimentos"}, status=status.HTTP_404_NOT_FOUND)
# Crear ZIP único
buffer = BytesIO()
missing_files = []
summary = {}
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for doc in docs:
ped_key = doc.pedimento.pedimento_app
if not doc.archivo.name or not default_storage.exists(doc.archivo.name):
missing_files.append(f"{doc.id} ({doc.archivo.name or 'sin archivo'})")
logger.warning("Archivo faltante: %s", doc.id)
continue
summary[ped_key] = summary.get(ped_key, 0) + 1
# Nombre seguro: pedimento_app + nombre del archivo
file_name = slugify(doc.archivo.name.rsplit('/', 1)[-1].rsplit('.', 1)[0])
ext = doc.archivo.name.split('.')[-1]
name_inside_zip = f"{doc.pedimento.pedimento_app}/{file_name}.{ext}"
with doc.archivo.open('rb') as f:
zip_file.writestr(name_inside_zip, f.read())
buffer.seek(0)
zip_name = slugify(f"expedientes_{len(summary)}_pedimentos")
response = HttpResponse(buffer, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename={zip_name}.zip'
response['X-Zip-Filename'] = f"{zip_name}.zip"
response['Access-Control-Expose-Headers'] = 'X-Zip-Filename'
if missing_files:
response['X-Missing-Files'] = ', '.join(missing_files)
return response

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.2.3 on 2025-10-21 23:56
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ReportDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filters', models.JSONField(blank=True, null=True)),
('status', models.CharField(choices=[('pending', 'Pendiente'), ('processing', 'Procesando'), ('ready', 'Listo'), ('error', 'Error')], default='pending', max_length=20)),
('file', models.FileField(blank=True, null=True, upload_to='reports/')),
('error_message', models.TextField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('finished_at', models.DateTimeField(blank=True, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='report_documents', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

View File

@@ -1,3 +1,21 @@
from django.db import models
# Create your models here.
from django.db import models
from django.contrib.auth import get_user_model
class ReportDocument(models.Model):
STATUS_CHOICES = [
('pending', 'Pendiente'),
('processing', 'Procesando'),
('ready', 'Listo'),
('error', 'Error'),
]
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='report_documents')
filters = models.JSONField(blank=True, null=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
file = models.FileField(upload_to='reports/', blank=True, null=True)
error_message = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
finished_at = models.DateTimeField(blank=True, null=True)
def __str__(self):
return f"Reporte {self.id} - {self.status}"

View File

@@ -0,0 +1,85 @@
from celery import shared_task
from django.core.files.base import ContentFile
from django.utils import timezone
from api.reports.models import ReportDocument
from api.customs.models import Pedimento, Cove, EDocument, Partida
from django.db.models import Q
import csv
import os
from django.conf import settings
@shared_task
def generate_report_document(report_id):
try:
report = ReportDocument.objects.get(id=report_id)
report.status = 'processing'
report.save(update_fields=['status'])
filters = report.filters or {}
# Construir Q para filtros complejos
pedimentos_filters = Q()
if filters.get('organizacion_id'):
pedimentos_filters &= Q(organizacion_id=filters['organizacion_id'])
if filters.get('fecha_pago__gte'):
pedimentos_filters &= Q(fecha_pago__gte=filters['fecha_pago__gte'])
if filters.get('fecha_pago__lte'):
pedimentos_filters &= Q(fecha_pago__lte=filters['fecha_pago__lte'])
if filters.get('contribuyente__rfc'):
pedimentos_filters &= Q(contribuyente__rfc=filters['contribuyente__rfc'])
if filters.get('patente'):
pedimentos_filters &= Q(patente=filters['patente'])
if filters.get('aduana'):
pedimentos_filters &= Q(aduana=filters['aduana'])
if filters.get('pedimento'):
pedimentos_filters &= Q(pedimento=filters['pedimento'])
if filters.get('pedimento_app'):
pedimentos_filters &= Q(pedimento_app=filters['pedimento_app'])
if filters.get('regimen'):
pedimentos_filters &= Q(regimen=filters['regimen'])
if filters.get('tipo_operacion'):
pedimentos_filters &= Q(tipo_operacion_id=filters['tipo_operacion'])
# Consulta asíncrona de los modelos
pedimentos = Pedimento.objects.filter(pedimentos_filters)
filename = filters.get('filename')
if filename:
filename = f"{filename}.csv" if not filename.endswith('.csv') else filename
else:
filename = f"report_{report.id}_{timezone.now().strftime('%Y%m%d%H%M%S')}.csv"
file_path = os.path.join(settings.MEDIA_ROOT, 'reports', filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
headers = [
'aduana', 'patente', 'regimen', 'pedimento', 'pedimento_app', 'clave_pedimento',
'tipo_operacion_id', 'contribuyente_id', 'tipo_documento', 'numero_documento', 'estado', 'acuse_estado'
]
writer.writerow(headers)
for ped in pedimentos:
for cove in Cove.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'COVE', cove.numero_cove, cove.cove_descargado, cove.acuse_cove_descargado
])
for edoc in EDocument.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'EDOC', edoc.numero_edocument, edoc.edocument_descargado, edoc.acuse_descargado
])
for partida in Partida.objects.filter(pedimento=ped):
writer.writerow([
ped.aduana, ped.patente, ped.regimen, ped.pedimento, ped.pedimento_app,
ped.clave_pedimento, ped.tipo_operacion_id, ped.contribuyente_id,
'PARTIDA', partida.numero_partida, partida.descargado, ''
])
# Guardar el archivo en el modelo
with open(file_path, 'rb') as f:
report.file.save(filename, ContentFile(f.read()), save=True)
report.status = 'ready'
report.finished_at = timezone.now()
report.save(update_fields=['status', 'file', 'finished_at'])
except Exception as e:
report.status = 'error'
report.error_message = str(e)
report.finished_at = timezone.now()
report.save(update_fields=['status', 'error_message', 'finished_at'])

View File

@@ -1,12 +1,14 @@
from django.urls import path, include
from .views import ExportModelView, dashboard_summary
# from .views_stats import documentos_por_fecha
# from .views_table import table_summary
from .views_table import table_summary, report_document_status, report_document_list, report_document_download
urlpatterns = [
path('exportmodel/', ExportModelView.as_view(), name='export-model'),
path('dashboard/summary/', dashboard_summary, name='dashboard-summary'),
#path('documentos-por-fecha/', documentos_por_fecha, name='documentos-por-fecha'),
#path('table-summary/', table_summary, name='table-summary'),
path('table-summary/', table_summary, name='table-summary'),
path('report-document-status/<int:report_id>/', report_document_status, name='report_document_status'),
path('report-document-list/', report_document_list, name='report_document_list'),
path('report-document-download/<int:report_id>/', report_document_download, name='report_document_download'),
]

View File

@@ -1,27 +1,19 @@
from api.reports.models import ReportDocument
from api.reports.tasks.report_document import generate_report_document
from django.http import FileResponse
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from django.db.models import Value, CharField, Q, Exists, OuterRef, Subquery
from django.db.models.functions import Cast
from datetime import datetime, timedelta
from api.customs.models import Pedimento, Cove, EDocument, Partida
class CustomPagination(PageNumberPagination):
page_size = 50
page_size_query_param = 'page_size'
max_page_size = 1000
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def table_summary(request):
"""
Endpoint que devuelve un resumen tabulado de pedimentos y sus documentos asociados.
Solo dispara la tarea asíncrona para generar el reporte CSV. No consulta ni procesa datos.
"""
org_id = request.query_params.get('organizacion_id')
if not org_id:
return Response({"error": "organizacion_id es requerido"}, status=400)
# Obtener filtros de query params
tipo_documento = request.query_params.get('tipo_documento')
rfc = request.query_params.get('contribuyente__rfc')
@@ -33,137 +25,93 @@ def table_summary(request):
pedimento_app = request.query_params.get('pedimento_app')
regimen = request.query_params.get('regimen')
tipo_operacion = request.query_params.get('tipo_operacion')
# Si no se proporcionan fechas, establecer un rango por defecto de los últimos 30 días
if not fecha_pago_gte and not fecha_pago_lte:
fecha_pago_lte = datetime.now().date()
fecha_pago_gte = fecha_pago_lte - timedelta(days=30)
# Construir filtros base para pedimentos
pedimentos_filters = Q(organizacion_id=org_id)
# Añadir filtros de fecha siempre para limitar el conjunto de datos
pedimentos_filters &= Q(fecha_pago__gte=fecha_pago_gte)
pedimentos_filters &= Q(fecha_pago__lte=fecha_pago_lte)
if rfc:
pedimentos_filters &= Q(contribuyente__rfc=rfc)
if patente:
pedimentos_filters &= Q(patente=patente)
if aduana:
pedimentos_filters &= Q(aduana=aduana)
if pedimento:
pedimentos_filters &= Q(pedimento=pedimento)
if pedimento_app:
pedimentos_filters &= Q(pedimento_app=pedimento_app)
if regimen:
pedimentos_filters &= Q(regimen=regimen)
if tipo_operacion:
pedimentos_filters &= Q(tipo_operacion_id=tipo_operacion)
# Query base desde pedimentos con todas las subconsultas necesarias
resultado = Pedimento.objects.filter(pedimentos_filters).values(
'aduana',
'patente',
'regimen',
'pedimento',
'pedimento_app',
'clave_pedimento',
'tipo_operacion_id',
'contribuyente_id'
filename_param = request.query_params.get('filename')
if filename_param:
filename = filename_param
else:
filename = None
# Si no se proporcionan fechas, no las incluyas en los filtros
if not fecha_pago_gte:
fecha_pago_gte_str = None
elif isinstance(fecha_pago_gte, str):
fecha_pago_gte_str = fecha_pago_gte
else:
fecha_pago_gte_str = fecha_pago_gte.isoformat()
if not fecha_pago_lte:
fecha_pago_lte_str = None
elif isinstance(fecha_pago_lte, str):
fecha_pago_lte_str = fecha_pago_lte
else:
fecha_pago_lte_str = fecha_pago_lte.isoformat()
filtros = {
"organizacion_id": org_id,
"tipo_documento": tipo_documento,
"contribuyente__rfc": rfc,
"fecha_pago__gte": fecha_pago_gte_str,
"fecha_pago__lte": fecha_pago_lte_str,
"patente": patente,
"aduana": aduana,
"pedimento": pedimento,
"pedimento_app": pedimento_app,
"regimen": regimen,
"tipo_operacion": tipo_operacion,
"filename": filename
}
report = ReportDocument.objects.create(
user=request.user,
filters=filtros,
status='pending'
)
generate_report_document.delay(report.id)
return Response({
"report_id": report.id,
"status": report.status,
"created_at": report.created_at,
"download_url": report.file.url if report.file else None
}, status=202)
# Generar queries según el tipo de documento solicitado
queries = []
if not tipo_documento or tipo_documento == 'ACUSE COVE':
coves_acuse = resultado.annotate(
identificador=Cast(Subquery(
Cove.objects.filter(pedimento_id=OuterRef('id')).values('numero_cove')[:1]
), CharField()),
documento=Value('ACUSE COVE', CharField()),
estado=Cast(Subquery(
Cove.objects.filter(pedimento_id=OuterRef('id')).values('acuse_cove_descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(coves_acuse)
if not tipo_documento or tipo_documento == 'COVE':
coves = resultado.annotate(
identificador=Cast(Subquery(
Cove.objects.filter(pedimento_id=OuterRef('id')).values('numero_cove')[:1]
), CharField()),
documento=Value('COVE', CharField()),
estado=Cast(Subquery(
Cove.objects.filter(pedimento_id=OuterRef('id')).values('cove_descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(coves)
if not tipo_documento or tipo_documento == 'ACUSE EDOC':
edocs_acuse = resultado.annotate(
identificador=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('numero_edocument')[:1]
), CharField()),
documento=Value('ACUSE EDOC', CharField()),
estado=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('acuse_descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(edocs_acuse)
if not tipo_documento or tipo_documento == 'EDOC':
edocs = resultado.annotate(
identificador=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('numero_edocument')[:1]
), CharField()),
documento=Value('EDOC', CharField()),
estado=Cast(Subquery(
EDocument.objects.filter(pedimento_id=OuterRef('id')).values('edocument_descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(edocs)
if not tipo_documento or tipo_documento == 'PARTIDA':
partidas = resultado.annotate(
identificador=Cast(Subquery(
Partida.objects.filter(pedimento_id=OuterRef('id')).values('numero_partida')[:1]
), CharField()),
documento=Value('PARTIDA', CharField()),
estado=Cast(Subquery(
Partida.objects.filter(pedimento_id=OuterRef('id')).values('descargado')[:1]
), CharField())
).filter(identificador__isnull=False)
queries.append(partidas)
# Unir los resultados usando UNION ALL para mejor rendimiento
if not queries:
return Response([])
resultado_final = queries[0]
for query in queries[1:]:
resultado_final = resultado_final.union(query, all=True)
# Aplicar paginación
paginator = CustomPagination()
page = paginator.paginate_queryset(
resultado_final.order_by('pedimento', 'documento'),
request
)
return paginator.get_paginated_response({
"results": page,
"filtros_aplicados": {
"organizacion_id": org_id,
"tipo_documento": tipo_documento,
"contribuyente__rfc": rfc,
"fecha_pago__gte": fecha_pago_gte,
"fecha_pago__lte": fecha_pago_lte,
"patente": patente,
"aduana": aduana,
"pedimento": pedimento,
"pedimento_app": pedimento_app,
"regimen": regimen,
"tipo_operacion": tipo_operacion
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def report_document_status(request, report_id):
try:
report = ReportDocument.objects.get(id=report_id, user=request.user)
data = {
"report_id": report.id,
"status": report.status,
"created_at": report.created_at,
"finished_at": report.finished_at,
"error_message": report.error_message,
"download_url": report.file.url if report.file else None
}
})
return Response(data)
except ReportDocument.DoesNotExist:
return Response({"error": "Reporte no encontrado"}, status=404)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def report_document_list(request):
reports = ReportDocument.objects.filter(user=request.user).order_by('-created_at')
data = [
{
"report_id": r.id,
"status": r.status,
"created_at": r.created_at,
"finished_at": r.finished_at,
"error_message": r.error_message,
"download_url": r.file.url if r.file else None
}
for r in reports
]
return Response(data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def report_document_download(request, report_id):
try:
report = ReportDocument.objects.get(id=report_id, user=request.user)
if not report.file:
return Response({"error": "El archivo aún no está disponible"}, status=404)
response = FileResponse(report.file.open('rb'), as_attachment=True, filename=report.file.name)
return response
except ReportDocument.DoesNotExist:
return Response({"error": "Reporte no encontrado"}, status=404)

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

@@ -1,61 +0,0 @@
#!/bin/bash
echo "Iniciando limpieza completa de archivos media huérfanos..."
# Función para ejecutar limpieza
cleanup_batch() {
echo "Ejecutando limpieza de lote..."
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --verbose" 2>/dev/null
return $?
}
# Contador de iteraciones
iteration=1
total_cleaned=0
while true; do
echo "=== Iteración $iteration ==="
# Ejecutar dry-run para ver si hay archivos huérfanos
result=$(docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --dry-run" 2>/dev/null)
# Extraer número de archivos huérfanos
orphaned_count=$(echo "$result" | grep -o "Archivos huérfanos encontrados: [0-9]*" | grep -o "[0-9]*")
if [ -z "$orphaned_count" ] || [ "$orphaned_count" -eq 0 ]; then
echo "✅ No se encontraron más archivos huérfanos."
echo "🎉 Limpieza completa finalizada después de $iteration iteraciones."
echo "📊 Total estimado de archivos eliminados: $total_cleaned"
break
fi
echo "📁 Encontrados $orphaned_count archivos huérfanos en esta iteración"
# Preguntar confirmación en la primera iteración
if [ $iteration -eq 1 ]; then
echo "¿Continuar con la limpieza automática? (s/N):"
read -r response
if [[ ! "$response" =~ ^[sS]([iI]|í)?$ ]]; then
echo "❌ Limpieza cancelada por el usuario."
exit 0
fi
fi
# Ejecutar limpieza
cleanup_result=$(docker exec -it EFC_backend_dev bash -c "cd /app && echo 's' | python manage.py cleanup_media_fast --quick-scan --verbose" 2>/dev/null)
# Extraer archivos eliminados
deleted_count=$(echo "$cleanup_result" | grep -o "Eliminados [0-9]* archivos" | grep -o "[0-9]*")
if [ -n "$deleted_count" ]; then
total_cleaned=$((total_cleaned + deleted_count))
echo "✅ Eliminados $deleted_count archivos en esta iteración"
fi
iteration=$((iteration + 1))
# Pausa pequeña para evitar sobrecarga
sleep 2
done
echo "🏁 Proceso completado exitosamente."

View File

@@ -1,50 +1,5 @@
# Celery Beat Schedule
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'),
},
}
"""
Django settings for config project.
@@ -70,6 +25,14 @@ smtplib.SMTP_SSL.default_context = ssl._create_unverified_context
from dotenv import load_dotenv
import re
# Celery Beat Schedule
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
}
# Cargar variables de entorno desde un archivo .env
load_dotenv()
@@ -305,7 +268,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Ojinaga' # Zona horaria de Cd. Juárez, Chihuahua
TIME_ZONE = 'America/Mexico_City' # Zona horaria de Cd. Juárez, Chihuahua
USE_I18N = True
USE_TZ = True

View File

@@ -0,0 +1,577 @@
type: collection.insomnia.rest/5.0
name: EFC
meta:
id: wrk_71a7fc7f009b45338f10d5aee912e90c
created: 1759368209169
modified: 1759415563572
description: ""
collection:
- url: "{{ _.API_REST
}}customs/coves/?pedimento=7e199074-db14-4431-8ded-2028dcd7ba23&page=1&pa\
ge_size=10"
name: Coves
meta:
id: req_a90087be376f42eb846e9c0a080c1e0d
created: 1759368209176
modified: 1759368596683
isPrivate: false
description: ""
sortKey: -1759368209176
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST
}}customs/edocuments/?pedimento=7e199074-db14-4431-8ded-2028dcd7ba23&page\
=1&page_size=10"
name: EDocs
meta:
id: req_40ed26a5256d49f0bfe05716237131dd
created: 1759368603480
modified: 1759368614527
isPrivate: false
description: ""
sortKey: -1759285093434.5
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST
}}customs/procesamientopedimentos/?pedimento=7e199074-db14-4431-8ded-2028\
dcd7ba23&page=1&page_size=10"
name: Procesamientos del Pedimento
meta:
id: req_bba2280f3e894ada879b734a24c78414
created: 1759368715006
modified: 1759368736956
isPrivate: false
description: ""
sortKey: -1759243535563.75
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: http://192.168.1.79:8000/api/v1/customs/partidas/?pedimento=5ed0a19a-8a42-4ab4-8627-1a614be7aee3
name: New Request
meta:
id: req_680b0285689f46d99857ca2765a36e30
created: 1759457224319
modified: 1759717890407
isPrivate: false
description: ""
sortKey: -1759457224319
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_ea89b2c93a934f72ab8183aa7bcddfb3
- id: pair_be239987c9a648debc7c318ab3d9edd4
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}customs/partidas/771281/"
name: Partida
meta:
id: req_c911fa79111e45fc9d592a4dff98889a
created: 1759521825542
modified: 1759522450563
isPrivate: false
description: ""
sortKey: -1759326651305.25
method: PUT
body:
mimeType: application/json
text: "{
\t\"id\": 771281,
\t\"numero_partida\": 1,
\t\"descargado\": true,
\t\"pedimento\": \"0731973d-3929-43f5-805b-a8680fbadd30\",
\t\"organizacion\": \"4fea91c7-4a1d-40b3-a433-f0122b5ea43e\"
}
\t"
headers:
- name: Content-Type
value: application/json
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}vucem/vucem/?importador={{pedimento.contribuyente_id}}"
name: Vucem
meta:
id: req_4b049e9b11844f158762682b9118e572
created: 1759523330397
modified: 1759523405870
isPrivate: false
description: ""
sortKey: -1759264314499.125
method: GET
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/dashboard/summary/"
name: dashboard_summary
meta:
id: req_0d68721909b54ae983f31f91c8acdd4c
created: 1759623976466
modified: 1759710185608
isPrivate: false
description: ""
sortKey: -1759253925031.4375
method: GET
parameters:
- id: pair_9805d9f69b664b9d903fd142a6a940fb
name: pedimento_app
value: 20-16-3910-1000
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/documentos-por-fecha/"
name: Documentos por fecha
meta:
id: req_733c9cae6cbe4ccca8ccebe2efa5c3ba
created: 1759628575231
modified: 1759634973470
isPrivate: false
description: ""
sortKey: -1759248730297.5938
method: GET
parameters:
- id: pair_ec4119b532f94b579dfc54ddc2ed8979
name: periodo
value: semana
description: ""
disabled: false
- id: pair_d10e813bf42a45038907b3658df2dbeb
name: start_date
value: 2010-01-01
description: ""
disabled: false
- id: pair_3ab5417081e34862b9d3cc80b7125432
name: end_date
value: 2026-01-01
description: ""
disabled: false
- id: pair_74805c59d3704db3893d424e6580110a
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_fb9f6ce9c82f4db8834b6443f3fcea44
name: tipo_documento
value: "1"
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/table-summary/"
name: Summary Table
meta:
id: req_259a849b4a8e4a34b85f53644e7d5939
created: 1759634983441
modified: 1761103843982
isPrivate: false
description: ""
sortKey: -1759251327664.5156
method: GET
parameters:
- id: pair_320fbf127da544acae95eb8524daa1db
name: csv_async
value: "1"
description: ""
disabled: false
- id: pair_99f1f48e9db0439a950c706d5767657c
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_8900c16ca9894ac8b559284dde8feca8
name: page
value: "1"
description: ""
disabled: true
- id: pair_9d3852c3e1bb4dd6b6ea838df070a65d
name: page_size
value: "100"
description: ""
disabled: true
- id: pair_7ea23e87e0874588bf4d17041eaaa3c6
name: fecha_pago__lte
value: 2024-01-01
description: ""
disabled: true
- id: pair_a3c85e2563d24ec0ba772a2f6e27804a
name: ultimos
value: "1"
description: ""
disabled: true
- id: pair_e013d47373884f99b3f4def8592b2f01
name: filename
value: test_kevin
description: ""
disabled: true
- id: pair_9ff11357a7044e5b8f40bb9bb33e6e12
name: contribuyente__rfc
value: TEC1406248Q2
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}customs/pedimentos/"
name: Pedimento
meta:
id: req_9a679fe2a36b42949200a0cc93e7cc76
created: 1759717886641
modified: 1759718950361
isPrivate: false
description: ""
sortKey: -1759717886641
method: POST
body:
mimeType: application/json
text: |-
{
"pedimento": "946",
"patente": "3910",
"aduana": "160",
"regimen": "ITE",
"clave_pedimento": "IN",
"fecha_inicio": "2025-08-07",
"fecha_fin": "2025-08-07",
"fecha_pago": "2020-03-11",
"alerta": true,
"agente_aduanal": "AAC020726T77",
"curp_apoderado": "DAXS571117HNEVXR09",
"importe_total": 0.0,
"saldo_disponible": 0.0,
"importe_pedimento": 0.0,
"existe_expediente": true,
"remesas": true,
"numero_partidas": 0,
"numero_operacion": "string",
"tipo_operacion": 1,
"contribuyente": "TEC1406248Q2",
"organizacion":"9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4"
}
headers:
- name: Content-Type
value: application/json
- name: User-Agent
value: insomnia/11.6.1
id: pair_ed0ede3d111d469ea71b9c09f99134df
- id: pair_1db698171c6a4534a683351e27b61d9f
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: api.efc-aduanasoft.com/api/v1/customs/auditor/crear-partidas/pedimento/
name: Auditor Partidas
meta:
id: req_82ccc9ebce664984826063dd2e86bbc1
created: 1759720753371
modified: 1759721186316
isPrivate: false
description: ""
sortKey: -1759619270481.5
method: POST
body:
mimeType: application/json
text: |-
{
"pedimento_id": "54ed7616-aea9-46ca-887e-3185feb35d09"
}
headers:
- name: Content-Type
value: application/json
id: pair_5afd216181cd4050a683cc050a15f8af
- name: User-Agent
value: insomnia/11.6.1
id: pair_ed0ede3d111d469ea71b9c09f99134df
- id: pair_1db698171c6a4534a683351e27b61d9f
name: Authorization
value: Token 27ad1a1a7a1e7a64e7f452e89265b54b5b3ef4c2
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/report-document-list/"
name: Reports List
meta:
id: req_7a951a204e8b454e81b25c059ad8ff18
created: 1761091292956
modified: 1761094591411
isPrivate: false
description: ""
sortKey: -1759250028981.0547
method: GET
parameters:
- id: pair_320fbf127da544acae95eb8524daa1db
name: csv_async
value: "1"
description: ""
disabled: false
- id: pair_99f1f48e9db0439a950c706d5767657c
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_8900c16ca9894ac8b559284dde8feca8
name: page
value: "1"
description: ""
disabled: true
- id: pair_9d3852c3e1bb4dd6b6ea838df070a65d
name: page_size
value: "100"
description: ""
disabled: true
- id: pair_7ea23e87e0874588bf4d17041eaaa3c6
name: fecha_pago__lte
value: 2024-01-01
description: ""
disabled: false
- id: pair_a3c85e2563d24ec0ba772a2f6e27804a
name: ultimos
value: "1"
description: ""
disabled: true
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: "{{ _.API_REST }}reports/report-document-download/11/"
name: Download Report
meta:
id: req_11dc9145994b43798c69ee3ecebe8ba8
created: 1761092998357
modified: 1761093919398
isPrivate: false
description: ""
sortKey: -1759249379639.3242
method: GET
parameters:
- id: pair_320fbf127da544acae95eb8524daa1db
name: csv_async
value: "1"
description: ""
disabled: false
- id: pair_99f1f48e9db0439a950c706d5767657c
name: organizacion_id
value: 9d705e97-d3f2-4b6c-8d92-9f1af2b2d4b4
description: ""
disabled: false
- id: pair_8900c16ca9894ac8b559284dde8feca8
name: page
value: "1"
description: ""
disabled: true
- id: pair_9d3852c3e1bb4dd6b6ea838df070a65d
name: page_size
value: "100"
description: ""
disabled: true
- id: pair_7ea23e87e0874588bf4d17041eaaa3c6
name: fecha_pago__lte
value: 2024-01-01
description: ""
disabled: false
- id: pair_a3c85e2563d24ec0ba772a2f6e27804a
name: ultimos
value: "1"
description: ""
disabled: false
- id: pair_ab1873fdc82f4b8d898a5f0004cae1fe
name: "rfc "
value: MTK861014317
description: ""
disabled: false
headers:
- name: User-Agent
value: insomnia/11.6.1
id: pair_586f587a78b540bd9f69c70966a3b12c
- id: pair_14420e46d71e4aa98a60238d96b520b3
name: Authorization
value: Token {{ _.Token }}
description: ""
disabled: false
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
cookieJar:
name: Default Jar
meta:
id: jar_7ef8f609d3e9f73da7feb30ab71eb8baaaf31c81
created: 1759368209171
modified: 1759368209171
environments:
name: Base Environment
meta:
id: env_7ef8f609d3e9f73da7feb30ab71eb8baaaf31c81
created: 1759368209170
modified: 1759719172699
isPrivate: false
data:
API_REST: http://192.168.1.79:8000/api/v1/
Token: 27ad1a1a7a1e7a64e7f452e89265b54b5b3ef4c2

View File

@@ -0,0 +1,52 @@
# Comando de Microservicios
Este comando permite disparar tareas de procesamiento masivo por organización o por pedimento en el sistema EFC.
## Uso básico
```bash
python manage.py microservicios [--organizacion_id=<id>] [--procesamiento=<tipo>] [--todos]
```
## Opciones
- `--organizacion_id <id>`: Ejecuta los procesamientos solo para la organización indicada. Si se omite, se procesan todas las organizaciones.
- `--procesamiento <tipo>`: Ejecuta solo el tipo de procesamiento indicado para la organización. Si se omite, se ejecutan todos los procesamientos.
- `--todos`: Ejecuta todos los procesos para todas las organizaciones.
## Tipos de procesamiento disponibles
- `coves`: Procesa COVEs por organización/pedimento
- `edocs`: Procesa E-Documents por organización/pedimento
- `acuses`: Procesa acuses por organización/pedimento
- `acuse_coves`: Procesa acuse de COVEs por organización/pedimento
- `partidas`: Procesa partidas por organización/pedimento
- `pedimentos_completos`: Procesa pedimentos completos por organización/pedimento
- `remesas`: Procesa remesas por organización/pedimento
## Ejemplos
### Procesar todos los tipos para todas las organizaciones
```bash
python manage.py microservicios --todos
```
### Procesar solo COVEs para una organización
```bash
python manage.py microservicios --organizacion_id=ID_ORG --procesamiento=coves
```
### Procesar todos los tipos para una organización
```bash
python manage.py microservicios --organizacion_id=ID_ORG
```
### Procesar todos los tipos para un pedimento específico
```bash
python manage.py microservicios --pedimento_id=ID_PEDIMENTO
```
## Notas
- El comando dispara las tareas de Celery de forma asíncrona.
- Revisa los logs y el estado de las tareas en el panel de administración de Celery o en los logs del sistema.
- Si agregas nuevos tipos de procesamiento, actualiza la función `ejecutar_procesamiento_por_organizacion` en `microservice_v2.py`.

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