T2026-05-030
This commit is contained in:
@@ -157,7 +157,7 @@ class ViewPedimentoServicesUtilInformation(LoggingMixin, APIView, FiltroPorOrgan
|
|||||||
|
|
||||||
# Si es importador de la organizacion, devuelve los servicios relacionados con sus pedimentos
|
# Si es importador de la organizacion, devuelve los servicios relacionados con sus pedimentos
|
||||||
if self.request.user.is_authenticated and self.request.user.groups.filter(name='importador').exists() and self.request.user.is_importador and self.request.user.groups.filter(name='user').exists():
|
if self.request.user.is_authenticated and self.request.user.groups.filter(name='importador').exists() and self.request.user.is_importador and self.request.user.groups.filter(name='user').exists():
|
||||||
return self.request.user.organizacion.procesamiento_pedimentos.filter(pedimento__contribuyente=self.request.user.rfc)
|
return self.request.user.organizacion.procesamiento_pedimentos.filter(pedimento__contribuyente__in=self.request.user.rfc.all())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,55 +47,31 @@ class PartidaSerializer(serializers.ModelSerializer):
|
|||||||
documentos = serializers.SerializerMethodField()
|
documentos = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def get_documentos(self, obj):
|
def get_documentos(self, obj):
|
||||||
"""
|
|
||||||
Busca documentos en la tabla `document` que coincidan EXACTAMENTE con:
|
|
||||||
'documents/vu_PT_{pedimentoApp}_{numero}' al inicio del nombre del archivo.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not obj or not getattr(obj, 'pedimento', None):
|
if not obj or not getattr(obj, 'pedimento', None):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not obj or not getattr(obj, 'numero_partida', None):
|
if not obj or not getattr(obj, 'numero_partida', None):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pedimentoApp = str(obj.pedimento.pedimento_app).strip()
|
pedimento_app = str(obj.pedimento.pedimento_app).strip()
|
||||||
numero = str(obj.numero_partida).strip()
|
numero = str(obj.numero_partida).strip()
|
||||||
|
# Incluir pedimento_app en el patrón para evitar falsos positivos
|
||||||
|
# entre partidas con números cortos (1 matchearía 10, 100, etc.)
|
||||||
|
patron = f"vu_PT_{pedimento_app}_{numero}_"
|
||||||
|
|
||||||
# Construir el patrón exacto de búsqueda
|
# 17 = REQUEST partida, 18 = ERROR partida
|
||||||
patron_exacto = f'documents/vu_PT_{pedimentoApp}_{numero}.xml'
|
|
||||||
|
|
||||||
# Buscar documentos que empiecen EXACTAMENTE con ese patrón
|
|
||||||
qs = Document.objects.filter(
|
qs = Document.objects.filter(
|
||||||
archivo=patron_exacto
|
pedimento=obj.pedimento,
|
||||||
)
|
archivo__icontains=patron,
|
||||||
|
).exclude(document_type_id__in=[17, 18])
|
||||||
|
|
||||||
# Opción 2: Si puede tener diferentes extensiones
|
|
||||||
# patron_base = f'documents/vu_PT_{pedimentoApp}_{numero}'
|
|
||||||
# qs = Document.objects.filter(
|
|
||||||
# archivo__startswith=patron_base
|
|
||||||
# ).filter(
|
|
||||||
# archivo__in=[
|
|
||||||
# f'{patron_base}.xml',
|
|
||||||
# f'{patron_base}.pdf',
|
|
||||||
# f'{patron_base}.zip'
|
|
||||||
# ]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Filtro adicional por pedimento si el modelo Document tiene este campo
|
|
||||||
if hasattr(Document, 'pedimento'):
|
|
||||||
qs = qs.filter(pedimento=obj.pedimento)
|
|
||||||
|
|
||||||
# Filtro por organización
|
|
||||||
if hasattr(obj, 'organizacion') and obj.organizacion:
|
if hasattr(obj, 'organizacion') and obj.organizacion:
|
||||||
qs = qs.filter(organizacion=obj.organizacion)
|
qs = qs.filter(organizacion=obj.organizacion)
|
||||||
|
|
||||||
serializer = DocumentSerializer(qs, many=True, context=self.context)
|
serializer = DocumentSerializer(qs, many=True, context=self.context)
|
||||||
return serializer.data
|
return serializer.data
|
||||||
|
|
||||||
#return []
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía
|
|
||||||
return []
|
return []
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Partida
|
model = Partida
|
||||||
@@ -208,10 +184,11 @@ class EDocumentSerializer(serializers.ModelSerializer):
|
|||||||
numero = str(obj.numero_edocument).strip()
|
numero = str(obj.numero_edocument).strip()
|
||||||
# id_pedimento = str(obj.pedimento_id).strip()
|
# id_pedimento = str(obj.pedimento_id).strip()
|
||||||
|
|
||||||
|
# excluir e documents de tipo request y de tipo error
|
||||||
qs = Document.objects.filter(
|
qs = Document.objects.filter(
|
||||||
pedimento=obj.pedimento,
|
pedimento=obj.pedimento,
|
||||||
archivo__icontains=numero,
|
archivo__icontains=numero,
|
||||||
)
|
).exclude(document_type_id__in=[21, 25])
|
||||||
|
|
||||||
# Filtro por organización si aplica
|
# Filtro por organización si aplica
|
||||||
if hasattr(obj, 'organizacion') and obj.organizacion:
|
if hasattr(obj, 'organizacion') and obj.organizacion:
|
||||||
@@ -263,10 +240,15 @@ class CoveSerializer(serializers.ModelSerializer):
|
|||||||
try:
|
try:
|
||||||
numero = str(obj.numero_cove).strip()
|
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
|
||||||
qs = Document.objects.filter(
|
qs = Document.objects.filter(
|
||||||
pedimento=obj.pedimento,
|
pedimento=obj.pedimento,
|
||||||
archivo__icontains=numero,
|
archivo__icontains=numero,
|
||||||
)
|
).exclude(document_type_id__in=[20, 24, 23, 19])
|
||||||
|
|
||||||
# Filtro por organización si aplica
|
# Filtro por organización si aplica
|
||||||
if hasattr(obj, 'organizacion') and obj.organizacion:
|
if hasattr(obj, 'organizacion') and obj.organizacion:
|
||||||
|
|||||||
@@ -87,8 +87,11 @@ def trigger_celery_task_on_cove_create(sender, instance, created, **kwargs):
|
|||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger('api.customs.async_operations')
|
logger = logging.getLogger('api.customs.async_operations')
|
||||||
logger.info(f"Cove creado: {instance.id}, creando procesamiento...")
|
logger.info(f"Cove creado: {instance.id}, creando procesamiento...")
|
||||||
crear_procesamiento_cove.apply_async(args=[str(instance.pedimento.id)])
|
pedimento_id = str(instance.pedimento.id)
|
||||||
crear_procesamiento_acuse_cove.apply_async(args=[str(instance.pedimento.id)])
|
def enqueue_cove_tasks():
|
||||||
|
crear_procesamiento_cove.apply_async(args=[pedimento_id])
|
||||||
|
crear_procesamiento_acuse_cove.apply_async(args=[pedimento_id])
|
||||||
|
transaction.on_commit(enqueue_cove_tasks)
|
||||||
|
|
||||||
@receiver(post_save, sender=EDocument)
|
@receiver(post_save, sender=EDocument)
|
||||||
def trigger_celery_task_on_edocument_create(sender, instance, created, **kwargs):
|
def trigger_celery_task_on_edocument_create(sender, instance, created, **kwargs):
|
||||||
@@ -96,5 +99,8 @@ def trigger_celery_task_on_edocument_create(sender, instance, created, **kwargs)
|
|||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger('api.customs.async_operations')
|
logger = logging.getLogger('api.customs.async_operations')
|
||||||
logger.info(f"EDocument creado: {instance.id}, creando procesamiento...")
|
logger.info(f"EDocument creado: {instance.id}, creando procesamiento...")
|
||||||
crear_procesamiento_edocument.apply_async(args=[str(instance.pedimento.id)])
|
pedimento_id = str(instance.pedimento.id)
|
||||||
crear_procesamiento_acuse.apply_async(args=[str(instance.pedimento.id)])
|
def enqueue_edocument_tasks():
|
||||||
|
crear_procesamiento_edocument.apply_async(args=[pedimento_id])
|
||||||
|
crear_procesamiento_acuse.apply_async(args=[pedimento_id])
|
||||||
|
transaction.on_commit(enqueue_edocument_tasks)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
from .microservice import *
|
from .microservice import *
|
||||||
from .internal_services import *
|
from .internal_services import *
|
||||||
from .bulk_upload import *
|
from .bulk_upload import *
|
||||||
|
from .microservice_v2 import *
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocumen
|
|||||||
from core.utils import xml_controller
|
from core.utils import xml_controller
|
||||||
import requests
|
import requests
|
||||||
from core.utils import xml_remesas_controller
|
from core.utils import xml_remesas_controller
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def obtener_pedimentos(organizacion_id):
|
def obtener_pedimentos(organizacion_id):
|
||||||
return Pedimento.objects.filter(organizacion_id=organizacion_id)
|
return Pedimento.objects.filter(organizacion_id=organizacion_id)
|
||||||
@@ -35,23 +37,31 @@ def auditor_descargas(pedimento, servicio, related_name, variable, mensaje):
|
|||||||
pedimento_id = pedimento.id
|
pedimento_id = pedimento.id
|
||||||
docs = getattr(pedimento, related_name).all()
|
docs = getattr(pedimento, related_name).all()
|
||||||
|
|
||||||
|
print(f"pedimento: {pedimento}, servicio: {servicio}, related_name: {related_name}, variable: {variable}, mensaje: {mensaje}")
|
||||||
|
logger.info(f"pedimento: {pedimento}, servicio: {servicio}, related_name: {related_name}, variable: {variable}, mensaje: {mensaje}")
|
||||||
|
|
||||||
# Si no hay documentos, marcar como completado
|
# Si no hay documentos, marcar como completado
|
||||||
if not docs.exists():
|
if not docs.exists():
|
||||||
proceso = modificar_estado_procesamiento(pedimento, servicio_id=servicio, nuevo_estado=3) # Estado "completado"
|
proceso = modificar_estado_procesamiento(pedimento, servicio_id=servicio, nuevo_estado=3) # Estado "completado"
|
||||||
print(f"✓ Pedimento {pedimento_id} no tiene {mensaje}s para procesar.")
|
print(f"✓ Pedimento {pedimento_id} no tiene {mensaje}s para procesar.")
|
||||||
|
logger.info(f"✓ Pedimento {pedimento_id} no tiene {mensaje}s para procesar.")
|
||||||
else:
|
else:
|
||||||
all_docs = all(getattr(doc, variable) for doc in docs)
|
all_docs = all(getattr(doc, variable) for doc in docs)
|
||||||
if all_docs:
|
if all_docs:
|
||||||
proceso = modificar_estado_procesamiento(pedimento, servicio_id=servicio, nuevo_estado=3) # Estado "completado"
|
proceso = modificar_estado_procesamiento(pedimento, servicio_id=servicio, nuevo_estado=3) # Estado "completado"
|
||||||
print(f"✓ Pedimento {pedimento_id} tiene todos sus {mensaje} descargados.")
|
print(f"✓ Pedimento {pedimento_id} tiene todos sus {mensaje} descargados.")
|
||||||
|
logger.info(f"✓ Pedimento {pedimento_id} tiene todos sus {mensaje} descargados.")
|
||||||
else:
|
else:
|
||||||
proceso = modificar_estado_procesamiento(pedimento, servicio_id=servicio, nuevo_estado=4) # Estado "en progreso"
|
proceso = modificar_estado_procesamiento(pedimento, servicio_id=servicio, nuevo_estado=4) # Estado "en progreso"
|
||||||
print(f"✗ Pedimento {pedimento_id} NO tiene todos sus {mensaje} descargados.")
|
print(f"✗ Pedimento {pedimento_id} NO tiene todos sus {mensaje} descargados.")
|
||||||
|
logger.info(f"✗ Pedimento {pedimento_id} NO tiene todos sus {mensaje} descargados.")
|
||||||
|
|
||||||
if proceso:
|
if proceso:
|
||||||
print(f"✓ Proceso de auditoría para pedimento {pedimento_id} completado.")
|
print(f"✓ Proceso de auditoría para pedimento {pedimento_id} completado.")
|
||||||
|
logger.info(f"✓ Proceso de auditoría para pedimento {pedimento_id} completado.")
|
||||||
else:
|
else:
|
||||||
print(f"✗ No se encontró proceso de auditoría para pedimento {pedimento_id}.")
|
print(f"✗ No se encontró proceso de auditoría para pedimento {pedimento_id}.")
|
||||||
|
logger.info(f"✗ No se encontró proceso de auditoría para pedimento {pedimento_id}.")
|
||||||
|
|
||||||
## Auditar pedimentos
|
## Auditar pedimentos
|
||||||
|
|
||||||
@@ -121,44 +131,66 @@ def auditar_procesamiento_remesa_por_pedimento(pedimento_id):
|
|||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def crear_partidas(organizacion_id):
|
def crear_partidas(organizacion_id):
|
||||||
|
from api.customs.models import Partida
|
||||||
|
|
||||||
pedimentos = obtener_pedimentos(organizacion_id)
|
pedimentos = obtener_pedimentos(organizacion_id)
|
||||||
total_pedimentos = pedimentos.count()
|
total_pedimentos = pedimentos.count()
|
||||||
pedimentos_procesados = 0
|
|
||||||
total_partidas_agregadas = 0
|
|
||||||
|
|
||||||
print(f"Iniciando procesamiento de {total_pedimentos} pedimentos para organización {organizacion_id}")
|
completados = []
|
||||||
|
con_pendientes = []
|
||||||
|
sin_datos = []
|
||||||
|
errores = []
|
||||||
|
|
||||||
for pedimento in pedimentos:
|
for pedimento in pedimentos:
|
||||||
pedimentos_procesados += 1
|
try:
|
||||||
partidas_agregadas_pedimento = 0
|
if not pedimento.numero_partidas or pedimento.numero_partidas <= 0:
|
||||||
|
sin_datos.append({
|
||||||
# Validar que numero_partidas no sea None y sea mayor que 0
|
'pedimento_id': str(pedimento.id),
|
||||||
if pedimento.numero_partidas is not None and pedimento.numero_partidas > 0:
|
'pedimento': pedimento.pedimento,
|
||||||
partidas_existentes = pedimento.partidas.count()
|
'razon': f'numero_partidas inválido ({pedimento.numero_partidas})',
|
||||||
if pedimento.numero_partidas > partidas_existentes:
|
})
|
||||||
print(f"Procesando pedimento {pedimento.id} ({pedimentos_procesados}/{total_pedimentos}) - Partidas existentes: {partidas_existentes}, Requeridas: {pedimento.numero_partidas}")
|
continue
|
||||||
|
|
||||||
for i in range(1, pedimento.numero_partidas + 1):
|
for i in range(1, pedimento.numero_partidas + 1):
|
||||||
from api.customs.models import Partida
|
Partida.objects.get_or_create(
|
||||||
partida, created = Partida.objects.get_or_create(
|
|
||||||
pedimento=pedimento,
|
pedimento=pedimento,
|
||||||
numero_partida=i,
|
numero_partida=i,
|
||||||
organizacion_id=organizacion_id
|
defaults={'organizacion_id': organizacion_id}
|
||||||
)
|
)
|
||||||
if created:
|
|
||||||
partidas_agregadas_pedimento += 1
|
|
||||||
total_partidas_agregadas += 1
|
|
||||||
|
|
||||||
print(f" → Partidas agregadas para pedimento {pedimento.id}: {partidas_agregadas_pedimento}")
|
partidas = list(pedimento.partidas.order_by('numero_partida'))
|
||||||
else:
|
no_descargadas = [p.numero_partida for p in partidas if not p.descargado]
|
||||||
print(f"Pedimento {pedimento.id} ya tiene todas sus partidas ({partidas_existentes}/{pedimento.numero_partidas})")
|
|
||||||
else:
|
|
||||||
print(f"Pedimento {pedimento.id} omitido - numero_partidas: {pedimento.numero_partidas} (inválido)")
|
|
||||||
|
|
||||||
print(f"\n=== RESUMEN ===")
|
if not no_descargadas:
|
||||||
print(f"Pedimentos procesados: {pedimentos_procesados}")
|
completados.append(str(pedimento.id))
|
||||||
print(f"Total de partidas agregadas: {total_partidas_agregadas}")
|
else:
|
||||||
print(f"Procesamiento completado para organización {organizacion_id}")
|
con_pendientes.append({
|
||||||
|
'pedimento_id': str(pedimento.id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
'total_partidas': len(partidas),
|
||||||
|
'descargadas': len(partidas) - len(no_descargadas),
|
||||||
|
'no_descargadas': no_descargadas,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errores.append({
|
||||||
|
'pedimento_id': str(pedimento.id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
'error': str(e),
|
||||||
|
})
|
||||||
|
logger.error(f"Error creando partidas para pedimento {pedimento.id}: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'organizacion_id': str(organizacion_id),
|
||||||
|
'total_pedimentos': total_pedimentos,
|
||||||
|
'completados': len(completados),
|
||||||
|
'con_pendientes': len(con_pendientes),
|
||||||
|
'sin_datos': len(sin_datos),
|
||||||
|
'con_errores': len(errores),
|
||||||
|
'detalle_pendientes': con_pendientes,
|
||||||
|
'detalle_sin_datos': sin_datos,
|
||||||
|
'detalle_errores': errores,
|
||||||
|
}
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def crear_partidas_por_pedimento(pedimento_id):
|
def crear_partidas_por_pedimento(pedimento_id):
|
||||||
@@ -169,6 +201,7 @@ def crear_partidas_por_pedimento(pedimento_id):
|
|||||||
return
|
return
|
||||||
|
|
||||||
print(f"Procesando pedimento individual {pedimento_id}...")
|
print(f"Procesando pedimento individual {pedimento_id}...")
|
||||||
|
logger.info(f"Procesando pedimento individual {pedimento_id}...")
|
||||||
partidas_agregadas = 0
|
partidas_agregadas = 0
|
||||||
|
|
||||||
# Validar que numero_partidas no sea None y sea mayor que 0
|
# Validar que numero_partidas no sea None y sea mayor que 0
|
||||||
@@ -176,6 +209,7 @@ def crear_partidas_por_pedimento(pedimento_id):
|
|||||||
partidas_existentes = pedimento.partidas.count()
|
partidas_existentes = pedimento.partidas.count()
|
||||||
if pedimento.numero_partidas > partidas_existentes:
|
if pedimento.numero_partidas > partidas_existentes:
|
||||||
print(f"Pedimento {pedimento_id} - Partidas existentes: {partidas_existentes}, Requeridas: {pedimento.numero_partidas}")
|
print(f"Pedimento {pedimento_id} - Partidas existentes: {partidas_existentes}, Requeridas: {pedimento.numero_partidas}")
|
||||||
|
logger.info(f"Pedimento {pedimento_id} - Partidas existentes: {partidas_existentes}, Requeridas: {pedimento.numero_partidas}")
|
||||||
|
|
||||||
for i in range(1, pedimento.numero_partidas + 1):
|
for i in range(1, pedimento.numero_partidas + 1):
|
||||||
from api.customs.models import Partida
|
from api.customs.models import Partida
|
||||||
@@ -188,62 +222,165 @@ def crear_partidas_por_pedimento(pedimento_id):
|
|||||||
partidas_agregadas += 1
|
partidas_agregadas += 1
|
||||||
|
|
||||||
print(f"✓ Partidas agregadas para pedimento {pedimento_id}: {partidas_agregadas}")
|
print(f"✓ Partidas agregadas para pedimento {pedimento_id}: {partidas_agregadas}")
|
||||||
|
logger.info(f"✓ Partidas agregadas para pedimento {pedimento_id}: {partidas_agregadas}")
|
||||||
else:
|
else:
|
||||||
print(f"Pedimento {pedimento_id} ya tiene todas sus partidas ({partidas_existentes}/{pedimento.numero_partidas})")
|
print(f"Pedimento {pedimento_id} ya tiene todas sus partidas ({partidas_existentes}/{pedimento.numero_partidas})")
|
||||||
|
logger.info(f"Pedimento {pedimento_id} ya tiene todas sus partidas ({partidas_existentes}/{pedimento.numero_partidas})")
|
||||||
else:
|
else:
|
||||||
print(f"Error: Pedimento {pedimento_id} tiene numero_partidas inválido: {pedimento.numero_partidas}")
|
print(f"Error: Pedimento {pedimento_id} tiene numero_partidas inválido: {pedimento.numero_partidas}")
|
||||||
|
logger.info(f"Error: Pedimento {pedimento_id} tiene numero_partidas inválido: {pedimento.numero_partidas}")
|
||||||
|
|
||||||
|
def _auditar_organizacion(organizacion_id, servicio, related_name, variable, label):
|
||||||
|
"""
|
||||||
|
Itera todos los pedimentos de una organización auditando el campo `variable`
|
||||||
|
en la relación `related_name`. Retorna un resumen estructurado por pedimento.
|
||||||
|
"""
|
||||||
|
pedimentos = obtener_pedimentos(organizacion_id)
|
||||||
|
total_pedimentos = pedimentos.count()
|
||||||
|
|
||||||
|
completados = []
|
||||||
|
pendientes = []
|
||||||
|
errores = []
|
||||||
|
|
||||||
|
for pedimento in pedimentos:
|
||||||
|
try:
|
||||||
|
docs = list(getattr(pedimento, related_name).all())
|
||||||
|
total = len(docs)
|
||||||
|
faltantes = [
|
||||||
|
getattr(doc, 'numero_cove', None) or getattr(doc, 'numero_edocument', None)
|
||||||
|
for doc in docs if not getattr(doc, variable)
|
||||||
|
]
|
||||||
|
|
||||||
|
if total == 0 or len(faltantes) == 0:
|
||||||
|
nuevo_estado = 3
|
||||||
|
completados.append(str(pedimento.id))
|
||||||
|
else:
|
||||||
|
nuevo_estado = 4
|
||||||
|
pendientes.append({
|
||||||
|
'pedimento_id': str(pedimento.id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
f'faltantes_{label}': faltantes,
|
||||||
|
'total': total,
|
||||||
|
'descargados': total - len(faltantes),
|
||||||
|
})
|
||||||
|
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=servicio, nuevo_estado=nuevo_estado)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errores.append({
|
||||||
|
'pedimento_id': str(pedimento.id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
'error': str(e),
|
||||||
|
})
|
||||||
|
logger.error(f"Error auditando pedimento {pedimento.id} [{label}]: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'organizacion_id': str(organizacion_id),
|
||||||
|
'auditoria': label,
|
||||||
|
'total_pedimentos': total_pedimentos,
|
||||||
|
'completados': len(completados),
|
||||||
|
'con_pendientes': len(pendientes),
|
||||||
|
'con_errores': len(errores),
|
||||||
|
'detalle_pendientes': pendientes,
|
||||||
|
'detalle_errores': errores,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Auditar coves
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def auditar_coves(organizacion_id):
|
def auditar_coves(organizacion_id):
|
||||||
for pedimento in obtener_pedimentos(organizacion_id):
|
return _auditar_organizacion(
|
||||||
auditor_descargas(
|
organizacion_id,
|
||||||
pedimento,
|
|
||||||
servicio=8,
|
servicio=8,
|
||||||
related_name='coves',
|
related_name='coves',
|
||||||
variable='cove_descargado',
|
variable='cove_descargado',
|
||||||
mensaje='COVE'
|
label='cove',
|
||||||
)
|
)
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def auditar_acuse_cove(organizacion_id):
|
def auditar_acuse_cove(organizacion_id):
|
||||||
for pedimento in obtener_pedimentos(organizacion_id):
|
return _auditar_organizacion(
|
||||||
auditor_descargas(
|
organizacion_id,
|
||||||
pedimento,
|
|
||||||
servicio=9,
|
servicio=9,
|
||||||
related_name='coves',
|
related_name='coves',
|
||||||
variable='acuse_cove_descargado',
|
variable='acuse_cove_descargado',
|
||||||
mensaje='acuse de COVE'
|
label='acuse_cove',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Revisa si el pedimento completo todos sus acuse coves
|
|
||||||
|
|
||||||
# Auditar edocuments
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def auditar_edocuments(organizacion_id):
|
def auditar_edocuments(organizacion_id):
|
||||||
for pedimento in obtener_pedimentos(organizacion_id):
|
return _auditar_organizacion(
|
||||||
auditor_descargas(
|
organizacion_id,
|
||||||
pedimento,
|
|
||||||
servicio=7,
|
servicio=7,
|
||||||
related_name='documentos',
|
related_name='documentos',
|
||||||
variable='edocument_descargado',
|
variable='edocument_descargado',
|
||||||
mensaje='EDocument'
|
label='edocument',
|
||||||
)
|
)
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def auditar_acuse(organizacion_id):
|
def auditar_acuse(organizacion_id):
|
||||||
for pedimento in obtener_pedimentos(organizacion_id):
|
return _auditar_organizacion(
|
||||||
auditor_descargas(
|
organizacion_id,
|
||||||
pedimento,
|
|
||||||
servicio=6,
|
servicio=6,
|
||||||
related_name='documentos',
|
related_name='documentos',
|
||||||
variable='acuse_descargado',
|
variable='acuse_descargado',
|
||||||
mensaje='acuse'
|
label='acuse',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def auditar_remesas(organizacion_id):
|
||||||
|
"""
|
||||||
|
Audita el estado de descarga de remesas para todos los pedimentos de una organización.
|
||||||
|
A diferencia de coves/edocuments, las remesas no tienen campo booleano propio —
|
||||||
|
se verifica la existencia de un documento de tipo 3 (Remesa) en el pedimento.
|
||||||
|
"""
|
||||||
|
pedimentos = obtener_pedimentos(organizacion_id)
|
||||||
|
total_pedimentos = pedimentos.count()
|
||||||
|
|
||||||
|
completados = []
|
||||||
|
pendientes = []
|
||||||
|
errores = []
|
||||||
|
|
||||||
|
for pedimento in pedimentos:
|
||||||
|
try:
|
||||||
|
if not pedimento.remesas:
|
||||||
|
# El pedimento no declara remesas — no aplica, marcar como completado
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=5, nuevo_estado=3)
|
||||||
|
completados.append(str(pedimento.id))
|
||||||
|
elif pedimento.documents.filter(document_type=3).exists():
|
||||||
|
# Documento de remesa ya descargado
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=5, nuevo_estado=3)
|
||||||
|
completados.append(str(pedimento.id))
|
||||||
|
else:
|
||||||
|
# Tiene remesas declaradas pero el documento aún no existe
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=5, nuevo_estado=4)
|
||||||
|
pendientes.append({
|
||||||
|
'pedimento_id': str(pedimento.id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
errores.append({
|
||||||
|
'pedimento_id': str(pedimento.id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
'error': str(e),
|
||||||
|
})
|
||||||
|
logger.error(f"Error auditando remesa de pedimento {pedimento.id}: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'organizacion_id': str(organizacion_id),
|
||||||
|
'auditoria': 'remesa',
|
||||||
|
'total_pedimentos': total_pedimentos,
|
||||||
|
'completados': len(completados),
|
||||||
|
'con_pendientes': len(pendientes),
|
||||||
|
'con_errores': len(errores),
|
||||||
|
'detalle_pendientes': pendientes,
|
||||||
|
'detalle_errores': errores,
|
||||||
|
}
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def auditar_cove_por_pedimento(pedimento_id):
|
def auditar_cove_por_pedimento(pedimento_id):
|
||||||
try:
|
try:
|
||||||
|
print(f"auditar_cove_por_pedimento >>>> {pedimento_id}")
|
||||||
|
logger.info(f"auditar_cove_por_pedimento >>>> {pedimento_id}")
|
||||||
from api.customs.models import Pedimento
|
from api.customs.models import Pedimento
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
pedimento = Pedimento.objects.get(id=pedimento_id)
|
||||||
auditor_descargas(
|
auditor_descargas(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# auditoria_xml.py
|
# auditoria_xml.py
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger('api.customs.auditoria_xml')
|
||||||
|
|
||||||
def extraer_info_pedimento_xml(xml_content):
|
def extraer_info_pedimento_xml(xml_content):
|
||||||
"""
|
"""
|
||||||
@@ -13,8 +15,10 @@ def extraer_info_pedimento_xml(xml_content):
|
|||||||
# Buscar el namespace (puede variar)
|
# Buscar el namespace (puede variar)
|
||||||
namespaces = {
|
namespaces = {
|
||||||
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
|
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||||
|
's': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||||
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
|
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
|
||||||
'ns3': 'http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta'
|
'ns3': 'http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta',
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resultado = {}
|
resultado = {}
|
||||||
@@ -181,10 +185,37 @@ def extraer_info_pedimento_xml(xml_content):
|
|||||||
if edocs_encontrados:
|
if edocs_encontrados:
|
||||||
resultado['edocuments_en_xml'] = edocs_encontrados
|
resultado['edocuments_en_xml'] = edocs_encontrados
|
||||||
|
|
||||||
# Verificar si hay error en la respuesta
|
# Verificar si hay error en la respuesta — 3 variantes según el servicio VUCEM:
|
||||||
|
# 1) Remesas/pedimentos: <ns3:tieneError> en namespace oxml/respuesta
|
||||||
|
# 2) eDocuments: <TieneError> en namespace tempuri.org, mensaje en <Errores>
|
||||||
|
# 3) Acuses: <error> sin namespace dentro de responseConsultaAcuses
|
||||||
tiene_error = root.find('.//ns3:tieneError', namespaces)
|
tiene_error = root.find('.//ns3:tieneError', namespaces)
|
||||||
if tiene_error is not None:
|
if tiene_error is not None:
|
||||||
resultado['tiene_error'] = tiene_error.text.lower() == 'true'
|
resultado['tiene_error'] = tiene_error.text.lower() == 'true'
|
||||||
|
if resultado['tiene_error']:
|
||||||
|
mensaje = root.find('.//ns3:error/ns3:mensaje', namespaces)
|
||||||
|
if mensaje is not None and mensaje.text:
|
||||||
|
resultado['error_mensaje'] = mensaje.text.strip()
|
||||||
|
else:
|
||||||
|
# Variante eDocuments (tempuri.org)
|
||||||
|
tiene_error_edoc = root.find('.//{http://tempuri.org/}TieneError')
|
||||||
|
if tiene_error_edoc is not None:
|
||||||
|
resultado['tiene_error'] = tiene_error_edoc.text.lower() == 'true'
|
||||||
|
if resultado['tiene_error']:
|
||||||
|
errores_elem = root.find('.//{http://tempuri.org/}Errores')
|
||||||
|
if errores_elem is not None and errores_elem.text:
|
||||||
|
resultado['error_mensaje'] = errores_elem.text.strip()
|
||||||
|
else:
|
||||||
|
# Variante acuses: <error> sin namespace
|
||||||
|
error_acuses = root.find('.//error')
|
||||||
|
if error_acuses is not None and error_acuses.text is not None:
|
||||||
|
resultado['tiene_error'] = error_acuses.text.lower() == 'true'
|
||||||
|
if resultado['tiene_error']:
|
||||||
|
descripciones = root.findall('.//mensajeErrores/descripcion')
|
||||||
|
if descripciones:
|
||||||
|
resultado['error_mensaje'] = ' | '.join(
|
||||||
|
d.text.strip() for d in descripciones if d.text
|
||||||
|
)
|
||||||
|
|
||||||
return resultado
|
return resultado
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from api.organization.models import Organizacion
|
||||||
from celery import group
|
from celery import group
|
||||||
from celery import shared_task, group
|
from celery import shared_task, group
|
||||||
from api.customs.models import *
|
from api.customs.models import *
|
||||||
@@ -8,6 +9,11 @@ import requests
|
|||||||
from config.settings import SERVICE_API_URL_V2
|
from config.settings import SERVICE_API_URL_V2
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
# este solo fue para pruebas personales, lo dejo por si en un futuro lo requiero
|
||||||
|
TEST_ORG_ID = uuid.UUID('defc7848-4f39-4d67-9dba-5bb445248d23')
|
||||||
|
logger = logging.getLogger('api.customs.microservice_v2')
|
||||||
|
|
||||||
def credenciales_to_dict(credenciales):
|
def credenciales_to_dict(credenciales):
|
||||||
if not credenciales:
|
if not credenciales:
|
||||||
@@ -132,7 +138,7 @@ def procesar_edocs_pedimento(pedimento_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{SERVICE_API_URL_V2}/services/download/edoc/",
|
f"{SERVICE_API_URL_V2}/services/download/all/edocs/",
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
headers={"Content-Type": "application/json"}
|
headers={"Content-Type": "application/json"}
|
||||||
)
|
)
|
||||||
@@ -277,10 +283,23 @@ def procesar_remesas(organizacion_id):
|
|||||||
pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id)
|
pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id)
|
||||||
|
|
||||||
for pedimento in pedimentos:
|
for pedimento in pedimentos:
|
||||||
if not pedimento.documents.filter(document_type=3).exists(): # Tipo 3: Remesa
|
logger.info(f"pedimento >>>> {pedimento}")
|
||||||
# Convertir el pedimento a JSON usando el serializer
|
try:
|
||||||
|
# if pedimento.documents.filter(document_type=3).exists(): # Remesa ya descargada
|
||||||
|
# logger.info(f"Pedimento {pedimento.pedimento} ya tiene remesa descargada, omitiendo.")
|
||||||
|
# continue
|
||||||
|
|
||||||
pedimento_dict = pedimento_to_dict(pedimento)
|
pedimento_dict = pedimento_to_dict(pedimento)
|
||||||
credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first()
|
|
||||||
|
credencial_importador = CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first()
|
||||||
|
if not credencial_importador:
|
||||||
|
logger.warning(f"Sin credenciales para RFC {pedimento.contribuyente} (pedimento {pedimento.pedimento}), omitiendo.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
credenciales = Vucem.objects.filter(id=credencial_importador.vucem.id).first()
|
||||||
|
if not credenciales:
|
||||||
|
logger.warning(f"Credencial Vucem no encontrada para pedimento {pedimento.pedimento}, omitiendo.")
|
||||||
|
continue
|
||||||
|
|
||||||
credenciales_dict = credenciales_to_dict(credenciales)
|
credenciales_dict = credenciales_to_dict(credenciales)
|
||||||
|
|
||||||
@@ -289,15 +308,15 @@ def procesar_remesas(organizacion_id):
|
|||||||
"credencial": credenciales_dict
|
"credencial": credenciales_dict
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{SERVICE_API_URL_V2}/services/remesas",
|
f"{SERVICE_API_URL_V2}/services/remesas/",
|
||||||
data=json.dumps(payload),
|
data=json.dumps(payload),
|
||||||
headers={"Content-Type": "application/json"}
|
headers={"Content-Type": "application/json"}
|
||||||
)
|
)
|
||||||
# Aquí puedes continuar con el resto de tu lógica
|
logger.info(f"Servicio enviado para pedimento {pedimento.pedimento} — status {response.status_code}")
|
||||||
|
|
||||||
print(f"Servicio enviado para pedimento {pedimento.pedimento}")
|
except Exception as e:
|
||||||
|
logger.error(f"Error procesando remesa para pedimento {pedimento.pedimento}: {e}", exc_info=True)
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def procesar_coves(organizacion_id):
|
def procesar_coves(organizacion_id):
|
||||||
@@ -522,6 +541,34 @@ def ejecutar_todos_por_organizacion(organizacion_id):
|
|||||||
procesar_pedimentos_completos.delay(organizacion_id)
|
procesar_pedimentos_completos.delay(organizacion_id)
|
||||||
procesar_remesas.delay(organizacion_id)
|
procesar_remesas.delay(organizacion_id)
|
||||||
|
|
||||||
|
def ejecutar_basicos_organizacion(organizacion_id):
|
||||||
|
# solo coves y e documents, si es necesario ya en un futuro se agregan los de partidas, pedimento completo y esas madres
|
||||||
|
procesar_coves.delay(organizacion_id)
|
||||||
|
procesar_acuse_coves.delay(organizacion_id)
|
||||||
|
procesar_edocs.delay(organizacion_id)
|
||||||
|
procesar_acuses.delay(organizacion_id)
|
||||||
|
# procesar_partidas.delay(organizacion_id)
|
||||||
|
# procesar_pedimentos_completos.delay(organizacion_id)
|
||||||
|
# procesar_remesas.delay(organizacion_id)
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def process_organization_batch(org_id):
|
||||||
|
"""
|
||||||
|
Procesa todos los tipos de documentos pendientes para una organización.
|
||||||
|
"""
|
||||||
|
ejecutar_basicos_organizacion(org_id)
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def process_all_organizations():
|
||||||
|
"""
|
||||||
|
Envía una tarea por organización activa a la cola org_processing.
|
||||||
|
"""
|
||||||
|
active_orgs = Organizacion.objects.filter(is_active=True, is_verified=True)
|
||||||
|
|
||||||
|
for org in active_orgs:
|
||||||
|
process_organization_batch.apply_async(
|
||||||
|
args=[org.id],
|
||||||
|
queue='org_processing'
|
||||||
|
)
|
||||||
|
return f"Dispatched {active_orgs.count()} organizations"
|
||||||
|
|
||||||
|
|||||||
@@ -8,27 +8,24 @@ from drf_yasg import openapi
|
|||||||
from core.permissions import IsSuperUser, IsSameOrganizationDeveloper
|
from core.permissions import IsSuperUser, IsSameOrganizationDeveloper
|
||||||
from .tasks.auditoria import (
|
from .tasks.auditoria import (
|
||||||
crear_partidas,
|
crear_partidas,
|
||||||
crear_partidas_por_pedimento,
|
|
||||||
auditar_procesamiento_remesa_por_pedimento,
|
|
||||||
auditar_coves,
|
auditar_coves,
|
||||||
auditar_acuse_cove,
|
auditar_acuse_cove,
|
||||||
auditar_edocuments,
|
auditar_edocuments,
|
||||||
auditar_acuse,
|
auditar_acuse,
|
||||||
auditar_cove_por_pedimento,
|
auditar_remesas,
|
||||||
auditar_acuse_cove_por_pedimento,
|
|
||||||
auditar_edocument_por_pedimento,
|
|
||||||
auditar_acuse_por_pedimento
|
|
||||||
)
|
)
|
||||||
from .tasks.internal_services import auditar_pedimentos
|
from .tasks.internal_services import auditar_pedimentos
|
||||||
from .tasks.microservice_v2 import procesar_pedimentos_completos
|
from .tasks.microservice_v2 import procesar_pedimentos_completos
|
||||||
from api.customs.models import Pedimento
|
from api.customs.models import Pedimento
|
||||||
from api.organization.models import Organizacion
|
from api.organization.models import Organizacion
|
||||||
from api.record.models import Document
|
from api.record.models import Document
|
||||||
from .tasks.auditoria import auditar_pedimento_por_id
|
|
||||||
from .tasks.auditoria_xml import extraer_info_pedimento_xml
|
from .tasks.auditoria_xml import extraer_info_pedimento_xml
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
from api.utils.storage_service import storage_service
|
from api.utils.storage_service import storage_service
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
logger = logging.getLogger('api.customs.views_auditor')
|
||||||
|
|
||||||
def get_document_content(documento):
|
def get_document_content(documento):
|
||||||
"""
|
"""
|
||||||
@@ -72,7 +69,7 @@ def get_document_path(documento):
|
|||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Crea partidas para todos los pedimentos de una organización",
|
operation_description="Crea partidas faltantes para todos los pedimentos de una organización e informa cuáles están descargadas",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={
|
||||||
@@ -81,7 +78,7 @@ def get_document_path(documento):
|
|||||||
required=['organizacion_id']
|
required=['organizacion_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea iniciada correctamente'),
|
202: openapi.Response('Tarea iniciada — usar task_id para consultar resultado'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes')
|
403: openapi.Response('No tiene permisos suficientes')
|
||||||
}
|
}
|
||||||
@@ -89,37 +86,27 @@ def get_document_path(documento):
|
|||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
||||||
def crear_partidas_organizacion(request):
|
def crear_partidas_organizacion(request):
|
||||||
"""
|
|
||||||
Crea partidas para todos los pedimentos de una organización específica.
|
|
||||||
"""
|
|
||||||
organizacion_id = request.data.get('organizacion_id')
|
organizacion_id = request.data.get('organizacion_id')
|
||||||
|
|
||||||
if not organizacion_id:
|
if not organizacion_id:
|
||||||
return Response(
|
return Response({'error': 'Debe proporcionar organizacion_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{'error': 'Debe proporcionar organizacion_id'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validar permisos
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
|
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
|
||||||
return Response(
|
return Response({'error': 'No tiene permisos para esta organización'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
{'error': 'No tiene permisos para esta organización'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ejecutar la tarea
|
|
||||||
task = crear_partidas.delay(organizacion_id)
|
task = crear_partidas.delay(organizacion_id)
|
||||||
message = f"Creación de partidas iniciada para la organización {organizacion_id}"
|
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'message': message,
|
'organizacion_id': organizacion_id,
|
||||||
'task_id': task.id
|
'auditoria': 'partidas',
|
||||||
}, status=status.HTTP_200_OK)
|
'task_id': task.id,
|
||||||
|
'mensaje': f'Creación de partidas iniciada. Consulta el resultado en GET /api/tasks/status/{task.id}/',
|
||||||
|
}, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Crea partidas para un pedimento específico",
|
operation_description="Crea partidas faltantes para un pedimento e informa cuáles están descargadas",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={
|
||||||
@@ -128,48 +115,74 @@ def crear_partidas_organizacion(request):
|
|||||||
required=['pedimento_id']
|
required=['pedimento_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea iniciada correctamente'),
|
200: openapi.Response('Resultado de creación y estado de descarga de partidas'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes'),
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
404: openapi.Response('Pedimento no encontrado')
|
404: openapi.Response('Pedimento no encontrado')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated ])
|
@permission_classes([IsAuthenticated])
|
||||||
def crear_partidas_pedimento(request):
|
def crear_partidas_pedimento(request):
|
||||||
"""
|
|
||||||
Crea partidas para un pedimento específico.
|
|
||||||
"""
|
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
|
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
return Response(
|
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{'error': 'Debe proporcionar pedimento_id'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validar permisos y existencia del pedimento
|
|
||||||
try:
|
try:
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
pedimento = Pedimento.objects.prefetch_related('partidas').select_related('organizacion').get(id=pedimento_id)
|
||||||
|
except Pedimento.DoesNotExist:
|
||||||
|
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
||||||
return Response(
|
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
{'error': 'No tiene permisos para este pedimento'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
except Pedimento.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Pedimento no encontrado'},
|
|
||||||
status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ejecutar la tarea
|
if not pedimento.numero_partidas or pedimento.numero_partidas <= 0:
|
||||||
task = crear_partidas_por_pedimento.delay(pedimento_id)
|
return Response({
|
||||||
message = f"Creación de partidas iniciada para el pedimento {pedimento_id}"
|
'pedimento_id': str(pedimento_id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
'estado': 'sin_datos',
|
||||||
|
'mensaje': f'El pedimento no tiene número de partidas definido (numero_partidas={pedimento.numero_partidas})',
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# Crear partidas faltantes (get_or_create por número)
|
||||||
|
from api.customs.models import Partida
|
||||||
|
partidas_creadas = 0
|
||||||
|
for i in range(1, pedimento.numero_partidas + 1):
|
||||||
|
_, created = Partida.objects.get_or_create(
|
||||||
|
pedimento=pedimento,
|
||||||
|
numero_partida=i,
|
||||||
|
defaults={'organizacion_id': pedimento.organizacion_id}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
partidas_creadas += 1
|
||||||
|
|
||||||
|
# Evaluar estado de descarga sobre el conjunto completo
|
||||||
|
partidas = list(pedimento.partidas.order_by('numero_partida'))
|
||||||
|
total = len(partidas)
|
||||||
|
descargadas = [p.numero_partida for p in partidas if p.descargado]
|
||||||
|
no_descargadas = [p.numero_partida for p in partidas if not p.descargado]
|
||||||
|
|
||||||
|
if not no_descargadas:
|
||||||
|
estado = 'completado'
|
||||||
|
mensaje = f'Todas las partidas están descargadas ({total}/{total})'
|
||||||
|
else:
|
||||||
|
estado = 'en_proceso'
|
||||||
|
mensaje = f'{len(no_descargadas)} de {total} partidas pendientes de descarga'
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'message': message,
|
'pedimento_id': str(pedimento_id),
|
||||||
'task_id': task.id
|
'pedimento': pedimento.pedimento,
|
||||||
|
'estado': estado,
|
||||||
|
'mensaje': mensaje,
|
||||||
|
'resumen': {
|
||||||
|
'total_partidas': total,
|
||||||
|
'partidas_creadas_ahora': partidas_creadas,
|
||||||
|
'descargadas': len(descargadas),
|
||||||
|
'no_descargadas': len(no_descargadas),
|
||||||
|
},
|
||||||
|
'no_descargadas': no_descargadas,
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
@@ -223,7 +236,7 @@ def auditar_pedimentos_endpoint(request):
|
|||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita el procesamiento de remesa para un pedimento específico",
|
operation_description="Audita el estado de procesamiento de remesa de un pedimento específico",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={
|
||||||
@@ -232,7 +245,7 @@ def auditar_pedimentos_endpoint(request):
|
|||||||
required=['pedimento_id']
|
required=['pedimento_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
200: openapi.Response('Estado de procesamiento de remesa del pedimento'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes'),
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
404: openapi.Response('Pedimento no encontrado')
|
404: openapi.Response('Pedimento no encontrado')
|
||||||
@@ -241,247 +254,179 @@ def auditar_pedimentos_endpoint(request):
|
|||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
||||||
def auditar_procesamiento_remesa_pedimento_endpoint(request):
|
def auditar_procesamiento_remesa_pedimento_endpoint(request):
|
||||||
"""
|
|
||||||
Inicia una tarea de auditoría de remesa para un pedimento específico.
|
|
||||||
Verifica el estado del procesamiento de remesa y la creación de COVEs.
|
|
||||||
"""
|
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
|
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
return Response(
|
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
{'error': 'Debe proporcionar pedimento_id'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validar permisos y existencia del pedimento
|
|
||||||
try:
|
try:
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
pedimento = Pedimento.objects.select_related('organizacion').prefetch_related('coves').get(id=pedimento_id)
|
||||||
|
except Pedimento.DoesNotExist:
|
||||||
|
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
||||||
return Response(
|
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
{'error': 'No tiene permisos para este pedimento'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
except Pedimento.DoesNotExist:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Pedimento no encontrado'},
|
|
||||||
status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ejecutar la tarea de auditoría
|
if not pedimento.remesas:
|
||||||
task = auditar_procesamiento_remesa_por_pedimento.delay(pedimento_id)
|
return Response({
|
||||||
message = f"Auditoría de remesa iniciada para el pedimento {pedimento_id}"
|
'pedimento_id': str(pedimento_id),
|
||||||
|
'pedimento': pedimento.pedimento,
|
||||||
|
'tiene_remesas': False,
|
||||||
|
'estado': 'completado',
|
||||||
|
'mensaje': 'El pedimento no tiene remesas para procesar',
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
tiene_documento_remesa = pedimento.documents.filter(document_type=3).exists()
|
||||||
|
coves = list(pedimento.coves.all())
|
||||||
|
total_coves = len(coves)
|
||||||
|
|
||||||
|
if not tiene_documento_remesa:
|
||||||
|
estado = 'en_proceso'
|
||||||
|
mensaje = 'Documento XML de remesa aún no descargado'
|
||||||
|
elif total_coves == 0:
|
||||||
|
estado = 'en_proceso'
|
||||||
|
mensaje = 'Documento de remesa disponible pero no se han creado COVEs'
|
||||||
|
else:
|
||||||
|
estado = 'completado'
|
||||||
|
mensaje = f'Remesa procesada — {total_coves} COVE(s) registrados'
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'message': message,
|
'pedimento_id': str(pedimento_id),
|
||||||
'task_id': task.id,
|
|
||||||
'pedimento': {
|
|
||||||
'id': str(pedimento.id),
|
|
||||||
'pedimento': pedimento.pedimento,
|
'pedimento': pedimento.pedimento,
|
||||||
'tiene_remesas': pedimento.remesas
|
'tiene_remesas': True,
|
||||||
}
|
'estado': estado,
|
||||||
|
'mensaje': mensaje,
|
||||||
|
'resumen': {
|
||||||
|
'tiene_documento_remesa': tiene_documento_remesa,
|
||||||
|
'total_coves_registrados': total_coves,
|
||||||
|
},
|
||||||
|
'coves': [c.numero_cove for c in coves],
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def _lanzar_auditoria_organizacion(request, task_fn, label):
|
||||||
|
"""Helper compartido para los 4 endpoints de auditoría masiva por organización."""
|
||||||
|
organizacion_id = request.data.get('organizacion_id')
|
||||||
|
if not organizacion_id:
|
||||||
|
return Response({'error': 'Debe proporcionar organizacion_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
user = request.user
|
||||||
|
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
|
||||||
|
return Response({'error': 'No tiene permisos para esta organización'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
task = task_fn.delay(organizacion_id)
|
||||||
|
return Response({
|
||||||
|
'organizacion_id': organizacion_id,
|
||||||
|
'auditoria': label,
|
||||||
|
'task_id': task.id,
|
||||||
|
'mensaje': f'Auditoría de {label} iniciada. Consulta el resultado en GET /api/tasks/status/{task.id}/',
|
||||||
|
}, status=status.HTTP_202_ACCEPTED)
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita los COVEs de una organización",
|
operation_description="Audita el estado de descarga de COVEs de todos los pedimentos de una organización",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING)},
|
||||||
'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la organización')
|
|
||||||
},
|
|
||||||
required=['organizacion_id']
|
required=['organizacion_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
202: openapi.Response('Tarea iniciada — usar task_id para consultar resultado'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes')
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
||||||
def auditar_coves_endpoint(request):
|
def auditar_coves_endpoint(request):
|
||||||
"""
|
return _lanzar_auditoria_organizacion(request, auditar_coves, 'COVEs')
|
||||||
Inicia una tarea de auditoría para los COVEs de una organización.
|
|
||||||
Verifica la existencia y validez de los COVEs generados.
|
|
||||||
"""
|
|
||||||
organizacion_id = request.data.get('organizacion_id')
|
|
||||||
|
|
||||||
if not organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Debe proporcionar organizacion_id'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validar permisos
|
|
||||||
user = request.user
|
|
||||||
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'No tiene permisos para esta organización'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ejecutar la tarea de auditoría
|
|
||||||
task = auditar_coves.delay(organizacion_id)
|
|
||||||
message = f"Auditoría de COVEs iniciada para la organización {organizacion_id}"
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'message': message,
|
|
||||||
'task_id': task.id
|
|
||||||
}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita los acuses de COVE de una organización",
|
operation_description="Audita el estado de descarga de acuses de COVE de todos los pedimentos de una organización",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING)},
|
||||||
'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la organización')
|
|
||||||
},
|
|
||||||
required=['organizacion_id']
|
required=['organizacion_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
202: openapi.Response('Tarea iniciada — usar task_id para consultar resultado'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes')
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
||||||
def auditar_acuse_cove_endpoint(request):
|
def auditar_acuse_cove_endpoint(request):
|
||||||
"""
|
return _lanzar_auditoria_organizacion(request, auditar_acuse_cove, 'acuses de COVE')
|
||||||
Inicia una tarea de auditoría para los acuses de COVE de una organización.
|
|
||||||
Verifica la recepción y validez de los acuses de COVE.
|
|
||||||
"""
|
|
||||||
organizacion_id = request.data.get('organizacion_id')
|
|
||||||
|
|
||||||
if not organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Debe proporcionar organizacion_id'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validar permisos
|
|
||||||
user = request.user
|
|
||||||
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'No tiene permisos para esta organización'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ejecutar la tarea de auditoría
|
|
||||||
task = auditar_acuse_cove.delay(organizacion_id)
|
|
||||||
message = f"Auditoría de acuses de COVE iniciada para la organización {organizacion_id}"
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'message': message,
|
|
||||||
'task_id': task.id
|
|
||||||
}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita los EDocuments de una organización",
|
operation_description="Audita el estado de descarga de EDocuments de todos los pedimentos de una organización",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING)},
|
||||||
'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la organización')
|
|
||||||
},
|
|
||||||
required=['organizacion_id']
|
required=['organizacion_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
202: openapi.Response('Tarea iniciada — usar task_id para consultar resultado'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes')
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
||||||
def auditar_edocuments_endpoint(request):
|
def auditar_edocuments_endpoint(request):
|
||||||
"""
|
return _lanzar_auditoria_organizacion(request, auditar_edocuments, 'EDocuments')
|
||||||
Inicia una tarea de auditoría para los EDocuments de una organización.
|
|
||||||
Verifica la existencia y validez de los EDocuments generados.
|
|
||||||
"""
|
|
||||||
organizacion_id = request.data.get('organizacion_id')
|
|
||||||
|
|
||||||
if not organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Debe proporcionar organizacion_id'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validar permisos
|
|
||||||
user = request.user
|
|
||||||
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'No tiene permisos para esta organización'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ejecutar la tarea de auditoría
|
|
||||||
task = auditar_edocuments.delay(organizacion_id)
|
|
||||||
message = f"Auditoría de EDocuments iniciada para la organización {organizacion_id}"
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'message': message,
|
|
||||||
'task_id': task.id
|
|
||||||
}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita los acuses de una organización",
|
operation_description="Audita el estado de descarga de acuses de EDocument de todos los pedimentos de una organización",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING)},
|
||||||
'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING, description='ID de la organización')
|
|
||||||
},
|
|
||||||
required=['organizacion_id']
|
required=['organizacion_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
202: openapi.Response('Tarea iniciada — usar task_id para consultar resultado'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes')
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
||||||
def auditar_acuse_endpoint(request):
|
def auditar_acuse_endpoint(request):
|
||||||
"""
|
return _lanzar_auditoria_organizacion(request, auditar_acuse, 'acuses de EDocument')
|
||||||
Inicia una tarea de auditoría para los acuses de una organización.
|
|
||||||
Verifica la recepción y validez de los acuses.
|
|
||||||
"""
|
|
||||||
organizacion_id = request.data.get('organizacion_id')
|
|
||||||
|
|
||||||
if not organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'Debe proporcionar organizacion_id'},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validar permisos
|
|
||||||
user = request.user
|
|
||||||
if not user.is_superuser and str(user.organizacion.id) != organizacion_id:
|
|
||||||
return Response(
|
|
||||||
{'error': 'No tiene permisos para esta organización'},
|
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ejecutar la tarea de auditoría
|
|
||||||
task = auditar_acuse.delay(organizacion_id)
|
|
||||||
message = f"Auditoría de acuses iniciada para la organización {organizacion_id}"
|
|
||||||
|
|
||||||
return Response({
|
|
||||||
'message': message,
|
|
||||||
'task_id': task.id
|
|
||||||
}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita el COVE de un pedimento específico",
|
operation_description="Audita el estado de descarga de remesas de todos los pedimentos de una organización",
|
||||||
|
request_body=openapi.Schema(
|
||||||
|
type=openapi.TYPE_OBJECT,
|
||||||
|
properties={'organizacion_id': openapi.Schema(type=openapi.TYPE_STRING)},
|
||||||
|
required=['organizacion_id']
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
202: openapi.Response('Tarea iniciada — usar task_id para consultar resultado'),
|
||||||
|
400: openapi.Response('Error en los parámetros'),
|
||||||
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api_view(['POST'])
|
||||||
|
@permission_classes([IsAuthenticated & (IsSuperUser | IsSameOrganizationDeveloper)])
|
||||||
|
def auditar_remesas_endpoint(request):
|
||||||
|
return _lanzar_auditoria_organizacion(request, auditar_remesas, 'remesas')
|
||||||
|
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
method='post',
|
||||||
|
operation_description="Audita el estado de descarga de COVEs de un pedimento específico",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={
|
||||||
@@ -490,7 +435,7 @@ def auditar_acuse_endpoint(request):
|
|||||||
required=['pedimento_id']
|
required=['pedimento_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
200: openapi.Response('Estado de descarga de COVEs del pedimento'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes'),
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
404: openapi.Response('Pedimento no encontrado')
|
404: openapi.Response('Pedimento no encontrado')
|
||||||
@@ -502,19 +447,48 @@ def auditar_cove_pedimento_endpoint(request):
|
|||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
pedimento = Pedimento.objects.select_related('organizacion').prefetch_related('coves').get(id=pedimento_id)
|
||||||
|
except Pedimento.DoesNotExist:
|
||||||
|
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
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)
|
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
except Pedimento.DoesNotExist:
|
|
||||||
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
coves = list(pedimento.coves.all())
|
||||||
task = auditar_cove_por_pedimento.delay(pedimento_id)
|
total = len(coves)
|
||||||
return Response({'message': f'Auditoría de COVE iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
|
descargados = sum(1 for c in coves if c.cove_descargado)
|
||||||
|
pendientes = [c.numero_cove for c in coves if not c.cove_descargado]
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'El pedimento no tiene COVEs registrados'
|
||||||
|
elif descargados == total:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'Todos los COVEs están descargados'
|
||||||
|
else:
|
||||||
|
nuevo_estado = 4
|
||||||
|
mensaje = f'{total - descargados} de {total} COVEs pendientes de descarga'
|
||||||
|
|
||||||
|
from api.customs.tasks.auditoria import modificar_estado_procesamiento
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=8, nuevo_estado=nuevo_estado)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'pedimento_id': str(pedimento_id),
|
||||||
|
'estado': 'completado' if nuevo_estado == 3 else 'en_proceso',
|
||||||
|
'mensaje': mensaje,
|
||||||
|
'resumen': {
|
||||||
|
'total_coves': total,
|
||||||
|
'coves_descargados': descargados,
|
||||||
|
},
|
||||||
|
'pendientes': pendientes,
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita el acuse de COVE de un pedimento específico",
|
operation_description="Audita el estado de descarga de acuses de COVE de un pedimento específico",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={
|
||||||
@@ -523,7 +497,7 @@ def auditar_cove_pedimento_endpoint(request):
|
|||||||
required=['pedimento_id']
|
required=['pedimento_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
200: openapi.Response('Estado de descarga de acuses de COVE del pedimento'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes'),
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
404: openapi.Response('Pedimento no encontrado')
|
404: openapi.Response('Pedimento no encontrado')
|
||||||
@@ -535,19 +509,48 @@ def auditar_acuse_cove_pedimento_endpoint(request):
|
|||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
pedimento = Pedimento.objects.select_related('organizacion').prefetch_related('coves').get(id=pedimento_id)
|
||||||
|
except Pedimento.DoesNotExist:
|
||||||
|
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
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)
|
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
except Pedimento.DoesNotExist:
|
|
||||||
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
coves = list(pedimento.coves.all())
|
||||||
task = auditar_acuse_cove_por_pedimento.delay(pedimento_id)
|
total = len(coves)
|
||||||
return Response({'message': f'Auditoría de acuse de COVE iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
|
descargados = sum(1 for c in coves if c.acuse_cove_descargado)
|
||||||
|
pendientes = [c.numero_cove for c in coves if not c.acuse_cove_descargado]
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'El pedimento no tiene COVEs registrados, no hay acuses que auditar'
|
||||||
|
elif descargados == total:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'Todos los acuses de COVE están descargados'
|
||||||
|
else:
|
||||||
|
nuevo_estado = 4
|
||||||
|
mensaje = f'{total - descargados} de {total} acuses de COVE pendientes de descarga'
|
||||||
|
|
||||||
|
from api.customs.tasks.auditoria import modificar_estado_procesamiento
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=9, nuevo_estado=nuevo_estado)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'pedimento_id': str(pedimento_id),
|
||||||
|
'estado': 'completado' if nuevo_estado == 3 else 'en_proceso',
|
||||||
|
'mensaje': mensaje,
|
||||||
|
'resumen': {
|
||||||
|
'total_coves': total,
|
||||||
|
'acuses_descargados': descargados,
|
||||||
|
},
|
||||||
|
'pendientes': pendientes,
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita el EDocument de un pedimento específico",
|
operation_description="Audita el estado de descarga de EDocuments de un pedimento específico",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={
|
||||||
@@ -556,7 +559,7 @@ def auditar_acuse_cove_pedimento_endpoint(request):
|
|||||||
required=['pedimento_id']
|
required=['pedimento_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
200: openapi.Response('Estado de descarga de EDocuments del pedimento'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes'),
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
404: openapi.Response('Pedimento no encontrado')
|
404: openapi.Response('Pedimento no encontrado')
|
||||||
@@ -568,19 +571,48 @@ def auditar_edocument_pedimento_endpoint(request):
|
|||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
pedimento = Pedimento.objects.select_related('organizacion').prefetch_related('documentos').get(id=pedimento_id)
|
||||||
|
except Pedimento.DoesNotExist:
|
||||||
|
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
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)
|
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
except Pedimento.DoesNotExist:
|
|
||||||
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
edocuments = list(pedimento.documentos.all())
|
||||||
task = auditar_edocument_por_pedimento.delay(pedimento_id)
|
total = len(edocuments)
|
||||||
return Response({'message': f'Auditoría de EDocument iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
|
descargados = sum(1 for d in edocuments if d.edocument_descargado)
|
||||||
|
pendientes = [d.numero_edocument for d in edocuments if not d.edocument_descargado]
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'El pedimento no tiene EDocuments registrados'
|
||||||
|
elif descargados == total:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'Todos los EDocuments están descargados'
|
||||||
|
else:
|
||||||
|
nuevo_estado = 4
|
||||||
|
mensaje = f'{total - descargados} de {total} EDocuments pendientes de descarga'
|
||||||
|
|
||||||
|
from api.customs.tasks.auditoria import modificar_estado_procesamiento
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=7, nuevo_estado=nuevo_estado)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'pedimento_id': str(pedimento_id),
|
||||||
|
'estado': 'completado' if nuevo_estado == 3 else 'en_proceso',
|
||||||
|
'mensaje': mensaje,
|
||||||
|
'resumen': {
|
||||||
|
'total_edocuments': total,
|
||||||
|
'edocuments_descargados': descargados,
|
||||||
|
},
|
||||||
|
'pendientes': pendientes,
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method='post',
|
method='post',
|
||||||
operation_description="Audita el acuse de un pedimento específico",
|
operation_description="Audita el estado de descarga de acuses de EDocument de un pedimento específico",
|
||||||
request_body=openapi.Schema(
|
request_body=openapi.Schema(
|
||||||
type=openapi.TYPE_OBJECT,
|
type=openapi.TYPE_OBJECT,
|
||||||
properties={
|
properties={
|
||||||
@@ -589,28 +621,56 @@ def auditar_edocument_pedimento_endpoint(request):
|
|||||||
required=['pedimento_id']
|
required=['pedimento_id']
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
200: openapi.Response('Tarea de auditoría iniciada correctamente'),
|
200: openapi.Response('Estado de descarga de acuses de EDocument del pedimento'),
|
||||||
400: openapi.Response('Error en los parámetros'),
|
400: openapi.Response('Error en los parámetros'),
|
||||||
403: openapi.Response('No tiene permisos suficientes'),
|
403: openapi.Response('No tiene permisos suficientes'),
|
||||||
404: openapi.Response('Pedimento no encontrado')
|
404: openapi.Response('Pedimento no encontrado')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def auditar_acuse_pedimento_endpoint(request):
|
def auditar_acuse_pedimento_endpoint(request):
|
||||||
pedimento_id = request.data.get('pedimento_id')
|
pedimento_id = request.data.get('pedimento_id')
|
||||||
if not pedimento_id:
|
if not pedimento_id:
|
||||||
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'error': 'Debe proporcionar pedimento_id'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
pedimento = Pedimento.objects.select_related('organizacion').prefetch_related('documentos').get(id=pedimento_id)
|
||||||
|
except Pedimento.DoesNotExist:
|
||||||
|
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
|
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)
|
return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
except Pedimento.DoesNotExist:
|
|
||||||
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
|
edocuments = list(pedimento.documentos.all())
|
||||||
task = auditar_acuse_por_pedimento.delay(pedimento_id)
|
total = len(edocuments)
|
||||||
return Response({'message': f'Auditoría de acuse iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK)
|
descargados = sum(1 for d in edocuments if d.acuse_descargado)
|
||||||
|
pendientes = [d.numero_edocument for d in edocuments if not d.acuse_descargado]
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'El pedimento no tiene EDocuments registrados, no hay acuses que auditar'
|
||||||
|
elif descargados == total:
|
||||||
|
nuevo_estado = 3
|
||||||
|
mensaje = 'Todos los acuses de EDocument están descargados'
|
||||||
|
else:
|
||||||
|
nuevo_estado = 4
|
||||||
|
mensaje = f'{total - descargados} de {total} acuses de EDocument pendientes de descarga'
|
||||||
|
|
||||||
|
from api.customs.tasks.auditoria import modificar_estado_procesamiento
|
||||||
|
modificar_estado_procesamiento(pedimento, servicio_id=6, nuevo_estado=nuevo_estado)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'pedimento_id': str(pedimento_id),
|
||||||
|
'estado': 'completado' if nuevo_estado == 3 else 'en_proceso',
|
||||||
|
'mensaje': mensaje,
|
||||||
|
'resumen': {
|
||||||
|
'total_edocuments': total,
|
||||||
|
'acuses_descargados': descargados,
|
||||||
|
},
|
||||||
|
'pendientes': pendientes,
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
### Procesamiento de pedimentos ###
|
### Procesamiento de pedimentos ###
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
@@ -1663,6 +1723,10 @@ def auditar_pedimento_endpoint(request):
|
|||||||
informacion_extraida = []
|
informacion_extraida = []
|
||||||
|
|
||||||
for documento in documentos_xml:
|
for documento in documentos_xml:
|
||||||
|
|
||||||
|
print(f"documento >>>> {documento}")
|
||||||
|
logger.info(f"documento >>>> {documento}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
xml_info = {
|
xml_info = {
|
||||||
'documento_id': str(documento.id),
|
'documento_id': str(documento.id),
|
||||||
@@ -1695,9 +1759,6 @@ def auditar_pedimento_endpoint(request):
|
|||||||
'error': f'Error procesando archivo: {str(e)}'
|
'error': f'Error procesando archivo: {str(e)}'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Ejecutar la tarea de auditoría completa
|
|
||||||
task = auditar_pedimento_por_id.delay(pedimento_id)
|
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
'pedimento_id': str(pedimento_id),
|
'pedimento_id': str(pedimento_id),
|
||||||
'pedimento': pedimento.pedimento,
|
'pedimento': pedimento.pedimento,
|
||||||
@@ -1706,7 +1767,6 @@ def auditar_pedimento_endpoint(request):
|
|||||||
'xmls_analizados': xmls_analizados,
|
'xmls_analizados': xmls_analizados,
|
||||||
'informacion_extraida': informacion_extraida,
|
'informacion_extraida': informacion_extraida,
|
||||||
'auditoria_completa': True,
|
'auditoria_completa': True,
|
||||||
'task_id': task.id,
|
|
||||||
'mensaje': f'Auditoría completada para el pedimento {pedimento.pedimento}'
|
'mensaje': f'Auditoría completada para el pedimento {pedimento.pedimento}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.test import TestCase
|
||||||
from rest_framework.test import APITestCase, APIClient
|
from rest_framework.test import APITestCase, APIClient
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
from api.organization.models import Organizacion, UsoAlmacenamiento
|
from api.organization.models import Organizacion, UsoAlmacenamiento
|
||||||
from api.cuser.models import CustomUser
|
from api.cuser.models import CustomUser
|
||||||
from api.customs.models import Pedimento
|
from api.customs.models import Pedimento
|
||||||
from .models import Document
|
from api.licence.models import Licencia
|
||||||
|
from api.customs.views import is_same_document, get_clean_base_filename
|
||||||
|
from .models import Document, DocumentType
|
||||||
import io
|
import io
|
||||||
|
|
||||||
class DocumentViewSetTests(APITestCase):
|
class DocumentViewSetTests(APITestCase):
|
||||||
@@ -95,3 +99,177 @@ class DocumentViewSetTests(APITestCase):
|
|||||||
url = reverse('descargar-documento', args=[doc.id])
|
url = reverse('descargar-documento', args=[doc.id])
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests unitarios para las funciones helper de comparación de documentos
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class DocumentNameHelperTests(TestCase):
|
||||||
|
"""Verifica que get_clean_base_filename e is_same_document manejan
|
||||||
|
correctamente el sufijo UUID de 8 chars que añade storage_service."""
|
||||||
|
|
||||||
|
def test_strips_uuid_suffix(self):
|
||||||
|
self.assertEqual(get_clean_base_filename('informe_a1b2c3d4.pdf'), 'informe')
|
||||||
|
|
||||||
|
def test_no_suffix_unchanged(self):
|
||||||
|
self.assertEqual(get_clean_base_filename('informe.pdf'), 'informe')
|
||||||
|
|
||||||
|
def test_is_same_document_matches_stored_uuid_name(self):
|
||||||
|
"""El archivo guardado tiene sufijo, el nuevo no — deben coincidir."""
|
||||||
|
doc = MagicMock()
|
||||||
|
doc.archivo.name = 'org_1/documents/24-01-3420-1234567/informe_a1b2c3d4.pdf'
|
||||||
|
doc.extension = 'pdf'
|
||||||
|
self.assertTrue(is_same_document(doc, 'informe.pdf'))
|
||||||
|
|
||||||
|
def test_is_same_document_different_name_no_match(self):
|
||||||
|
doc = MagicMock()
|
||||||
|
doc.archivo.name = 'org_1/documents/ped/informe_a1b2c3d4.pdf'
|
||||||
|
doc.extension = 'pdf'
|
||||||
|
self.assertFalse(is_same_document(doc, 'otro.pdf'))
|
||||||
|
|
||||||
|
def test_is_same_document_different_extension_no_match(self):
|
||||||
|
doc = MagicMock()
|
||||||
|
doc.archivo.name = 'org_1/documents/ped/informe_a1b2c3d4.pdf'
|
||||||
|
doc.extension = 'pdf'
|
||||||
|
self.assertFalse(is_same_document(doc, 'informe.xml'))
|
||||||
|
|
||||||
|
def test_both_clean_names_equal(self):
|
||||||
|
"""Dos archivos con UUID distintos pero mismo nombre base deben coincidir."""
|
||||||
|
doc = MagicMock()
|
||||||
|
doc.archivo.name = 'org_1/documents/ped/pedimento_a1b2c3d4.xml'
|
||||||
|
doc.extension = 'xml'
|
||||||
|
self.assertTrue(is_same_document(doc, 'pedimento_b5c6d7e8.xml'))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests de integración para bulk-upload (DocumentViewSet.bulk_upload)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class BulkUploadReplaceTests(APITestCase):
|
||||||
|
"""Verifica que bulk-upload reemplaza documentos existentes en vez de duplicar
|
||||||
|
y que no quedan archivos residuales en el storage."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.licencia = Licencia.objects.create(nombre="Lic100GB", almacenamiento=100)
|
||||||
|
self.org = Organizacion.objects.create(
|
||||||
|
nombre="OrgBulkUpload",
|
||||||
|
licencia=self.licencia,
|
||||||
|
is_active=True,
|
||||||
|
is_verified=True,
|
||||||
|
)
|
||||||
|
self.user = CustomUser.objects.create_user(
|
||||||
|
username="bulkuploaduser", password="pass", organizacion=self.org
|
||||||
|
)
|
||||||
|
self.pedimento = Pedimento.objects.create(
|
||||||
|
organizacion=self.org,
|
||||||
|
pedimento="1234567",
|
||||||
|
pedimento_app="24-01-3420-1234567",
|
||||||
|
)
|
||||||
|
self.doc_type = DocumentType.objects.get_or_create(nombre="Documento General")[0]
|
||||||
|
self.url = reverse("Document-bulk-upload")
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
def _post_file(self, filename, content=b"contenido de prueba"):
|
||||||
|
archivo = SimpleUploadedFile(filename, content, content_type="application/pdf")
|
||||||
|
return self.client.post(
|
||||||
|
self.url,
|
||||||
|
{"pedimento_id": str(self.pedimento.id), "files": [archivo]},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("api.record.views.storage_service")
|
||||||
|
def test_new_file_creates_document(self, mock_st):
|
||||||
|
"""Subir un archivo nuevo crea exactamente un Document."""
|
||||||
|
mock_st.save_document.return_value = "org_1/documents/ped/informe_a1b2c3d4.pdf"
|
||||||
|
|
||||||
|
response = self._post_file("informe.pdf")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(Document.objects.filter(pedimento=self.pedimento).count(), 1)
|
||||||
|
mock_st.delete_file.assert_not_called()
|
||||||
|
|
||||||
|
@patch("api.record.views.storage_service")
|
||||||
|
def test_duplicate_replaces_not_creates(self, mock_st):
|
||||||
|
"""Re-subir el mismo archivo debe actualizar el Document existente,
|
||||||
|
no crear uno nuevo."""
|
||||||
|
old_path = "org_1/documents/24-01-3420-1234567/informe_a1b2c3d4.pdf"
|
||||||
|
old_doc = Document.objects.create(
|
||||||
|
organizacion=self.org,
|
||||||
|
pedimento=self.pedimento,
|
||||||
|
document_type=self.doc_type,
|
||||||
|
archivo=old_path,
|
||||||
|
size=500,
|
||||||
|
extension="pdf",
|
||||||
|
)
|
||||||
|
new_path = "org_1/documents/24-01-3420-1234567/informe_b5c6d7e8.pdf"
|
||||||
|
mock_st.save_document.return_value = new_path
|
||||||
|
mock_st.delete_file.return_value = True
|
||||||
|
|
||||||
|
response = self._post_file("informe.pdf", b"contenido actualizado")
|
||||||
|
|
||||||
|
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_207_MULTI_STATUS])
|
||||||
|
docs = Document.objects.filter(pedimento=self.pedimento)
|
||||||
|
# Un único Document — sin duplicados
|
||||||
|
self.assertEqual(docs.count(), 1)
|
||||||
|
# Es el mismo registro (mismo UUID)
|
||||||
|
self.assertEqual(docs.first().id, old_doc.id)
|
||||||
|
# El campo archivo fue actualizado
|
||||||
|
old_doc.refresh_from_db()
|
||||||
|
self.assertEqual(old_doc.archivo.name, new_path)
|
||||||
|
|
||||||
|
@patch("api.record.views.storage_service")
|
||||||
|
def test_replace_deletes_old_storage_file(self, mock_st):
|
||||||
|
"""Al reemplazar, delete_file debe llamarse con la ruta del archivo viejo."""
|
||||||
|
old_path = "org_1/documents/24-01-3420-1234567/informe_a1b2c3d4.pdf"
|
||||||
|
Document.objects.create(
|
||||||
|
organizacion=self.org,
|
||||||
|
pedimento=self.pedimento,
|
||||||
|
document_type=self.doc_type,
|
||||||
|
archivo=old_path,
|
||||||
|
size=500,
|
||||||
|
extension="pdf",
|
||||||
|
)
|
||||||
|
mock_st.save_document.return_value = "org_1/documents/24-01-3420-1234567/informe_b5c6d7e8.pdf"
|
||||||
|
mock_st.delete_file.return_value = True
|
||||||
|
|
||||||
|
self._post_file("informe.pdf")
|
||||||
|
|
||||||
|
mock_st.delete_file.assert_called_once_with(old_path)
|
||||||
|
|
||||||
|
@patch("api.record.views.storage_service")
|
||||||
|
def test_different_filename_creates_new_document(self, mock_st):
|
||||||
|
"""Archivo con nombre diferente debe crear un Document adicional."""
|
||||||
|
Document.objects.create(
|
||||||
|
organizacion=self.org,
|
||||||
|
pedimento=self.pedimento,
|
||||||
|
document_type=self.doc_type,
|
||||||
|
archivo="org_1/documents/ped/informe_a1b2c3d4.pdf",
|
||||||
|
size=500,
|
||||||
|
extension="pdf",
|
||||||
|
)
|
||||||
|
mock_st.save_document.return_value = "org_1/documents/ped/otro_b5c6d7e8.pdf"
|
||||||
|
|
||||||
|
self._post_file("otro.pdf")
|
||||||
|
|
||||||
|
self.assertEqual(Document.objects.filter(pedimento=self.pedimento).count(), 2)
|
||||||
|
mock_st.delete_file.assert_not_called()
|
||||||
|
|
||||||
|
@patch("api.record.views.storage_service")
|
||||||
|
def test_multiple_files_no_cross_replacement(self, mock_st):
|
||||||
|
"""Subir dos archivos distintos en la misma petición crea dos Documents."""
|
||||||
|
mock_st.save_document.side_effect = [
|
||||||
|
"org_1/documents/ped/a_a1b2c3d4.pdf",
|
||||||
|
"org_1/documents/ped/b_a1b2c3d4.pdf",
|
||||||
|
]
|
||||||
|
archivos = [
|
||||||
|
SimpleUploadedFile("a.pdf", b"contenido a", content_type="application/pdf"),
|
||||||
|
SimpleUploadedFile("b.pdf", b"contenido b", content_type="application/pdf"),
|
||||||
|
]
|
||||||
|
self.client.post(
|
||||||
|
self.url,
|
||||||
|
{"pedimento_id": str(self.pedimento.id), "files": archivos},
|
||||||
|
format="multipart",
|
||||||
|
)
|
||||||
|
self.assertEqual(Document.objects.filter(pedimento=self.pedimento).count(), 2)
|
||||||
|
mock_st.delete_file.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user