fix: Se crean endpoints para mostrar la informacion de peticiones y respuestas de los webservices, en el area del auditor del sistema.

This commit is contained in:
2026-01-02 08:07:30 -07:00
parent 1cb2830d71
commit 8a4e732703
7 changed files with 1780 additions and 11 deletions

View File

@@ -1,3 +1,6 @@
import os
from datetime import datetime
from django.db import models
from celery import shared_task, group
from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocument
from core.utils import xml_controller
@@ -198,7 +201,7 @@ def auditar_coves(organizacion_id):
pedimento,
servicio=8,
related_name='coves',
variable='acuse_descargado',
variable='cove_descargado',
mensaje='COVE'
)
@@ -247,7 +250,7 @@ def auditar_cove_por_pedimento(pedimento_id):
pedimento,
servicio=8,
related_name='coves',
variable='acuse_descargado',
variable='cove_descargado',
mensaje='COVE'
)
return {'success': True, 'pedimento_id': str(pedimento_id)}
@@ -302,3 +305,154 @@ def auditar_acuse_por_pedimento(pedimento_id):
except Exception as e:
return {'success': False, 'error': str(e), 'pedimento_id': str(pedimento_id)}
@shared_task
def auditar_pedimento_por_id(pedimento_id):
"""
Tarea para auditar un pedimento específico verificando todos sus documentos y datos.
"""
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
resultado = {
'pedimento_id': str(pedimento_id),
'pedimento': pedimento.pedimento,
'pedimento_app': pedimento.pedimento_app,
'organizacion': str(pedimento.organizacion.id),
'fecha_auditoria': datetime.now().isoformat(),
'estado_general': 'EN_PROGRESO',
'detalles': {}
}
# 1. Verificar documentos XML
from api.record.models import Document
documentos_xml = Document.objects.filter(
pedimento=pedimento,
archivo__endswith='.xml'
)
resultado['detalles']['documentos_xml'] = {
'total': documentos_xml.count(),
'archivos': []
}
for doc in documentos_xml:
try:
xml_info = {
'id': str(doc.id),
'nombre': os.path.basename(doc.archivo.name),
'tamanio': doc.size,
'extension': doc.extension,
'tipo': doc.document_type.descripcion if doc.document_type else 'Desconocido'
}
# Verificar si el archivo existe físicamente
if os.path.exists(doc.archivo.path):
xml_info['existe_fisicamente'] = True
# Intentar leer el XML
try:
with open(doc.archivo.path, 'r', encoding='utf-8') as f:
content = f.read()
xml_info['es_xml_valido'] = '<?xml' in content[:100]
xml_info['tamanio_bytes'] = len(content)
except Exception as e:
xml_info['error_lectura'] = str(e)
else:
xml_info['existe_fisicamente'] = False
except Exception as e:
xml_info['error'] = str(e)
resultado['detalles']['documentos_xml']['archivos'].append(xml_info)
# 2. Verificar si hay documentos asociados
resultado['detalles']['documentos_totales'] = {
'total': pedimento.documents.count(),
'por_tipo': {}
}
for doc_type in pedimento.documents.values('document_type__descripcion').annotate(total=models.Count('id')):
tipo = doc_type['document_type__descripcion'] or 'Sin tipo'
resultado['detalles']['documentos_totales']['por_tipo'][tipo] = doc_type['total']
# 3. Verificar COVEs
resultado['detalles']['coves'] = {
'total': pedimento.coves.count(),
'descargados': pedimento.coves.filter(cove_descargado=True).count(),
'con_acuse': pedimento.coves.filter(acuse_cove_descargado=True).count()
}
# 4. Verificar EDocuments
resultado['detalles']['edocuments'] = {
'total': pedimento.documentos.count(),
'descargados': pedimento.documentos.filter(edocument_descargado=True).count(),
'con_acuse': pedimento.documentos.filter(acuse_descargado=True).count()
}
# 5. Verificar procesamientos
resultado['detalles']['procesamientos'] = {
'total': pedimento.procesamientos.count(),
'por_estado': {}
}
for proc in pedimento.procesamientos.values('estado__estado').annotate(total=models.Count('id')):
estado = proc['estado__estado'] or 'Sin estado'
resultado['detalles']['procesamientos']['por_estado'][estado] = proc['total']
# 6. Verificar campos importantes del pedimento
campos_revisados = {
'numero_operacion': bool(pedimento.numero_operacion),
'numero_partidas': bool(pedimento.numero_partidas),
'importe_total': bool(pedimento.importe_total),
'contribuyente': bool(pedimento.contribuyente),
'tiene_remesas': pedimento.remesas,
'partidas_creadas': pedimento.partidas.count() > 0,
'fecha_pago': bool(pedimento.fecha_pago)
}
resultado['detalles']['campos_pedimento'] = campos_revisados
resultado['detalles']['campos_completos'] = sum(campos_revisados.values())
resultado['detalles']['campos_totales'] = len(campos_revisados)
# 7. Determinar estado general
campos_completos = resultado['detalles']['campos_completos']
total_campos = resultado['detalles']['campos_totales']
if documentos_xml.count() == 0:
resultado['estado_general'] = 'SIN_XML'
resultado['mensaje'] = 'No se encontraron documentos XML'
elif campos_completos == total_campos:
resultado['estado_general'] = 'COMPLETO'
resultado['mensaje'] = 'Pedimento completamente procesado'
elif campos_completos >= total_campos * 0.7:
resultado['estado_general'] = 'PARCIAL'
resultado['mensaje'] = 'Pedimento parcialmente procesado'
else:
resultado['estado_general'] = 'INCOMPLETO'
resultado['mensaje'] = 'Pedimento con información incompleta'
resultado['porcentaje_completitud'] = (campos_completos / total_campos) * 100 if total_campos > 0 else 0
# 8. Sugerencias
sugerencias = []
if not pedimento.numero_operacion:
sugerencias.append("Falta el número de operación")
if not pedimento.numero_partidas:
sugerencias.append("Falta el número de partidas")
if pedimento.numero_partidas and pedimento.numero_partidas > pedimento.partidas.count():
sugerencias.append(f"Faltan partidas: {pedimento.numero_partidas - pedimento.partidas.count()} de {pedimento.numero_partidas}")
if not pedimento.contribuyente:
sugerencias.append("Falta el contribuyente asociado")
resultado['sugerencias'] = sugerencias
return resultado
except Pedimento.DoesNotExist:
return {
'error': f'Pedimento con ID {pedimento_id} no encontrado',
'pedimento_id': str(pedimento_id)
}
except Exception as e:
return {
'error': f'Error auditar pedimento {pedimento_id}: {str(e)}',
'pedimento_id': str(pedimento_id)
}

View File

@@ -0,0 +1,194 @@
# auditoria_xml.py
import xml.etree.ElementTree as ET
from datetime import datetime
def extraer_info_pedimento_xml(xml_content):
"""
Extrae información específica de un XML de pedimento.
"""
try:
# Parsear el XML
root = ET.fromstring(xml_content)
# Buscar el namespace (puede variar)
namespaces = {
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
'ns3': 'http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta'
}
resultado = {}
# Extraer número de operación
num_op = root.find('.//ns2:numeroOperacion', namespaces)
if num_op is not None and num_op.text:
resultado['numero_operacion'] = num_op.text
# Extraer información del pedimento
pedimento_elem = root.find('.//ns2:pedimento', namespaces)
if pedimento_elem is not None:
# Número de pedimento
ped_num = pedimento_elem.find('ns2:pedimento', namespaces)
if ped_num is not None and ped_num.text:
resultado['numero_pedimento'] = ped_num.text
# Número de partidas
partidas = pedimento_elem.find('ns2:partidas', namespaces)
if partidas is not None and partidas.text:
try:
resultado['numero_partidas'] = int(partidas.text)
except (ValueError, TypeError):
pass
# Tipo de operación clave
tipo_op_clave = pedimento_elem.find('.//ns2:tipoOperacion/ns2:clave', namespaces)
if tipo_op_clave is not None and tipo_op_clave.text:
if tipo_op_clave.text.strip() == '1':
resultado['tipo_operacion'] = 'Importacion'
resultado['tipo_operacion_descripcion'] = 'Indica operacion como Importaciones'
elif tipo_op_clave.text.strip() == '2':
resultado['tipo_operacion'] = 'Exportacion'
resultado['tipo_operacion_descripcion'] = 'Indica operacion de exportacion'
# Clave del documento (clave_pedimento)
clave_doc = pedimento_elem.find('.//ns2:claveDocumento/ns2:clave', namespaces)
if clave_doc is not None and clave_doc.text:
resultado['clave_pedimento'] = clave_doc.text.strip()
# Aduana (patente)
aduana = pedimento_elem.find('.//ns2:aduanaEntradaSalida/ns2:clave', namespaces)
if aduana is not None and aduana.text:
resultado['aduana_clave'] = aduana.text.strip()
# Importador/Exportador
importador = pedimento_elem.find('.//ns2:importadorExportador', namespaces)
if importador is not None:
rfc = importador.find('ns2:rfc', namespaces)
if rfc is not None and rfc.text:
resultado['contribuyente_rfc'] = rfc.text.strip()
razon_social = importador.find('ns2:razonSocial', namespaces)
if razon_social is not None and razon_social.text:
resultado['contribuyente_nombre'] = razon_social.text.strip()
# Valor en dólares
valor_dolares = importador.find('ns2:valorDolares', namespaces)
if valor_dolares is not None and valor_dolares.text:
try:
resultado['valor_dolares'] = float(valor_dolares.text)
except (ValueError, TypeError):
pass
# Aduana de despacho
aduana_despacho = importador.find('ns2:aaduanaDespacho/ns2:clave', namespaces)
if aduana_despacho is not None and aduana_despacho.text:
resultado['aduana_despacho'] = aduana_despacho.text.strip()
# Encabezado del pedimento
encabezado = pedimento_elem.find('ns2:encabezado', namespaces)
if encabezado is not None:
# Aduana
aduana = encabezado.find('ns2:aduanaEntradaSalida/ns2:clave', namespaces)
if aduana is not None and aduana.text:
resultado['aduana_clave'] = aduana.text.strip()
# Tipo de cambio
tipo_cambio = encabezado.find('ns2:tipoCambio', namespaces)
if tipo_cambio is not None and tipo_cambio.text:
try:
resultado['tipo_cambio'] = float(tipo_cambio.text)
except (ValueError, TypeError):
pass
# RFC Agente Aduanal
rfc_agente = encabezado.find('ns2:rfcAgenteAduanalSocFactura', namespaces)
if rfc_agente is not None and rfc_agente.text:
resultado['rfc_agente_aduanal'] = rfc_agente.text.strip()
# CURP Apoderado
curp_apoderado = encabezado.find('ns2:curpApoderadomandatario', namespaces)
if curp_apoderado is not None and curp_apoderado.text:
resultado['curp_apoderado'] = curp_apoderado.text.strip()
# Valor Aduanal Total
valor_aduanal = encabezado.find('ns2:valorAduanalTotal', namespaces)
if valor_aduanal is not None and valor_aduanal.text:
try:
resultado['valor_aduanal_total'] = float(valor_aduanal.text)
except (ValueError, TypeError):
pass
# Valor Comercial Total
valor_comercial = encabezado.find('ns2:valorComercialTotal', namespaces)
if valor_comercial is not None and valor_comercial.text:
try:
resultado['valor_comercial_total'] = float(valor_comercial.text)
except (ValueError, TypeError):
pass
# Fechas
fechas = pedimento_elem.findall('.//ns2:fechas', namespaces)
for fecha_elem in fechas:
fecha = fecha_elem.find('ns2:fecha', namespaces)
clave_fecha = fecha_elem.find('ns2:tipo/ns2:clave', namespaces)
if fecha is not None and fecha.text and clave_fecha is not None and clave_fecha.text:
fecha_texto = fecha.text.strip()
clave_fecha_texto = clave_fecha.text.strip()
# Mapeo de claves según especificación
if clave_fecha_texto == '1': # Entrada
resultado['fecha_entrada'] = fecha_texto
elif clave_fecha_texto == '2': # Pago
resultado['fecha_pago'] = fecha_texto
elif clave_fecha_texto == '3': # Extracción
resultado['fecha_extraccion'] = fecha_texto
elif clave_fecha_texto == '5': # Presentación
resultado['fecha_presentacion'] = fecha_texto
elif clave_fecha_texto == '6': # Importación
resultado['fecha_importacion'] = fecha_texto
elif clave_fecha_texto == '7': # Original
resultado['fecha_original'] = fecha_texto
else:
resultado[f'fecha_clave_{clave_fecha_texto}'] = fecha_texto
# Facturas (para COVEs)
facturas = pedimento_elem.findall('.//ns2:facturas', namespaces)
coves_encontrados = []
for factura in facturas:
numero = factura.find('ns2:numero', namespaces)
if numero is not None and numero.text:
coves_encontrados.append(numero.text.strip())
if coves_encontrados:
resultado['coves_en_xml'] = coves_encontrados
# E-Documents
identificadores = pedimento_elem.findall('.//ns2:identificadores/ns2:identificadores', namespaces)
edocs_encontrados = []
for ident in identificadores:
clave = ident.find('claveIdentificador/descripcion', namespaces)
complemento = ident.find('complemento1', namespaces)
if clave is not None and clave.text and 'E_DOCUMENT' in clave.text:
if complemento is not None and complemento.text:
edocs_encontrados.append(complemento.text.strip())
if edocs_encontrados:
resultado['edocuments_en_xml'] = edocs_encontrados
# Verificar si hay error en la respuesta
tiene_error = root.find('.//ns3:tieneError', namespaces)
if tiene_error is not None:
resultado['tiene_error'] = tiene_error.text.lower() == 'true'
return resultado
except ET.ParseError as e:
return {'error_parse': str(e)}
except Exception as e:
return {'error': str(e)}

View File

@@ -42,7 +42,24 @@ from .views_auditor import (
auditar_acuse_cove_pedimento_endpoint,
auditar_edocument_pedimento_endpoint,
auditar_acuse_pedimento_endpoint,
auditor_procesar_pedimentos_organizacion
auditar_procesamiento_remesa_pedimento_endpoint,
auditor_procesar_pedimentos_organizacion,
auditar_peticion_respuesta_pedimento_completo,
auditor_obtener_peticion_pedimento_vu,
auditor_obtener_respuesta_pedimento_vu,
auditor_obtener_peticion_remesa_vu,
auditor_obtener_respuesta_remesa_vu,
auditor_obtener_peticion_partidas_vu,
auditor_obtener_respuesta_partidas_vu,
auditor_obtener_peticion_acuse_vu,
auditor_obtener_respuesta_acuse_vu,
auditor_obtener_peticion_cove_vu,
auditor_obtener_respuesta_cove_vu,
auditor_obtener_peticion_acuse_cove_vu,
auditor_obtener_respuesta_acuse_cove_vu,
auditor_obtener_peticion_edocument_vu,
auditor_obtener_respuesta_edocument_vu,
auditar_pedimento_endpoint,
)
urlpatterns = [
@@ -58,5 +75,24 @@ urlpatterns = [
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/auditar-remesa/pedimento/', auditar_procesamiento_remesa_pedimento_endpoint, name='auditar-remesa-pedimento'),
path('auditor/auditar-pedimento/', auditar_pedimento_endpoint, name='auditar-pedimento'),
path('auditor/procesar-pedimentos/organizaciones/', auditor_procesar_pedimentos_organizacion, name='procesar-pedimentos-organizaciones'),
path('auditor/peticion-respuesta/pedimento-vu/', auditar_peticion_respuesta_pedimento_completo, name='peticion-respuesta-pedimento-vu'),
path('auditor/obtener-peticion/pedimento-vu/', auditor_obtener_peticion_pedimento_vu, name='obtener-peticion-pedimento-vu'),
path('auditor/obtener-respuesta/pedimento-vu/', auditor_obtener_respuesta_pedimento_vu, name='obtener-respuesta-pedimento-vu'),
path('auditor/obtener-peticion/remesa-vu/', auditor_obtener_peticion_remesa_vu, name='obtener-peticion-remesa-vu'),
path('auditor/obtener-respuesta/remesa-vu/', auditor_obtener_respuesta_remesa_vu, name='obtener-respuesta-remesa-vu'),
path('auditor/obtener-peticion/partidas-vu/', auditor_obtener_peticion_partidas_vu, name='obtener-peticion-partidas-vu'),
path('auditor/obtener-respuesta/partidas-vu/', auditor_obtener_respuesta_partidas_vu, name='obtener-respuesta-partidas-vu'),
path('auditor/obtener-peticion/acuse-vu/', auditor_obtener_peticion_acuse_vu, name='obtener-peticion-acuse-vu'),
path('auditor/obtener-respuesta/acuse-vu/', auditor_obtener_respuesta_acuse_vu, name='obtener-respuesta-acuse-vu'),
path('auditor/obtener-peticion/cove-vu/', auditor_obtener_peticion_cove_vu, name='obtener-peticion-cove-vu'),
path('auditor/obtener-respuesta/cove-vu/', auditor_obtener_respuesta_cove_vu, name='obtener-respuesta-cove-vu'),
path('auditor/obtener-peticion/acuse-cove-vu/', auditor_obtener_peticion_acuse_cove_vu, name='obtener-peticion-acuse-cove-vu'),
path('auditor/obtener-respuesta/acuse-cove-vu/', auditor_obtener_respuesta_acuse_cove_vu, name='obtener-respuesta-acuse-cove-vu'),
path('auditor/obtener-peticion/edocument-vu/', auditor_obtener_peticion_edocument_vu, name='obtener-peticion-edocument-vu'),
path('auditor/obtener-respuesta/edocument-vu/', auditor_obtener_respuesta_edocument_vu, name='obtener-respuesta-edocument-vu'),
]

View File

@@ -57,6 +57,9 @@ try:
except ImportError:
RAR_SUPPORT = False
# Importar tarea de procesamiento de pedimento (Celery)
from api.customs.tasks.microservice import procesar_pedimento_completo_individual
def get_available_extractors():
"""
Devuelve lista de extractores disponibles en orden de preferencia
@@ -369,6 +372,25 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
]
}
@action(detail=True, methods=['post'], url_path='procesar-completo')
def procesar_completo(self, request, pk=None):
"""
Acción para disparar el procesamiento completo de un pedimento existente.
Dispara la tarea `procesar_pedimento_completo_individual` de forma asíncrona
y devuelve el `task_id`.
"""
pedimento = self.get_object()
try:
# Usar el nombre del servicio de Docker Compose en lugar de localhost
task = procesar_pedimento_completo_individual.delay(pedimento.id, pedimento.organizacion.id)
# Verificar si la respuesta fue exitosa
if task.id:
return Response({"status": "Recurso creado exitosamente en API", "task_id": task.id}, status=status.HTTP_202_ACCEPTED)
else:
return Response({"status": "Servicio API respondió con error", "task_id": 0}, status=status.HTTP_202_ACCEPTED)
except Exception as e:
return Response({"error": f"Error inesperado al llamar al servicio API: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['post'], url_path='bulk-delete')
def bulk_delete(self, request):
"""

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,8 @@ from .views import (DocumentViewSet
, DocumentTypeView
, ExpedienteZipDownloadView
, MultiPedimentoZipDownloadView
, PedimentoDocumentViewSet)
, PedimentoDocumentViewSet
, TriggerPedimentoCompletoView)
# Create a router and register your viewsets with it
@@ -35,5 +36,6 @@ urlpatterns = [
path('documents/expediente-zip/', ExpedienteZipDownloadView.as_view(), name='expediente-zip-download'),
path('documents/multi-pedimento-zip/', MultiPedimentoZipDownloadView.as_view(), name='multi-pedimento-zip-download'),
path('pedimento-documents/', PedimentoDocumentViewSet.as_view({'get': 'list'}), name='pedimento-document-list'),
path('microservice/pedimento-completo/', TriggerPedimentoCompletoView.as_view(), name='trigger-pedimento-completo'),
path('', include(router.urls)),
]

View File

@@ -14,6 +14,7 @@ from rest_framework.exceptions import ValidationError
from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer
from .models import Document, Fuente, DocumentType
from ..customs.models import Pedimento
from ..vucem.models import CredencialesImportador
from api.organization.models import UsoAlmacenamiento
from io import BytesIO
import zipfile
@@ -35,9 +36,91 @@ logger = logging.getLogger(__name__)
import os
from django.core.files.storage import default_storage
from django.conf import settings
import requests
import re
from mixins.filtrado_organizacion import DocumentosFiltradosMixin
# Configuración de patrones y mapeos (definirlo fuera del try para reutilización)
DOCUMENT_PATTERNS = {
'REQUEST': {
'VU_PC': (r".*VU_PC.*REQUEST\.xml$", 13, "Request Pedimento Completo VU"),
'VU_ED': (r".*VU_ED.*REQUEST\.xml$", 21, "Request E-Document VU"),
'VU_PT': (r".*VU_PT.*REQUEST\.xml$", 17, "Request Partidas VU"),
'VU_AC_COVE': (r".*VU_AC_COVE.*REQUEST\.xml$", 23, "Request Acuses COVES VU"),
'VU_COVE': (r".*VU_COVE.*REQUEST\.xml$", 19, "Request COVES VU"),
'VU_RM': (r".*VU_RM.*REQUEST\.xml$", 15, "Request Remesas VU"),
'VU_AC': (r".*VU_AC.*REQUEST\.xml$", 25, "Request Acuses VU"),
},
'ERROR': {
'VU_PC': (r".*VU_PC.*ERROR\.xml$", 14, "Error Pedimento Completo VU"),
'VU_ED': (r".*VU_ED.*ERROR\.xml$", 22, "Error E-Document VU"),
'VU_PT': (r".*VU_PT.*ERROR\.xml$", 18, "Error Partidas VU"),
'VU_AC_COVE': (r".*VU_AC_COVE.*ERROR\.xml$", 24, "Error Acuses COVES VU"),
'VU_COVE': (r".*VU_COVE.*ERROR\.xml$", 20, "Error COVES VU"),
'VU_RM': (r".*VU_RM.*ERROR\.xml$", 16, "Error Remesas VU"),
'VU_AC': (r".*VU_AC.*ERROR\.xml$", 26, "Error Acuses VU"),
}
}
def eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id):
"""Elimina documentos existentes con el mismo nombre base"""
documentos_existentes = Document.objects.filter(
archivo__icontains=nombre_sin_extension,
organizacion=organizacion,
pedimento_id=pedimento_id
)
if not documentos_existentes.exists():
return
for doc_existente in documentos_existentes:
# Eliminar archivo físico si existe
if doc_existente.archivo and os.path.exists(doc_existente.archivo.path):
try:
os.remove(doc_existente.archivo.path)
except Exception as e:
logger.error(f"Error al eliminar archivo físico: {e}")
# Eliminar registros de la base de datos
documentos_existentes.delete()
def obtener_tipo_documento_por_patron(nombre_archivo, organizacion, pedimento_id):
"""Determina el tipo de documento basado en patrones de nombre"""
nombre_sin_extension = nombre_archivo.rsplit('.', 1)[0]
# Verificar patrones REQUEST
for doc_key, (patron, type_id, descripcion) in DOCUMENT_PATTERNS['REQUEST'].items():
if re.search(patron, nombre_archivo, re.IGNORECASE):
try:
# Eliminar documentos existentes
eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id)
# Obtener tipo de documento
return DocumentType.objects.get(id=type_id)
except DocumentType.DoesNotExist:
raise ValidationError({
"error": f"El tipo de documento '{descripcion}' no existe. Por favor, créelo primero."
})
# Verificar patrones ERROR (si es necesario procesarlos)
for doc_key, (patron, type_id, descripcion) in DOCUMENT_PATTERNS['ERROR'].items():
if re.search(patron, nombre_archivo, re.IGNORECASE):
try:
# Eliminar documentos existentes
eliminar_documentos_existentes(organizacion, nombre_sin_extension, pedimento_id)
# Obtener tipo de documento
return DocumentType.objects.get(id=type_id)
except DocumentType.DoesNotExist:
raise ValidationError({
"error": f"El tipo de documento '{descripcion}' no existe. Por favor, créelo primero."
})
return None
class CustomPagination(PageNumberPagination):
"""
@@ -144,12 +227,45 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
"codigo": "storage_limit_exceeded"
}, code=status.HTTP_400_BAD_REQUEST)
# Guardar documento y actualizar espacio atómicamente
documento = serializer.save(
organizacion=organizacion,
size=archivo.size,
extension=archivo.name.split('.')[-1].lower()
)
try:
pedimento_id = serializer.validated_data.get('pedimento').id if serializer.validated_data.get('pedimento') else None
document_type = serializer.validated_data.get('document_type')
# Determinar el tipo de documento basado en el nombre del archivo
detected_type = obtener_tipo_documento_por_patron(archivo.name, organizacion, pedimento_id)
if detected_type:
document_type = detected_type
else:
# Lógica para archivos que no coinciden con los patrones conocidos
logger.warning(f"No se encontró patrón para archivo: {archivo.name}")
# Puedes mantener el document_type original o manejarlo de otra forma
except ValidationError as ve:
raise ve
except Exception as e:
# Como fallback, intentar obtener cualquier DocumentType existente
logger.error(f"Error al determinar el tipo de documento basado en el nombre del archivo: {e}")
try:
# Guardar documento y actualizar espacio atómicamente
documento = serializer.save(
document_type=document_type,
organizacion=organizacion,
size=archivo.size,
extension=archivo.name.split('.')[-1].lower()
)
except Exception as e:
# Guardar documento y actualizar espacio atómicamente
documento = serializer.save(
organizacion=organizacion,
size=archivo.size,
extension=archivo.name.split('.')[-1].lower()
)
uso.espacio_utilizado = nuevo_espacio_utilizado
uso.save()
@@ -833,7 +949,114 @@ class PedimentoDocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero)
return queryset
class TriggerPedimentoCompletoView(APIView):
"""
Endpoint interno para disparar la descarga de pedimento completo
en el microservicio FastAPI. Reenvía el payload tal cual y devuelve
la respuesta del microservicio (normalmente un `task_id`).
"""
# permission_classes = [IsAuthenticated]
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
my_tags = ['Microservice - Pedimento Completo']
def post(self, request):
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
return Response({"error": "Usuario no autenticado o sin organización"}, status=401)
# Validación mínima
# if not payload.get('credencial') or not payload.get('pedimento_id'):
# return Response({"error": "Se requieren 'credencial' y 'pedimento'"}, status=status.HTTP_400_BAD_REQUEST)
if not request.data.get('pedimento_id'):
return Response({"error": "Se requieren 'credencial' y 'pedimento'"}, status=status.HTTP_400_BAD_REQUEST)
pedimento_id = request.data.get('pedimento_id')
# Verificar que el pedimento existe y pertenece a la organización del usuario
try:
pedimento = Pedimento.objects.get(id=pedimento_id)
if not pedimento.contribuyente:
return Response({"error": "El pedimento no tiene un contribuyente asociado"}, status=status.HTTP_400_BAD_REQUEST)
contribuyente_rfc = pedimento.contribuyente.rfc
payload = {
"pedimento": {
"id": str(pedimento.id),
"pedimento": pedimento.pedimento,
"pedimento_app": pedimento.pedimento_app,
"aduana": pedimento.aduana,
"patente": pedimento.patente,
"contribuyente": contribuyente_rfc,
"organizacion": str(pedimento.organizacion.id)
},
"credencial": {
"id": "",
"user": "",
"password": "",
"efirma": "",
"key": "",
"cer": "",
"is_active": False,
"organizacion": ""
}
}
except Pedimento.DoesNotExist:
return Response({"error": "Pedimento no encontrado"}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({"error": f"Error al buscar pedimento: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
try:
credenciales = CredencialesImportador.objects.get(rfc=contribuyente_rfc)
vucem = credenciales.vucem
# Obtener las rutas de los archivos, no los objetos FieldFile
key_path = vucem.key.path if vucem.key else ""
cer_path = vucem.cer.path if vucem.cer else ""
payload['credencial'] = {
"id": str(credenciales.id),
"user": vucem.usuario if vucem.usuario else "",
"password": vucem.password if vucem.password else "",
"efirma": vucem.efirma if vucem.efirma else "",
"key": key_path,
"cer": cer_path,
"is_active": vucem.is_active if vucem.is_active else False,
"organizacion": str(credenciales.organizacion.id) if credenciales.organizacion else ""
}
except CredencialesImportador.DoesNotExist:
return Response({"error": "No se encontró credencial VUCEM para la organización del pedimento"}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({"error": f"Error al buscar credencial VUCEM: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
try:
# Obtener la URL desde las variables de entorno
api_url = os.getenv('SERVICE_API_URL_V2')
logger.info(f"Usando MICROSERVICE_BASE_URL: {api_url}")
endpoint = f"{api_url.rstrip('/')}/services/auditar_pedimento_completo"
# endpoint = "http://localhost:8001/api/v2/services/auditar_pedimento_completo"
except requests.exceptions.RequestException as e:
logger.error(f"Error obteniendo MICROSERVICE_BASE_URL: {e}")
return Response({"error": "Error obteniendo la URL del microservicio", "detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error inesperado obteniendo MICROSERVICE_BASE_URL: {e}")
return Response({"error": "Error inesperado obteniendo la URL del microservicio", "detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
try:
resp = requests.post(endpoint, json=payload, timeout=30)
except requests.exceptions.RequestException as e:
logger.error(f"Error comunicándose con microservice: {e}")
return Response({"error": "No se pudo conectar con el microservicio", "detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY)
try:
content = resp.json()
except ValueError:
content = {"detail": resp.text}
return Response(content, status=resp.status_code)