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

@@ -0,0 +1,94 @@
"""
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
)

View File

@@ -11,10 +11,8 @@ from api.customs.models import (
)
from django.db import models
from django.db.models import Q
from api.record.models import Document # Asegúrate de importar el modelo Documento
from api.record.serializers import DocumentSerializer
from api.vucem.serializers import VucemSerializer
from core.partida_docs import es_doc_de_partida
import logging
logger = logging.getLogger(__name__)
@@ -52,32 +50,16 @@ class PartidaSerializer(serializers.ModelSerializer):
documentos = serializers.SerializerMethodField()
def get_documentos(self, obj):
if not obj or not getattr(obj, 'pedimento', None) or not getattr(obj, 'numero_partida', None):
if not obj:
return []
try:
# El matching documento→partida se hace por nombre de archivo con
# frontera real (core.partida_docs); document_type_id=1 son los
# documentos de respuesta de partida (excluye REQUEST/ERROR 17/18).
mapa = self.context.get('docs_por_partida')
if mapa is not None:
# Camino optimizado: la vista precargó el mapa de la página.
docs = mapa.get((obj.pedimento_id, obj.numero_partida), [])
else:
# Fallback (retrieve u otros callers): una consulta por partida.
qs = Document.objects.filter(
pedimento=obj.pedimento,
document_type_id=1,
).select_related('pedimento') # evita N+1 en DocumentSerializer.get_pedimento_numero
app = obj.pedimento.pedimento_app
docs = [d for d in qs if es_doc_de_partida(d.archivo.name, app, obj.numero_partida)]
org_id = getattr(obj, 'organizacion_id', None)
if org_id:
docs = [d for d in docs if d.organizacion_id == org_id]
# Documentos de respuesta de la partida (tipo 1) vía la FK real
# document.partida. 'documentos_vu' lo precarga el ViewSet con prefetch;
# si no está, se consulta directo (retrieve u otros callers).
docs = getattr(obj, 'documentos_vu', None)
if docs is None:
docs = list(obj.documents.filter(document_type_id=1).select_related('pedimento', 'fuente'))
return DocumentSerializer(docs, many=True, context=self.context).data
except Exception as e:
logger.warning("get_documentos partida %s: %s", getattr(obj, 'id', '?'), e)
return []
@@ -170,43 +152,18 @@ class EDocumentSerializer(serializers.ModelSerializer):
documentos = serializers.SerializerMethodField()
def get_documentos(self, obj):
"""
Busca documentos en la tabla `document` que coincidan con el
`numero_edocument` dentro del nombre del archivo (`archivo`). Se
filtra por organización para evitar devolver documentos de otras orgs.
Devuelve la serialización completa de los documentos encontrados:
1. Empiecen con 'vu_EDOCUMENT' en el nombre del archivo
2. Terminen con el numero_edocument + .xml
3. Pertenezcan a la misma organización
"""
if not obj or not getattr(obj, 'numero_edocument', None):
"""Documentos del e-documento (incluye acuse y errores; excluye solo los
REQUEST 21/25) vía la FK real document.edocument. 'documentos_vu' lo
precarga el ViewSet con prefetch; si no está, se consulta directo."""
if not obj:
return []
if not obj or not getattr(obj, 'pedimento', None):
return []
# if not obj or not getattr(obj, 'pedimento_id', None):
# return []
try:
numero = str(obj.numero_edocument).strip()
# id_pedimento = str(obj.pedimento_id).strip()
# excluir solo request (21, 25); errores (22, 26) se incluyen para detección en frontend
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
).exclude(document_type_id__in=[21, 25])
# Filtro por organización si aplica
if hasattr(obj, 'organizacion') and obj.organizacion:
qs = qs.filter(organizacion=obj.organizacion)
serializer = DocumentSerializer(qs, many=True, context=self.context)
return serializer.data
except Exception:
# En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía
docs = getattr(obj, 'documentos_vu', None)
if docs is None:
docs = list(obj.documents.exclude(document_type_id__in=[21, 25]).select_related('pedimento', 'fuente'))
return DocumentSerializer(docs, many=True, context=self.context).data
except Exception as e:
logger.warning("get_documentos edocument %s: %s", getattr(obj, 'id', '?'), e)
return []
class Meta:
@@ -262,39 +219,18 @@ class CoveSerializer(serializers.ModelSerializer):
return attrs
def get_documentos(self, obj):
"""
Busca documentos en la tabla `document` que coincidan con el
`numero_cove` dentro del nombre del archivo (`archivo`). Se
filtra por organización para evitar devolver documentos de otras orgs.
Devuelve la serialización completa de los documentos encontrados:
1. Empiecen con 'vu_COVE' en el nombre del archivo
2. Terminen con el numero_cove + .xml
3. Pertenezcan a la misma organización
"""
if not obj or not getattr(obj, 'numero_cove', None):
"""Documentos del cove (incluye acuse cove y errores; excluye solo los
REQUEST 19/23) vía la FK real document.cove. 'documentos_vu' lo precarga
el ViewSet con prefetch; si no está, se consulta directo."""
if not obj:
return []
if not obj or not getattr(obj, 'pedimento', None):
return []
try:
numero = str(obj.numero_cove).strip()
# Excluir solo request (19, 23); errores (20, 24) se incluyen para detección en frontend
qs = Document.objects.filter(
pedimento=obj.pedimento,
archivo__icontains=numero,
).exclude(document_type_id__in=[19, 23])
# Filtro por organización si aplica
if hasattr(obj, 'organizacion') and obj.organizacion:
qs = qs.filter(organizacion=obj.organizacion)
serializer = DocumentSerializer(qs, many=True, context=self.context)
return serializer.data
except Exception:
# En caso de cualquier error (por ejemplo, importaciones circulares), devolver lista vacía
docs = getattr(obj, 'documentos_vu', None)
if docs is None:
docs = list(obj.documents.exclude(document_type_id__in=[19, 23]).select_related('pedimento', 'fuente'))
return DocumentSerializer(docs, many=True, context=self.context).data
except Exception as e:
logger.warning("get_documentos cove %s: %s", getattr(obj, 'id', '?'), e)
return []
class ImportadorSerializer(serializers.ModelSerializer):

View File

@@ -238,8 +238,7 @@ from types import SimpleNamespace
from django.core.management import call_command
from django.test import TestCase, SimpleTestCase
from core.partida_docs import es_doc_de_partida, patron_regex_partida
from api.customs.serializers import PartidaSerializer
from api.customs.views import PartidaViewSet
from api.customs.serializers import PartidaSerializer, CoveSerializer, EDocumentSerializer
from api.customs.models import Partida
from api.record.models import Document, DocumentType
@@ -615,24 +614,16 @@ class PartidaDocumentosSerializerTests(TestCase):
data = PartidaSerializer(self.p1).data
self.assertEqual(len(data["documentos"]), 1)
def test_get_documentos_mismo_resultado_con_y_sin_prefetch(self):
self._doc(f"vu_PT_{self.APP}_1.xml")
def test_get_documentos_via_fk(self):
# save() liga la FK al crear; get_documentos lee por la FK real.
d1 = self._doc(f"vu_PT_{self.APP}_1.xml")
self._doc(f"vu_PT_{self.APP}_11.xml")
vs = PartidaViewSet()
mapa = vs._mapa_docs_partida([self.p1, self.p11])
ids_p1 = {d.id for d in mapa[(self.pedimento.id, 1)]}
ids_p11 = {d.id for d in mapa[(self.pedimento.id, 11)]}
self.assertEqual(len(ids_p1), 1)
self.assertEqual(len(ids_p11), 1)
self.assertTrue(ids_p1.isdisjoint(ids_p11))
sin_ctx = PartidaSerializer(self.p1).data
con_ctx = PartidaSerializer(self.p1, context={"docs_por_partida": mapa}).data
self.assertEqual(len(sin_ctx["documentos"]), 1)
self.assertEqual(len(con_ctx["documentos"]), 1)
self.assertIn(f"vu_PT_{self.APP}_1.xml", self._blob(con_ctx))
self.assertNotIn("_11", self._blob(con_ctx))
d1.refresh_from_db()
self.assertEqual(d1.partida_id, self.p1.id) # FK ligada en save()
data = PartidaSerializer(self.p1).data
self.assertEqual(len(data["documentos"]), 1)
self.assertIn(f"vu_PT_{self.APP}_1.xml", self._blob(data))
self.assertNotIn("_11", self._blob(data))
def test_patron_regex_partida_en_bd(self):
d1 = self._doc(f"vu_PT_{self.APP}_1")
@@ -648,14 +639,144 @@ class PartidaDocumentosSerializerTests(TestCase):
self.assertEqual(ids, {d1.id, d2.id, d3.id})
self.assertNotIn(d_otro.id, ids)
def test_mapa_docs_partida_es_una_sola_consulta(self):
# documentos para varias partidas del mismo pedimento
def test_prefetch_documentos_vu_evita_n_plus_1(self):
from django.db.models import Prefetch
self._doc(f"vu_PT_{self.APP}_1.xml")
self._doc(f"vu_PT_{self.APP}_11.xml")
partidas = list(
Partida.objects.filter(pedimento=self.pedimento).select_related("pedimento")
prefetch = Prefetch(
'documents',
queryset=Document.objects.filter(document_type_id=1).select_related('pedimento'),
to_attr='documentos_vu',
)
vs = PartidaViewSet()
# Una sola consulta sin importar cuántas partidas (evita el N+1).
with self.assertNumQueries(1):
vs._mapa_docs_partida(partidas)
partidas = list(
Partida.objects.filter(pedimento=self.pedimento)
.order_by('numero_partida').prefetch_related(prefetch)
)
# Serializar la lista no dispara consultas extra: todo viene del prefetch.
with self.assertNumQueries(0):
data = PartidaSerializer(partidas, many=True).data
por_num = {d['numero_partida']: d['documentos'] for d in data}
self.assertEqual(len(por_num[1]), 1)
self.assertEqual(len(por_num[11]), 1)
class DocumentLinksHelperTests(SimpleTestCase):
"""Resolver tipo→sección y matcher por frontera (core.document_links), sin BD."""
# pedimento_app con guiones bajos (caso real): no se puede extraer la llave
# partiendo por '_'; por eso se itera la entidad con su número exacto.
APP = "0101_230_1703_3004804"
def test_seccion_de_tipo(self):
from core.document_links import seccion_de_tipo
self.assertEqual(seccion_de_tipo(1), 'partida')
self.assertEqual(seccion_de_tipo(8), 'cove')
self.assertEqual(seccion_de_tipo(7), 'cove') # acuse cove
self.assertEqual(seccion_de_tipo(5), 'edocument')
self.assertEqual(seccion_de_tipo(4), 'edocument') # acuse edoc
self.assertIsNone(seccion_de_tipo(2)) # PC nativo
self.assertIsNone(seccion_de_tipo(3)) # remesa nativo
self.assertIsNone(seccion_de_tipo(None))
def test_coincide_cove(self):
from core.document_links import coincide
self.assertTrue(coincide(f"documents/vu_COVE_{self.APP}_654001.xml", 'cove', self.APP, "654001"))
self.assertTrue(coincide(f"documents/vu_AC_COVE_{self.APP}_654001.pdf", 'cove', self.APP, "654001"))
self.assertTrue(coincide(f"documents/vu_COVE_{self.APP}_654001_REQUEST.xml", 'cove', self.APP, "654001"))
# colisión de prefijo: 654001 no debe matchear 6540012
self.assertFalse(coincide(f"documents/vu_COVE_{self.APP}_6540012.xml", 'cove', self.APP, "654001"))
def test_coincide_edocument(self):
from core.document_links import coincide
self.assertTrue(coincide(f"documents/vu_ED_{self.APP}_EDOC001.pdf", 'edocument', self.APP, "EDOC001"))
self.assertTrue(coincide(f"documents/vu_AC_{self.APP}_EDOC001.pdf", 'edocument', self.APP, "EDOC001"))
self.assertFalse(coincide(f"documents/vu_ED_{self.APP}_EDOC0011.pdf", 'edocument', self.APP, "EDOC001"))
class DocumentFKResolutionTests(TestCase):
"""save()-resolución de FK por sección, lectura cove/edoc por FK y backfill."""
APP = "0101_230_1703_3004804" # con guiones bajos
def setUp(self):
from api.organization.models import Organizacion
from api.licence.models import Licencia
from .models import Pedimento, Cove, EDocument
self.licencia = Licencia.objects.create(nombre="LicFK", almacenamiento=100)
self.org = Organizacion.objects.create(
nombre="OrgFK", licencia=self.licencia, is_active=True, is_verified=True
)
self.pedimento = Pedimento.objects.create(
organizacion=self.org, pedimento="1234567", pedimento_app=self.APP,
aduana="034", patente="3420", numero_operacion="12345678",
)
self.partida = Partida.objects.create(
pedimento=self.pedimento, organizacion=self.org, numero_partida=3, descargado=True
)
self.cove = Cove.objects.create(
pedimento=self.pedimento, organizacion=self.org, numero_cove="654001"
)
self.edoc = EDocument.objects.create(
pedimento=self.pedimento, organizacion=self.org, numero_edocument="EDOC001"
)
for tid, nombre in [(1, "PT"), (8, "COVE"), (7, "AC_COVE"), (5, "ED"),
(4, "AC_ED"), (2, "PC"), (19, "COVE_REQ")]:
DocumentType.objects.get_or_create(id=tid, defaults={"nombre": nombre})
def _doc(self, filename, type_id):
return Document.objects.create(
organizacion=self.org, pedimento=self.pedimento, document_type_id=type_id,
archivo=f"documents/{filename}", size=100, extension="xml",
)
def test_save_liga_fk_por_seccion(self):
d_pt = self._doc(f"vu_PT_{self.APP}_3.xml", 1)
d_cv = self._doc(f"vu_COVE_{self.APP}_654001.xml", 8)
d_accv = self._doc(f"vu_AC_COVE_{self.APP}_654001.pdf", 7)
d_ed = self._doc(f"vu_ED_{self.APP}_EDOC001.pdf", 5)
d_aced = self._doc(f"vu_AC_{self.APP}_EDOC001.pdf", 4)
d_pc = self._doc(f"vu_PC_{self.APP}.xml", 2) # nativo: sin FK
for d in (d_pt, d_cv, d_accv, d_ed, d_aced, d_pc):
d.refresh_from_db()
self.assertEqual(d_pt.partida_id, self.partida.id)
self.assertEqual(d_cv.cove_id, self.cove.id)
self.assertEqual(d_accv.cove_id, self.cove.id) # acuse cove → cove padre
self.assertEqual(d_ed.edocument_id, self.edoc.id)
self.assertEqual(d_aced.edocument_id, self.edoc.id) # acuse edoc → edoc padre
self.assertIsNone(d_pc.partida_id)
self.assertIsNone(d_pc.cove_id)
self.assertIsNone(d_pc.edocument_id)
def test_lectura_cove_edoc_por_fk(self):
self._doc(f"vu_COVE_{self.APP}_654001.xml", 8)
self._doc(f"vu_AC_COVE_{self.APP}_654001.pdf", 7)
self._doc(f"vu_COVE_{self.APP}_654001_REQUEST.xml", 19) # request: excluido en lectura
self._doc(f"vu_ED_{self.APP}_EDOC001.pdf", 5)
cove_data = CoveSerializer(self.cove).data
edoc_data = EDocumentSerializer(self.edoc).data
self.assertEqual(len(cove_data["documentos"]), 2) # cove + acuse, sin request
self.assertEqual(len(edoc_data["documentos"]), 1)
def test_backfill_liga_filas_existentes(self):
d_pt = self._doc(f"vu_PT_{self.APP}_3.xml", 1)
d_cv = self._doc(f"vu_COVE_{self.APP}_654001.xml", 8)
# Simular filas viejas sin ligar (save() las ligó; las desligamos en BD).
Document.objects.filter(id__in=[d_pt.id, d_cv.id]).update(
partida=None, cove=None, edocument=None
)
# dry-run no escribe
call_command("backfill_document_links", pedimento=str(self.pedimento.id),
dry_run=True, stdout=StringIO())
d_pt.refresh_from_db()
self.assertIsNone(d_pt.partida_id)
# ejecución real liga
call_command("backfill_document_links", pedimento=str(self.pedimento.id), stdout=StringIO())
d_pt.refresh_from_db()
d_cv.refresh_from_db()
self.assertEqual(d_pt.partida_id, self.partida.id)
self.assertEqual(d_cv.cove_id, self.cove.id)
# idempotente: re-ejecutar no rompe ni cambia
call_command("backfill_document_links", pedimento=str(self.pedimento.id), stdout=StringIO())
d_pt.refresh_from_db()
self.assertEqual(d_pt.partida_id, self.partida.id)

View File

@@ -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):