Merge pull request 'fix/forzar-carga-acuses' (#31) from fix/forzar-carga-acuses into main

Reviewed-on: #31
This commit is contained in:
2026-05-25 20:55:06 +00:00
6 changed files with 581 additions and 186 deletions

View File

@@ -9,7 +9,7 @@ class CustomUserSerializer(serializers.ModelSerializer):
Serializer for the CustomUser model.
"""
password = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True, required=False)
groups = serializers.PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
rfc = serializers.PrimaryKeyRelatedField(
queryset=Importador.objects.all(),
@@ -23,6 +23,17 @@ class CustomUserSerializer(serializers.ModelSerializer):
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'password', 'profile_picture', 'organizacion', 'is_importador', 'rfc', 'is_active', 'is_superuser', 'groups']
read_only_fields = ['id', 'organizacion', 'is_superuser']
def validate_password(self, value):
if not value or not value.strip():
raise serializers.ValidationError("La contraseña no puede estar vacía o contener solo espacios.")
return value
def validate(self, attrs):
# En create, la contraseña es obligatoria
if self.instance is None and not attrs.get('password'):
raise serializers.ValidationError({"password": "Este campo es requerido."})
return attrs
def create(self, validated_data):
groups = validated_data.pop('groups', [])
rfcs = validated_data.pop('rfc', [])

View File

@@ -184,7 +184,7 @@ class EDocumentSerializer(serializers.ModelSerializer):
numero = str(obj.numero_edocument).strip()
# id_pedimento = str(obj.pedimento_id).strip()
# excluir e documents de tipo request y de tipo error
# excluir solo request (21, 25); errores (22, 26) se incluyen para detección en frontend
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
@@ -240,15 +240,11 @@ class CoveSerializer(serializers.ModelSerializer):
try:
numero = str(obj.numero_cove).strip()
# Excluir los tipo de documento 20, 24, 23 y 19
# 20 = error solicitud cove
# 24 = error solicitud acuse cove
# 23 = request acuse cove
# 19 = request cove
# Excluir solo request (19, 23); errores (20, 24) se incluyen para detección en frontend
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
).exclude(document_type_id__in=[20, 24, 23, 19])
).exclude(document_type_id__in=[19, 23])
# Filtro por organización si aplica
if hasattr(obj, 'organizacion') and obj.organizacion:

View File

@@ -91,12 +91,18 @@ def procesar_coves_pedimento(pedimento_id):
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/coves",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
print(f"Servicio de COVEs enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/coves",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando COVEs para pedimento {pedimento.pedimento}: {e}")
raise
@shared_task
def procesar_acuse_coves_pedimento(pedimento_id):
@@ -107,19 +113,25 @@ def procesar_acuse_coves_pedimento(pedimento_id):
id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id
).first()
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"coves": [cove_to_dict(cove) for cove in pedimento.coves.filter(acuse_cove_descargado=False)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
print(f"Servicio de acuses de COVEs enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"Acuses de COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando acuses de COVEs para pedimento {pedimento.pedimento}: {e}")
raise
@shared_task
def procesar_edocs_pedimento(pedimento_id):
@@ -130,19 +142,25 @@ def procesar_edocs_pedimento(pedimento_id):
id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id
).first()
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(edocument_descargado=False)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
print(f"Servicio de E-documents enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"E-documents encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando E-documents para pedimento {pedimento.pedimento}: {e}")
raise
@shared_task
def procesar_acuses_pedimento(pedimento_id):
@@ -153,19 +171,25 @@ def procesar_acuses_pedimento(pedimento_id):
id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id
).first()
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(acuse_descargado=False)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
print(f"Servicio de acuses enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"Acuses encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando acuses para pedimento {pedimento.pedimento}: {e}")
raise
@shared_task
def procesar_partidas_pedimento(pedimento_id):
@@ -176,19 +200,32 @@ def procesar_partidas_pedimento(pedimento_id):
id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id
).first()
credenciales_dict = credenciales_to_dict(credenciales)
partidas_pendientes = list(pedimento.partidas.filter(descargado=False))
payload = {
"partidas": [partida_to_dict(partida) for partida in pedimento.partidas.filter(descargado=False)],
"partidas": [partida_to_dict(p) for p in partidas_pendientes],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/partidas/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
print(f"Servicio de partidas enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/partidas/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
result = response.json()
logging.info(
f"Partidas encoladas para pedimento {pedimento.pedimento}: "
f"{result.get('total', 0)} de {len(partidas_pendientes)}"
)
except requests.exceptions.RequestException as e:
logging.error(
f"Error encolando partidas para pedimento {pedimento.pedimento}: {e}"
)
raise
@shared_task
def procesar_remesas_pedimento(pedimento_id):
@@ -205,12 +242,18 @@ def procesar_remesas_pedimento(pedimento_id):
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/remesas",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
print(f"Servicio de remesas enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/remesas",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"Remesa encolada para pedimento {pedimento.pedimento}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando remesa para pedimento {pedimento.pedimento}: {e}")
raise
@shared_task
def procesar_pedimento_completo_individual(pedimento_id):
@@ -225,13 +268,19 @@ def procesar_pedimento_completo_individual(pedimento_id):
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/pedimento_completo",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
return response
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/pedimento_completo",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"Pedimento completo encolado: {pedimento.pedimento}")
return response
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando pedimento completo {pedimento.pedimento}: {e}")
raise
@shared_task
def procesar_pedimentos_completos(organizacion_id):
@@ -270,13 +319,18 @@ def procesar_pedimentos_completos(organizacion_id):
url = f"{SERVICE_API_URL_V2}/services/pedimento_completo"
dataJson = json.dumps(payload)
response = requests.post(
url,
data=dataJson,
headers={"Content-Type": "application/json"}
)
# Aquí puedes continuar con el resto de tu lógica
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
url,
data=dataJson,
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"Pedimento completo encolado: {pedimento.pedimento}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando pedimento completo {pedimento.pedimento}: {e}")
continue
@shared_task
def procesar_remesas(organizacion_id):
@@ -311,9 +365,11 @@ def procesar_remesas(organizacion_id):
response = requests.post(
f"{SERVICE_API_URL_V2}/services/remesas/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
headers={"Content-Type": "application/json"},
timeout=60
)
logger.info(f"Servicio enviado para pedimento {pedimento.pedimento} — status {response.status_code}")
response.raise_for_status()
logger.info(f"Remesa encolada para pedimento {pedimento.pedimento} — status {response.status_code}")
except Exception as e:
logger.error(f"Error procesando remesa para pedimento {pedimento.pedimento}: {e}", exc_info=True)
@@ -339,14 +395,18 @@ def procesar_coves(organizacion_id):
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/coves",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
# Aquí puedes continuar con el resto de tu lógica
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/coves",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando COVEs para pedimento {pedimento.pedimento}: {e}")
continue
@shared_task
def procesar_acuse_coves(organizacion_id):
@@ -370,14 +430,18 @@ def procesar_acuse_coves(organizacion_id):
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
# Aquí puedes continuar con el resto de tu lógica
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/cove/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"Acuses de COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando acuses de COVEs para pedimento {pedimento.pedimento}: {e}")
continue
@shared_task
def procesar_acuses(organizacion_id):
@@ -401,14 +465,18 @@ def procesar_acuses(organizacion_id):
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
# Aquí puedes continuar con el resto de tu lógica
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"Acuses encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando acuses para pedimento {pedimento.pedimento}: {e}")
continue
@shared_task
def procesar_edocs(organizacion_id):
@@ -432,14 +500,18 @@ def procesar_edocs(organizacion_id):
"credencial": credenciales_dict
}
response = requests.post(
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
)
# Aquí puedes continuar con el resto de tu lógica
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
timeout=60
)
response.raise_for_status()
logging.info(f"E-documents encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}")
except requests.exceptions.RequestException as e:
logging.error(f"Error encolando E-documents para pedimento {pedimento.pedimento}: {e}")
continue
@shared_task
def procesar_partidas(organizacion_id):
@@ -447,29 +519,42 @@ def procesar_partidas(organizacion_id):
organizacion_id=organizacion_id,
partidas__isnull=False
).distinct()
for pedimento in pedimentos:
if pedimento.partidas.filter(descargado=False).exists(): # Tipo 4: Partidas
# Convertir el pedimento a JSON usando el serializer
pedimento_dict = pedimento_to_dict(pedimento)
credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first()
partidas_pendientes = list(pedimento.partidas.filter(descargado=False))
if not partidas_pendientes:
continue
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"partidas": [partida_to_dict(partida) for partida in pedimento.partidas.filter(descargado=False)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
pedimento_dict = pedimento_to_dict(pedimento)
credenciales = Vucem.objects.filter(
id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id
).first()
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"partidas": [partida_to_dict(p) for p in partidas_pendientes],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
try:
response = requests.post(
f"{SERVICE_API_URL_V2}/services/all/partidas/",
f"{SERVICE_API_URL_V2}/services/all/partidas/",
data=json.dumps(payload),
headers={"Content-Type": "application/json"}
headers={"Content-Type": "application/json"},
timeout=60
)
# Aquí puedes continuar con el resto de tu lógica
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
response.raise_for_status()
result = response.json()
logging.info(
f"Partidas encoladas para pedimento {pedimento.pedimento}: "
f"{result.get('total', 0)} de {len(partidas_pendientes)}"
)
except requests.exceptions.RequestException as e:
logging.error(
f"Error encolando partidas para pedimento {pedimento.pedimento}: {e}"
)
continue
@shared_task
def documentos_con_errores(organizacion_id):

View File

@@ -62,6 +62,7 @@ from .views_auditor import (
auditor_obtener_peticion_edocument_vu,
auditor_obtener_respuesta_edocument_vu,
auditar_pedimento_endpoint,
procesar_pedimento_completo_endpoint,
)
urlpatterns = [
@@ -80,6 +81,7 @@ urlpatterns = [
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-pedimento-completo/pedimento/', procesar_pedimento_completo_endpoint, name='procesar-pedimento-completo-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'),

View File

@@ -2505,6 +2505,7 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
'partial_update': 'edocuments.edit',
'destroy': 'edocuments.delete',
'bulk_delete_edocs_vu': 'edocuments.delete',
'reset_acuse': 'edocuments.edit',
}
codename = perms.get(self.action, 'edocuments.view')
return [IsAuthenticated(), require_permission(codename)()]
@@ -2531,6 +2532,88 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
def perform_destroy(self, instance):
instance.delete()
@action(detail=True, methods=['post'], url_path='reset-acuse')
def reset_acuse(self, request, pk=None):
"""
Detecta inconsistencia cuando acuse_descargado=True pero no existe el documento
de acuse (tipo 4). Crea un registro de error tipo 26 para Errores VU y
restablece acuse_descargado=False para permitir reintentar.
"""
from api.record.models import Document, DocumentType
import logging
logger = logging.getLogger('api.customs.views')
edoc = self.get_object()
if not edoc.acuse_descargado:
return Response(
{"error": "El acuse no está marcado como descargado"},
status=status.HTTP_400_BAD_REQUEST
)
# Verificar si el acuse PDF (tipo 4 = Pedimento Acuse) existe realmente
acuse_disponible = Document.objects.filter(
pedimento=edoc.pedimento,
archivo__icontains=edoc.numero_edocument,
document_type_id=4
).exists()
if acuse_disponible:
return Response(
{"status": "El acuse está disponible correctamente", "acuse_disponible": True},
status=status.HTTP_200_OK
)
# Inconsistencia confirmada: crear documento de error tipo 26 para Errores VU
doc_type_error = DocumentType.objects.filter(id=26).first()
if doc_type_error:
error_content = (
f"Inconsistencia detectada: el acuse del EDocument {edoc.numero_edocument} "
f"fue marcado como descargado pero el documento no se encuentra disponible. "
f"El estado fue restablecido para permitir reprocesamiento."
).encode('utf-8')
try:
with tempfile.NamedTemporaryFile(
mode='wb', suffix='.txt', delete=False
) as f:
f.write(error_content)
tmp_path = f.name
pedimento_app = getattr(edoc.pedimento, 'pedimento_app', str(edoc.pedimento.pedimento))
file_name = f"error_acuse_{edoc.numero_edocument}.txt"
saved_path = storage_service.save_document_from_path(
file_path=tmp_path,
file_name=file_name,
organizacion_id=edoc.organizacion_id,
pedimento_app=pedimento_app
)
if saved_path:
Document.objects.create(
organizacion=edoc.organizacion,
pedimento=edoc.pedimento,
archivo=saved_path,
document_type=doc_type_error,
extension='TXT',
size=len(error_content),
fuente=None,
)
except Exception as e:
logger.error(
f"Error creando documento de error para acuse {edoc.numero_edocument}: {e}"
)
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
edoc.acuse_descargado = False
edoc.save()
serializer = self.get_serializer(edoc)
return Response(serializer.data, status=status.HTTP_200_OK)
class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for Cove model.

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.