""" 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__{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))