""" Fuente única de la convención de nombres documento → partida. Las tablas `document` y `partida` no tienen FK entre sí; solo comparten `pedimento_id`. La pertenencia de un documento a una partida se infiere del nombre del archivo, que tiene tres formatos para los documentos de partida (`document_type_id = 1`): 1. vu_PT_{pedimento_app}_{numero} (sin extensión — formato original) 2. vu_PT_{pedimento_app}_{numero}.xml 3. vu_PT_{pedimento_app}_{numero}_{hex}.xml (sufijo de unicidad del storage) REQUEST / ERROR son vu_PT_{app}_{numero}_REQUEST.xml / _ERROR.xml (tipos 17/18). Formato legacy: vu_PT_..._{numero}.xml (el número de partida queda al final). El matching debe usar una FRONTERA real tras el número (`_`, `.` o fin de cadena) para cubrir los tres formatos sin confundir la partida 1 con la 11/100. Aquí viven las dos implementaciones equivalentes de esa regla: - patron_regex_partida : para filtros en BD (Document.archivo__iregex). - es_doc_de_partida : para asignación en memoria (basename). """ import posixpath import re def patron_regex_partida(pedimento_app, numero_partida, incluir_legacy=False): """Construye el patrón para `Document.objects.filter(archivo__iregex=...)`. PostgreSQL aplica el patrón con `~*` (no anclado), por lo que el prefijo de ruta `documents/` es irrelevante. La frontera `(_|\\.|$)` tras el número cubre los tres formatos y rechaza la colisión de prefijo (la partida 1 no matchea con 11/100 porque tras el `1` vendría un dígito, no la frontera). El filtro `document_type_id` se decide en el call site: el patrón también matchea `_REQUEST`/`_ERROR`, así que quien quiera excluirlos debe filtrar por tipo. `pedimento_app` se escapa siempre (es un CharField sin validación). incluir_legacy: agrega el formato viejo con el número al final. Es un match difuso (`.+`), úsalo solo en lectura, nunca en operaciones destructivas. """ app = re.escape(str(pedimento_app).strip()) num = re.escape(str(numero_partida).strip()) patron = rf"vu_PT_{app}_{num}(_|\.|$)" if incluir_legacy: patron = rf"({patron}|vu_PT_.+_{num}\.xml$)" return patron def es_doc_de_partida(nombre_archivo, pedimento_app, numero_partida, incluir_legacy=True): """Indica si `nombre_archivo` pertenece a la partida, evaluado en memoria. Equivalente a `patron_regex_partida` pero sobre el basename en minúsculas. Fuente única para la lectura (PartidaSerializer) y para el comando fix_partidas_error. La frontera exige que tras `vu_pt_{app}_{numero}` venga `_`, `.` o el fin de la cadena (formato #1 sin extensión). """ base = posixpath.basename(nombre_archivo or "").lower() prefijo = f"vu_pt_{pedimento_app}_{numero_partida}".lower() if base.startswith(prefijo) and (len(base) == len(prefijo) or base[len(prefijo)] in "_."): return True if incluir_legacy: return re.match(rf"^vu_pt_.+_{re.escape(str(numero_partida))}\.xml$", base) is not None return False