fix/forzar-carga-acuses

This commit is contained in:
2026-05-25 14:52:06 -06:00
parent e378f2d949
commit 94846fec8a
6 changed files with 581 additions and 186 deletions

View File

@@ -15,7 +15,7 @@ from .tasks.auditoria import (
auditar_remesas,
)
from .tasks.internal_services import auditar_pedimentos
from .tasks.microservice_v2 import procesar_pedimentos_completos
from .tasks.microservice_v2 import procesar_pedimentos_completos, procesar_pedimento_completo_individual
from api.customs.models import Pedimento
from api.organization.models import Organizacion
from api.record.models import Document
@@ -25,6 +25,9 @@ import os
from api.utils.storage_service import storage_service
import logging
import uuid
_ERROR_DOCUMENT_TYPES = [10, 14, 16, 18, 20, 22, 24, 26]
logger = logging.getLogger('api.customs.views_auditor')
def get_document_content(documento):
@@ -1680,98 +1683,155 @@ def auditor_obtener_respuesta_edocument_vu(request):
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def auditar_pedimento_endpoint(request):
"""
Audita un pedimento específico verificando si existe su XML y extrayendo información.
Audita el pedimento completo (PC): ¿está descargado? ¿se puede procesar?
Incluye diagnóstico de campos y errores detectados por tipo de documento.
"""
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:
# Validar permisos y existencia del pedimento
pedimento = Pedimento.objects.get(id=pedimento_id)
pedimento = Pedimento.objects.select_related(
'organizacion', 'contribuyente'
).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
)
# Buscar documentos XML del pedimento
documentos_xml = Document.objects.filter(
pedimento=pedimento,
archivo__endswith='.xml',
# PC descargado (type 2)
pc_descargado = pedimento.documents.filter(
document_type_id=2,
organizacion=pedimento.organizacion
).exists()
# Fuente de carga
fuente = 'datastage' if pedimento.consultar_vucem else 'manual'
# Diagnóstico de campos
aduana = pedimento.aduana or ''
patente = pedimento.patente or ''
numero_pedimento = pedimento.pedimento or ''
aduana_valida = bool(aduana) and aduana.isdigit() and 2 <= len(aduana) <= 3
patente_valida = bool(patente) and patente.isdigit() and len(patente) == 4
pedimento_valido = bool(numero_pedimento) and numero_pedimento.isdigit() and len(numero_pedimento) >= 7
numero_operacion_presente = bool(pedimento.numero_operacion)
from api.vucem.models import CredencialesImportador
tiene_contribuyente = pedimento.contribuyente is not None
tiene_credenciales = False
credenciales_detalle = None
if tiene_contribuyente:
credencial = CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first()
tiene_credenciales = bool(credencial and credencial.vucem)
if credencial and not credencial.vucem:
credenciales_detalle = 'Credencial encontrada pero sin cuenta VUCEM asociada'
elif not credencial:
credenciales_detalle = f'Sin credenciales VUCEM para RFC {pedimento.contribuyente.rfc}'
razones = []
if not aduana_valida:
razones.append(f'Aduana inválida o ausente (valor: "{aduana}")')
if not patente_valida:
razones.append(f'Patente inválida o ausente (valor: "{patente}")')
if not pedimento_valido:
razones.append(f'Número de pedimento inválido (valor: "{numero_pedimento}")')
if not tiene_contribuyente:
razones.append('Sin contribuyente asignado')
elif not tiene_credenciales:
razones.append(credenciales_detalle or 'Sin credenciales VUCEM')
puede_procesar = len(razones) == 0
datos = {
'aduana': aduana or None,
'patente': patente or None,
'numero_pedimento': numero_pedimento or None,
'numero_operacion': pedimento.numero_operacion,
'contribuyente_rfc': pedimento.contribuyente.rfc if pedimento.contribuyente else None,
'contribuyente_nombre': str(pedimento.contribuyente) if pedimento.contribuyente else None,
}
validacion = {
'aduana_valida': aduana_valida,
'patente_valida': patente_valida,
'pedimento_valido': pedimento_valido,
'numero_operacion_presente': numero_operacion_presente,
'tiene_contribuyente': tiene_contribuyente,
'tiene_credenciales_vucem': tiene_credenciales,
}
# Errores por tipo de documento
docs_error = (
Document.objects
.filter(
pedimento=pedimento,
organizacion=pedimento.organizacion,
document_type_id__in=_ERROR_DOCUMENT_TYPES,
)
.select_related('document_type')
.order_by('document_type_id')
)
if not documentos_xml.exists():
return Response({
'pedimento_id': str(pedimento_id),
'pedimento': pedimento.pedimento,
'pedimento_app': pedimento.pedimento_app,
'archivos_xml_encontrados': 0,
'mensaje': 'No se encontraron archivos XML para este pedimento',
'auditoria_completa': False
}, status=status.HTTP_200_OK)
# Lista para almacenar información de cada XML
xmls_analizados = []
informacion_extraida = []
for documento in documentos_xml:
errores_detectados = [
{
'documento_id': str(doc.id),
'nombre_archivo': os.path.basename(str(doc.archivo)),
'tipo_error': doc.document_type.descripcion if doc.document_type else 'Error desconocido',
'tipo_id': doc.document_type_id,
}
for doc in docs_error
]
print(f"documento >>>> {documento}")
logger.info(f"documento >>>> {documento}")
# XML del PC si existe
informacion_xml = None
doc_pc = pedimento.documents.filter(
document_type_id=2,
organizacion=pedimento.organizacion,
archivo__endswith='.xml',
).first()
if doc_pc:
xml_content = get_document_content(doc_pc)
if xml_content:
info_pedimento = extraer_info_pedimento_xml(xml_content)
if info_pedimento:
informacion_xml = info_pedimento
actualizar_info_pedimento(pedimento, info_pedimento)
try:
xml_info = {
'documento_id': str(documento.id),
'nombre_archivo': os.path.basename(str(documento.archivo)),
'tamanio': documento.size,
'extension': documento.extension,
'tipo_documento': documento.document_type.descripcion if documento.document_type else 'Desconocido'
}
xml_content = get_document_content(documento)
if xml_content is None:
xml_info['error_lectura'] = 'No se pudo descargar el archivo'
else:
info_pedimento = extraer_info_pedimento_xml(xml_content)
if info_pedimento:
xml_info['informacion_extraida'] = info_pedimento
informacion_extraida.append(info_pedimento)
hay_pendientes = not pc_descargado
hay_errores = bool(errores_detectados)
# Actualizar el pedimento con la información encontrada si es necesario
actualizar_info_pedimento(pedimento, info_pedimento)
xmls_analizados.append(xml_info)
except Exception as e:
xmls_analizados.append({
'documento_id': str(documento.id),
'nombre_archivo': os.path.basename(str(documento.archivo)),
'error': f'Error procesando archivo: {str(e)}'
})
response_data = {
if hay_errores:
estado = 'CON_ERRORES'
elif hay_pendientes:
estado = 'PENDIENTE'
else:
estado = 'COMPLETO'
return Response({
'pedimento_id': str(pedimento_id),
'pedimento': pedimento.pedimento,
'pedimento_app': pedimento.pedimento_app,
'archivos_xml_encontrados': len(xmls_analizados),
'xmls_analizados': xmls_analizados,
'informacion_extraida': informacion_extraida,
'auditoria_completa': True,
'mensaje': f'Auditoría completada para el pedimento {pedimento.pedimento}'
}
return Response(response_data, status=status.HTTP_200_OK)
'estado': estado,
'hay_pendientes': hay_pendientes,
'hay_errores': hay_errores,
'pc_descargado': pc_descargado,
'puede_procesar': puede_procesar,
'razones_no_puede_procesar': razones,
'fuente': fuente,
'datos': datos,
'validacion': validacion,
'errores_detectados': errores_detectados,
'informacion_xml': informacion_xml,
}, status=status.HTTP_200_OK)
except Pedimento.DoesNotExist:
return Response(
{'error': 'Pedimento no encontrado'},
@@ -1783,6 +1843,164 @@ def auditar_pedimento_endpoint(request):
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@swagger_auto_schema(
method='post',
operation_description="Procesa el pedimento completo (tipo 2) de un pedimento específico llamando al microservicio VUCEM.",
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={
202: openapi.Response('Procesamiento encolado — usar task_id para consultar resultado'),
200: openapi.Response('El pedimento ya tiene su documento completo descargado'),
400: openapi.Response('Error en los parámetros o prerequisitos faltantes'),
403: openapi.Response('No tiene permisos suficientes'),
404: openapi.Response('Pedimento no encontrado'),
}
)
@api_view(['POST'])
@permission_classes([IsAuthenticated, require_permission('auditoria.process')])
def procesar_pedimento_completo_endpoint(request):
"""
Diagnostica el pedimento completo y, si todo está en orden y aún no se ha
descargado, encola la tarea de procesamiento.
Siempre devuelve diagnóstico completo: validación de campos, fuente, estado
del PC y razones por las que no se puede procesar si aplica.
"""
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.select_related(
'organizacion', 'contribuyente', 'tipo_operacion'
).get(id=pedimento_id)
except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
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)
# --- Diagnóstico de campos ---
aduana = pedimento.aduana or ''
patente = pedimento.patente or ''
numero_pedimento = pedimento.pedimento or ''
aduana_valida = bool(aduana) and aduana.isdigit() and 2 <= len(aduana) <= 3
patente_valida = bool(patente) and patente.isdigit() and len(patente) == 4
pedimento_valido = bool(numero_pedimento) and numero_pedimento.isdigit() and len(numero_pedimento) >= 7
numero_operacion_presente = bool(pedimento.numero_operacion)
# --- Fuente de carga ---
# consultar_vucem=True indica que fue originado desde datastage
fuente = 'datastage' if pedimento.consultar_vucem else 'manual'
# --- Estado del PC ---
pc_descargado = pedimento.documents.filter(
document_type_id=2,
organizacion=pedimento.organizacion
).exists()
# --- Credenciales VUCEM ---
from api.vucem.models import CredencialesImportador
tiene_contribuyente = pedimento.contribuyente is not None
tiene_credenciales = False
credenciales_detalle = None
if tiene_contribuyente:
credencial = CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first()
tiene_credenciales = bool(credencial and credencial.vucem)
if credencial and not credencial.vucem:
credenciales_detalle = 'Credencial encontrada pero sin cuenta VUCEM asociada'
elif not credencial:
credenciales_detalle = f'Sin credenciales VUCEM para RFC {pedimento.contribuyente.rfc}'
# --- Puede procesar ---
razones = []
if not aduana_valida:
razones.append(f'Aduana inválida o ausente (valor: "{aduana}")')
if not patente_valida:
razones.append(f'Patente inválida o ausente (valor: "{patente}")')
if not pedimento_valido:
razones.append(f'Número de pedimento inválido (valor: "{numero_pedimento}")')
if not tiene_contribuyente:
razones.append('Sin contribuyente asignado')
elif not tiene_credenciales:
razones.append(credenciales_detalle or 'Sin credenciales VUCEM')
puede_procesar = len(razones) == 0
datos = {
'aduana': aduana or None,
'patente': patente or None,
'numero_pedimento': numero_pedimento or None,
'numero_operacion': pedimento.numero_operacion,
'regimen': pedimento.regimen,
'clave_pedimento': pedimento.clave_pedimento,
'fecha_pago': str(pedimento.fecha_pago) if pedimento.fecha_pago else None,
'contribuyente_rfc': pedimento.contribuyente.rfc if pedimento.contribuyente else None,
'contribuyente_nombre': str(pedimento.contribuyente) if pedimento.contribuyente else None,
'remesas': pedimento.remesas,
'numero_partidas': pedimento.numero_partidas,
}
validacion = {
'aduana_valida': aduana_valida,
'patente_valida': patente_valida,
'pedimento_valido': pedimento_valido,
'numero_operacion_presente': numero_operacion_presente,
'tiene_contribuyente': tiene_contribuyente,
'tiene_credenciales_vucem': tiene_credenciales,
'puede_procesar': puede_procesar,
'razones_no_puede_procesar': razones,
}
base_response = {
'pedimento_id': str(pedimento_id),
'pedimento': pedimento.pedimento,
'pedimento_app': pedimento.pedimento_app,
'fuente': fuente,
'datos': datos,
'validacion': validacion,
'pc_descargado': pc_descargado,
}
# Ya descargado — devolver diagnóstico sin encolar
if pc_descargado:
return Response({
**base_response,
'estado': 'ya_descargado',
'mensaje': 'El pedimento completo ya fue descargado',
}, status=status.HTTP_200_OK)
# No puede procesar — devolver diagnóstico con razones
if not puede_procesar:
return Response({
**base_response,
'estado': 'no_puede_procesar',
'mensaje': 'El pedimento no cumple los requisitos para procesar',
}, status=status.HTTP_200_OK)
# Todo en orden — encolar
task = procesar_pedimento_completo_individual.delay(str(pedimento_id))
logger.info(f"Procesamiento PC encolado: {pedimento.pedimento} (task={task.id})")
return Response({
**base_response,
'estado': 'encolado',
'task_id': task.id,
'mensaje': f'Procesamiento encolado para {pedimento.pedimento_app}',
}, status=status.HTTP_202_ACCEPTED)
def actualizar_info_pedimento(pedimento, info_xml):
"""
Actualiza la información del pedimento con los datos extraídos del XML.