""" Backfill de la FK de sub-entidad en documentos existentes (T2025-09-004). Liga cada Document a su Partida / Cove / EDocument por nombre de archivo, usando el mismo resolver que `Document.save()` (core.document_links). Solo toca documentos aún no ligados (idempotente: re-ejecutar converge). Los documentos nativos del pedimento (PC, remesa, subidas generales) y los que no matchean ninguna entidad se dejan sin FK. Uso: python manage.py backfill_document_links [--pedimento UUID] [--organizacion UUID] [--offset N] [--limit N] [--dry-run] """ from django.core.management.base import BaseCommand from django.db import transaction from api.customs.models import Pedimento from api.record.models import Document from core.document_links import SECCION_CAMPO, match_entidad, seccion_de_tipo # related_name del FK a Pedimento en cada entidad (EDocument usa 'documentos'). _RELACION = {'partida': 'partidas', 'cove': 'coves', 'edocument': 'documentos'} class Command(BaseCommand): help = "Liga documentos existentes a su sub-entidad (partida/cove/edocument) por nombre de archivo." def add_arguments(self, parser): parser.add_argument('--pedimento', type=str, default=None, help='UUID de un solo pedimento') parser.add_argument('--organizacion', type=str, default=None, help='UUID de organización') parser.add_argument('--offset', type=int, default=0) parser.add_argument('--limit', type=int, default=None) parser.add_argument('--dry-run', action='store_true', help='Reporta sin escribir') def handle(self, *args, **opts): dry_run = opts['dry_run'] peds = Pedimento.objects.all().order_by('created_at', 'id') if opts['pedimento']: peds = peds.filter(id=opts['pedimento']) if opts['organizacion']: peds = peds.filter(organizacion_id=opts['organizacion']) offset = opts['offset'] or 0 peds = peds[offset:offset + opts['limit']] if opts['limit'] else peds[offset:] if dry_run: self.stdout.write(self.style.WARNING("== DRY-RUN: no se escribe nada ==")) stats = {'pedimentos': 0, 'partida': 0, 'cove': 0, 'edocument': 0, 'sin_match': 0} for ped in peds.iterator(): self._procesar_pedimento(ped, dry_run, stats) self.stdout.write("") self.stdout.write(self.style.SUCCESS( f"Pedimentos: {stats['pedimentos']} | ligados → partida={stats['partida']} " f"cove={stats['cove']} edocument={stats['edocument']} | " f"tipados sin entidad que matchee={stats['sin_match']}" )) def _procesar_pedimento(self, ped, dry_run, stats): # Solo documentos aún NO ligados (idempotente). docs = list(Document.objects.filter( pedimento=ped, partida__isnull=True, cove__isnull=True, edocument__isnull=True, )) if not docs: return stats['pedimentos'] += 1 # Precargar las entidades del pedimento una sola vez; el match es en memoria. entidades = {sec: list(getattr(ped, rel).all()) for sec, rel in _RELACION.items()} app = ped.pedimento_app lote = [] for doc in docs: seccion = seccion_de_tipo(doc.document_type_id) if not seccion or not doc.archivo: continue # nativo de pedimento o sin archivo inst = match_entidad(doc.archivo.name, seccion, app, entidades[seccion]) if inst is None: stats['sin_match'] += 1 continue setattr(doc, SECCION_CAMPO[seccion], inst) lote.append(doc) stats[seccion] += 1 # SELECT + COUNT previo antes de escribir (estándar de la organización). self.stdout.write( f" {ped.pedimento_app}: {len(lote)} de {len(docs)} doc(s) sin ligar serán ligados" ) if lote and not dry_run: with transaction.atomic(): Document.objects.bulk_update( lote, ['partida', 'cove', 'edocument'], batch_size=1000 )