from api.organization.models import Organizacion from celery import group from celery import shared_task, group from api.customs.models import * from api.record.models import * from api.customs.serializers import PedimentoSerializer from api.vucem.models import * from django.db.models import F from django.utils import timezone import requests from config.settings import SERVICE_API_URL_V2, MAX_INTENTOS_AUTO from datetime import datetime 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): if not credenciales: return {} key_value = None if credenciales.key: if hasattr(credenciales.key, 'url'): key_value = credenciales.key.url else: key_value = str(credenciales.key) cer_value = None if credenciales.cer: if hasattr(credenciales.cer, 'url'): cer_value = credenciales.cer.url else: cer_value = str(credenciales.cer) return { "id": str(credenciales.id), "user": credenciales.usuario, "password": credenciales.password, "efirma": credenciales.efirma, "key": key_value, "cer": cer_value, "is_active": credenciales.is_active, "organizacion": str(credenciales.organizacion.id) if credenciales.organizacion else None, } def pedimento_to_dict(pedimento): return { "id": str(pedimento.id), "pedimento": str(pedimento.pedimento), "pedimento_app": str(pedimento.pedimento_app), "aduana": str(pedimento.aduana), "patente": str(pedimento.patente), "organizacion": str(pedimento.organizacion.id), # nunca None "regimen": str(pedimento.regimen or ""), "clave_pedimento": str(pedimento.clave_pedimento or ""), "numero_operacion": str(pedimento.numero_operacion or "") } def cove_to_dict(cove): return { "id": cove.id, "cove": str(cove.numero_cove), } def edoc_to_dict(edoc): return { "id": edoc.id, "numero_edocument": str(edoc.numero_edocument), } def partida_to_dict(partida): return { "id": partida.id, "numero": partida.numero_partida, } @shared_task def procesar_coves_pedimento(pedimento_id): # Flujo manual: incluye registros en 'error' y no aplica tope ni contador de intentos pedimento = Pedimento.objects.get(id=pedimento_id) estados_reprocesables = [EstadoDescarga.PENDIENTE, EstadoDescarga.ERROR] if pedimento.coves.filter(cove_estado__in=estados_reprocesables).exists(): pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "coves": [cove_to_dict(cove) for cove in pedimento.coves.filter(cove_estado__in=estados_reprocesables)], "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/coves", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando COVEs para pedimento {pedimento.pedimento}: {e}") raise @shared_task def procesar_acuse_coves_pedimento(pedimento_id): # Flujo manual: incluye registros en 'error' y no aplica tope ni contador de intentos pedimento = Pedimento.objects.get(id=pedimento_id) estados_reprocesables = [EstadoDescarga.PENDIENTE, EstadoDescarga.ERROR] if pedimento.coves.filter(acuse_cove_estado__in=estados_reprocesables).exists(): pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "coves": [cove_to_dict(cove) for cove in pedimento.coves.filter(acuse_cove_estado__in=estados_reprocesables)], "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/acuse/cove/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"Acuses de COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando acuses de COVEs para pedimento {pedimento.pedimento}: {e}") raise @shared_task def procesar_edocs_pedimento(pedimento_id): # Flujo manual: incluye registros en 'error' y no aplica tope ni contador de intentos pedimento = Pedimento.objects.get(id=pedimento_id) estados_reprocesables = [EstadoDescarga.PENDIENTE, EstadoDescarga.ERROR] if pedimento.documentos.filter(edocument_estado__in=estados_reprocesables).exists(): pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(edocument_estado__in=estados_reprocesables)], "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/download/all/edocs/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"E-documents encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando E-documents para pedimento {pedimento.pedimento}: {e}") raise @shared_task def procesar_acuses_pedimento(pedimento_id): # Flujo manual: incluye registros en 'error' y no aplica tope ni contador de intentos pedimento = Pedimento.objects.get(id=pedimento_id) estados_reprocesables = [EstadoDescarga.PENDIENTE, EstadoDescarga.ERROR] if pedimento.documentos.filter(acuse_estado__in=estados_reprocesables).exists(): pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(acuse_estado__in=estados_reprocesables)], "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"Acuses encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando acuses para pedimento {pedimento.pedimento}: {e}") raise @shared_task def procesar_partidas_pedimento(pedimento_id): pedimento = Pedimento.objects.get(id=pedimento_id) if pedimento.partidas.filter(descargado=False).exists(): pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) partidas_pendientes = list(pedimento.partidas.filter(descargado=False)) payload = { "partidas": [partida_to_dict(p) for p in partidas_pendientes], "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/partidas/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() result = response.json() logging.info( f"Partidas encoladas para pedimento {pedimento.pedimento}: " f"{result.get('total', 0)} de {len(partidas_pendientes)}" ) except requests.exceptions.RequestException as e: logging.error( f"Error encolando partidas para pedimento {pedimento.pedimento}: {e}" ) raise @shared_task def procesar_remesas_pedimento(pedimento_id): pedimento = Pedimento.objects.get(id=pedimento_id) if not pedimento.documents.filter(document_type=3).exists(): # Tipo 3: Remesa pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/remesas", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"Remesa encolada para pedimento {pedimento.pedimento}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando remesa para pedimento {pedimento.pedimento}: {e}") raise @shared_task def procesar_pedimento_completo_individual(pedimento_id, force=False): pedimento = Pedimento.objects.get(id=pedimento_id) if force or not pedimento.documents.filter(document_type=2).exists(): # Tipo 2: Pedimento Completo pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/pedimento_completo", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"Pedimento completo encolado: {pedimento.pedimento}") return response except requests.exceptions.RequestException as e: logging.error(f"Error encolando pedimento completo {pedimento.pedimento}: {e}") raise @shared_task def procesar_pedimentos_completos(organizacion_id): pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id) respuestas = [] for pedimento in pedimentos: if not pedimento.contribuyente: print(f"Pedimento {pedimento.pedimento} no tiene contribuyente") continue credencial_importador = CredencialesImportador.objects.filter( rfc=pedimento.contribuyente ).first() if not credencial_importador: print(f"No credencial para RFC {pedimento.contribuyente.rfc}") continue if not pedimento.documents.filter(document_type=2).exists(): # Tipo 2: Pedimento Completo # Convertir el pedimento a JSON usando el serializer pedimento_dict = pedimento_to_dict(pedimento) # credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first() credenciales = Vucem.objects.filter(id=credencial_importador.vucem.id).first() if not credenciales: print(f"No se encontraron credenciales para el pedimento {pedimento.pedimento_app}") continue credenciales_dict = credenciales_to_dict(credenciales) payload = { "pedimento": pedimento_dict, "credencial": credenciales_dict } url = f"{SERVICE_API_URL_V2}/services/pedimento_completo" dataJson = json.dumps(payload) try: response = requests.post( url, data=dataJson, headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"Pedimento completo encolado: {pedimento.pedimento}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando pedimento completo {pedimento.pedimento}: {e}") continue @shared_task def procesar_remesas(organizacion_id): pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id) for pedimento in pedimentos: logger.info(f"pedimento >>>> {pedimento}") 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) 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) payload = { "pedimento": pedimento_dict, "credencial": credenciales_dict } response = requests.post( f"{SERVICE_API_URL_V2}/services/remesas/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logger.info(f"Remesa encolada para pedimento {pedimento.pedimento} — status {response.status_code}") except Exception as e: logger.error(f"Error procesando remesa para pedimento {pedimento.pedimento}: {e}", exc_info=True) @shared_task def procesar_coves(organizacion_id): pedimentos = Pedimento.objects.filter( organizacion_id=organizacion_id, coves__isnull=False ).distinct() for pedimento in pedimentos: # Compuerta del automático (T2026-05-027): solo 'pendiente' con intentos disponibles; # registros en 'error' o con tope agotado solo se relanzan de forma manual pendientes = pedimento.coves.filter( cove_estado=EstadoDescarga.PENDIENTE, cove_intentos__lt=MAX_INTENTOS_AUTO, ) coves_batch = list(pendientes) if coves_batch: # Convertir el pedimento a JSON usando el serializer pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "coves": [cove_to_dict(cove) for cove in coves_batch], "pedimento": pedimento_dict, "credencial": credenciales_dict } # Un ciclo de orquestación = un intento; los reintentos internos # del worker (Celery/SOAP) pertenecen a este mismo intento pendientes.update(cove_intentos=F('cove_intentos') + 1, ultimo_intento_at=timezone.now()) try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/coves", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando COVEs para pedimento {pedimento.pedimento}: {e}") continue @shared_task def procesar_acuse_coves(organizacion_id): pedimentos = Pedimento.objects.filter( organizacion_id=organizacion_id, coves__isnull=False ).distinct() for pedimento in pedimentos: # Compuerta del automático (T2026-05-027): solo 'pendiente' con intentos disponibles pendientes = pedimento.coves.filter( acuse_cove_estado=EstadoDescarga.PENDIENTE, acuse_cove_intentos__lt=MAX_INTENTOS_AUTO, ) coves_batch = list(pendientes) if coves_batch: # Convertir el pedimento a JSON usando el serializer pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "coves": [cove_to_dict(cove) for cove in coves_batch], "pedimento": pedimento_dict, "credencial": credenciales_dict } # Un ciclo de orquestación = un intento pendientes.update(acuse_cove_intentos=F('acuse_cove_intentos') + 1, ultimo_intento_at=timezone.now()) try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/acuse/cove/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"Acuses de COVEs encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando acuses de COVEs para pedimento {pedimento.pedimento}: {e}") continue @shared_task def procesar_acuses(organizacion_id): pedimentos = Pedimento.objects.filter( organizacion_id=organizacion_id, documentos__isnull=False ).distinct() for pedimento in pedimentos: # Compuerta del automático (T2026-05-027): solo 'pendiente' con intentos disponibles pendientes = pedimento.documentos.filter( acuse_estado=EstadoDescarga.PENDIENTE, acuse_intentos__lt=MAX_INTENTOS_AUTO, ) edocs_batch = list(pendientes) if edocs_batch: # Convertir el pedimento a JSON usando el serializer pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "edocs": [edoc_to_dict(edoc) for edoc in edocs_batch], "pedimento": pedimento_dict, "credencial": credenciales_dict } # Un ciclo de orquestación = un intento pendientes.update(acuse_intentos=F('acuse_intentos') + 1, ultimo_intento_at=timezone.now()) try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/acuse/pedimento/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"Acuses encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando acuses para pedimento {pedimento.pedimento}: {e}") continue @shared_task def procesar_edocs(organizacion_id): pedimentos = Pedimento.objects.filter( organizacion_id=organizacion_id, documentos__isnull=False ).distinct() for pedimento in pedimentos: # Compuerta del automático (T2026-05-027): solo 'pendiente' con intentos disponibles pendientes = pedimento.documentos.filter( edocument_estado=EstadoDescarga.PENDIENTE, edocument_intentos__lt=MAX_INTENTOS_AUTO, ) edocs_batch = list(pendientes) if edocs_batch: # Convertir el pedimento a JSON usando el serializer pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter(id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "edocs": [edoc_to_dict(edoc) for edoc in edocs_batch], "pedimento": pedimento_dict, "credencial": credenciales_dict } # Un ciclo de orquestación = un intento pendientes.update(edocument_intentos=F('edocument_intentos') + 1, ultimo_intento_at=timezone.now()) try: response = requests.post( f"{SERVICE_API_URL_V2}/services/download/all/edocs/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() logging.info(f"E-documents encolados para pedimento {pedimento.pedimento}: {response.json().get('total', '?')}") except requests.exceptions.RequestException as e: logging.error(f"Error encolando E-documents para pedimento {pedimento.pedimento}: {e}") continue @shared_task def procesar_partidas(organizacion_id): pedimentos = Pedimento.objects.filter( organizacion_id=organizacion_id, partidas__isnull=False ).distinct() for pedimento in pedimentos: partidas_pendientes = list(pedimento.partidas.filter(descargado=False)) if not partidas_pendientes: continue pedimento_dict = pedimento_to_dict(pedimento) credenciales = Vucem.objects.filter( id=CredencialesImportador.objects.filter(rfc=pedimento.contribuyente).first().vucem.id ).first() credenciales_dict = credenciales_to_dict(credenciales) payload = { "partidas": [partida_to_dict(p) for p in partidas_pendientes], "pedimento": pedimento_dict, "credencial": credenciales_dict } try: response = requests.post( f"{SERVICE_API_URL_V2}/services/all/partidas/", data=json.dumps(payload), headers={"Content-Type": "application/json"}, timeout=60 ) response.raise_for_status() result = response.json() logging.info( f"Partidas encoladas para pedimento {pedimento.pedimento}: " f"{result.get('total', 0)} de {len(partidas_pendientes)}" ) except requests.exceptions.RequestException as e: logging.error( f"Error encolando partidas para pedimento {pedimento.pedimento}: {e}" ) continue @shared_task def documentos_con_errores(organizacion_id): documentos = Document.objects.filter(organizacion_id=organizacion_id) for doc in documentos: if doc.document_type is None or doc.size is None or doc.archivo is None: print(f"Documento con error: {doc.id} en organización {organizacion_id}") # Aquí puedes agregar lógica adicional para manejar documentos con errores # como enviar notificaciones, registrar en un log, etc. @shared_task def procesar_procesamiento_pedimento(organizacion_id): # print("Creando procesamientos de pedimentos para organización:", organizacion_id) pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id) # pedimentos = Pedimento.objects.filter(id='1c061182-ac68-45b0-b3d7-35bf2264982b') if not pedimentos.exists(): print("No se encontraron pedimentos para la organización:", organizacion_id) return for pedimento in pedimentos: if not pedimento.documents.filter(document_type=2).exists(): # Tipo 2: Pedimento Completo procesamiento_pedimento = ProcesamientoPedimento.objects.filter( pedimento_id=pedimento.id, servicio_id=3, # servicio 3: Pedimento Completo ) if not procesamiento_pedimento.exists(): ProcesamientoPedimento.objects.create( pedimento_id=pedimento.id , organizacion_id=pedimento.organizacion_id , estado_id =1 , servicio_id=3 , tipo_procesamiento_id=2) # servicio 3: Pedimento Completo # print("Procesamiento creado para pedimento:", pedimento.pedimento_app) procesar_pedimentos_completos.delay(organizacion_id) def ejecutar_por_organizacion_y_procesamiento(organizacion_id, procesamiento): if procesamiento == 'coves': procesar_coves.delay(organizacion_id) elif procesamiento == 'edocs': procesar_edocs.delay(organizacion_id) elif procesamiento == 'acuses': procesar_acuses.delay(organizacion_id) elif procesamiento == 'acuse_coves': procesar_acuse_coves.delay(organizacion_id) elif procesamiento == 'partidas': procesar_partidas.delay(organizacion_id) elif procesamiento == 'pedimentos_completos': procesar_pedimentos_completos.delay(organizacion_id) elif procesamiento == 'remesas': procesar_remesas.delay(organizacion_id) elif procesamiento == 'procesamiento_pedimento': procesar_procesamiento_pedimento.delay(organizacion_id) else: # Procesamiento no reconocido # print(f"Procesamiento no reconocido: {procesamiento}") pass def ejecutar_todos_por_organizacion(organizacion_id): procesar_coves.delay(organizacion_id) procesar_edocs.delay(organizacion_id) procesar_acuses.delay(organizacion_id) procesar_acuse_coves.delay(organizacion_id) procesar_partidas.delay(organizacion_id) procesar_pedimentos_completos.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, apply_auto_download=True, ) for org in active_orgs: process_organization_batch.apply_async( args=[str(org.id)], queue='org_processing' ) return f"Dispatched {active_orgs.count()} organizations" @shared_task def reintentar_descargas_pendientes(): """ Reintento recurrente de descargas VUCEM (T2026-05-027): transiciona a 'error' los registros que agotaron MAX_INTENTOS_AUTO y relanza los pendientes por organización. El incremento del contador vive en las tareas procesar_* (puerta común de todos los flujos automáticos), por lo que aquí solo se orquesta. """ ahora = timezone.now() mensaje_tope = ( f"Se agotaron {MAX_INTENTOS_AUTO} intentos automáticos de descarga; " f"requiere reproceso manual" ) # 1) Transicionar a 'error' lo que agotó el tope automático. # update() no pasa por save(): sincronizar también el booleano legado y updated_at. edocs_err = EDocument.objects.filter( edocument_estado=EstadoDescarga.PENDIENTE, edocument_intentos__gte=MAX_INTENTOS_AUTO, ).update(edocument_estado=EstadoDescarga.ERROR, edocument_descargado=False, ultimo_error=mensaje_tope, updated_at=ahora) acuses_err = EDocument.objects.filter( acuse_estado=EstadoDescarga.PENDIENTE, acuse_intentos__gte=MAX_INTENTOS_AUTO, ).update(acuse_estado=EstadoDescarga.ERROR, acuse_descargado=False, ultimo_error=mensaje_tope, updated_at=ahora) coves_err = Cove.objects.filter( cove_estado=EstadoDescarga.PENDIENTE, cove_intentos__gte=MAX_INTENTOS_AUTO, ).update(cove_estado=EstadoDescarga.ERROR, cove_descargado=False, ultimo_error=mensaje_tope, updated_at=ahora) acuse_coves_err = Cove.objects.filter( acuse_cove_estado=EstadoDescarga.PENDIENTE, acuse_cove_intentos__gte=MAX_INTENTOS_AUTO, ).update(acuse_cove_estado=EstadoDescarga.ERROR, acuse_cove_descargado=False, ultimo_error=mensaje_tope, updated_at=ahora) if edocs_err or acuses_err or coves_err or acuse_coves_err: logger.info( f"Tope de intentos agotado -> error: edocs={edocs_err}, acuses={acuses_err}, " f"coves={coves_err}, acuse_coves={acuse_coves_err}" ) # 2) Relanzar por organización (procesar_* aplica la compuerta e incrementa el contador) active_orgs = Organizacion.objects.filter( is_active=True, is_verified=True, apply_auto_download=True, ) for org in active_orgs: process_organization_batch.apply_async( args=[str(org.id)], queue='org_processing' ) return f"Reintentos despachados para {active_orgs.count()} organizaciones"