Skip to content

Warehouse — qué está replicado y qué falta

Estado de trivasa_dw (PostgreSQL :5433): qué tablas del ERP ya viven en Postgres, con qué estrategia se mantienen, y qué falta.

Verificado 2026-08-12.

Schemas

Schema Contenido Quién lo puebla
raw Tablas crudas desde SQL Server, 1:1 con la fuente trivasa-bi-core/dlt/
raw_staging Staging interno de dlt para escrituras merge dlt (automático, no tocar)
raw_sat Datos derivados de XMLs CFDI del SAT — fuente distinta a MPRO cargar_cfdi_recibidos.py
analytics_staging Vistas stg_* de dbt (marcadas hidden) dbt
analytics_marts Tablas fct_*/dim_* dbt
monitoring Trace de corridas de dlt (duración, filas, éxito/error) log_run_metrics.py

Estrategias de carga

Modo Cuándo Riesgo
merge + incremental sobre Fecha_Ult_Modif Tabla grande con PK única y cursor poblado Nunca borra en destino: si en el origen se borran registros, quedan huérfanos para siempre
replace Tablas chicas (hasta cientos de miles de filas), sin cursor o sin PK única Recarga completa cada vez

La lección que fijó el criterio: reorden estaba en merge y Postgres acumuló 2,053 filas contra 2,022 en origen — 31 huérfanas. Con replace quedó exacto. Para tablas chicas, replace es más simple y más correcto.

Catálogos (replace)

familia (116) · sub_familia (297) · categoria (19) · departamento (37) · almacen (464) · sucursal (41) · proveedor (4,774) · reorden (2,022)

Incrementales (merge)

Tabla Filas Fuente PK Cursor
movimiento 4,546,520 Movimiento (mv_folio, mv_id) Fecha_Ult_Modif
comprobante_digital 690,418 Comprobante_Digital (cd_tabla, cd_documento) Fecha_Ult_Modif
compra 150,190 Compra (co_folio, co_id) Fecha_Ult_Modif
orden_compra 122,417 Orden_Compra (oc_folio, oc_id) Fecha_Ult_Modif
existencia 84,857 Existencia 5 columnas Fecha_Ult_Modif
compra_encabezado 69,297 Compra_Encabezado (co_folio) Fecha_Ult_Modif
producto 27,462 Producto (pr_cve_producto) Fecha_Ult_Modif
requisicion_compra 83,146 Requisicion_Compra (rc_folio, rc_id) Fecha_Ult_Modif
transferencia — (agregado 2026-08-21) Transferencia (tr_folio, tr_id) Fecha_Ult_Modif

comprobante_digital cubre 2012-01-10 en adelante, con 14 valores de cd_tabla (FACTURA, TRASLADO, GASTO_REGISTRO, NOMINA, COMPROBANTE_PAGO, COMPRA, CHEQUE, NOTA_CREDITO, CUENTA_X_PAGAR, COMPRA_INDIRECTO, NOTA_CREDITO_PROVEEDOR, ANTICIPO_CXP, CONSTANCIA_RETENCION, CONTABILIDAD_ELECTRONICA).

Solicitudes de material

9 tablas, 1,109,448 filas, reconciliadas al 100 % contra .207. Backfill ~3 min, incremental diario ~1 min.

Tabla Filas Modo Clave / cursor
ztrv_estado_solicitud 310,661 replace sin PK ni cursor
ztrv_solicitud_material_detalle 250,987 merge incremental PK Sm_Folio, Sm_ID · Fecha_Ult_Modif
ztrv_solicitud_materia_documento 189,107 replace sin PK ni cursor
ztrv_presupuesto_autorizacion_documento 104,064 merge incremental PK Pad_Tabla, Pad_Documento, Pad_Estado, Pad_Fecha · Pad_Fecha (sin Fecha_Ult_Modif)
ztrv_solicitud_material 114,459 merge incremental PK Sm_Folio · Fecha_Ult_Modif
ztrv_solicitu_material_producto 74,359 replace PK ok, sin cursor
ztrv_apartado 30,518 merge incremental PK Ap_Folio, Ap_ID · Fecha_Ult_Modif
ztrv_solicitud_material_ceco 35,123 replace PK ok, sin cursor
ztrv_solicitud_agenda_logistica 170 replace cursor 96 % NULL

Solo 4 de 9 tienen cursor usable; las demás son tablas hijas puras de Sm_Folio sin columnas de auditoría. Ver Calidad de datos.

2026-08-13: se agregaron ztrv_apartado, ztrv_presupuesto_autorizacion_documento (en load_solicitudes.py) y requisicion_compra (arriba, en load_compras_inventario.py) — tablas usadas en la reconciliación de notificacion-solicitud-material. A diferencia de las 7 tablas originales de este dominio (que se saltan el backfill desde TRIVASADB3 por un problema de escritura activa detectado en su momento — ver load_solicitudes.py:backfill_inicial(), renombrada 2026-08-27, antes main()), estas 3 sí se backfillearon desde TRIVASADB3 siguiendo el patrón estándar del runbook: pre-flight confirmó que la copia actual no tiene esa señal de escritura activa (MAX(Fecha_Ult_Modif)/MAX(Pad_Fecha) de las 3 caen en la misma ventana de ~3h, señal de snapshot restaurado una sola vez).

Importante — la IP de TRIVASADB3 cambió de .200 a .205. Mismo servidor físico, misma base, solo se movió. Verificado empíricamente (2026-08-13): .205 trae ZTRV_Solicitud_Material ~1 día más fresco que .200 (114,501 filas / MAX(Fecha_Ult_Modif) 2026-08-11 vs 113,758 / 2026-08-10) — .200 quedó como copia congelada, no usar para nada nuevo. connections/connection_205_trivasadb3.py en trivasa-bi-core ya actualizado. Ojo: esto es específico a TRIVASADB3 — se confirmó que TRIVASADB (sin el 3, la base ya marcada como obsoleta en el ADR de .200 vs .200/TRIVASADB3) tiene el patrón inverso en .205 (copia de abril, más vieja que la de .200), así que la conexión que apuntaba a TRIVASADB sin el 3 (connections/connection.py) no se tocó en su momento — se dejó en .200 a propósito. Borrada 2026-08-27: ese nombre genérico sin sufijo era el más fácil de asumir "el" default de connections/, pero apuntaba a esta misma copia congelada — ver Stack de BI.

Marts de dbt

dim_producto (14,668) · fct_compras (62,762) · fct_existencias (1,516,397) · fct_gastos_cfdi (12,229) · fct_ordenes_compra · fct_solicitud_material_pipeline (250,992, grano línea) · fct_solicitud_material_autorizacion (31,294, grano folio) · fct_transferencia (143 filas, grano folio — agregado 2026-08-21) · fct_documento_trazabilidad (388,908 filas, grano etapa más avanzada — agregado 2026-08-21, ampliado el mismo día)

fct_documento_trazabilidad (2026-08-21)

Mart nuevo, no es de agregados de negocio: es un buscador de un documento a la vez. Dado cualquiera de los tres folios de entrada al proceso de compras (solicitud de material, requisición de compra, u orden de compra), la fila trae el resto de la cadena (los otros folios, si existen) + la info descriptiva de cada documento (fechas, estado, montos, sucursal, proveedor, producto). Grano: la etapa más avanzada alcanzada por cada línea — una fila de OC si ya hay OC, si no una de RC si ya hay RC, si no una de (solicitud_folio, producto_id) si la solicitud nunca generó ni RC ni OC.

Enlaces siempre por el patrón polimórfico Xx_Tabla/Xx_Documento (nunca folio=folio, ver Calidad de datos), incluida la dirección hacia Compra_Encabezado (Co_Documento=Oc_Folio) que no estaba expuesta en stg_compra_encabezado — se le agregaron esas dos columnas (origen_tabla/origen_documento) para este mart.

Hallazgo nuevo confirmado con conteos reales (origen_tabla de Orden_Compra/Requisicion_Compra): no toda OC/RC nace de una solicitud de material. De 42,271 líneas de OC con origen_tabla='REQUISICION_COMPRA', la requisición detrás solo tiene origen_tabla='ZTRV_Solicitud_Material' en 19,526 de ellas — el resto viene vacío (53,224, la mayoría del total de RC), o de 'Resurtido'/'RESURTIDO' (resurtido automático, mismo casing duplicado que el gotcha ya documentado de Pad_Tabla) u 'ORDEN_SERVICIO'. Por eso solicitud_folio sale NULL en ~44% de las líneas en etapa OC y ~86% en etapa RC — es dato real, no un hueco de join: la mayoría de las compras del ERP no pasan por el flujo de solicitud de material en absoluto. Detalle completo en el modelo dbt.

Validado con dbt run + queries reales contra postgres-dw y contra la API de Lightdash (filtro equals con folio real resuelve la cadena completa; con values vacío no filtra nada — confirma el comportamiento esperado del dashboard). No filtra por estado "vigente" como los marts de pipeline — es un buscador/auditor, tiene que encontrar un folio sin importar si ya quedó cancelado, cerrado, o recibido total.

Dashboard publicado: Documento · Buscador y trazabilidad (5 charts + 1 dashboard, contenido-como-código en lightdash/). Filtros independientes por folio de solicitud/requisición/orden de compra, sin valor por defecto — se llena solo el que se tenga a la mano. Cada tabla de detalle trae su propio filtro base "folio no nulo" para no mostrar filas en blanco cuando esa etapa de la cadena no aplica.

Rediseño 2026-08-21 (mismo día): doble estado + autorización por documento + OC canceladas

Tres cambios acordados tras analizar los catálogos de estado reales de los 4 documentos de la cadena (ver también Calidad de datos para el hallazgo de origen):

  1. int_documento_autorizacion.sql (intermediate, nuevo): generaliza int_orden_compra_autorizacion.sql (que se deja intacto, solo cubre OC y lo usa fct_requisiciones_compra) a los 3 documentos que pueden llevar autorización presupuestal. Cobertura real confirmada, aislando el período con el proceso ya vigente (desde 2024-03-31 — antes de esa fecha no hay ninguna fila, es corte temporal, no estructural):
Documento % con autorización de documento (Pad) % con cambio de presupuesto (Psc)
Solicitud de material 98.3% 68.2%
Requisición de compra 37.8% 0.4%
Orden de compra 42.6% 38.4%

Solo Solicitud pasa casi siempre por autorización de documento — en Requisición y Orden de compra la mayoría de los folios nunca tocan esa bitácora. sin_registro/sin_solicitud es por eso un estado de negocio real para esos dos, no un hueco de dato. fct_documento_trazabilidad trae categoria_documento/categoria_presupuesto para los 3 documentos, con coalesce a esos valores solo cuando el folio SÍ existe en esa fila (NULL real sigue significando "esta fila no tiene ese documento" — ej. una fila en etapa REQUISICION_COMPRA sin OC no debe mostrar categoría de OC).

  1. Doble estado de Solicitud: solicitud_estado_encabezado y solicitud_estado_detalle (antes solo se exponía cabecera, columna renombrada de solicitud_estado). Verificado con un join real cabecera↔detalle: divergen la mayoría de las veces — el patrón más común de todos (129,055 líneas / 69,593 folios) es cabecera CE (cerrada) con detalle AC (activa), más frecuente que cabecera=detalle=CE (66,370 líneas). Mostrar solo cabecera escondía que el folio puede decir "cerrado" con líneas todavía activas. Para contraste, el mismo join en Compra_Encabezado↔Compra no tiene este problema: de 150,727 líneas, 150,726 coinciden exactamente con su cabecera.

  2. stg_orden_compra_todas.sql (staging, nuevo): igual a stg_orden_compra.sql pero sin el filtro Es_Cve_Estado<>'CA'. El buscador ahora sí encuentra órdenes de compra canceladas — 47% del histórico de Orden_Compra está cancelado (verificado en vivo 2026-08-21), y antes de este cambio esas quedaban invisibles para el buscador. stg_orden_compra se deja intacto para los marts de pipeline, donde filtrar CA sí es correcto.

Filas: 357,296 → 388,908 (creció por incluir las OC canceladas). Validado con dbt run + queries reales + la API de Lightdash (folio 05-0052765: cabecera=FN vs detalle=AC, categoria_documento='sin_registro' cuando corresponde).

Bug corregido el mismo día: int_documento_autorizacion duplicaba filas

Al construir la ficha vertical (ver abajo) se encontró que solicitud_categoria_documento variaba dentro del mismo solicitud_folio (un folio real, 05-0053650, salía con 'reenviado' y 'autorizado' a la vez). Causa: el row_number() de "estado más reciente" particionaba por origen_tabla crudo, sin normalizar el casing duplicado (ZTRV_Solicitud_Material vs ZTRV_SOLICITUD_MATERIAL, el mismo gotcha ya documentado en Calidad de datos para Pad_Tabla) — un folio con filas bajo ambas variantes sacaba un "más reciente" por variante, dejando 2 filas para el mismo (documento_tabla, documento_folio) normalizado. No era solo cosmético: el LEFT JOIN en fct_documento_trazabilidad fanaba-out esas filas, inflando el conteo total del mart (388,908 filas antes del fix). Corregido normalizando documento_tabla antes del row_number() — filas bajaron a 388,798 (las ~110 de más eran duplicados reales).

fct_documento_ficha (2026-08-21, mismo día): ficha vertical campo/valor

Mart adicional que despivota fct_documento_trazabilidad (33 campos) a formato campo/valor en SQL (una fila del mart por cada campo), para mostrarlo como una ficha vertical con un chart de tabla normal de Lightdash en vez de un chart tipo Vega/Custom — se decidió así porque los charts Vega en Lightdash pierden "view underlying data" y el cross-filtering con el resto del dashboard (limitaciones documentadas del tipo de chart, no específicas de este caso), mientras que despivotar en SQL conserva todo eso sin trucos. Mantiene solicitud_folio/ requisicion_folio/orden_compra_folio como columnas propias para que los mismos 3 filtros del dashboard sigan aplicando. 12,830,334 filas (388,798 × 33 campos) — grande, pero cualquier consulta real siempre va filtrada por folio.

Agregada al dashboard como tarjeta junto a la intro (chart "Documento · Ficha vertical") — vista rápida de todo el documento antes de bajar a las 5 tablas de detalle existentes.

Actualizado el mismo día: la ficha única mostraba TODOS los campos de la cadena (solicitud + requisición + OC + compra) aunque el usuario hubiera filtrado por un solo tipo de documento. Se reemplazó por 4 fichas independientes ("Documento · Ficha de solicitud/requisición/ orden-compra/compra"), cada una filtrando campo (operador include) a exactamente el mismo conjunto que ya usa su tabla horizontal — "Documento · Ficha vertical" se borró. Como corren sobre fct_documento_ficha (un explore distinto al que apuntan los 3 filtros del dashboard, que targetean fct_documento_trazabilidad), hizo falta agregar tileTargets a los 3 filtros mapeando cada una de las 4 fichas al campo equivalente — sin eso el filtro del dashboard simplemente no las tocaba (confirmado con la API: el metricQuery resultante ni siquiera incluía el filtro cuando faltaba el tileTargets).

Manifiesto de Compras (2026-08-21, mismo día): buscador como Claude Artifact

El mismo caso de uso (buscar un documento, ver su cadena, navegar a los relacionados) también se construyó como Claude Artifact standalone (HTML+JS autocontenido, sin backend) con una UX que Lightdash no puede dar: ruta visual tipo línea de tiempo/milestones, y clic en un documento relacionado para saltar a su ficha. Un Artifact no tiene salida de red (sandbox sin acceso a Postgres/Lightdash), así que el dato va embebido como snapshot al publicar — cubre solo los últimos 6 meses, regenerar a mano cuando haga falta refrescar.

Código promovido en trivasa-bi-core/artifacts/manifiesto-compras/ (export + template + instrucciones de regeneración en su README.md) — el fct_documento_trazabilidad de este mart es su única fuente. Publicado en https://claude.ai/code/artifact/e5d2f943-bf71-4b6d-8c7a-50d83729d2c7 (privado).

Actualizado el mismo día — campos reales del detalle de Solicitud de material (solicitud_linea_id/Sm_ID, solicitud_concepto/Descripción, solicitud_tipo_gasto_id/Clave, solicitud_cantidad_control_1, solicitud_apartado, solicitud_existencia_disponible, solicitud_reorden_minimo/_maximo), agregados a fct_documento_trazabilidad vía joins nuevos a stg_ztrv_apartado, stg_existencia (ya existían) y stg_reorden (nuevo, Re_Tipo='PR' es el 100% de las filas). Existencia y Reorden se resuelven por el almacén de la solicitud (Sm_Al_Cve_Almacen), no un almacén genérico.

Hallazgo/corrección importante: el "NUMERO PARTE" que muestra la pantalla nativa de Solicitud de material (ZTRV098) no es Producto.Pr_Clave_Corta (producto_clave_corta en dim_producto — así se había etiquetado por error en la sesión anterior) — es Pr_Cve_Producto (producto_id), ya presente en el mart. Confirmado contra un folio real de pantalla del usuario: producto_id='0000037010' = "CINTA 20 MTS FIBRA DE VIDRIO", coincidencia exacta, junto con el resto de la fila (Clave=0036, Control-1=1 PZ, Apartado=0, Existencia=0, Max=0, Min=0, Sm_ID=0002, Estado=CE).

No se agregó Pedido/Comprado/Surtido/Saldo a surtir (esa pantalla los calcula en vivo, no son columnas almacenadas en ninguna tabla — replicarlos exigiría reconstruir esa lógica y validarla contra un baseline real de pantalla, mismo patrón ya documentado como "sin converger" en Solicitud de material para casos similares) ni el nombre del tipo de gasto (el catálogo Tipo_Gasto no está replicado en el warehouse todavía).

fct_transferencia (2026-08-21)

Mart nuevo para Lightdash, origen: reporte nativo "Transferencias por recibir" (RPTRF01L). Detalle de la tabla fuente en Dominios → Transferencia. Fila count re-confirmada en analytics_marts.fct_transferencia: 143 filas (2026-08-21, vía docker exec postgres-dw psql).

Pipeline completo:

  • dlt/load_transferencia.py — archivo dedicado (decisión explícita: no colgarlo de load_movimiento.py). Funciones (renombradas 2026-08-27 a la convención estándar de los 6 archivos load_*.py): transferencia(), backfill_inicial_transferencia() (antes backfill_205_transferencia()), run_daily() (antes run_incremental_207_transferencia()). pipeline_name="trivasa_transferencia".
  • dbt/models/staging/stg_transferencia.sql — view, filtra tr_tipo='EN' AND es_cve_estado='AC', sin joins.
  • dbt/models/intermediate/int_transferencia.sql — view, LEFT JOIN a sucursal (x2: origen y destino) + almacen.
  • dbt/models/marts/fct_transferencia.sql — table, agregado por folio: 143 filas desde 207 líneas de detalle.
  • _sources.yml y _marts.yml actualizados.
  • Cron: línea agregada a las 06:55 en el crontab de ealcocer (ruta ya migrada a ~/ehalso/trivasa-bi-core/dlt, no la vieja trivasa-bi-dev — ver Cron actual).
  • Deployado a Lightdash (proyecto trivasa_dw), 12/12 explores.

Pendiente, no resuelto: - Resolución de nombre de operador (EMPRESAS_2.Operadores) — el mart se queda con la clave Oper_Alta por ahora. - Significado del estado CE en Transferencia.

fct_movimientos ya está materializado en analytics_marts (re-verificado 2026-08-21) — la advertencia anterior sobre que le faltaba tabla ya no aplica.

12 marts materializados en analytics_marts a 2026-08-21 (verificado por information_schema): dim_producto, fct_compras, fct_existencias, fct_gastos_cfdi, fct_movimientos, fct_ordenes_compra, fct_requisiciones_compra, fct_requisiciones_compra_flujo, fct_requisiciones_compra_worklist, fct_solicitud_material_autorizacion, fct_solicitud_material_pipeline, fct_transferencia — coincide con el "12/12 explores" del deploy de Lightdash de la sesión 2026-08-21.

Los dos marts de solicitudes tienen un dashboard de Lightdash publicado como código en lightdash/dashboards/solicitudes-de-material-backlog-vivo.yml: Solicitudes de material · Backlog vivo. Lectura de negocio (no metodología) en hallazgos-de-negocio.md.

⚠️ Gotcha nuevo (2026-08-14) al construir int_solicitud_material_autorizacion: ZTRV_Presupuesto_Autorizacion_Documento.Pad_Tabla tiene dos variantes de casing para la misma tabla (ZTRV_SOLICITUD_MATERIAL, 61,841 filas · ZTRV_Solicitud_Material, 25 filas). Filtrar con upper(pad_tabla) = ... funciona pero arruina la estimación de cardinalidad de Postgres (sin estadísticas sobre la expresión, estimó ~520 filas en vez de ~62k, eligió nested loop para los joins siguientes, y la tabla tardó >4 minutos en vez de <1 segundo). Usar pad_tabla IN ('ZTRV_SOLICITUD_MATERIAL', 'ZTRV_Solicitud_Material') en su lugar.

Fuente SAT (no MPRO)

Tres tablas, derivadas de los XMLs en //192.168.117.211/SincronizarXml (no de MPRO) — ver consulta-xmls para el ELT y los tres loaders (raw_sat_xml/cargar_cfdi_*.py). Schema separado a propósito: es otra fuente, aunque represente "lo mismo" a nivel negocio.

  • raw_sat.cfdi_recibidos (111,123 filas, periodo 2025-01 → 2026-09).
  • raw_sat.cfdi_emitidos (19,710 filas, periodo 2025-09 → 2026-02 — el origen no ha subido facturas emitidas más recientes que febrero 2026 al share, aunque la carpeta 2026 ya tiene 6,502 archivos para ese año).
  • raw_sat.cfdi_retencion (2,055 filas, periodo 2016-12 → 2026-08).

Columnas de cfdi_recibidos/cfdi_emitidos (mismo esquema en ambas)

Base (desde que existe el loader): uuid (PK), fecha_emision, rfc_emisor, nombre_emisor, rfc_receptor, direccion (RECIBIDO / EMITIDO / AUTOEMITIDO / OTRO, por comparación de RFC contra Trivasa), tipo_comprobante, subtotal, iva (solo IVA, Impuesto='002', resumen a nivel Comprobante), total, uso_cfdi, metodo_pago, forma_pago, periodo (fecha_emision[:7]), archivo_origen, fecha_carga.

Agregadas 2026-09-10 (dos tandas el mismo día, ver PROGRESS de consulta-xmls para el detalle de implementación) — todas nullable, pobladas al parsear el XML durante la ingesta:

Columna Origen en el XML
descuento Comprobante/@Descuento
ieps_trasladado Suma de Impuestos/Traslados/Traslado[@Impuesto≠'002']/@Importe (resumen a nivel Comprobante)
impuestos_locales_trasladados / _retenidos Complemento implocal:ImpuestosLocales/@TotaldeTraslados/@TotaldeRetenciones (0 si no existe)
total_impuestos_retenidos Impuestos/@TotalImpuestosRetenidos
ret_iva / ret_isr Suma de Impuestos/Retenciones/Retencion/@Importe por @Impuesto='002'/'001'
pagos_monto_total Complemento Pagos: Pagos/Totales/@MontoTotalPagos, o suma Pago/@Monto si no hay nodo Totales (variantes viejas)
pagos_iva_total Pagos/Totales/@TotalTrasladosImpuestoIVA16 + @TotalTrasladosImpuestoIVA8
pagos_dr_uuids Pago/DoctoRelacionado/@IdDocumento de todos los Pago, ;-separado, mayúsculas, únicos, orden alfabético
pagos_dr_pagado Suma de @ImpPagado sobre todos los DoctoRelacionado
vales_despensa_total Complemento ValesDeDespensa/@total
vales_despensa_n_trab Conteo de hijos de ValesDeDespensa/Conceptos
complementos Nombres de los complementos presentes (sin TimbreFiscalDigital), ;-separados, ordenados — el más común en 2026 es CartaPorte (28,971 de 45,747 CFDI recibidos)
base_exenta Suma de @Base en Traslado[@TipoFactor='Exento']

Todo se busca como hijo directo del nodo que corresponde (nunca .// para Impuestos/Traslados/Retenciones — un resumen por Concepto además del de nivel Comprobante da 0 si se busca recursivo); el complemento Pagos se busca por localname, no por prefijo fijo, porque su namespace varía (Pagos10/Pagos20).

Reconciliación contra el mount (no contra MPRO)

observability/checks/check_raw_volume.py trata estas dos tablas distinto del resto de raw/raw_sat: en vez de comparar contra el conteo de la corrida anterior, compara filas del año en curso contra archivos que existen hoy en la carpeta correspondiente del mount — y si no coincide, AUTOHEAL borra las huérfanas confirmadas (raw_sat_xml/maintenance.py, mismo patrón que dlt/maintenance.py, con límite de seguridad antes de borrar de verdad). Ver el gotcha completo en consulta-xmls.

Cron actual

Decisión 2026-08-27: cron, no systemd timers. Hasta esa fecha esta nota decía que la convención "oficial" era systemd timers con la migración pendiente — se decidió quedarse en cron (ver Stack de BI), es lo que corre de verdad y no daba problemas. Todo lo de este repo vive en el crontab de ealcocer, incluido dbt build.

2026-08-25 — ruta reparada, run_tracked.sh agregado, 3 crons nuevos. El crontab había apuntado a ~/trivasa-bi-dev/dlt-pipelines (ruta muerta) desde el 2026-08-11 — verificado que ya estaba corregido a ~/ehalso/trivasa-bi-core/ en algún momento entre el 2026-08-21 y esta fecha (no queda claro por quién). Lo que sí se hizo hoy: cada job de dlt y de dbt build ahora corre envuelto en observability/checks/run_tracked.sh <stage> <comando> — mide duración/exit code y postea a Loki (job=pipeline_run) sin tocar el comando en sí, para alimentar el dashboard "ELT Pipeline Run Health" en Perses. Se agregaron 3 crons: check_pipeline_run_health.py (puente monitoring.pipeline_runs → Loki), check_raw_volume.py (conteo total por tabla vs. corrida anterior, cubre las 28 tablas de raw/raw_sat completas) y dbt/scripts/deploy_docs.sh (dbt docs generate + deploy a Cloudflare Pages, dbtdocs.ehas.uk — resuelto el mismo día, ver el detalle completo en stack-bi.md).

2026-08-27 — convención de nombres unificada en los 6 load_*.py de dlt, tras revisar mejores prácticas del ecosistema (dlthub docs, verified-sources, "7 shared modules for 27 dlt pipelines"). El número de scripts y el patrón merge (facts con cursor) / replace (catálogos) ya seguían la práctica recomendada — lo que no existía era una convención de nombres, y main() significaba cosas distintas según el archivo (backfill en unos, la corrida diaria real en otro). Cambios:

  • Una sola función "la que corre en cron" en los 6 archivos: run_daily(). La tabla de abajo ya refleja los nombres nuevos.
  • main() (backfill histórico) → backfill_inicial(), en los 3 archivos donde así se usaba.
  • Backfills de tabla específica: patrón backfill_inicial_<tabla>(). Backfills de gap puntual: patrón backfill_gap_<fecha>_<tabla>().
  • load_movimiento.py tiene pipeline_name propio ahora (trivasa_movimiento) — antes reusaba "trivasa_compras_inventario" (el de otro dominio), así que el cursor incremental de movimiento vivía mezclado en el estado de ese otro pipeline. Migrado a mano: se sembró initial_value con el MAX(fecha_ult_modif) real de raw.movimiento (menos 1 día de margen, el merge dedupea el traslape) para no forzar una recarga completa de las 4.58M filas. Verificado antes de comitear: la migración cargó solo 1,224 filas nuevas en 3.29s, y la corrida siguiente sin initial_value dio "0 load packages" — confirma que ya usa el estado persistido bajo el nombre nuevo.

Detalle completo y verificación contra producción en el commit trivasa-bi-core@b3d2898.

Hora Proceso
05:00 cargar_cfdi_recibidos.py → raw_sat.cfdi_recibidos
06:00 load_comprobante_digital.run_daily()
06:15 load_reorden.py (reorden + producto + 6 catálogos, función run_daily() bajo if __name__=='__main__')
06:30 load_compras_inventario.run_daily() (incluye requisicion_compra desde 2026-08-13)
06:45 load_movimiento.run_daily()
06:50 load_solicitudes.run_daily() (incluye ztrv_apartado/ztrv_presupuesto_autorizacion_documento desde 2026-08-13)
06:55 load_transferencia.run_daily() (agregado 2026-08-21)
06:56 check_pipeline_run_health.py — puente monitoring.pipeline_runs (dlt) → Loki, job=pipeline_run (agregado 2026-08-25)
06:58 dbt build (trivasa-bi-core/dbt, target prod) — reconstruye analytics_staging/analytics_marts a partir del raw recién cargado (agregado 2026-08-21)
07:00 check_raw_freshness.py
07:02 dbt/scripts/deploy_docs.sh — dbt docs generate + deploy a Cloudflare Pages, dbtdocs.ehas.uk (agregado 2026-08-25)
07:05 check_raw_volume.py — conteo total por tabla vs. corrida anterior, 28 tablas de raw/raw_sat (agregado 2026-08-25)

2026-08-21 — se agregó el job de dbt build, no existía ninguno

Hallazgo: analytics_marts.fct_requisiciones_compra (mart detrás del dashboard Requisiciones de compra · Pipeline) llevaba desde el 2026-08-18 sin reconstruirse (MAX(fecha) de la tabla tope en 2026-08-13) mientras que raw.orden_compra/raw.requisicion_compra sí estaban al día vía dlt. Causa raíz: el crontab solo tenía los jobs de dlt (arriba) y check_raw_freshness.py (que valida raw, no los marts) — ningún proceso corría dbt build después de la carga. Se agregó a las 06:58 (después del último load de dlt a las 06:55, antes del check de las 07:00) y se corrió una vez manualmente para poner los marts al día (fct_requisiciones_compra: 45,040 → 45,272 filas, MAX(fecha) 2026-08-13 → 2026-08-20). Log en trivasa-bi-core/logs/dbt_build.log.

Los diagramas ER de estas tablas y sus relaciones verificadas están en Modelos de datos de raw.

⚠️ Riesgo abierto (reportado 2026-08-21): dbt run no está agendado en ningún cron/systemd. Solo dlt y check_raw_freshness corren automático. Todo mart materializado como table (incluido fct_transferencia) se queda congelado en el warehouse hasta que alguien corra dbt run a mano — no es un problema de una tabla en particular, es un hueco del repo completo.

Qué falta — 800 tablas con datos

La cobertura está sesgada a compras e inventario. Prioridades:

Prioridad Ausente Por qué importa
Alta — dimensiones Cliente, Empresa, Zona, Vendedor, Estado, Linea, Marca, Segmento, Centro_Costo, Comprador, Ruta, Forma_Pago, Impuesto, Color, Talla Juntas < 8,000 filas y aparecen en cientos de reportes. El mejor valor/esfuerzo del backlog.
Alta — Ventas Venta_Encabezado, Venta, Venta_Total_Impuesto, Remision*, Pedido*, Factura* No hay ni un mart de ventas
Alta — CXC Cuenta_X_Cobrar, Pago_CXC, Recibo_Pago Sin cobranza ni antigüedad de saldos
Media — Gastos Gasto_Registro* (3 tablas, 4.05 M filas) Hoy solo se cubre desde XMLs del SAT, sin el lado ERP
Media — CXP Cuenta_X_Pagar, Pago_Cxp_Comprobante Cierra el ciclo con compras, ya cargado
Media — Logística Orden_Entrega, Entrega_Documento, Viaje, Complemento_Carta_Porte Costo de reparto, cumplimiento
Baja Poliza_Detalle (14.3 M), Poliza_Control (6.1 M) Alto volumen; solo con caso de uso claro
Baja Pre_Nomina (4.6 M), Nomina (1.8 M) Datos sensibles — definir acceso antes
Excluir Imagen_Objeto, Adjunto, ZTRV_Almacen_Digital, Comentario Binarios: 62 GB sin valor analítico
Excluir opc_* (25 tablas) Duplican tablas base

Nota de reconciliación — corregida 2026-08-21

La entrada original de esta nota afirmaba raw.almacen (465) vs TRIVASADB3 con 928 filas. Ese 928 era incorrecto — verificado por Claude Code vía SSH a ctunlinux con SELECT COUNT(*) FROM Almacen directo contra .205/TRIVASADB3: el conteo real es 464, prácticamente idéntico a raw.almacen (465, diferencia de 1 explicable por timing entre la query y el último incremental). raw.almacen no tiene gap — ya está reconciliado.

raw.sucursal (41) tampoco tiene gap: coincide exacto contra .205 (SELECT COUNT(*) FROM Sucursal → 41).

Ambas cifras confirmadas en la misma sesión de verificación 2026-08-21; no queda gap abierto de este tipo entre raw.* y TRIVASADB3 para estas dos tablas.