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:
@@ -60,8 +60,7 @@ from django.core.files.base import ContentFile
|
||||
from django.db import transaction
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from api.record.models import Document, DocumentType, Fuente
|
||||
from core.partida_docs import es_doc_de_partida
|
||||
from collections import defaultdict
|
||||
from django.db.models import Prefetch
|
||||
from unicodedata import normalize
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
@@ -2334,14 +2333,21 @@ class PartidaViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
user = self.request.user
|
||||
# Precarga los documentos de respuesta (tipo 1) de cada partida vía la FK
|
||||
# real document.partida, para que get_documentos no genere N+1.
|
||||
prefetch_docs = Prefetch(
|
||||
'documents',
|
||||
queryset=Document.objects.filter(document_type_id=1).select_related('pedimento', 'fuente'),
|
||||
to_attr='documentos_vu',
|
||||
)
|
||||
if is_internal_service_request(self.request):
|
||||
return Partida.objects.all()
|
||||
return Partida.objects.all().prefetch_related(prefetch_docs)
|
||||
if not user_has_permission(user, 'partidas.view'):
|
||||
return Partida.objects.none()
|
||||
org = get_org_context(user)
|
||||
if not org:
|
||||
return Partida.objects.none()
|
||||
qs = Partida.objects.filter(pedimento__organizacion=org)
|
||||
qs = Partida.objects.filter(pedimento__organizacion=org).prefetch_related(prefetch_docs)
|
||||
# Misma precedencia que los mixins de filtrado: superuser y roles
|
||||
# operativos ven todo lo de su org; is_importador no los degrada.
|
||||
if (
|
||||
@@ -2356,41 +2362,6 @@ class PartidaViewSet(viewsets.ModelViewSet):
|
||||
return qs.filter(pedimento__contribuyente__in=user.rfc.all())
|
||||
return Partida.objects.none()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
# Precarga los documentos de la página en una sola consulta para evitar
|
||||
# el N+1 de get_documentos (una consulta regex por cada partida).
|
||||
queryset = self.filter_queryset(self.get_queryset()).select_related('pedimento')
|
||||
page = self.paginate_queryset(queryset)
|
||||
objetos = page if page is not None else list(queryset)
|
||||
ctx = self.get_serializer_context()
|
||||
ctx['docs_por_partida'] = self._mapa_docs_partida(objetos)
|
||||
serializer = self.get_serializer(objetos, many=True, context=ctx)
|
||||
if page is not None:
|
||||
return self.get_paginated_response(serializer.data)
|
||||
return Response(serializer.data)
|
||||
|
||||
def _mapa_docs_partida(self, partidas):
|
||||
"""Asigna los documentos de respuesta (tipo 1) de los pedimentos de la
|
||||
página a cada partida por nombre de archivo, en memoria.
|
||||
Devuelve {(pedimento_id, numero_partida): [Document, ...]}."""
|
||||
ped_ids = {p.pedimento_id for p in partidas}
|
||||
if not ped_ids:
|
||||
return {}
|
||||
docs_por_ped = defaultdict(list)
|
||||
qs = Document.objects.filter(
|
||||
pedimento_id__in=ped_ids, document_type_id=1,
|
||||
).select_related('pedimento')
|
||||
for d in qs:
|
||||
docs_por_ped[d.pedimento_id].append(d)
|
||||
mapa = {}
|
||||
for p in partidas:
|
||||
app = p.pedimento.pedimento_app
|
||||
mapa[(p.pedimento_id, p.numero_partida)] = [
|
||||
d for d in docs_por_ped.get(p.pedimento_id, [])
|
||||
if es_doc_de_partida(d.archivo.name, app, p.numero_partida)
|
||||
]
|
||||
return mapa
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if is_internal_service_request(self.request):
|
||||
serializer.save()
|
||||
@@ -2633,7 +2604,14 @@ class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
||||
def get_queryset(self):
|
||||
if not user_has_permission(self.request.user, 'edocuments.view'):
|
||||
return EDocument.objects.none()
|
||||
return self.get_queryset_filtrado_por_organizacion()
|
||||
# Precarga documentos del e-doc (incluye acuse; excluye REQUEST 21/25)
|
||||
# vía la FK document.edocument, para que get_documentos no genere N+1.
|
||||
prefetch_docs = Prefetch(
|
||||
'documents',
|
||||
queryset=Document.objects.exclude(document_type_id__in=[21, 25]).select_related('pedimento', 'fuente'),
|
||||
to_attr='documentos_vu',
|
||||
)
|
||||
return self.get_queryset_filtrado_por_organizacion().prefetch_related(prefetch_docs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if is_internal_service_request(self.request):
|
||||
@@ -2801,7 +2779,14 @@ class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
|
||||
def get_queryset(self):
|
||||
if not user_has_permission(self.request.user, 'coves.view'):
|
||||
return Cove.objects.none()
|
||||
return self.get_queryset_filtrado_por_organizacion()
|
||||
# Precarga documentos del cove (incluye acuse cove; excluye REQUEST 19/23)
|
||||
# vía la FK document.cove, para que get_documentos no genere N+1.
|
||||
prefetch_docs = Prefetch(
|
||||
'documents',
|
||||
queryset=Document.objects.exclude(document_type_id__in=[19, 23]).select_related('pedimento', 'fuente'),
|
||||
to_attr='documentos_vu',
|
||||
)
|
||||
return self.get_queryset_filtrado_por_organizacion().prefetch_related(prefetch_docs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
if is_internal_service_request(self.request):
|
||||
|
||||
Reference in New Issue
Block a user