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):
int_documento_autorizacion.sql(intermediate, nuevo): generalizaint_orden_compra_autorizacion.sql(que se deja intacto, solo cubre OC y lo usafct_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).
-
Doble estado de Solicitud:
solicitud_estado_encabezadoysolicitud_estado_detalle(antes solo se exponía cabecera, columna renombrada desolicitud_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 cabeceraCE(cerrada) con detalleAC(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 enCompra_Encabezado↔Comprano tiene este problema: de 150,727 líneas, 150,726 coinciden exactamente con su cabecera. -
stg_orden_compra_todas.sql(staging, nuevo): igual astg_orden_compra.sqlpero sin el filtroEs_Cve_Estado<>'CA'. El buscador ahora sí encuentra órdenes de compra canceladas — 47% del histórico deOrden_Compraestá cancelado (verificado en vivo 2026-08-21), y antes de este cambio esas quedaban invisibles para el buscador.stg_orden_comprase deja intacto para los marts de pipeline, donde filtrarCAsí 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 deload_movimiento.py). Funciones (renombradas 2026-08-27 a la convención estándar de los 6 archivosload_*.py):transferencia(),backfill_inicial_transferencia()(antesbackfill_205_transferencia()),run_daily()(antesrun_incremental_207_transferencia()).pipeline_name="trivasa_transferencia".dbt/models/staging/stg_transferencia.sql— view, filtratr_tipo='EN' AND es_cve_estado='AC', sin joins.dbt/models/intermediate/int_transferencia.sql— view,LEFT JOINasucursal(x2: origen y destino) +almacen.dbt/models/marts/fct_transferencia.sql— table, agregado por folio: 143 filas desde 207 líneas de detalle._sources.ymly_marts.ymlactualizados.- Cron: línea agregada a las 06:55 en el crontab de
ealcocer(ruta ya migrada a~/ehalso/trivasa-bi-core/dlt, no la viejatrivasa-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_movimientosya está materializado enanalytics_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,periodo2025-01 → 2026-09).raw_sat.cfdi_emitidos(19,710 filas,periodo2025-09 → 2026-02 — el origen no ha subido facturas emitidas más recientes que febrero 2026 al share, aunque la carpeta2026ya tiene 6,502 archivos para ese año).raw_sat.cfdi_retencion(2,055 filas,periodo2016-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
crontabdeealcocer, incluidodbt 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ónbackfill_gap_<fecha>_<tabla>(). load_movimiento.pytienepipeline_namepropio ahora (trivasa_movimiento) — antes reusaba"trivasa_compras_inventario"(el de otro dominio), así que el cursor incremental demovimientovivía mezclado en el estado de ese otro pipeline. Migrado a mano: se sembróinitial_valuecon elMAX(fecha_ult_modif)real deraw.movimiento(menos 1 día de margen, elmergededupea 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 sininitial_valuedio "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 runno está agendado en ningún cron/systemd. Solo dlt ycheck_raw_freshnesscorren automático. Todo mart materializado comotable(incluidofct_transferencia) se queda congelado en el warehouse hasta que alguien corradbt runa 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.