Algunos documentos viejos quedaron con nomenclatura olvidada (otro pedimento_app y/o prefijo, p.ej. vu_EDC_0201_800_..._04382515ZIFF5) que el matcher estricto no liga. Como el numero_cove/numero_edocument es unico y esta en el nombre, este comando los liga por ese numero (con frontera), sin exigir app ni prefijo. Solo cove/edoc (llaves unicas); partida queda fuera (enteros cortos -> colisiones) y nativos no tienen entidad. Correr despues de backfill_document_links. Agrega core.document_links.numero_en_nombre. Fix: el DISTINCT de pedimento_id limpia el ordering por defecto del modelo para no duplicar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
133 lines
5.7 KiB
Python
133 lines
5.7 KiB
Python
"""
|
|
Resolución de la sub-entidad (partida / cove / edocument) a la que pertenece un
|
|
Document, para poblar sus FK reales (`document.partida`/`cove`/`edocument`).
|
|
|
|
Fuente única, reusada por `Document.save()` (alta de filas nuevas) y por el comando
|
|
`backfill_document_links` (filas existentes). Generaliza la frontera de
|
|
[core.partida_docs] al resto de secciones.
|
|
|
|
Discriminador de sección: el `document_type_id` (lo asigna el microservicio al
|
|
generar el archivo). El nombre de archivo solo decide **cuál** registro dentro de
|
|
la sección, vía frontera tras `vu_<sec>_{pedimento_app}_{numero}` — NO se extrae la
|
|
llave partiendo por `_`, porque `pedimento_app` puede contener `_`
|
|
(p.ej. `vu_AC_0101_230_1703_3004804_4.pdf`): se itera la entidad con su número exacto.
|
|
|
|
Mapa tipo→sección (autoritativo, microservice/api/api_v2/modules/*/services.py):
|
|
Partida : 1 (respuesta), 17 (request), 18 (error)
|
|
Cove : 8 (respuesta), 7 (acuse), 19/20 (request/error), 23/24 (acuse request/error)
|
|
EDocument : 5 (respuesta), 4 (acuse), 21/22 (request/error), 25/26 (acuse request/error)
|
|
Nativos (sin FK): 2 (PC), 3 (remesa), 6, 13-16, y subidas sin tipo.
|
|
"""
|
|
|
|
import posixpath
|
|
import re
|
|
|
|
from core.partida_docs import es_doc_de_partida
|
|
|
|
PARTIDA_TYPES = {1, 17, 18}
|
|
COVE_TYPES = {7, 8, 19, 20, 23, 24}
|
|
EDOCUMENT_TYPES = {4, 5, 21, 22, 25, 26}
|
|
|
|
TYPE_TO_SECTION = {}
|
|
for _t in PARTIDA_TYPES:
|
|
TYPE_TO_SECTION[_t] = 'partida'
|
|
for _t in COVE_TYPES:
|
|
TYPE_TO_SECTION[_t] = 'cove'
|
|
for _t in EDOCUMENT_TYPES:
|
|
TYPE_TO_SECTION[_t] = 'edocument'
|
|
|
|
# Campo FK en Document y campo de llave de negocio en la entidad, por sección.
|
|
SECCION_CAMPO = {'partida': 'partida', 'cove': 'cove', 'edocument': 'edocument'}
|
|
SECCION_LLAVE = {'partida': 'numero_partida', 'cove': 'numero_cove', 'edocument': 'numero_edocument'}
|
|
# related_name del FK a Pedimento en cada entidad (EDocument usa 'documentos').
|
|
SECCION_RELACION = {'partida': 'partidas', 'cove': 'coves', 'edocument': 'documentos'}
|
|
# Prefijos de nombre por sección (cove y edoc tienen variante de acuse).
|
|
SECCION_PREFIJOS = {
|
|
'partida': ('vu_pt',),
|
|
'cove': ('vu_cove', 'vu_ac_cove'),
|
|
'edocument': ('vu_ed', 'vu_ac'),
|
|
}
|
|
|
|
|
|
def seccion_de_tipo(document_type_id):
|
|
"""Sección a la que pertenece un document_type_id, o None si es nativo."""
|
|
if document_type_id is None:
|
|
return None
|
|
return TYPE_TO_SECTION.get(int(document_type_id))
|
|
|
|
|
|
def _coincide_prefijo(base, prefijo):
|
|
"""True si `base` empieza con `prefijo` seguido de frontera (`_`, `.` o fin)."""
|
|
return base.startswith(prefijo) and (len(base) == len(prefijo) or base[len(prefijo)] in "_.")
|
|
|
|
|
|
def coincide(nombre_archivo, seccion, pedimento_app, numero):
|
|
"""Indica si `nombre_archivo` corresponde a (seccion, pedimento_app, numero)."""
|
|
if seccion == 'partida':
|
|
# Reusa el matcher de partidas (incluye formato legacy).
|
|
return es_doc_de_partida(nombre_archivo, pedimento_app, numero)
|
|
base = posixpath.basename(nombre_archivo or "").lower()
|
|
app = str(pedimento_app).strip().lower()
|
|
num = str(numero).strip().lower()
|
|
return any(
|
|
_coincide_prefijo(base, f"{pref}_{app}_{num}")
|
|
for pref in SECCION_PREFIJOS[seccion]
|
|
)
|
|
|
|
|
|
def numero_en_nombre(nombre_archivo, numero):
|
|
"""True si `numero` aparece como token completo en el basename (con frontera
|
|
`_`/`.`/inicio/fin), SIN exigir prefijo ni pedimento_app.
|
|
|
|
Para documentos LEGADOS cuyo nombre trae otro app/prefijo (p.ej.
|
|
`vu_EDC_0201_800_..._04382515ZIFF5_hex.pdf`) pero cuyo número de cove/edoc
|
|
(único y largo) sí está en el nombre. NO usar con partida (enteros cortos →
|
|
colisiones).
|
|
"""
|
|
base = posixpath.basename(nombre_archivo or "").lower()
|
|
num = re.escape(str(numero).strip().lower())
|
|
if not num:
|
|
return False
|
|
return re.search(rf"(?:^|_){num}(?:_|\.|$)", base) is not None
|
|
|
|
|
|
def match_entidad(nombre_archivo, seccion, pedimento_app, entidades):
|
|
"""Devuelve la entidad de `entidades` cuyo número coincide con el archivo, o None.
|
|
|
|
`entidades` es un iterable de instancias del modelo de la sección (Partida /
|
|
Cove / EDocument). Pensado para uso en memoria (backfill con listas precargadas).
|
|
"""
|
|
llave = SECCION_LLAVE[seccion]
|
|
for ent in entidades:
|
|
if coincide(nombre_archivo, seccion, pedimento_app, getattr(ent, llave)):
|
|
return ent
|
|
return None
|
|
|
|
|
|
def resolver_fk(document):
|
|
"""Resuelve la FK de sub-entidad de un Document → (campo, instancia | None).
|
|
|
|
`campo` es 'partida' | 'cove' | 'edocument' (el atributo a setear), o (None, None)
|
|
si el documento es nativo de pedimento o no tiene archivo. Hace las consultas
|
|
necesarias; para lotes usar `match_entidad` con entidades precargadas.
|
|
"""
|
|
seccion = seccion_de_tipo(getattr(document, 'document_type_id', None))
|
|
if not seccion or not document.archivo:
|
|
return (None, None)
|
|
pedimento = document.pedimento
|
|
entidades = getattr(pedimento, SECCION_RELACION[seccion]).all()
|
|
inst = match_entidad(document.archivo.name, seccion, pedimento.pedimento_app, entidades)
|
|
return (SECCION_CAMPO[seccion], inst)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Lectura / borrado / descarga: SIEMPRE por la FK real (no por nombre). El nombre
|
|
# solo se usa para ESTABLECER la FK (Document.save() en altas, backfill en filas
|
|
# viejas). Requiere que el backfill esté completo para datos previos.
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def ids_documentos_entidad(entidad, seccion):
|
|
"""IDs de los documentos de la entidad (borrado/descarga) por la FK real."""
|
|
from api.record.models import Document # import diferido: evita ciclo con record.models
|
|
return list(Document.objects.filter(**{SECCION_CAMPO[seccion]: entidad}).values_list('id', flat=True))
|