fix: filtrado de partidas por nomenclatura de documento (core/partida_docs)
Frontera (_|.|$) tras vu_PT_{app}_{numero} para cubrir los 3 formatos sin
confundir partida 1 con 11/100. Fuente unica en core/partida_docs.py, reusada
por get_documentos, handlers de borrado/descarga y fix_partidas_error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -236,7 +236,12 @@ class BulkCreateDocumentReplaceTests(APITestCase):
|
||||
from io import StringIO
|
||||
from types import SimpleNamespace
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
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.models import Partida
|
||||
from api.record.models import Document, DocumentType
|
||||
|
||||
|
||||
XML_RESPUESTA_VALIDA = (
|
||||
@@ -496,3 +501,161 @@ class FixPartidasErrorCommandTests(TestCase):
|
||||
|
||||
self.assertEqual(ids_p1, {1, 2})
|
||||
self.assertEqual(ids_p11, {3, 4})
|
||||
|
||||
|
||||
class PartidaDocsHelperTests(SimpleTestCase):
|
||||
"""Matching documento→partida (core.partida_docs), sin BD."""
|
||||
|
||||
APP = "24-01-3420-1234567"
|
||||
|
||||
def test_es_doc_de_partida_cubre_los_tres_formatos(self):
|
||||
for nombre in (
|
||||
f"documents/vu_PT_{self.APP}_1", # #1 sin extensión
|
||||
f"documents/vu_PT_{self.APP}_1.xml", # #2
|
||||
f"documents/vu_PT_{self.APP}_1_a1b2c3.xml", # #3 sufijo hex del storage
|
||||
f"documents/vu_PT_{self.APP}_1_REQUEST.xml", # REQUEST (coincide por nombre)
|
||||
):
|
||||
self.assertTrue(es_doc_de_partida(nombre, self.APP, 1), nombre)
|
||||
|
||||
def test_es_doc_de_partida_no_confunde_1_con_11_ni_100(self):
|
||||
for n in (11, 12, 100):
|
||||
self.assertFalse(es_doc_de_partida(f"vu_PT_{self.APP}_{n}.xml", self.APP, 1))
|
||||
self.assertFalse(es_doc_de_partida(f"vu_PT_{self.APP}_{n}_x.xml", self.APP, 1))
|
||||
# a la inversa: la 11 sí coincide con la 11, no con la 1
|
||||
self.assertTrue(es_doc_de_partida(f"vu_PT_{self.APP}_11.xml", self.APP, 11))
|
||||
self.assertFalse(es_doc_de_partida(f"vu_PT_{self.APP}_1.xml", self.APP, 11))
|
||||
|
||||
def test_es_doc_de_partida_case_insensitive_y_con_ruta(self):
|
||||
self.assertTrue(es_doc_de_partida(f"ORG/X/VU_PT_{self.APP}_1.XML", self.APP, 1))
|
||||
|
||||
def test_es_doc_de_partida_vacio_o_none(self):
|
||||
self.assertFalse(es_doc_de_partida("", self.APP, 1))
|
||||
self.assertFalse(es_doc_de_partida(None, self.APP, 1))
|
||||
|
||||
def test_es_doc_de_partida_legacy_segun_flag(self):
|
||||
legacy = "org/x/vu_PT_010Imp_034_3420_1234567_1.xml" # número de partida al final
|
||||
self.assertTrue(es_doc_de_partida(legacy, self.APP, 1, incluir_legacy=True))
|
||||
self.assertFalse(es_doc_de_partida(legacy, self.APP, 1, incluir_legacy=False))
|
||||
# el formato legacy tampoco confunde la 1 con la 11
|
||||
legacy11 = "org/x/vu_PT_010Imp_034_3420_1234567_11.xml"
|
||||
self.assertFalse(es_doc_de_partida(legacy11, self.APP, 1, incluir_legacy=True))
|
||||
|
||||
def test_patron_regex_partida_semantica(self):
|
||||
import re
|
||||
rx = re.compile(patron_regex_partida(self.APP, 1), re.IGNORECASE)
|
||||
self.assertTrue(rx.search(f"documents/vu_PT_{self.APP}_1"))
|
||||
self.assertTrue(rx.search(f"documents/vu_PT_{self.APP}_1.xml"))
|
||||
self.assertTrue(rx.search(f"documents/vu_PT_{self.APP}_1_a1b2.xml"))
|
||||
self.assertFalse(rx.search(f"documents/vu_PT_{self.APP}_11.xml"))
|
||||
self.assertFalse(rx.search(f"documents/vu_PT_{self.APP}_100.xml"))
|
||||
# pedimento_app se trata como literal: otro pedimento no coincide
|
||||
self.assertFalse(rx.search("documents/vu_PT_99-99-9999-9999999_1.xml"))
|
||||
# legacy solo cuando se pide
|
||||
rxl = re.compile(patron_regex_partida(self.APP, 1, incluir_legacy=True), re.IGNORECASE)
|
||||
self.assertTrue(rxl.search("vu_PT_010Imp_034_3420_1234567_1.xml"))
|
||||
|
||||
|
||||
class PartidaDocumentosSerializerTests(TestCase):
|
||||
"""get_documentos (PartidaSerializer) y el prefetch de PartidaViewSet asignan
|
||||
los documentos correctos a cada partida por nombre de archivo."""
|
||||
|
||||
APP = "24-01-3420-1234567"
|
||||
|
||||
def setUp(self):
|
||||
from api.organization.models import Organizacion
|
||||
from api.licence.models import Licencia
|
||||
from .models import Pedimento
|
||||
|
||||
self.licencia = Licencia.objects.create(nombre="LicPartDocs", almacenamiento=100)
|
||||
self.org = Organizacion.objects.create(
|
||||
nombre="OrgPartDocs", 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.p1 = Partida.objects.create(
|
||||
pedimento=self.pedimento, organizacion=self.org, numero_partida=1, descargado=True
|
||||
)
|
||||
self.p11 = Partida.objects.create(
|
||||
pedimento=self.pedimento, organizacion=self.org, numero_partida=11, descargado=True
|
||||
)
|
||||
self.type_resp = DocumentType.objects.get_or_create(id=1, defaults={"nombre": "XML"})[0]
|
||||
self.type_req = DocumentType.objects.get_or_create(id=17, defaults={"nombre": "PT Request"})[0]
|
||||
|
||||
def _doc(self, filename, doc_type=None):
|
||||
return Document.objects.create(
|
||||
organizacion=self.org, pedimento=self.pedimento,
|
||||
document_type=doc_type or self.type_resp,
|
||||
archivo=f"documents/{filename}", size=100, extension="xml",
|
||||
)
|
||||
|
||||
def _blob(self, data):
|
||||
return " ".join(d["archivo"] for d in data["documentos"])
|
||||
|
||||
def test_get_documentos_tres_formatos_sin_confundir(self):
|
||||
self._doc(f"vu_PT_{self.APP}_1") # #1 sin extensión
|
||||
self._doc(f"vu_PT_{self.APP}_1.xml") # #2
|
||||
self._doc(f"vu_PT_{self.APP}_1_a1b2c3.xml") # #3 hex
|
||||
self._doc(f"vu_PT_{self.APP}_1_REQUEST.xml", self.type_req) # tipo 17: no debe salir
|
||||
self._doc(f"vu_PT_{self.APP}_11.xml") # partida 11
|
||||
self._doc(f"vu_PT_{self.APP}_11_a1b2c3.xml") # partida 11
|
||||
|
||||
data = PartidaSerializer(self.p1).data # sin contexto -> camino fallback
|
||||
blob = self._blob(data)
|
||||
# los 3 documentos tipo-1 de la partida 1; el REQUEST (17) excluido
|
||||
self.assertEqual(len(data["documentos"]), 3)
|
||||
self.assertIn(f"vu_PT_{self.APP}_1.xml", blob)
|
||||
self.assertIn(f"vu_PT_{self.APP}_1_a1b2c3.xml", blob)
|
||||
self.assertNotIn("_11", blob) # no arrastra la partida 11
|
||||
self.assertNotIn("REQUEST", blob) # no incluye el REQUEST tipo 17
|
||||
|
||||
def test_get_documentos_incluye_legacy(self):
|
||||
self._doc("vu_PT_010Imp_034_3420_1234567_1.xml") # legacy tipo 1, número al final
|
||||
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")
|
||||
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))
|
||||
|
||||
def test_patron_regex_partida_en_bd(self):
|
||||
d1 = self._doc(f"vu_PT_{self.APP}_1")
|
||||
d2 = self._doc(f"vu_PT_{self.APP}_1.xml")
|
||||
d3 = self._doc(f"vu_PT_{self.APP}_1_a1b2c3.xml")
|
||||
d_otro = self._doc(f"vu_PT_{self.APP}_11.xml")
|
||||
ids = set(
|
||||
Document.objects.filter(
|
||||
pedimento=self.pedimento,
|
||||
archivo__iregex=patron_regex_partida(self.APP, 1),
|
||||
).values_list("id", flat=True)
|
||||
)
|
||||
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
|
||||
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")
|
||||
)
|
||||
vs = PartidaViewSet()
|
||||
# Una sola consulta sin importar cuántas partidas (evita el N+1).
|
||||
with self.assertNumQueries(1):
|
||||
vs._mapa_docs_partida(partidas)
|
||||
|
||||
Reference in New Issue
Block a user