Estilo de comprobación — guía para replicar en otras sesiones
Empaquetado como skill (2026-08-10): trivasa-comprobacion, en el repo claude-skills — mismo contenido que este documento, pero es la fuente viva a partir de ahora (el ejemplo de código de este documento quedó desactualizado: usa Console()/Table() sueltos en vez de helpers_output.py, ver rich-output-checks). Esta página queda como referencia histórica de origen — para escribir o revisar un script nuevo, usar el skill, no este documento.
Este documento describe el patrón usado en layout_gastos/ para validar
queries contra datos reales. Está pensado para pegarse al inicio de una
sesión nueva de Claude Code, así no hay que re-explicar el estilo cada vez.
Filosofía
- Nunca confiar en documentación vieja sin validar con datos reales.
Confirmar contra la base de datos, aunque exista un
.mdprevio que diga lo contrario. - Incremental, una pieza a la vez. No construir la query completa de una — validar un bloque chico (un folio conocido) antes de escalar al universo completo.
- Aislar antes de generalizar. Cuando algo no cuadra, separar los casos que fallan en su propio CSV/query antes de intentar arreglar la query principal a ciegas.
- Explicar el “no cuadra” con causa raíz, no forzar la regla. Un folio que no cuadra por una razón de negocio conocida (reversión, reclasificación, redondeo en cientos de líneas) se documenta como excepción — no se ajusta la fórmula para que “pase” artificialmente.
Conexión por default
connection_200_trivasadb3.py (.200/TRIVASADB3) — nunca cambiar a
connection_207.py (.207, producción) salvo que el usuario lo pida
explícito. .200 puede estar rezagada respecto a producción para el mes
más reciente — si algo no cuadra y podría ser por eso, decirlo, no asumir.
Convención de las queries SQL (docs/queries/gastos/*.sql)
- Un archivo por pieza de negocio (ej.
v_impuestos_por_ceco.sql, no una query gigante que lo haga todo). - Comentario al inicio explicando el porqué de cada decisión no obvia (por qué ese join, por qué ese filtro, qué bug evita) — no solo qué hace la query. Ejemplo real de esta sesión:
-- Comparacion en MONEDA ORIGINAL, sin convertir con tipo de cambio: tanto
-- Grd_Precio_Neto_Importe como Cxp_Precio_Neto_Importe estan en la moneda
-- del documento -- confirmado exacto con folio USD real (05-0175047,
-- ambos lados = 3,201.60 USD, sin necesidad de multiplicar por
-- Grd_Tipo_Cambio ni Cxp_Tipo_Cambio).- Parámetros con
:fecha_ini/:fecha_fin(no hardcodeados) — el script Python los reemplaza con.replace()antes de correr. Gr_Folioya es el string completo ('SS-FFFFFFF'), nunca reconstruir conCONVERT/RIGHTa menos que se necesite un formato de display distinto al nativo.- Ojo con columnas
money/numericde pocos decimales en divisiones para prorrateo (Grc_Importe,Grc_Factor) —CAST(... AS FLOAT)antes de dividir, o la suma de pesos no da 1.0 exacto (bug real encontrado esta sesión, verdocs/queries/gastos/v_impuestos_por_ceco.sql).
La baseline: contra qué se compara
Toda comprobación necesita un ESPERADO que sea una fuente de verdad YA
CONFIABLE — no un valor derivado de la misma query que se está validando
(eso sería circular, siempre “cuadraría”). En este proyecto, las fuentes
de verdad usadas hasta ahora:
| Qué se valida | Contra qué (ESPERADO) | Por qué es confiable |
|---|---|---|
| Universo de folios (v0.1) | Export nativo de MPRO (Excel) | Es lo que ve Contabilidad hoy |
| Cargo/Abono por folio (v0.2/v1.0) | Gasto_Registro_Documento.Grd_Precio_Descontado_Importe | Columna fuente, no derivada de póliza |
| Impuesto por CECO (v2.3) | Gasto_Registro_Documento.Grd_Impuesto_Importe | Mismo principio — columna fuente |
| Monto CXP (v2.2) | Cuenta_X_Pagar.Cxp_Precio_Neto_Importe (moneda original, sin convertir) | Registro independiente de la CXP, no un cálculo nuestro |
| Reconciliación completa de un mes | v01_baseline_[mes].csv (CSV ya validado en una pieza anterior) | Una vez que una pieza cierra al 100%, su CSV se vuelve baseline para las siguientes |
Regla: si no hay una columna/tabla independiente contra la cual comparar, no se puede “comprobar” — solo se puede “calcular”. Dejarlo explícito en el mensaje al usuario en vez de presentar un cálculo como si fuera una validación.
Colores y umbrales (regla fija en este proyecto)
color = "green" if pct >= 99 else "yellow" if pct >= 95 else "red"- Verde (≥99%): aceptable para cerrar la pieza como validada, aunque no sea exactamente 100% — el resto normalmente son excepciones de negocio ya conocidas (ver protocolo de diagnóstico más abajo).
- Amarillo (95-99%): revisar antes de cerrar — puede que falte identificar un patrón de excepción real.
- Rojo (<95%): no cerrar la pieza — señal de que falta algo estructural en la query, no solo ruido de redondeo.
Estos umbrales aplican a la columna % de las tablas rich, tanto en el
resumen por origen como en el resumen por mes (nunca solo texto plano —
siempre tabla, siempre con color en el %, para que salte a la vista dónde
está el problema sin tener que leer cada número).
Estructura estándar del script de comprobación (Python + rich)
"""
[Nombre de la pieza] - Comprobacion contra [fuente de verdad].
Lee [archivo].sql (la query guardada, no una copia duplicada en el script).
"""
import sys
sys.path.append("..")
from connection_200_trivasadb3 import engine
import pandas as pd
from rich.console import Console
from rich.table import Table
console = Console()
FECHA_INI, FECHA_FIN = "2026-01-01", "2026-02-01"
with open("docs/queries/gastos/[archivo].sql") as f:
QUERY = f.read().replace(":fecha_ini", f"'{FECHA_INI}'").replace(":fecha_fin", f"'{FECHA_FIN}'")
df = pd.read_sql(QUERY, engine)
df["FOLIO"] = df["FOLIO"].str.strip() # por si acaso, aunque Gr_Folio nativo no debería traer espacios
# --- Comparar contra la fuente de verdad conocida ---
df["DIFERENCIA"] = df.CALCULADO - df.ESPERADO
df["CUADRA"] = df.DIFERENCIA.abs() < 1
n, c = len(df), df.CUADRA.sum()
console.print(f"\n[bold]Folios:[/bold] {n}")
console.print(f"[bold]Cuadran:[/bold] {c} ({100*c/n:.2f}%)\n")
# --- Tabla resumen por ORIGEN, con importes (no solo %) ---
resumen = df.groupby("ORIGEN").agg(
n=("FOLIO", "count"), cuadran=("CUADRA", "sum"),
esperado=("ESPERADO", "sum"), calculado=("CALCULADO", "sum"),
).reset_index()
t1 = Table(title="Resumen por ORIGEN")
t1.add_column("Origen"); t1.add_column("n", justify="right")
t1.add_column("Cuadran", justify="right"); t1.add_column("%", justify="right")
t1.add_column("Esperado ($)", justify="right"); t1.add_column("Calculado ($)", justify="right")
for _, r in resumen.iterrows():
pct = 100 * r.cuadran / r.n
color = "green" if pct >= 99 else "yellow" if pct >= 95 else "red"
t1.add_row(str(r.ORIGEN), str(r.n), str(r.cuadran), f"[{color}]{pct:.1f}%[/{color}]",
f"{r.esperado:,.2f}", f"{r.calculado:,.2f}")
console.print(t1)
# --- Folios que NO cuadran, con importes, ordenados por magnitud de diferencia ---
mal = df[~df.CUADRA]
console.print(f"\n[bold red]No cuadran:[/bold red] {len(mal)}")
if len(mal):
t2 = Table()
for c_ in ["FOLIO", "ORIGEN", "ESPERADO", "CALCULADO", "DIFERENCIA"]:
t2.add_column(c_, overflow="fold")
for _, r in mal.sort_values("DIFERENCIA", key=abs, ascending=False).head(15).iterrows():
t2.add_row(str(r.FOLIO), str(r.ORIGEN), f"{r.ESPERADO:,.2f}", f"{r.CALCULADO:,.2f}", f"{r.DIFERENCIA:,.2f}")
console.print(t2)
df.to_csv("[nombre]_validacion.csv", index=False)
console.print(f"\n[green]Guardado: [nombre]_validacion.csv[/green]")Para semestre/rango largo, agregar una segunda tabla “por mes” con la misma
estructura de columnas (Folios, Cuadran, % con color, Esperado ($),
Calculado ($)), derivando MES de la fecha con pd.to_datetime(...).dt.strftime("%Y-%m").
Cuando algo no cuadra: protocolo de diagnóstico
- No arreglar a ciegas. Aislar los folios que fallan en su propio CSV
(
df[~df.CUADRA].to_csv(...)). - Buscar patrones en la muestra antes de investigar uno por uno: ¿la diferencia es siempre el doble exacto? ¿siempre negativa? ¿se repiten los mismos montos en folios distintos? Esas firmas casi siempre delatan la causa (fan-out de join, filtro de moneda faltante, bug de tipo de dato) antes de tocar la base de datos de nuevo.
- Drill-down de UN folio real con todo el detalle crudo (sin filtros, sin agregar) antes de generalizar el fix — nunca arreglar la query completa basándose en una sospecha sin confirmar con un caso concreto.
- Explicaciones de negocio válidas para “no cuadra” ya confirmadas en
este proyecto (reusar antes de investigar de cero):
GASTO_RECLASIFICACION: Cargo=Abono simétrico, Importe neto $0.- Folios de reversión (
Importe <= -$1): usar solo Abono, Cargo cae en cuenta de Provisión/Pasivo. Gr_Genera_Cxp = 'NO': folio no genera CXP individual por diseño.CONTROL_COMBUSTIBLE: CXP consolidada semanal, link porCxp_Referencia = Grd_Referencia+ mismo proveedor, no por folio.- Ruido de redondeo en folios con cientos de líneas de póliza (diferencia de $1-5, acumulada en centavos).
Nomenclatura de archivos
- Query:
docs/queries/gastos/v_[nombre_descriptivo].sql(nov1,v2genéricos salvo que sea literalmente una nueva versión de algo que ya existía). - Script de comprobación:
validar_[nombre]_[periodo].py. - CSV de salida:
[nombre]_validacion.csvo[nombre]_[periodo].csv. .mdde cierre por pieza:docs/v[X]_cierre.mdcuando una pieza queda 100% validada y lista para no tocarse más.