111 lines
5.1 KiB
Python
111 lines
5.1 KiB
Python
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
|
|
from api.customs.models import EDocument, Cove, EstadoDescarga
|
|
from api.record.models import Document
|
|
from api.utils.storage_service import storage_service
|
|
|
|
|
|
class Command(BaseCommand):
|
|
"""
|
|
Reconciliación de estatus de descarga VUCEM (T2026-05-027).
|
|
|
|
Detecta registros marcados como 'descargado' cuyo documento no existe en BD
|
|
o cuyo archivo falta físicamente en storage (MinIO), y los transiciona a
|
|
estado 'error' para que sean visibles y reprocesables. Sin --apply solo
|
|
reporta (dry-run).
|
|
|
|
Uso:
|
|
python manage.py reconciliar_descargas # reporte
|
|
python manage.py reconciliar_descargas --apply # corrige
|
|
python manage.py reconciliar_descargas --organizacion <uuid>
|
|
"""
|
|
|
|
help = "Reconcilia estatus de descarga de EDocs/COVEs contra documentos reales (BD + storage)"
|
|
|
|
# Catálogo confirmado de document_type:
|
|
# 4 = acuse EDoc, 7 = acuse COVE, 19/23 = request COVE, 21/25 = request EDoc,
|
|
# 20 = error COVE, 22 = error EDoc, 24 = error acuse COVE, 26 = error acuse EDoc
|
|
EXCLUIR_EDOC_GENERAL = [4, 21, 22, 25, 26]
|
|
EXCLUIR_COVE_GENERAL = [7, 19, 20, 23, 24]
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
'--apply', action='store_true',
|
|
help='Aplica las correcciones; sin esta bandera solo reporta (dry-run)'
|
|
)
|
|
parser.add_argument(
|
|
'--organizacion', type=str, default=None,
|
|
help='Limitar la reconciliación a una organización (UUID)'
|
|
)
|
|
parser.add_argument(
|
|
'--pedimento', type=str, default=None,
|
|
help='Limitar la reconciliación a un pedimento (UUID)'
|
|
)
|
|
|
|
def handle(self, *args, **opts):
|
|
apply_changes = opts['apply']
|
|
detectados = []
|
|
|
|
flujos = [
|
|
# (modelo, campo_estado, campo_intentos, etiqueta, fn_documentos)
|
|
(EDocument, 'acuse_estado', 'acuse_intentos', 'edoc.acuse',
|
|
lambda r: Document.objects.filter(
|
|
pedimento=r.pedimento,
|
|
archivo__icontains=r.numero_edocument,
|
|
document_type_id=4)),
|
|
(EDocument, 'edocument_estado', 'edocument_intentos', 'edoc.general',
|
|
lambda r: Document.objects.filter(
|
|
pedimento=r.pedimento,
|
|
archivo__icontains=r.numero_edocument,
|
|
).exclude(document_type_id__in=self.EXCLUIR_EDOC_GENERAL)),
|
|
(Cove, 'acuse_cove_estado', 'acuse_cove_intentos', 'cove.acuse',
|
|
lambda r: Document.objects.filter(
|
|
pedimento=r.pedimento,
|
|
archivo__icontains=r.numero_cove,
|
|
document_type_id=7)),
|
|
(Cove, 'cove_estado', 'cove_intentos', 'cove.general',
|
|
lambda r: Document.objects.filter(
|
|
pedimento=r.pedimento,
|
|
archivo__icontains=r.numero_cove,
|
|
).exclude(document_type_id__in=self.EXCLUIR_COVE_GENERAL)),
|
|
]
|
|
|
|
for modelo, campo_estado, campo_intentos, etiqueta, fn_documentos in flujos:
|
|
qs = modelo.objects.filter(**{campo_estado: EstadoDescarga.DESCARGADO})
|
|
if opts['organizacion']:
|
|
qs = qs.filter(organizacion_id=opts['organizacion'])
|
|
if opts['pedimento']:
|
|
qs = qs.filter(pedimento_id=opts['pedimento'])
|
|
|
|
for registro in qs.select_related('pedimento').iterator():
|
|
numero = getattr(registro, 'numero_edocument', None) or registro.numero_cove
|
|
docs = fn_documentos(registro)
|
|
# Disponible = al menos un documento con fila en BD, tamaño > 0
|
|
# y archivo físicamente presente en storage
|
|
disponible = any(
|
|
doc.size and storage_service.file_exists(doc.archivo.name)
|
|
for doc in docs
|
|
)
|
|
if disponible:
|
|
continue
|
|
|
|
detectados.append((etiqueta, str(registro.id), numero, str(registro.pedimento_id)))
|
|
if apply_changes:
|
|
with transaction.atomic():
|
|
setattr(registro, campo_estado, EstadoDescarga.ERROR)
|
|
registro.ultimo_error = (
|
|
f"Reconciliación: {etiqueta} marcado como descargado "
|
|
f"sin archivo disponible en BD/storage"
|
|
)
|
|
# save() del modelo sincroniza el booleano legado
|
|
registro.save(update_fields=[campo_estado, 'ultimo_error'])
|
|
|
|
modo = 'CORREGIDOS' if apply_changes else 'DETECTADOS (dry-run, usa --apply para corregir)'
|
|
self.stdout.write(self.style.WARNING(f"{modo}: {len(detectados)}"))
|
|
for etiqueta, registro_id, numero, pedimento_id in detectados:
|
|
self.stdout.write(f" [{etiqueta}] id={registro_id} numero={numero} pedimento={pedimento_id}")
|
|
|
|
if not detectados:
|
|
self.stdout.write(self.style.SUCCESS("Sin inconsistencias: todos los 'descargado' tienen archivo disponible"))
|