""" Backfill LEGADO de la FK de cove/edocument por número único (T2025-09-004). Para documentos viejos cuyo nombre quedó con una nomenclatura olvidada —otro `pedimento_app` y/u otro prefijo (p.ej. `vu_EDC_0201_800_..._04382515ZIFF5_hex.pdf` en un pedimento cuyo `pedimento_app` es `25-80-3452-5000586`)— el matcher estricto de `backfill_document_links` no liga porque arma `vu_ed_{pedimento_app}_{numero}`. Pero el **número de cove/edoc** (único y largo) sí está en el nombre y la entidad existe. Este comando, SOLO para cove y edocument (llaves únicas; partida queda fuera por ser enteros cortos con colisiones, y los nativos no tienen entidad), liga la FK buscando el `numero_cove`/`numero_edocument` como token en el nombre, sin exigir app ni prefijo. Correr DESPUÉS de `backfill_document_links` (solo toca lo que quedó sin ligar). Uso: python manage.py backfill_document_links_legacy [--pedimento UUID] [--organizacion UUID] [--dry-run] """ from django.core.management.base import BaseCommand from django.db import transaction from api.customs.models import Cove, EDocument from api.record.models import Document from core.document_links import ( COVE_TYPES, EDOCUMENT_TYPES, SECCION_CAMPO, SECCION_LLAVE, numero_en_nombre, seccion_de_tipo, ) class Command(BaseCommand): help = "Liga documentos legados de cove/edoc (FK nula) por su número único en el nombre, ignorando app/prefijo viejos. Correr después de backfill_document_links." def add_arguments(self, parser): parser.add_argument('--pedimento', type=str, default=None) parser.add_argument('--organizacion', type=str, default=None) parser.add_argument('--dry-run', action='store_true', help='Reporta sin escribir') def handle(self, *args, **opts): dry_run = opts['dry_run'] tipos = sorted(COVE_TYPES | EDOCUMENT_TYPES) base = Document.objects.filter( partida__isnull=True, cove__isnull=True, edocument__isnull=True, document_type_id__in=tipos, ) if opts['pedimento']: base = base.filter(pedimento_id=opts['pedimento']) if opts['organizacion']: base = base.filter(pedimento__organizacion_id=opts['organizacion']) if dry_run: self.stdout.write(self.style.WARNING("== DRY-RUN: no se escribe nada ==")) # order_by() limpia el ordering por defecto del modelo (created_at), que si # no se quita se cuela en el DISTINCT y duplica los pedimento_id. ped_ids = list(base.order_by().values_list('pedimento_id', flat=True).distinct()) stats = {'pedimentos': 0, 'cove': 0, 'edocument': 0, 'ambiguos': 0, 'sin_match': 0} for ped_id in ped_ids: docs = list(base.filter(pedimento_id=ped_id)) entidades = { 'cove': list(Cove.objects.filter(pedimento_id=ped_id)), 'edocument': list(EDocument.objects.filter(pedimento_id=ped_id)), } lote = [] for doc in docs: seccion = seccion_de_tipo(doc.document_type_id) llave = SECCION_LLAVE[seccion] matches = [ e for e in entidades[seccion] if numero_en_nombre(doc.archivo.name, getattr(e, llave)) ] if len(matches) == 1: setattr(doc, SECCION_CAMPO[seccion], matches[0]) lote.append(doc) stats[seccion] += 1 elif len(matches) > 1: stats['ambiguos'] += 1 # número ambiguo: se deja sin ligar else: stats['sin_match'] += 1 # ni por número aparece la entidad if lote: stats['pedimentos'] += 1 self.stdout.write(" %s: %d doc(s) ligados por número" % (str(ped_id)[:8], len(lote))) if not dry_run: with transaction.atomic(): Document.objects.bulk_update(lote, ['cove', 'edocument'], batch_size=500) self.stdout.write("") self.stdout.write(self.style.SUCCESS( "Pedimentos: %d | ligados → cove=%d edocument=%d | ambiguos: %d | sin match ni por número: %d" % (stats['pedimentos'], stats['cove'], stats['edocument'], stats['ambiguos'], stats['sin_match']) ))