20 Commits

Author SHA1 Message Date
8349b85714 Update api/reports/views.py 2025-11-25 22:17:25 +00:00
93f7445725 Update api/reports/tasks/report_document.py 2025-11-25 22:08:05 +00:00
Dulce
1a2909a5ac organizacion update, eliminar parametro descripcion ya que no existe e impedia realizar la busqueda 2025-11-25 13:18:31 -07:00
Dulce
a765026075 actualizacion de reportes 2025-11-25 13:17:41 -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
27 changed files with 2609 additions and 313 deletions

View File

@@ -17,4 +17,7 @@ EMAIL_HOST_PASSWORD=N036p7y!
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST=secure.emailsrvr.com 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 django.core.management.base import BaseCommand
from api.customs.tasks.auditoria import ( from api.customs.tasks.auditoria import (
auditar_procesamiento_remesas,
auditar_coves, auditar_coves,
auditar_acuse_cove, auditar_acuse_cove,
auditar_edocuments, auditar_edocuments,
@@ -15,7 +14,6 @@ class Command(BaseCommand):
# Definir las tareas disponibles # Definir las tareas disponibles
TAREAS_DISPONIBLES = { TAREAS_DISPONIBLES = {
'remesas': (auditar_procesamiento_remesas, "Auditoría de remesas"),
'partidas': (crear_partidas, "Creación de partidas"), 'partidas': (crear_partidas, "Creación de partidas"),
'coves': (auditar_coves, "Auditoría de COVEs"), 'coves': (auditar_coves, "Auditoría de COVEs"),
'acuse-cove': (auditar_acuse_cove, "Auditoría de acuses 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}.") print(f"✗ No se encontró proceso de auditoría para pedimento {pedimento_id}.")
## Auditar pedimentos ## 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 @shared_task
def auditar_procesamiento_remesa_por_pedimento(pedimento_id): def auditar_procesamiento_remesa_por_pedimento(pedimento_id):
@@ -260,3 +238,67 @@ def auditar_acuse(organizacion_id):
mensaje='acuse' 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}") print(f"Servicio enviado para pedimento {pedimento.pedimento}")
@shared_task @shared_task
def procesar_coves(organizacion_id): def procesar_coves(organizacion_id):
pedimentos = Pedimento.objects.filter( 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}") print(f"Documento con error: {doc.id} en organización {organizacion_id}")
# Aquí puedes agregar lógica adicional para manejar documentos con errores # Aquí puedes agregar lógica adicional para manejar documentos con errores
# como enviar notificaciones, registrar en un log, etc. # 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_organizacion,
crear_partidas_pedimento, crear_partidas_pedimento,
auditar_pedimentos_endpoint, auditar_pedimentos_endpoint,
auditar_procesamiento_remesas_endpoint, auditar_coves_endpoint,
auditar_procesamiento_remesa_pedimento_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 = [ urlpatterns = [
@@ -43,6 +50,13 @@ urlpatterns = [
path('auditor/crear-partidas/organizacion/', crear_partidas_organizacion, name='crear-partidas-organizacion'), 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/crear-partidas/pedimento/', crear_partidas_pedimento, name='crear-partidas-pedimento'),
path('auditor/auditar-pedimentos/', auditar_pedimentos_endpoint, name='auditar-pedimentos'), 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-coves/', auditar_coves_endpoint, name='auditar-coves'),
path('auditor/auditar-procesamiento-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-procesamiento-remesa-pedimento'), 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 # Verificar si la respuesta fue exitosa
if response.status_code == 200: if response.status_code == 200:
print(f"Servicio FastAPI ejecutado exitosamente: {response.status_code}") print(f"Servicio FastAPI ejecutado exitosamente: {response.status_code}")
print(f"📄 Respuesta: {response.json()}") print(f"Respuesta: {response.json()}")
elif response.status_code == 201: elif response.status_code == 201:
print(f"Recurso creado exitosamente en FastAPI: {response.status_code}") print(f"Recurso creado exitosamente en FastAPI: {response.status_code}")
print(f"📄 Respuesta: {response.json()}") print(f"Respuesta: {response.json()}")
else: else:
print(f"⚠️ Servicio FastAPI respondió con error: {response.status_code}") print(f"Servicio FastAPI respondió con error: {response.status_code}")
print(f"📄 Respuesta: {response.text}") print(f"Respuesta: {response.text}")
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
print(f"No se pudo conectar al servicio FastAPI: {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"Verifica que el servicio FastAPI esté corriendo en {SERVICE_API_URL}")
except requests.exceptions.Timeout as e: 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: 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: 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): def perform_update(self, serializer):
""" """

View File

@@ -8,11 +8,20 @@ from core.permissions import IsSuperUser, IsSameOrganizationDeveloper
from .tasks.auditoria import ( from .tasks.auditoria import (
crear_partidas, crear_partidas,
crear_partidas_por_pedimento, 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.internal_services import auditar_pedimentos
from .tasks.microservice_v2 import procesar_pedimentos_completos
from api.customs.models import Pedimento from api.customs.models import Pedimento
from api.organization.models import Organizacion
@swagger_auto_schema( @swagger_auto_schema(
@@ -230,7 +239,7 @@ def auditar_procesamiento_remesa_pedimento_endpoint(request):
@swagger_auto_schema( @swagger_auto_schema(
method='post', 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( request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
properties={ properties={
@@ -246,10 +255,10 @@ def auditar_procesamiento_remesa_pedimento_endpoint(request):
) )
@api_view(['POST']) @api_view(['POST'])
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)]) @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. Inicia una tarea de auditoría para los COVEs de una organización.
Verifica el estado y la integridad del procesamiento de remesas. Verifica la existencia y validez de los COVEs generados.
""" """
organizacion_id = request.data.get('organizacion_id') organizacion_id = request.data.get('organizacion_id')
@@ -268,8 +277,8 @@ def auditar_procesamiento_remesas_endpoint(request):
) )
# Ejecutar la tarea de auditoría # Ejecutar la tarea de auditoría
task = auditar_procesamiento_remesas.delay(organizacion_id) task = auditar_coves.delay(organizacion_id)
message = f"Auditoría de procesamiento de remesas iniciada para la organización {organizacion_id}" message = f"Auditoría de COVEs iniciada para la organización {organizacion_id}"
return Response({ return Response({
'message': message, 'message': message,
@@ -277,3 +286,350 @@ def auditar_procesamiento_remesas_endpoint(request):
}, status=status.HTTP_200_OK) }, 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

@@ -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
import logging
logger = logging.getLogger()
@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 {}
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'])
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, ''
])
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

@@ -27,7 +27,7 @@ class ViewSetOrganizacion(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltr
queryset = Organizacion.objects.all() queryset = Organizacion.objects.all()
serializer_class = OrganizacionSerializer serializer_class = OrganizacionSerializer
filterset_fields = ['nombre', 'descripcion'] filterset_fields = ['nombre']
my_tags = ['Organizaciones'] my_tags = ['Organizaciones']

View File

@@ -313,6 +313,211 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
return Response(response_data, status=response_status) return Response(response_data, status=response_status)
@action(detail=False, methods=['post'], url_path='bulk-upload', 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): class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = DocumentSerializer serializer_class = DocumentSerializer

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,26 @@
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'),
]
TYPE_REPORT = [
('cumplimiento', 'cumplimiento'),
('control_pedimento', 'control_pedimento'),
]
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)
report_type = models.CharField(max_length=30, choices=TYPE_REPORT, default='cumplimiento')
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,252 @@
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, Exists, OuterRef
# from django.db.models import Q,
from api.record.models import Document
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 {}
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'])
@shared_task
def generate_report_control_pedimento(report_id):
try:
report = ReportDocument.objects.get(id=report_id)
report.status = 'processing'
report.save(update_fields=['status'])
filters = report.filters or {}
# Construir filtros
pedimentos_filters = {}
if filters.get('organizacion_id'):
pedimentos_filters['organizacion_id'] = filters['organizacion_id']
if filters.get('fecha_pago__gte'):
pedimentos_filters['fecha_pago__gte'] = filters['fecha_pago__gte']
if filters.get('fecha_pago__lte'):
pedimentos_filters['fecha_pago__lte'] = filters['fecha_pago__lte']
if filters.get('pedimento_app'):
pedimentos_filters['pedimento_app'] = filters['pedimento_app']
# pedimentos por organizacion
pedimentos_qs = Pedimento.objects.filter(**pedimentos_filters)
pedimentos_total = pedimentos_qs.count()
pedimento_ids = list(pedimentos_qs.values_list('id', flat=True))
# inicializar totales
pedimentos_completos = 0
total_documentos = 0
documentos_sin_descargar = 0
# Para cada pedimento, verificar si está completo
for pedimento in pedimentos_qs:
# Contar documentos de este pedimento
docs_pedimento = 0
docs_pendientes_pedimento = 0
# COVES
coves_count = Cove.objects.filter(pedimento_id=pedimento.id).count()
coves_pendientes = Cove.objects.filter(pedimento_id=pedimento.id, cove_descargado=False).count()
docs_pedimento += coves_count
docs_pendientes_pedimento += coves_pendientes
# PARTIDAS
partidas_count = Partida.objects.filter(pedimento_id=pedimento.id).count()
partidas_pendientes = Partida.objects.filter(pedimento_id=pedimento.id, descargado=False).count()
docs_pedimento += partidas_count
docs_pendientes_pedimento += partidas_pendientes
# EDOCUMENTS
edocs_count = EDocument.objects.filter(pedimento_id=pedimento.id).count()
edocs_pendientes = EDocument.objects.filter(pedimento_id=pedimento.id, edocument_descargado=False).count()
docs_pedimento += edocs_count
docs_pendientes_pedimento += edocs_pendientes
# Acumular totales
total_documentos += docs_pedimento
documentos_sin_descargar += docs_pendientes_pedimento
# Si no tiene documentos pendientes, está completo
if docs_pendientes_pedimento == 0 and docs_pedimento > 0:
pedimentos_completos += 1
# 3. PORCENTAJE
porcentaje_faltantes = (documentos_sin_descargar / total_documentos * 100) if total_documentos > 0 else 0
# 4. GENERAR CSV CON DETALLES
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)
todas_las_filas = []
# Recopilar datos detallados - UNA FILA POR CADA DOCUMENTO
for pedimento in pedimentos_qs:
# DATOS BASE DEL PEDIMENTO (se repiten en cada fila)
datos_base_pedimento = [
pedimento.aduana or '',
pedimento.patente or '',
pedimento.regimen or '',
pedimento.pedimento or '', # No. Pedimento (7 dígitos)
pedimento.pedimento_app or '', # No. Pedimento App completo
pedimento.clave_pedimento or '',
pedimento.tipo_operacion.tipo if pedimento.tipo_operacion else '',
str(pedimento.contribuyente_id) if pedimento.contribuyente_id else ''
]
# COVES - Una fila por cada COVE
coves = Cove.objects.filter(pedimento_id=pedimento.id)
for cove in coves:
estado = 'VERDADERO' if cove.cove_descargado else 'FALSO'
fila = datos_base_pedimento + [
# str(cove.id), # Identificador de documento
cove.numero_cove,
'COVE', # Tipo de documento
estado
]
todas_las_filas.append(fila)
# PARTIDAS - Una fila por cada Partida
partidas = Partida.objects.filter(pedimento_id=pedimento.id)
for partida in partidas:
estado = 'VERDADERO' if partida.descargado else 'FALSO'
fila = datos_base_pedimento + [
# str(partida.id),
partida.numero_partida,
'PARTIDA', # Tipo de documento
estado
]
todas_las_filas.append(fila)
# EDOCUMENTS - Una fila por cada EDocument
edocuments = EDocument.objects.filter(pedimento_id=pedimento.id)
for edoc in edocuments:
estado = 'VERDADERO' if edoc.edocument_descargado else 'FALSO'
fila = datos_base_pedimento + [
# str(edoc.id),
edoc.numero_edocument,
'EDOCUMENT', # Tipo de documento
estado
]
todas_las_filas.append(fila)
# 5. ESCRIBIR ARCHIVO CSV
with open(file_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
# SECCIÓN DE TOTALES
writer.writerow(['RESUMEN DEL REPORTE - CONTROL DE PEDIMENTOS'])
writer.writerow([])
writer.writerow(['TOTAL DE EXPEDIENTES:', pedimentos_total])
writer.writerow(['TOTAL DE EXPEDIENTES COMPLETOS:', pedimentos_completos])
writer.writerow(['TOTAL DE DOCUMENTOS:', total_documentos])
writer.writerow(['DOCUMENTOS SIN DESCARGAR:', documentos_sin_descargar])
writer.writerow(['PORCENTAJE DE DOCUMENTOS FALTANTES (%):', f"{porcentaje_faltantes:.2f}%"])
writer.writerow([])
writer.writerow([])
# ENCABEZADOS DE DATOS (según requerimiento)
headers = [
'ADUANA', 'PATENTE', 'REGIMEN', 'NO. PEDIMENTO', 'PEDIMENTO_APP',
'CLAVE_PEDIMENTO', 'TIPO_OPERACION', 'CONTRIBUYENTE_ID',
'IDENTIFICADOR_DOCUMENTO', 'TIPO_DOCUMENTO', 'ESTADO'
]
writer.writerow(headers)
# DATOS DETALLADOS
for fila in todas_las_filas:
writer.writerow(fila)
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,16 @@
from django.urls import path, include from django.urls import path, include
from .views import ExportModelView, dashboard_summary from .views import ExportModelView, ExportDataStageView, dashboard_summary
# from .views_stats import documentos_por_fecha # 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, control_pedimento
urlpatterns = [ urlpatterns = [
path('exportmodel/', ExportModelView.as_view(), name='export-model'), path('exportmodel/', ExportModelView.as_view(), name='export-model'),
path('exportmodel/datastage/', ExportDataStageView.as_view(), name='export-datastage-model'),
path('control-pedimento/', control_pedimento, name='control_pedimento'),
path('dashboard/summary/', dashboard_summary, name='dashboard-summary'), path('dashboard/summary/', dashboard_summary, name='dashboard-summary'),
#path('documentos-por-fecha/', documentos_por_fecha, name='documentos-por-fecha'), #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

@@ -48,7 +48,10 @@ from core.permissions import (
IsSuperUser IsSuperUser
) )
from .serializers import ExportModelSerializer from .serializers import ExportModelSerializer
import uuid
import datetime
import zipfile
from django.db import models
def export_model_to_csv(request, model_name, fields, module='datastage', filters=None): def export_model_to_csv(request, model_name, fields, module='datastage', filters=None):
model = apps.get_model(module, model_name) model = apps.get_model(module, model_name)
@@ -86,11 +89,657 @@ def export_model_to_excel(request, model_name, fields, module='datastage', filte
response['Content-Disposition'] = f'attachment; filename="{model_name}.xlsx"' response['Content-Disposition'] = f'attachment; filename="{model_name}.xlsx"'
return response return response
# class ControlPedimentoView(APIView):
# my_tags = ['Control-Pedimento']
# permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
# @swagger_auto_schema(request_body=ExportModelSerializer, responses={200: 'Archivo generado (Excel o CSV)'})
# def post(self, request, *args, **kwargs):
# """
# Endpoint específico para exportación de DataStage con soporte múltiple
# """
# # Verificar si es modo múltiple
# modo = request.data.get('modo', 'simple')
# if modo == 'multiple':
# return self.handle_multiple_export(request)
# else:
# return self.handle_simple_export(request)
class ExportDataStageView(APIView):
my_tags = ['Reportes-DataStage']
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
# Constantes para partición
# MAX_RECORDS_PER_FILE = 100 # Límite seguro por archivo
MAX_RECORDS_PER_FILE = 50000 # Límite seguro por archivo
def safe_excel_value(self, value):
"""
Convierte cualquier valor a un formato seguro para Excel
"""
if value is None:
return ''
elif isinstance(value, (uuid.UUID,)):
return str(value)
elif hasattr(value, 'uuid'):
return str(value.uuid)
elif hasattr(value, 'id'):
return str(value.id)
elif isinstance(value, (datetime.datetime, datetime.date)):
return value.isoformat()
elif isinstance(value, (dict, list)):
return str(value)
else:
return str(value)
@swagger_auto_schema(request_body=ExportModelSerializer, responses={200: 'Archivo generado (Excel o CSV)'})
def post(self, request, *args, **kwargs):
"""
Endpoint específico para exportación de DataStage con soporte múltiple
"""
# Verificar si es modo múltiple
modo = request.data.get('modo', 'simple')
if modo == 'multiple':
return self.handle_multiple_export(request)
else:
return self.handle_simple_export(request)
def handle_simple_export(self, request):
"""Maneja exportación simple de DataStage (un solo modelo)"""
model_name = request.data.get('model')
fields = request.data.get('fields')
global_filters = request.data.get('globalFilters', {})
export_type = request.data.get('format', 'csv')
module = 'datastage'
if not model_name or not fields:
return Response({'error': 'model and fields are required'}, status=status.HTTP_400_BAD_REQUEST)
try:
model = apps.get_model(module, model_name)
filters = self.apply_global_filters_to_model(global_filters, model, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if export_type == 'excel':
# Verificar si necesita partición
if total_records > self.MAX_RECORDS_PER_FILE:
return self.export_single_model_partitioned(request, model_name, fields, filters, total_records)
else:
return export_model_to_excel(request, model_name, fields, module, filters)
else:
if total_records > self.MAX_RECORDS_PER_FILE:
return self.export_single_model_csv_partitioned(request, model_name, fields, filters, total_records)
else:
return export_model_to_csv(request, model_name, fields, module, filters)
except LookupError:
return Response({'error': f'Model {model_name} not found'}, status=status.HTTP_404_NOT_FOUND)
def handle_multiple_export(self, request):
"""Maneja exportación múltiple de DataStage (varios modelos)"""
models_data = request.data.get('models', [])
export_type = request.data.get('format', 'csv')
global_filters = request.data.get('globalFilters', {})
if not models_data:
return Response({'error': 'models are required for multiple export'}, status=status.HTTP_400_BAD_REQUEST)
related_keys = self.get_related_keys_from_filters(global_filters, models_data, request.user)
total_estimated_records = self.estimate_total_records(models_data, global_filters, related_keys, request.user)
if total_estimated_records > self.MAX_RECORDS_PER_FILE:
if export_type == 'excel':
return self.export_datastage_multiple_partitioned_excel(request, models_data, global_filters, related_keys)
else:
return self.export_datastage_multiple_partitioned_csv(request, models_data, global_filters, related_keys)
else:
if export_type == 'excel':
return self.export_datastage_multiple_to_excel(request, models_data, global_filters, related_keys)
else:
return self.export_datastage_multiple_to_csv(request, models_data, global_filters, related_keys)
def estimate_total_records(self, models_data, global_filters, related_keys, user):
"""Estima el total de registros para todos los modelos"""
total = 0
for model_data in models_data:
model_name = model_data.get('model')
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, user)
total += model.objects.filter(**filters).count()
except:
continue
return total
def export_datastage_multiple_to_excel(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage con filtrado relacionado (múltiples hojas)"""
wb = openpyxl.Workbook()
wb.remove(wb.active)
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
# 🔥 APLICAR FILTROS RELACIONADOS
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
# Si hay filtros, aplicarlos; si no, obtener todos los registros
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none() # No obtener nada si no hay filtros
# Si no hay registros, saltar este modelo
if queryset.count() == 0:
continue
# Crear hoja (limitar nombre a 31 caracteres)
sheet_name = model_name[:31]
ws = wb.create_sheet(title=sheet_name)
# Escribir encabezados
ws.append(fields)
# Escribir datos
for row in queryset:
row_values = []
for field in fields:
value = row[field]
# 🔥 USAR safe_excel_value para convertir valores
row_values.append(self.safe_excel_value(value))
ws.append(row_values)
except LookupError:
continue
# Si no se crearon hojas, crear una vacía
if len(wb.sheetnames) == 0:
ws = wb.create_sheet(title="Sin datos")
ws.append(["No se encontraron datos para los modelos especificados"])
output = io.BytesIO()
wb.save(output)
output.seek(0)
response = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = 'attachment; filename="datastage_related_report.xlsx"'
return response
def export_datastage_multiple_partitioned_excel(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos Excel particionados"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
# Si hay filtros, aplicarlos; si no, obtener todos los registros
if filters:
queryset = model.objects.filter(**filters).values(*fields)
else:
queryset = model.objects.none() # No obtener nada si no hay filtros
total_records = queryset.count()
if total_records == 0:
continue
if total_records > self.MAX_RECORDS_PER_FILE:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
wb = openpyxl.Workbook()
ws = wb.active
ws.title = f"Parte_{page_num}"[:31]
ws.append(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
# Guardar parte en ZIP
part_buffer = io.BytesIO()
wb.save(part_buffer)
part_buffer.seek(0)
filename = f"{model_name}_part{page_num}.xlsx"
zip_file.writestr(filename, part_buffer.getvalue())
else:
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Datos"[:31]
ws.append(fields)
# Escribir datos
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
part_buffer = io.BytesIO()
wb.save(part_buffer)
part_buffer.seek(0)
filename = f"{model_name}.xlsx"
zip_file.writestr(filename, part_buffer.getvalue())
except LookupError as e:
continue
except Exception as e:
continue
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error en exportación particionada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_datastage_multiple_to_csv(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos CSV en ZIP"""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if total_records == 0:
continue
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
except LookupError:
continue
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
def export_datastage_multiple_partitioned_csv(self, request, models_data, global_filters, related_keys):
"""Exporta múltiples modelos de DataStage a múltiples archivos CSV particionados en ZIP"""
try:
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
for model_data in models_data:
model_name = model_data.get('model')
fields = model_data.get('fields', [])
if not model_name or not fields:
continue
try:
model = apps.get_model('datastage', model_name)
filters = self.apply_related_filters(global_filters, model, related_keys, request.user)
queryset = model.objects.filter(**filters).values(*fields)
total_records = queryset.count()
if total_records == 0:
continue
if total_records > self.MAX_RECORDS_PER_FILE:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}_part{page_num}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
else:
# Modelo pequeño, exportar completo
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
# Escribir encabezados
writer.writerow(fields)
# Escribir datos
for row in queryset:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
except LookupError as e:
continue
except Exception as e:
continue
zip_buffer.seek(0)
response = HttpResponse(zip_buffer.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="datastage_reports.zip"'
return response
except Exception as e:
return Response({'error': f'Error en exportación CSV particionada: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_single_model_partitioned(self, request, model_name, fields, filters, total_records):
"""Exporta un solo modelo particionado a ZIP"""
try:
zip_buffer = io.BytesIO()
module = 'datastage'
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**filters).values(*fields)
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
# Crear Excel para esta parte
wb = openpyxl.Workbook()
ws = wb.active
ws.title = f"Parte_{page_num}"[:31]
ws.append(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
ws.append(row_values)
part_buffer = io.BytesIO()
wb.save(part_buffer)
part_buffer.seek(0)
filename = f"{model_name}_part{page_num}.xlsx"
zip_file.writestr(filename, part_buffer.getvalue())
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename="{model_name}_particionado.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error exportando modelo: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def export_single_model_csv_partitioned(self, request, model_name, fields, filters, total_records):
"""Exporta un solo modelo CSV particionado a ZIP"""
try:
zip_buffer = io.BytesIO()
module = 'datastage'
model = apps.get_model(module, model_name)
queryset = model.objects.filter(**filters).values(*fields)
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
from django.core.paginator import Paginator
paginator = Paginator(queryset, self.MAX_RECORDS_PER_FILE)
for page_num in paginator.page_range:
page = paginator.page(page_num)
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(fields)
for row in page.object_list:
row_values = [self.safe_excel_value(row[field]) for field in fields]
writer.writerow(row_values)
# Agregar al ZIP
filename = f"{model_name}_part{page_num}.csv"
zip_file.writestr(filename, csv_buffer.getvalue())
zip_buffer.seek(0)
zip_content = zip_buffer.getvalue()
response = HttpResponse(zip_content, content_type='application/zip')
response['Content-Disposition'] = f'attachment; filename="{model_name}_particionado.zip"'
response['Content-Length'] = len(zip_content)
return response
except Exception as e:
return Response({'error': f'Error exportando modelo CSV: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_related_keys_from_filters(self, global_filters, models_data, user):
"""
Obtiene patentes, pedimentos y datastages que cumplen EXACTAMENTE con TODOS los filtros globales
para usarlos como relación entre modelos
"""
related_keys = {
'patentes': set(),
'pedimentos': set(),
'datastage_ids': set()
}
# Si no hay filtros globales, retornar vacío (no hay relación)
if not any(global_filters.values()):
return {}
all_records_with_filters = []
# Buscar en TODOS los modelos que puedan tener los campos de filtro
for model_data in models_data:
model_name = model_data.get('model')
try:
model = apps.get_model('datastage', model_name)
model_fields = [f.name for f in model._meta.get_fields()]
# Construir filtros EXACTOS con TODOS los campos disponibles
filters = {}
has_any_filter = False
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
has_any_filter = True
if 'patente' in model_fields and global_filters.get('patente'):
filters['patente'] = global_filters['patente']
has_any_filter = True
if 'pedimento' in model_fields and global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
has_any_filter = True
if 'rfc' in model_fields and global_filters.get('rfc'):
filters['rfc'] = global_filters['rfc']
has_any_filter = True
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
has_any_filter = True
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
has_any_filter = True
if has_any_filter:
records = model.objects.filter(**filters).values('patente', 'pedimento', 'datastage_id')
record_count = records.count()
all_records_with_filters.extend(list(records))
except LookupError:
continue
if not all_records_with_filters:
return {'patentes': set(), 'pedimentos': set(), 'datastage_ids': set()}
for record in all_records_with_filters:
if record.get('patente'):
related_keys['patentes'].add(record['patente'])
if record.get('pedimento'):
related_keys['pedimentos'].add(record['pedimento'])
if record.get('datastage_id'):
related_keys['datastage_ids'].add(record['datastage_id'])
related_keys = {k: list(v) for k, v in related_keys.items() if v}
return related_keys
def apply_global_filters_to_model(self, global_filters, model, user):
"""
Aplica filtros globales específicamente para modelos DataStage (modo simple)
"""
filters = {}
model_fields = [f.name for f in model._meta.get_fields()]
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
if 'patente' in model_fields and global_filters.get('patente'):
filters['patente'] = global_filters['patente']
if 'pedimento' in model_fields and global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
if 'rfc' in model_fields and global_filters.get('rfc'):
filters['rfc'] = global_filters['rfc']
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
return filters
def apply_related_filters(self, global_filters, model, related_keys, user):
"""
Aplica filtros relacionados basados en campos comunes de manera ESTRICTA - VERSIÓN CORREGIDA
"""
filters = {}
model_fields = [f.name for f in model._meta.get_fields()]
# 🔥 ESTRATEGIA MEJORADA: Usar claves relacionadas SI HAY, sino aplicar filtros directos SOLO si existen
has_related_keys = any(related_keys.values())
if has_related_keys:
# 🔥 MODO RELACIONADO ESTRICTO: Usar SOLO las claves obtenidas
# Crear condiciones para las claves relacionadas
from django.db.models import Q
related_conditions = Q()
has_related_conditions = False
if related_keys.get('patentes') and 'patente' in model_fields:
filters['patente__in'] = related_keys['patentes']
has_related_conditions = True
if related_keys.get('pedimentos') and 'pedimento' in model_fields:
filters['pedimento__in'] = related_keys['pedimentos']
has_related_conditions = True
if related_keys.get('datastage_ids') and 'datastage_id' in model_fields:
filters['datastage_id__in'] = related_keys['datastage_ids']
has_related_conditions = True
# Si NO HAY condiciones relacionadas para este modelo (no tiene los campos)
if not has_related_conditions:
return {} # Retornar filtro vacío hará que no se obtengan registros
else:
# 🔥 MODO DIRECTO: No hay claves relacionadas, aplicar filtros directos SOLO si existen
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
if 'patente' in model_fields and global_filters.get('patente'):
filters['patente'] = global_filters['patente']
if 'pedimento' in model_fields and global_filters.get('pedimento'):
filters['pedimento'] = global_filters['pedimento']
if 'rfc' in model_fields and global_filters.get('rfc'):
filters['rfc'] = global_filters['rfc']
# 🔥 APLICAR ORGANIZACIÓN SIEMPRE si existe (en ambos modos)
if 'organizacion' in model_fields and global_filters.get('organizacion'):
filters['organizacion'] = global_filters['organizacion']
# 🔥 APLICAR FILTROS DE FECHA SIEMPRE (si el campo existe)
if 'fecha_pago_real' in model_fields:
if global_filters.get('fecha_pago_desde'):
filters['fecha_pago_real__gte'] = global_filters['fecha_pago_desde']
if global_filters.get('fecha_pago_hasta'):
filters['fecha_pago_real__lte'] = global_filters['fecha_pago_hasta']
return filters
class ExportModelView(APIView): class ExportModelView(APIView):
my_tags = ['Reportes'] my_tags = ['Reportes']
permission_classes = [IsAuthenticated & ( permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
@swagger_auto_schema( @swagger_auto_schema(
manual_parameters=[ manual_parameters=[

View File

@@ -1,27 +1,22 @@
from api.reports.models import ReportDocument
from api.reports.tasks.report_document import generate_report_document, generate_report_control_pedimento
from django.http import FileResponse
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response 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']) @api_view(['GET'])
@permission_classes([IsAuthenticated]) @permission_classes([IsAuthenticated])
def table_summary(request): 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') org_id = request.query_params.get('organizacion_id')
# hasta aqui si llega y crea el registro en la base de datos
print(f'🖼️🖼️🖼️🖼️🖼️🖼️🖼️ table_summary organizacion id = {org_id}')
if not org_id: if not org_id:
return Response({"error": "organizacion_id es requerido"}, status=400) return Response({"error": "organizacion_id es requerido"}, status=400)
# Obtener filtros de query params # Obtener filtros de query params
tipo_documento = request.query_params.get('tipo_documento') tipo_documento = request.query_params.get('tipo_documento')
rfc = request.query_params.get('contribuyente__rfc') rfc = request.query_params.get('contribuyente__rfc')
@@ -33,137 +28,141 @@ def table_summary(request):
pedimento_app = request.query_params.get('pedimento_app') pedimento_app = request.query_params.get('pedimento_app')
regimen = request.query_params.get('regimen') regimen = request.query_params.get('regimen')
tipo_operacion = request.query_params.get('tipo_operacion') tipo_operacion = request.query_params.get('tipo_operacion')
filename_param = request.query_params.get('filename')
# Si no se proporcionan fechas, establecer un rango por defecto de los últimos 30 días if filename_param:
if not fecha_pago_gte and not fecha_pago_lte: filename = filename_param
fecha_pago_lte = datetime.now().date() else:
fecha_pago_gte = fecha_pago_lte - timedelta(days=30) filename = None
# Si no se proporcionan fechas, no las incluyas en los filtros
# Construir filtros base para pedimentos if not fecha_pago_gte:
pedimentos_filters = Q(organizacion_id=org_id) fecha_pago_gte_str = None
elif isinstance(fecha_pago_gte, str):
# Añadir filtros de fecha siempre para limitar el conjunto de datos fecha_pago_gte_str = fecha_pago_gte
pedimentos_filters &= Q(fecha_pago__gte=fecha_pago_gte) else:
pedimentos_filters &= Q(fecha_pago__lte=fecha_pago_lte) fecha_pago_gte_str = fecha_pago_gte.isoformat()
if not fecha_pago_lte:
if rfc: fecha_pago_lte_str = None
pedimentos_filters &= Q(contribuyente__rfc=rfc) elif isinstance(fecha_pago_lte, str):
if patente: fecha_pago_lte_str = fecha_pago_lte
pedimentos_filters &= Q(patente=patente) else:
if aduana: fecha_pago_lte_str = fecha_pago_lte.isoformat()
pedimentos_filters &= Q(aduana=aduana) filtros = {
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'
)
# 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, "organizacion_id": org_id,
"tipo_documento": tipo_documento, "tipo_documento": tipo_documento,
"contribuyente__rfc": rfc, "contribuyente__rfc": rfc,
"fecha_pago__gte": fecha_pago_gte, "fecha_pago__gte": fecha_pago_gte_str,
"fecha_pago__lte": fecha_pago_lte, "fecha_pago__lte": fecha_pago_lte_str,
"patente": patente, "patente": patente,
"aduana": aduana, "aduana": aduana,
"pedimento": pedimento, "pedimento": pedimento,
"pedimento_app": pedimento_app, "pedimento_app": pedimento_app,
"regimen": regimen, "regimen": regimen,
"tipo_operacion": tipo_operacion "tipo_operacion": tipo_operacion,
"filename": filename
} }
}) report = ReportDocument.objects.create(
user=request.user,
filters=filtros,
status='pending',
report_type='cumplimiento'
)
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)
@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,
"report_type": r.report_type,
"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)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def control_pedimento(request):
"""
Dispara la tarea asíncrona para generar el reporte CSV de control de Pedimentos.
"""
org_id = request.query_params.get('organizacion_id')
if not org_id:
return Response({"error": "organizacion_id es requerido"}, status=400)
# Simplificar la lógica de fechas
fecha_pago_gte = request.query_params.get('fecha_pago__gte')
fecha_pago_lte = request.query_params.get('fecha_pago__lte')
pedimento_app = request.query_params.get('pedimento_app')
# Si las fechas vienen como string, mantenerlas como están
fecha_pago_gte_str = fecha_pago_gte if fecha_pago_gte else None
fecha_pago_lte_str = fecha_pago_lte if fecha_pago_lte else None
filtros = {
"pedimento_app": pedimento_app,
"organizacion_id": org_id,
"fecha_pago__gte": fecha_pago_gte_str,
"fecha_pago__lte": fecha_pago_lte_str,
}
# Crear el reporte
report = ReportDocument.objects.create(
user=request.user,
filters=filtros,
status='pending',
report_type='control_pedimento'
)
# Disparar la tarea asíncrona
generate_report_control_pedimento.delay(report.id)
return Response({
"report_id": report.id,
"status": report.status,
"created_at": report.created_at,
"message": "Reporte en proceso de generación",
"download_url": report.file.url if report.file else None
}, status=202)

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() message = models.TextField()
status = models.CharField(max_length=50) status = models.CharField(max_length=50)
servicio = models.ForeignKey('customs.Servicio', on_delete=models.CASCADE, null=True, blank=True) 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 from .models import Task
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
pedimento_app = serializers.CharField(source='pedimento.pedimento_app', read_only=True)
class Meta: class Meta:
model = Task model = Task
fields = '__all__' fields = '__all__'

View File

@@ -1,9 +1,12 @@
from django.shortcuts import render 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 api.logger.mixins import LoggingMixin
from .models import Task from .models import Task
from .serializers import TaskSerializer from .serializers import TaskSerializer
from .filters import TaskFilter
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
# Create your views here. # Create your views here.
@@ -14,9 +17,19 @@ from core.permissions import (
IsSuperUser IsSuperUser
) )
class TaskPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class TaskViewSet(LoggingMixin,viewsets.ModelViewSet): class TaskViewSet(LoggingMixin,viewsets.ModelViewSet):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
queryset = Task.objects.all() queryset = Task.objects.select_related('pedimento', 'servicio').all()
serializer_class = TaskSerializer 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'] 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. Django settings for config project.
@@ -70,6 +25,14 @@ smtplib.SMTP_SSL.default_context = ssl._create_unverified_context
from dotenv import load_dotenv from dotenv import load_dotenv
import re import re
# Celery Beat Schedule
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
}
# Cargar variables de entorno desde un archivo .env # Cargar variables de entorno desde un archivo .env
load_dotenv() load_dotenv()
@@ -305,7 +268,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
LANGUAGE_CODE = 'en-us' 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_I18N = True
USE_TZ = 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 python-multipart==0.0.12
pytz==2025.2 pytz==2025.2
PyYAML==6.0.2 PyYAML==6.0.2
rarfile==4.1
redis==6.2.0 redis==6.2.0
referencing==0.36.2 referencing==0.36.2
requests==2.32.4 requests==2.32.4