feature/T2026-05-016-y-T2026-05-031 #28

Merged
jcedilloAS merged 4 commits from feature/T2026-05-016-y-T2026-05-031 into main 2026-05-18 18:05:27 +00:00
9 changed files with 825 additions and 383 deletions
Showing only changes of commit 3a636c14ae - Show all commits

View File

@@ -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())

View File

@@ -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,18 +240,23 @@ 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:
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
except Exception: except Exception:
# En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía # En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía
return [] return []

View File

@@ -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)

View File

@@ -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 *

View File

@@ -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):
from api.customs.models import Partida
partida, created = Partida.objects.get_or_create(
pedimento=pedimento,
numero_partida=i,
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}")
else:
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 ===") for i in range(1, pedimento.numero_partidas + 1):
print(f"Pedimentos procesados: {pedimentos_procesados}") Partida.objects.get_or_create(
print(f"Total de partidas agregadas: {total_partidas_agregadas}") pedimento=pedimento,
print(f"Procesamiento completado para organización {organizacion_id}") numero_partida=i,
defaults={'organizacion_id': organizacion_id}
)
partidas = list(pedimento.partidas.order_by('numero_partida'))
no_descargadas = [p.numero_partida for p in partidas if not p.descargado]
if not no_descargadas:
completados.append(str(pedimento.id))
else:
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}")
# Auditar coves 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,
}
@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', label='cove',
mensaje='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', label='acuse_cove',
mensaje='acuse de 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', label='edocument',
mensaje='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', label='acuse',
mensaje='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(

View File

@@ -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

View File

@@ -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,27 +283,40 @@ 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)
payload = { payload = {
"pedimento": pedimento_dict, "pedimento": pedimento_dict,
"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"

View File

@@ -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)
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
)
except Pedimento.DoesNotExist: except Pedimento.DoesNotExist:
return Response( return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
{'error': 'Pedimento no encontrado'},
status=status.HTTP_404_NOT_FOUND
)
# Ejecutar la tarea user = request.user
task = crear_partidas_por_pedimento.delay(pedimento_id) if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
message = f"Creación de partidas iniciada para el pedimento {pedimento_id}" return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
if not pedimento.numero_partidas or pedimento.numero_partidas <= 0:
return Response({
'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)
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
)
except Pedimento.DoesNotExist: except Pedimento.DoesNotExist:
return Response( return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
{'error': 'Pedimento no encontrado'},
status=status.HTTP_404_NOT_FOUND
)
# Ejecutar la tarea de auditoría user = request.user
task = auditar_procesamiento_remesa_por_pedimento.delay(pedimento_id) if not user.is_superuser and str(pedimento.organizacion.id) != str(user.organizacion.id):
message = f"Auditoría de remesa iniciada para el pedimento {pedimento_id}" return Response({'error': 'No tiene permisos para este pedimento'}, status=status.HTTP_403_FORBIDDEN)
if not pedimento.remesas:
return Response({
'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': pedimento.pedimento,
'pedimento': { 'tiene_remesas': True,
'id': str(pedimento.id), 'estado': estado,
'pedimento': pedimento.pedimento, 'mensaje': mensaje,
'tiene_remesas': pedimento.remesas '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)
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)
except Pedimento.DoesNotExist: except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_cove_por_pedimento.delay(pedimento_id)
return Response({'message': f'Auditoría de COVE iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK) 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)
coves = list(pedimento.coves.all())
total = len(coves)
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)
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)
except Pedimento.DoesNotExist: except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_acuse_cove_por_pedimento.delay(pedimento_id)
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) 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)
coves = list(pedimento.coves.all())
total = len(coves)
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)
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)
except Pedimento.DoesNotExist: except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_edocument_por_pedimento.delay(pedimento_id)
return Response({'message': f'Auditoría de EDocument iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK) 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)
edocuments = list(pedimento.documentos.all())
total = len(edocuments)
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)
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)
except Pedimento.DoesNotExist: except Pedimento.DoesNotExist:
return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': 'Pedimento no encontrado'}, status=status.HTTP_404_NOT_FOUND)
task = auditar_acuse_por_pedimento.delay(pedimento_id)
return Response({'message': f'Auditoría de acuse iniciada para el pedimento {pedimento_id}', 'task_id': task.id}, status=status.HTTP_200_OK) 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)
edocuments = list(pedimento.documentos.all())
total = len(edocuments)
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}'
} }

View File

@@ -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()