feat: FK polimorfica Document -> {partida, cove, edocument} + backfill (T2025-09-004)

Reemplaza el matching fragil por nombre de archivo con FK reales:
- 3 FK nullables (CASCADE) en Document; resolucion central en save() por
  document_type + nombre (core.document_links), cubre toda ruta de creacion
  incluida la ingesta del microservicio; set explicito en create_vu_record.
- Comando backfill_document_links (idempotente, dry-run) para filas existentes.
- Lectura/descarga/borrado SIEMPRE por la FK (id); el nombre solo ESTABLECE la
  FK en save()/backfill. Prefetch con select_related(pedimento, fuente) sin N+1.
- Migraciones: 0004 (campos), 0005 (indices CONCURRENTLY IF NOT EXISTS, idempotente
  via SeparateDatabaseAndState), 0006 (ANALYZE document para estadisticas del planner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 08:13:47 -06:00
parent 244bbcb21c
commit 2e7d78fd8b
11 changed files with 592 additions and 196 deletions

View File

@@ -33,7 +33,7 @@ from core.permissions import (
user_has_permission,
IsInternalService,
)
from core.partida_docs import patron_regex_partida
from core.document_links import ids_documentos_entidad
import logging
logger = logging.getLogger(__name__)
@@ -725,14 +725,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
# incluir_legacy=False: el borrado es destructivo, no se elimina por match difuso.
doc_ids = []
for partida in partidas:
docs = Document.objects.filter(
pedimento_id=partida.pedimento_id,
archivo__iregex=patron_regex_partida(
partida.pedimento.pedimento_app, partida.numero_partida,
incluir_legacy=False,
),
).values_list('id', flat=True)
doc_ids.extend(docs)
doc_ids.extend(ids_documentos_entidad(partida, 'partida'))
queryset = self.get_queryset()
existing_documents = queryset.filter(id__in=doc_ids)
@@ -863,11 +856,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
# Buscar documentos que contengan el numero_cove en el nombre de archivo
doc_ids = []
for cove in coves:
docs = Document.objects.filter(
pedimento_id=cove.pedimento.id,
archivo__icontains=cove.numero_cove
).values_list('id', flat=True)
doc_ids.extend(docs)
doc_ids.extend(ids_documentos_entidad(cove, 'cove'))
queryset = self.get_queryset()
existing_documents = queryset.filter(id__in=doc_ids)
@@ -996,11 +985,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
# Buscar documentos que contengan el numero_edocument en el nombre de archivo
doc_ids = []
for edoc in edocs:
docs = Document.objects.filter(
pedimento_id=edoc.pedimento.id,
archivo__icontains=edoc.numero_edocument
).values_list('id', flat=True)
doc_ids.extend(docs)
doc_ids.extend(ids_documentos_entidad(edoc, 'edocument'))
queryset = self.get_queryset()
existing_documents = queryset.filter(id__in=doc_ids)
@@ -1907,6 +1892,10 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
document.delete()
raise Exception(f"El archivo no se encuentra en storage tras guardarlo: {file.name}")
document.archivo = ruta
# Ligar explícitamente la sub-entidad recién creada
# (exacto, sin depender del matching por nombre).
_campo_fk = {'partida': 'partida', 'cove': 'cove', 'edoc': 'edocument'}[tab_seccion]
setattr(document, _campo_fk, expediente_obj)
document.save()
else:
document.delete()
@@ -2008,14 +1997,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
# la descarga no es destructiva, así que sí incluye archivos legacy.
doc_ids = []
for partida in partidas:
docs = Document.objects.filter(
pedimento_id=partida.pedimento_id,
archivo__iregex=patron_regex_partida(
partida.pedimento.pedimento_app, partida.numero_partida,
incluir_legacy=True,
),
).values_list('id', flat=True)
doc_ids.extend(docs)
doc_ids.extend(ids_documentos_entidad(partida, 'partida'))
queryset = self.get_queryset()
docs_qs = queryset.filter(id__in=doc_ids)
@@ -2071,11 +2053,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
doc_ids = []
for cove in coves:
docs = Document.objects.filter(
pedimento_id=cove.pedimento.id,
archivo__icontains=cove.numero_cove
).values_list('id', flat=True)
doc_ids.extend(docs)
doc_ids.extend(ids_documentos_entidad(cove, 'cove'))
queryset = self.get_queryset()
docs_qs = queryset.filter(id__in=doc_ids)
@@ -2131,11 +2109,7 @@ class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
doc_ids = []
for edoc in edocs:
docs = Document.objects.filter(
pedimento_id=edoc.pedimento.id,
archivo__icontains=edoc.numero_edocument
).values_list('id', flat=True)
doc_ids.extend(docs)
doc_ids.extend(ids_documentos_entidad(edoc, 'edocument'))
queryset = self.get_queryset()
docs_qs = queryset.filter(id__in=doc_ids)