Skip to content

consulta-xmls

Objetivo

App de solo consulta para contabilidad: XMLs (CFDI 4.0) timbrados del mes sin registro ligado en MPRO (match por UUID), más panel de historial de proveedor. Documentado también en Stack de BI — es el lado //.211/SincronizarXml ──py──► raw_sat del diagrama de flujo de datos.

Dónde vive el código

Dos piezas separadas, en dos lugares distintos (split 2026-08-31 — antes vivían juntas, fuera de git, en ~/por_ordenar/exploracion/consulta_xmls_gastos/):

  • El ELT (carga de XML → Postgres): trivasa-bi-core/raw_sat_xml/, en git. Ya no es exploración — es la rama //.211/SincronizarXml ──py──► raw_sat del diagrama de flujo de datos, documentada como parte de la arquitectura desde antes en Stack de BI pero nunca colocada físicamente ahí. Se movió en vez de solo repuntar rutas porque ya tenía cron + mount persistentes — dejó de ser "prototipo descartable" hace tiempo. Tres loaders independientes, una tabla cada uno (extendido 2026-09-02, originalmente solo recibidos):
  • cargar_cfdi_recibidos.py → raw_sat.cfdi_recibidos. Carpeta XML RECIBIDOS, convención de carpetas estable ({año}/{año}_{mes}/ {año}_{mes}_AC).
  • cargar_cfdi_emitidos.py → raw_sat.cfdi_emitidos. Carpeta XML EMITIDOS — mezcla CFDI 3.3 y 4.0 (namespace detectado por archivo, no fijo) y perdió la convención de carpetas desde 2025 (ver --anio en el docstring del script para el backfill histórico). Excluye recibos de nómina (carpeta _CA en años viejos, detectado por contenido — complemento Nomina — en años sin ese split): son otro tipo de negocio, no ingreso, decisión explícita 2026-09-02.
  • cargar_cfdi_retencion.py → raw_sat.cfdi_retencion. Carpeta XML RETENCIONES — esquema de SAT distinto (CFDI de Retenciones e Información de Pagos, namespace retencionpago/1 o /2, no cfdi:Comprobante), caso de negocio real: ISR retenido sobre intereses pagados a accionistas/inversionistas. Volumen chico (~2,400 XML en 10 años) así que no filtra por periodo — recorre toda la carpeta cada corrida, costo trivial y se auto-repara si la carpeta cambia de convención otra vez.
  • mnt_xml/ — punto de montaje del recurso de red (ver abajo), gitignored, compartido por los tres loaders (mismo mount, subcarpetas distintas).
  • Credenciales de Postgres: shared/db_credentials.py → db_credentials.postgres_dw() (accessor agregado 2026-08-31 al mismo módulo que ya usan los 6 dlt/load_*.py — lee dlt/.dlt/secrets.toml, mismo patrón que el resto del repo, sin duplicar una copia propia). Ver Credenciales y conexiones.
  • La webapp de consulta (UI de pendientes): sigue en ~/por_ordenar/exploracion/consulta_xmls_gastos/ en ctunlinux — fuera de git, por convención de este repo (ver CLAUDE.md, regla de promoción). Inactiva, fuera de alcance de la reactivación 2026-08-31 (decisión explícita: solo se reactivó el ELT). Componentes:
  • webapp/ (Flask + waitress) — UI de consulta de pendientes, puerto 8600, solo red local (http://192.168.117.7:8600, no pasa por el tunnel público).
  • scripts/logic.py / scripts/db.py — lógica de match, usa connection_207 (ver abajo) para cruzar contra MPRO en vivo.
  • Nada de esto se ha promovido todavía a docs/proyectos/consulta-xmls/scripts/ en trivasa-context — sigue pendiente si se retoma.

Arquitectura

Fuente de los XML — mount CIFS, solo lectura: - Servidor: //192.168.117.211/SincronizarXml - Ruta real dentro del share: TRI970922TL2/XML RECIBIDOS/{año}/{año}_{mes}/{año}_{mes}_AC/*.xml - Credenciales: Infisical, proyecto secret-management (mismo proyecto compartido de todo el homelab, workspaceId 2aefdbd1-389c-4fd0-bdb8-a5621af8aac1), env prod — SAMBA_SINCRONIZARXML_USERNAME, SAMBA_SINCRONIZARXML_PASSWORD, SAMBA_SINCRONIZARXML_DOMAIN. Antes vivían en texto plano en ~/.smbcredentials/sincronizarxml (permisos 600, root-only) — ese archivo sigue siendo la fuente que /etc/fstab necesita leer directamente (fstab no puede llamar a Infisical), así que Infisical es el respaldo/documentación de esas credenciales, no reemplaza el archivo local que el mount necesita. - Gotcha de negocio: el UUID en el nombre del archivo tiene casing inconsistente (mayúsculas/minúsculas mezcladas) — normalizar siempre a mayúsculas al comparar.

Destino: Postgres :5433, schema raw_sat (ver Stack de BI; cfdi_recibidos vivió hasta 2026-07-29 en un schema contabilidad propio — se movió porque raw quedó reservado para datos de fuente externa a MPRO). Tres tablas, una por loader: cfdi_recibidos, cfdi_emitidos, cfdi_retencion — ver arriba.

Orquestación: - Carga diaria: crontab de ealcocer, tres líneas escalonadas (0 5 recibidos, 5 5 emitidos, 10 5 retención — 5 min de por medio para no pegarle al mismo mount CIFS a la vez). Recibidos y emitidos procesan mes actual + mes anterior (por si llegan XMLs tarde); retención recorre toda la carpeta cada vez (volumen chico, ver arriba). Las tres corren envueltas en trivasa-bi-core/observability/checks/run_tracked.sh (mismo wrapper que los dlt/load_*.py) — aparecen en el dashboard elt-health de Perses, job=pipeline_run, stage=raw_sat_xml / raw_sat_xml_emitidos / raw_sat_xml_retencion. - Webapp: inactiva (ver arriba) — consulta-xmls-gastos.service sigue disabled desde 2026-08-28, fuera de alcance de la reactivación 2026-08-31. Cuando se retome: refresco de XMLs pensado como manual (botón en la UI), no cron — decisión explícita, no hay urgencia de frescura y así se mantiene control del usuario. - Las tablas de match (Comprobante_Digital, Pago_Cxp_Comprobante) sí se cargan vía dlt a raw.* en trivasa-bi-core/dlt/, con cron propio — ver load_comprobante_digital.py. Ese pipeline está sano y es independiente de este.

Decisiones de arquitectura (resumen)

Prototipo sin dlt/dbt completo (mismo patrón que layout-gastos y consumo_interno_trazabilidad) — se descartó por sobre-ingeniería para este alcance. Excepción: las tablas de match de UUID sí van por dlt (se consultan en cada refresh, se benefician de carga incremental). El resto (historial de proveedor, Compra_Encabezado) sigue siendo consulta en vivo contra .207 — impacto trivial confirmado.

Fuentes de UUID para el match de pendientes: - Gastos: Pago_Cxp_Comprobante.Pcc_Timbre_UUID - Compras: Comprobante_Digital.Cd_Timbre_UUID (filtrar cd_tabla = 'COMPRA')

Decisiones de negocio: RFC con múltiples Pv_Cve_Proveedor combina historial de todos los códigos que matcheen; historial de proveedor cubre últimos 24 meses.

Gotchas

La carpeta XML RETENCIONES tiene subcarpetas por concepto, y archiva por periodo del GASTO, no por fecha de timbrado

Confirmado 2026-09-10 (conciliacion-cfdi), navegando el share directo vía smbclient (credenciales Infisical SAMBA_SINCRONIZARXML_*, mismo proyecto/env prod que arriba). Dentro de TRI970922TL2/XML RETENCIONES/{año}/{año}_{mes}/ hay dos subcarpetas, no archivos sueltos:

  • ACCIONISTAS/ — retenciones de dividendos (cve_retenc=14 en raw_sat.cfdi_retencion), nomenclatura de archivo MNNNNN.xml (ej. M20432.xml).
  • INVERSIONISTAS/ — retenciones de intereses a prestamista (cve_retenc=16), nomenclatura TRI970922TL2_B-NNNN_<timestamp timbrado>.xml.

Un CFDI que corrige/sustituye una retención de un periodo anterior se archiva bajo la carpeta de ese periodo original, no bajo el mes en que se timbró. Caso real: un CFDI de intereses de noviembre 2025, timbrado tarde (enero 2026, sustituyendo una retención cancelada del mismo periodo — ver el gotcha de CfdiRetenRelacionados en calidad de datos), vive en 2025/11 Noviembre 2025/INVERSIONISTAS/, no en 2026/01 Enero 2026/. Como el loader recorre toda la carpeta sin filtrar por periodo (ver arriba), esto no le afecta — pero si algo busca el archivo por convención de nombre/fecha de timbrado en vez de usar archivo_origen (columna ya cargada en raw_sat.cfdi_retencion con el nombre real), no lo va a encontrar donde lo esperaría.

El mount CIFS a veces trae una página HTML de error en vez del XML

Confirmado 2026-09-03: cargar_cfdi_recibidos.py fallaba parseando 23 de 286 archivos del periodo con Entity 'uacute' not defined — parecía XML corrupto, pero el contenido real es una página HTML (Ha ocurrido un error al procesar su última acción...) que SincronizarXml (.211) guardó con extensión .xml cuando el origen SAT/PAC falló (sesión vencida / error del lado de ellos, no confirmado cuál). El mount es de solo lectura — no hay nada que arreglar del lado de este repo salvo detectar el caso: parse_cfdi() ahora revisa los primeros 512 bytes en busca de <!doctype html/<html antes de intentar el parseo XML, y tira un error explícito en vez del genérico de lxml. Sigue contando como fallido — no hay CFDI real que recuperar de ese archivo, solo se ahorra tener que re-diagnosticarlo desde cero la próxima vez. Si el volumen de este caso crece, vale la pena preguntar del lado de quien administra SincronizarXml en .211 qué está fallando ahí.

Buscar un complemento opcional por etree.QName(el).localname sobre .iter() truena si el XML trae un <!-- comentario -->

Confirmado 2026-09-10, al agregar la extracción del complemento implocal:ImpuestosLocales (buscado por localname, no por prefijo, para no depender de qué namespace use el XML — ver conciliacion-cfdi). comprobante.iter() de lxml, sin argumento, recorre todos los nodos del árbol, incluidos Comment/ProcessingInstruction — su .tag no es un str sino una función interna de lxml, y etree.QName(el) truena con Invalid input tag of type <class '...cython_function_or_method'> en vez de un error de XML. 10 archivos reales de agosto 2026 (con comentarios <!-- ... --> legítimos en el XML) fallaron así, contados como "fallidos" en el log — no eran XML corruptos. Corregido con comprobante.iter("*") (el argumento "*" filtra a solo elementos reales) en los dos loaders que recorren el árbol completo (cargar_cfdi_recibidos.py, cargar_cfdi_emitidos.py).

El mount CIFS rota/purga XMLs viejos — filas huérfanas en Postgres, limpieza automática desde 2026-09-10

Confirmado 2026-09-10: 171 filas en raw_sat.cfdi_recibidos (periodo 2026) tenían subtotal/iva/total cargados correctamente (de una corrida de julio/agosto) pero su archivo_origen ya no existe en ninguna carpeta del mount hoy — no es el gotcha de "HTML guardado como .xml" (ese tumba también subtotal/total) ni el de "UUID de archivo ≠ UUID de contenido" (el nombre de archivo coincidía exacto). El mount simplemente deja de tener el XML con el tiempo. Los loaders son upsert-only a propósito (nunca truncan/borran solos, para no perder histórico si el mount tiene un hueco transitorio) — así que nunca se auto-corregían.

Se agregó raw_sat_xml/maintenance.py (mismo patrón que dlt/maintenance.py: diff contra la fuente real, re-confirmar, solo entonces borrar, con límite de seguridad MAX_BORRAR_POR_CORRIDA=500 antes de borrar de verdad — si el mount no montó bien y "no hay archivos" por error transitorio, el número de candidatos se dispara muy por encima de lo esperado y ahí no se borra nada solo). El diff busca el archivo en toda la carpeta del año, recursivo, no solo en la carpeta del mes de periodo (que viene de fecha_emision, no de qué carpeta se escaneó) — un CFDI que llega tarde a otra carpeta del mismo año no se marca huérfano por error.

observability/checks/check_raw_volume.py ahora trata raw_sat.cfdi_recibidos/cfdi_emitidos como caso especial: en vez de comparar contra el conteo de la corrida anterior (que nunca detecta huérfanas — no bajan el total de golpe), compara filas del año en curso contra archivos que existen hoy en esa carpeta del mount. Si no da pass, AUTOHEAL llama a borrar_huerfanos_recibidos/emitidos automáticamente (mismo patrón ya aprobado para comprobante_digital en check_raw_freshness.py) — evento aparte en Loki (job=raw_volume_autoheal) para que quede visible en el dashboard.

Estado

Ver PROGRESS.md — el ELT se reactivó 2026-08-31, promovido a trivasa-bi-core/raw_sat_xml/ (ya no en la ruta rota de antes). La webapp de consulta sigue inactiva, fuera de alcance de esa reactivación.