from config.settings import SERVICE_API_URL from django.shortcuts import render from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.pagination import PageNumberPagination from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.exceptions import PermissionDenied from rest_framework import status from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter, OrderingFilter from core.permissions import ( IsSameOrganization, IsSameOrganizationDeveloper, IsSameOrganizationAndAdmin, IsSuperUser ) from api.customs.models import ( Pedimento, TipoOperacion, ProcesamientoPedimento, EDocument, Cove, Importador, Partida ) from api.customs.serializers import ( PedimentoSerializer, TipoOperacionSerializer, ProcesamientoPedimentoSerializer, EDocumentSerializer, CoveSerializer, ImportadorSerializer, PartidaSerializer ) from api.logger.mixins import LoggingMixin from mixins.filtrado_organizacion import OrganizacionFiltradaMixin, ProcesosPorOrganizacionMixin import requests from .tasks.microservice_v2 import * from .tasks.auditoria import crear_partidas_por_pedimento class CustomPagination(PageNumberPagination): """ Paginación personalizada con parámetros flexibles - Si no se especifica page_size, devuelve todos los resultados (sin paginación) - Si se especifica page_size, usa paginación normal """ page_size = None # Sin paginación por defecto page_size_query_param = 'page_size' max_page_size = 10000 # Límite máximo de seguridad page_query_param = 'page' def paginate_queryset(self, queryset, request, view=None): """ Si no se especifica page_size en los parámetros, devolver None (sin paginación) Si se especifica, usar paginación normal """ # Verificar si se especificó page_size en la query if self.page_size_query_param not in request.query_params: # No hay page_size, devolver None para indicar "sin paginación" return None # Hay page_size, usar paginación normal try: page_size = int(request.query_params[self.page_size_query_param]) if page_size <= 0: return None # Establecer el page_size temporalmente para esta request self.page_size = min(page_size, self.max_page_size) except (ValueError, TypeError): return None return super().paginate_queryset(queryset, request, view) class PedimentoPagination(PageNumberPagination): """ Paginación personalizada con parámetros flexibles - Si no se especifica page_size, devuelve todos los resultados (sin paginación) - Si se especifica page_size, usa paginación normal """ page_size = None # Sin paginación por defecto page_size_query_param = 'page_size' max_page_size = 1000 # Límite máximo de seguridad page_query_param = 'page' def paginate_queryset(self, queryset, request, view=None): """ Si no se especifica page_size en los parámetros, devolver None (sin paginación) Si se especifica, usar paginación normal """ # Verificar si se especificó page_size en la query if self.page_size_query_param not in request.query_params: # No hay page_size, devolver None para indicar "sin paginación" return None # Hay page_size, usar paginación normal try: page_size = int(request.query_params[self.page_size_query_param]) if page_size <= 0: return None # Establecer el page_size temporalmente para esta request self.page_size = min(page_size, self.max_page_size) except (ValueError, TypeError): return None return super().paginate_queryset(queryset, request, view) # Create your views here. class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin): # Pendiente de permisos de creacion """ ViewSet for Pedimento model. Soporta paginación, filtros y búsqueda. Parámetros disponibles: - page: Número de página (solo si se especifica page_size) - page_size: Elementos por página (si NO se especifica, devuelve TODOS los resultados) - search: Búsqueda en pedimento, contribuyente, agente_aduanal - pedimento: Filtro por número de pedimento - existe_expediente: Filtro por expediente (True/False) - contribuyente: Filtro por contribuyente - curp_apoderado: Filtro por curp del apoderado - fecha_pago: Filtro por fecha de pago (YYYY-MM-DD) - patente: Filtro por patente - aduana: Filtro por aduana - tipo_operacion: Filtro por tipo de operación - clave_pedimento: Filtro por clave de pedimento - ordering: Ordenar por campo (ej: -created_at, pedimento) Ejemplos: - /pedimentos/ → Devuelve TODOS los pedimentos - /pedimentos/?page_size=10 → Devuelve los primeros 10 - /pedimentos/?page_size=10&page=2 → Devuelve los pedimentos 11-20 - /pedimentos/?pedimento=12345678 → Filtra por número de pedimento - /pedimentos/?existe_expediente=true → Filtra por expediente existente - /pedimentos/?contribuyente=EMPRESA → Filtra por contribuyente - /pedimentos/?curp_apoderado=XXXX → Filtra por curp apoderado - /pedimentos/?fecha_pago=2025-07-18 → Filtra por fecha de pago """ permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] serializer_class = PedimentoSerializer pagination_class = PedimentoPagination filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] model = Pedimento filterset_fields = ['patente', 'aduana', 'tipo_operacion', 'clave_pedimento', 'pedimento', 'existe_expediente', 'contribuyente', 'curp_apoderado', 'fecha_pago', 'pedimento_app'] search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento'] def get_queryset(self): return self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador def perform_create(self, serializer): """ Asigna automáticamente la organización del usuario autenticado al crear un pedimento. """ if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): raise ValueError("Usuario no autenticado o sin organización") data = serializer.validated_data if not data.get('pedimento_app'): fecha_pago = data.get('fecha_pago') aduana = data.get('aduana') patente = data.get('patente') pedimento = data.get('pedimento') if fecha_pago and aduana and patente and pedimento: pedimento_app = f"{str(fecha_pago.year)[-2:]}-{str(aduana).zfill(2)[-2:]}-{str(patente).zfill(4)[-4:]}-{str(pedimento).zfill(7)[-7:]}" serializer.save(organizacion=self.request.user.organizacion, pedimento_app=pedimento_app) try: # Usar el nombre del servicio de Docker Compose en lugar de localhost response = procesar_pedimento_completo_individual(serializer.instance.id) # Verificar si la respuesta fue exitosa if response.status_code == 200: print(f"✅ Servicio FastAPI ejecutado exitosamente: {response.status_code}") print(f"📄 Respuesta: {response.json()}") elif response.status_code == 201: print(f"✅ Recurso creado exitosamente en FastAPI: {response.status_code}") print(f"📄 Respuesta: {response.json()}") else: print(f"⚠️ Servicio FastAPI respondió con error: {response.status_code}") print(f"📄 Respuesta: {response.text}") except requests.exceptions.ConnectionError as e: print(f"❌ No se pudo conectar al servicio FastAPI: {e}") print(f"🔧 Verifica que el servicio FastAPI esté corriendo en {SERVICE_API_URL}") except requests.exceptions.Timeout as e: print(f"⏰ Timeout al conectar con el servicio FastAPI: {e}") except requests.exceptions.RequestException as e: print(f"🚨 Error de request al servicio FastAPI: {e}") except Exception as e: print(f"💥 Error inesperado al llamar al servicio FastAPI: {e}") def perform_update(self, serializer): """ Ejecuta acciones después de actualizar un pedimento basado en los campos modificados. """ # Obtener los campos que se están actualizando updated_fields = set(serializer.validated_data.keys()) # Guardar los cambios pedimento = serializer.save() # Si se actualizó el campo existe_expediente, procesar el pedimento completo if 'existe_expediente' in updated_fields: # Iniciar todas las tareas procesar_remesas_pedimento(pedimento.id) crear_partidas_por_pedimento(pedimento.id) procesar_acuse_coves_pedimento(pedimento.id) procesar_edocs_pedimento(pedimento.id) procesar_acuses_pedimento(pedimento.id) procesar_partidas_pedimento(pedimento.id) procesar_coves_pedimento(pedimento.id) # Agregar mensaje de tareas iniciadas al serializer serializer._data = { **serializer.data, "message": "Tareas de procesamiento iniciadas", "tasks": [ "Procesamiento de remesas", "Creación de partidas", "Procesamiento de acuses de COVEs", "Procesamiento de E-documents", "Procesamiento de acuses", "Procesamiento de partidas", "Procesamiento de COVEs" ] } @action(detail=False, methods=['post'], url_path='bulk-delete') def bulk_delete(self, request): """ Endpoint para eliminar múltiples pedimentos de manera masiva. Payload esperado: { "ids": ["uuid1", "uuid2", "uuid3", ...] } Respuesta exitosa: { "message": "Pedimentos eliminados exitosamente", "deleted_count": 3, "deleted_ids": ["uuid1", "uuid2", "uuid3"] } Respuesta con errores: { "message": "Algunos pedimentos no pudieron ser eliminados", "deleted_count": 2, "deleted_ids": ["uuid1", "uuid2"], "failed_ids": ["uuid3"], "errors": ["No se encontró el pedimento con ID uuid3"] } """ # Obtener los IDs del payload ids = request.data.get('ids', []) if not ids: return Response( {"error": "Se requiere una lista de IDs para eliminar"}, status=status.HTTP_400_BAD_REQUEST ) if not isinstance(ids, list): return Response( {"error": "El campo 'ids' debe ser una lista"}, status=status.HTTP_400_BAD_REQUEST ) # Obtener el queryset filtrado por organización queryset = self.get_queryset() # Filtrar solo los pedimentos que existen y pertenecen a la organización del usuario existing_pedimentos = queryset.filter(id__in=ids) existing_ids = list(existing_pedimentos.values_list('id', flat=True)) # Convertir UUIDs a strings para comparación existing_ids_str = [str(id) for id in existing_ids] requested_ids_str = [str(id) for id in ids] # Identificar IDs que no existen o no pertenecen a la organización failed_ids = [id for id in requested_ids_str if id not in existing_ids_str] deleted_count = 0 errors = [] if existing_pedimentos.exists(): try: # Eliminar los pedimentos encontrados deleted_count = existing_pedimentos.count() existing_pedimentos.delete() except Exception as e: return Response( {"error": f"Error al eliminar pedimentos: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # Agregar errores para IDs no encontrados if failed_ids: errors = [f"No se encontró el pedimento con ID {id} o no pertenece a su organización" for id in failed_ids] # Preparar respuesta response_data = { "deleted_count": deleted_count, "deleted_ids": existing_ids_str } if failed_ids: response_data.update({ "message": "Algunos pedimentos no pudieron ser eliminados", "failed_ids": failed_ids, "errors": errors }) response_status = status.HTTP_207_MULTI_STATUS else: response_data["message"] = "Pedimentos eliminados exitosamente" response_status = status.HTTP_200_OK return Response(response_data, status=response_status) my_tags = ['Pedimentos'] class PartidaViewSet(viewsets.ModelViewSet): """ ViewSet for Partida model. Permite filtrar por: - pedimento: UUID del pedimento (query parameter principal) - pedimento__id: UUID del pedimento (alternativo) Ejemplo: GET /api/partidas/?pedimento=6782d22e-5e97-4efc-87c9-bd8497c8ac7e """ permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] queryset = Partida.objects.all() serializer_class = PartidaSerializer pagination_class = CustomPagination filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = { 'pedimento': ['exact'], # Filtro directo por UUID del pedimento 'pedimento__id': ['exact'], # Filtro alternativo 'numero_partida': ['exact', 'gte', 'lte'], # Filtros por número de partida 'descargado': ['exact'], # Filtro por estado de descarga 'created_at': ['exact', 'gte', 'lte'], # Filtros por fecha de creación 'updated_at': ['exact', 'gte', 'lte'] # Filtros por fecha de actualización } search_fields = ['pedimento__pedimento', 'pedimento__pedimento_app'] ordering_fields = ['numero_partida', 'pedimento__pedimento', 'id', 'created_at', 'updated_at'] ordering = ['numero_partida'] # Ordenar por número de partida por defecto my_tags = ['Partidas'] class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet): """ ViewSet for TipoOperacion model. """ permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] queryset = TipoOperacion.objects.all() serializer_class = TipoOperacionSerializer pagination_class = CustomPagination filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ['tipo'] search_fields = ['tipo', 'descripcion'] ordering_fields = ['tipo', 'descripcion'] ordering = ['tipo'] my_tags = ['Tipos_Operacion'] def perform_create(self, serializer): """ Asigna automáticamente la organización del usuario autenticado al crear un tipo de operación. """ if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): raise ValueError("Usuario no autenticado o sin organización") # Solo el supoerusuario puede crear tipos de operación if not self.request.user.is_superuser: raise PermissionDenied("Solo los superusuarios pueden crear tipos de operación") serializer.save(organizacion=self.request.user.organizacion) def perform_update(self, serializer): """ Solo el superusuario puede actualizar tipos de operación. """ if not self.request.user.is_superuser: raise PermissionDenied("Solo los superusuarios pueden actualizar tipos de operación") serializer.save() class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizacionMixin): """ ViewSet for ProcesamientoPedimento model. Soporta paginación, filtros y búsqueda. Parámetros disponibles: - page: Número de página (solo si se especifica page_size) - page_size: Elementos por página (si NO se especifica, devuelve TODOS los resultados) - pedimento: Filtro por pedimento - estado: Filtro por estado - servicio: Filtro por servicio - tipo_procesamiento: Filtro por tipo de procesamiento - ordering: Ordenar por campo (ej: -created_at, -updated_at) Ejemplos: - /procesamientopedimentos/ → Devuelve TODOS los procesamientos - /procesamientopedimentos/?page_size=5 → Devuelve los primeros 5 """ permission_classes = [IsAuthenticated, IsSuperUser | IsSameOrganizationDeveloper ] serializer_class = ProcesamientoPedimentoSerializer pagination_class = CustomPagination model = ProcesamientoPedimento filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = { 'pedimento': ['exact'], 'pedimento__pedimento_app': ['exact', 'icontains'], 'estado': ['exact'], 'servicio': ['exact'], 'tipo_procesamiento': ['exact'], } search_fields = ['pedimento__pedimento_app', 'pedimento__pedimento'] ordering_fields = ['created_at', 'updated_at'] ordering = ['-created_at'] def get_queryset(self): return self.get_queryset_filtrado_por_organizacion() def perform_create(self, serializer): """ Asigna siempre la organización al crear un procesamiento de pedimento. - Para superusuarios: requiere que la organización venga explícitamente en los datos validados. - Para usuarios normales: asigna la organización del usuario autenticado. """ user = self.request.user if not user.is_authenticated: raise ValueError("Usuario no autenticado") # Si es superusuario, debe venir la organización en los datos validados if user.is_superuser: organizacion = serializer.validated_data.get('organizacion', None) if not organizacion: raise ValueError("El superusuario debe especificar una organización al crear el procesamiento de pedimento.") serializer.save() return # Para usuarios normales, asignar siempre la organización del usuario if not hasattr(user, 'organizacion') or not user.organizacion: raise ValueError("Usuario sin organización") serializer.save(organizacion=user.organizacion) def perform_update(self, serializer): """ Permite actualizar un procesamiento de pedimento, pero solo si el usuario es superusuario o pertenece a la misma organización. """ if not self.request.user.is_authenticated: raise ValueError("Usuario no autenticado") if self.request.user.is_superuser: serializer.save() return if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists(): # Para usuarios normales, usar siempre su organización if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion: raise ValueError("Usuario sin organización") serializer.save(organizacion=self.request.user.organizacion) return raise ValueError("Usuario no autenticado o sin permisos para actualizar ProcesamientoPedimento") my_tags = ['Procesamientos_Pedimentos'] class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin): """ ViewSet for EDocument model. """ permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] serializer_class = EDocumentSerializer pagination_class = CustomPagination filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ['pedimento', 'numero_edocument', 'organizacion'] search_fields = ['numero_edocument', 'descripcion', 'organizacion'] ordering_fields = ['created_at', 'updated_at', 'numero_edocument'] ordering = ['-created_at'] model = EDocument my_tags = ['EDocuments'] def get_queryset(self): return self.get_queryset_filtrado_por_organizacion() def perform_create(self, serializer): """ Asigna automáticamente la organización del usuario autenticado al crear un EDocument. Para superusuarios, permite especificar una organización diferente. """ if not self.request.user.is_authenticated: raise ValueError("Usuario no autenticado") # Si es superusuario y se especifica organizacion en los datos validados if self.request.user.is_superuser: # Permitir que el superusuario especifique la organización serializer.save() return if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists(): # Para usuarios normales, usar siempre su organización if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion: raise ValueError("Usuario sin organización") serializer.save(organizacion=self.request.user.organizacion) return raise ValueError("Usuario no autenticado o sin permisos para crear EDocument") def perform_update(self, serializer): """ Permite actualizar un EDocument, pero solo si el usuario es superusuario o pertenece a la misma organización. """ if not self.request.user.is_authenticated: raise ValueError("Usuario no autenticado") # Si es superusuario, permite actualizar sin restricciones if self.request.user.is_superuser: serializer.save() return if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists(): # Para usuarios normales, usar siempre su organización if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion: raise ValueError("Usuario sin organización") serializer.save(organizacion=self.request.user.organizacion) raise ValueError("Usuario no autenticado o sin permisos para actualizar EDocument") class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin): """ ViewSet for Cove model. """ permission_classes = [IsAuthenticated & (IsSuperUser |IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )] serializer_class = CoveSerializer pagination_class = CustomPagination filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ['pedimento', 'numero_cove', 'organizacion'] search_fields = ['numero_cove', 'descripcion', 'organizacion'] ordering_fields = ['created_at', 'updated_at', 'numero_cove'] ordering = ['-created_at'] model = Cove my_tags = ['Coves'] def get_queryset(self): return self.get_queryset_filtrado_por_organizacion() def perform_create(self, serializer): """ Asigna automáticamente la organización del usuario autenticado al crear un Cove. Para superusuarios, permite especificar una organización diferente. """ if not self.request.user.is_authenticated: raise ValueError("Usuario no autenticado") # Si es superusuario y se especifica organizacion en los datos validados if self.request.user.is_superuser: # Permitir que el superusuario especifique la organización serializer.save() return if ( self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists() ) and self.request.user.groups.filter(name='Agente Aduanal').exists(): # Para usuarios normales, usar siempre su organización if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion: raise ValueError("Usuario sin organización") serializer.save(organizacion=self.request.user.organizacion) return raise ValueError("Usuario no autenticado o sin permisos para crear Cove") def perform_update(self, serializer): """ Permite actualizar un Cove, pero solo si el usuario es superusuario o pertenece a la misma organización. """ if not self.request.user.is_authenticated: raise ValueError("Usuario no autenticado") # Si es superusuario, permite actualizar sin restricciones if self.request.user.is_superuser: serializer.save() return if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user .groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists(): # Para usuarios normales, usar siempre su organización if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion: raise ValueError("Usuario sin organización") serializer.save(organizacion=self.request.user.organizacion) class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin): """ ViewSet for Importador model. """ permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)] serializer_class = ImportadorSerializer pagination_class = CustomPagination filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ['rfc', 'nombre', 'organizacion'] search_fields = ['rfc', 'nombre'] ordering_fields = ['created_at', 'updated_at', 'rfc'] ordering = ['-created_at'] model = Importador def get_queryset(self): return self.get_queryset_filtrado_por_organizacion() def perform_create(self, serializer): if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): raise ValueError("Usuario no autenticado o sin organización") serializer.save(organizacion=self.request.user.organizacion) def perform_update(self, serializer): if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'): raise ValueError("Usuario no autenticado o sin organización") # Si es superusuario, permite actualizar sin restricciones if self.request.user.is_superuser: serializer.save() return if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists(): # Para usuarios normales, usar siempre su organización if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion: raise ValueError("Usuario sin organización") serializer.save(organizacion=self.request.user.organizacion) return raise ValueError("Usuario no autenticado o sin permisos para actualizar Importador") my_tags = ['Importadores']