fix/de los tickets T2026-05-027, T2025-09-004 y T2025-09-056

This commit is contained in:
2026-06-15 11:18:58 -06:00
parent 7644446267
commit 23ed52c78a
29 changed files with 2992 additions and 987 deletions

View File

@@ -5,8 +5,10 @@ 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
from config.settings import SERVICE_API_URL_V2, MAX_INTENTOS_AUTO
from datetime import datetime
import json
import logging
@@ -77,16 +79,18 @@ def partida_to_dict(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)
if pedimento.coves.filter(cove_descargado=False).exists():
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_descargado=False)],
"coves": [cove_to_dict(cove) for cove in pedimento.coves.filter(cove_estado__in=estados_reprocesables)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
@@ -106,8 +110,10 @@ def procesar_coves_pedimento(pedimento_id):
@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)
if pedimento.coves.filter(acuse_cove_descargado=False).exists():
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
@@ -115,7 +121,7 @@ def procesar_acuse_coves_pedimento(pedimento_id):
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"coves": [cove_to_dict(cove) for cove in pedimento.coves.filter(acuse_cove_descargado=False)],
"coves": [cove_to_dict(cove) for cove in pedimento.coves.filter(acuse_cove_estado__in=estados_reprocesables)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
@@ -135,8 +141,10 @@ def procesar_acuse_coves_pedimento(pedimento_id):
@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)
if pedimento.documentos.filter(edocument_descargado=False).exists():
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
@@ -144,7 +152,7 @@ def procesar_edocs_pedimento(pedimento_id):
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(edocument_descargado=False)],
"edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(edocument_estado__in=estados_reprocesables)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
@@ -164,8 +172,10 @@ def procesar_edocs_pedimento(pedimento_id):
@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)
if pedimento.documentos.filter(acuse_descargado=False).exists():
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
@@ -173,7 +183,7 @@ def procesar_acuses_pedimento(pedimento_id):
credenciales_dict = credenciales_to_dict(credenciales)
payload = {
"edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(acuse_descargado=False)],
"edocs": [edoc_to_dict(edoc) for edoc in pedimento.documentos.filter(acuse_estado__in=estados_reprocesables)],
"pedimento": pedimento_dict,
"credencial": credenciales_dict
}
@@ -381,20 +391,31 @@ def procesar_coves(organizacion_id):
coves__isnull=False
).distinct()
for pedimento in pedimentos:
if pedimento.coves.filter(cove_descargado=False).exists(): # Tipo 3: Remesa
# 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 pedimento.coves.filter(cove_descargado=False)],
"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",
@@ -416,20 +437,29 @@ def procesar_acuse_coves(organizacion_id):
).distinct()
for pedimento in pedimentos:
if pedimento.coves.filter(acuse_cove_descargado=False).exists(): # Tipo 3: Remesa
# 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 pedimento.coves.filter(acuse_cove_descargado=False)],
"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/",
@@ -451,20 +481,29 @@ def procesar_acuses(organizacion_id):
).distinct()
for pedimento in pedimentos:
if pedimento.documentos.filter(acuse_descargado=False).exists(): # Tipo 3: Remesa
# 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 pedimento.documentos.filter(acuse_descargado=False)],
"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/",
@@ -486,20 +525,29 @@ def procesar_edocs(organizacion_id):
).distinct()
for pedimento in pedimentos:
if pedimento.documentos.filter(edocument_descargado=False).exists(): # Tipo 3: Remesa
# 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 pedimento.documentos.filter(edocument_descargado=False)],
"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/",
@@ -660,3 +708,59 @@ def process_all_organizations():
)
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"