Caso de estudio

Del Excel de 7.000 líneas a un sistema de trazabilidad fiscal

Un grupo de 17 empresas del sector construcción en México operaba toda su documentación comercial desde un solo archivo .xlsm. Este es el sistema que lo reemplazó, y la auditoría de datos que casi nadie habría hecho.

Construido y probado con datos reales 2021–2025 · en fase de despliegue

El problema

Diecisiete empresas relacionadas gestionaban cotizaciones, órdenes de compra, remisiones y facturas dentro de un único libro de Excel con macros: 24 módulos VBA, cerca de 7.000 líneas de código y 23 hojas de cálculo, en uso diario.

El síntoma que más dolía era el de los folios. Se generaban con MAX + 1 sobre la hoja correspondiente, así que bastaba con que dos personas capturaran al mismo tiempo para que dos documentos distintos nacieran con el mismo número. En un archivo compartido esto no es un caso raro: es el día a día.

Debajo había un problema mayor. Nadie podía responder con certeza de qué cotización venía una factura determinada. La relación existía en la cabeza de quien había capturado el documento, no en los datos.

Antes de escribir una línea del sistema nuevo hice ingeniería inversa del archivo completo. Los catálogos formales estaban incompletos o desactualizados; las abreviaturas reales con las que el equipo identificaba a cada empresa terminé recuperándolas de una función VBA enterrada en un módulo, que era el único lugar donde estaban correctas.

Las restricciones

Las dos cadenas

El cliente entregó un diagrama de su flujo. Al modelarlo contra los datos reales apareció que describía solo la mitad: existían dos cadenas de compraventa encadenadas, no una. El mismo grupo actuaba como comprador en una y como vendedor en la otra, y los documentos de una alimentaban a la otra.

Cadena comercial Proveedor Materialista (el grupo: 17 empresas) Constructora cadena de compra cadena de venta Cadena documental (trazable en ambos sentidos) Cotización Orden de compra Remisión Factura CFDI · UUID Desde cualquier factura se recorre la cadena hacia atrás hasta su cotización de origen.
Las dos cadenas y los cuatro documentos que las conectan. El UUID del CFDI es la llave natural de todo el modelo.

Qué construí

Un sistema web con backend en Python y FastAPI, base de datos PostgreSQL y frontend en Next.js 16 con App Router. Son 35 endpoints, 22 tablas, 12 migraciones versionadas y 15 pantallas de trabajo.

El ingestor de CFDI lee el XML del comprobante, extrae el Timbre Fiscal Digital y usa el UUID como llave natural, de modo que reprocesar el mismo archivo no crea un duplicado. Cada concepto se clasifica automáticamente como material o servicio a partir de los catálogos oficiales del SAT (c_ClaveProdServ y c_ClaveUnidad), con una bandeja de clasificación donde el equipo revisa y confirma los casos que lo requieren.

La pantalla de trazabilidad es la que justifica el proyecto: dada una factura, recorre hacia atrás la cadena hasta la cotización de origen y muestra cada eslabón con su documento, su folio y su fecha.

El generador documental replica celda por celda las cinco plantillas del Excel original, con el branding de cada una de las empresas y salida en PDF sin el límite de ítems que tenía la macro.

Está diseñado además el conector de Descarga Masiva del SAT con e.firma, para que los comprobantes entren solos en vez de cargarse a mano.

El modelo de datos

Los folios dejaron de calcularse con MAX + 1 y pasaron a funciones respaldadas por secuencias reales de PostgreSQL: dos capturas simultáneas ya no pueden colisionar. La trazabilidad se resuelve con una consulta recursiva que recorre la cadena documental completa, y hay vistas de saldo y triggers de indexado para las búsquedas.

Probar el esquema contra la base real —no contra datos de prueba— hizo aparecer tres errores que en un entorno limpio no se habrían visto: una columna demasiado angosta para los valores reales, una duplicación de filas causada por un JOIN mal cardinalizado, y una recursión que no terminaba cuando la cadena documental contenía un ciclo. Los tres se corrigieron antes de la carga definitiva.

La decisión difícil

La hoja BDRemisiones del Excel contenía 35.365 registros con lo que parecía un histórico completo de remisiones, cada una con su UUID. Era, con diferencia, la fuente más rica del archivo, y el plan era migrarla entera.

Antes de cargarla crucé esos UUID contra los comprobantes fiscales reales. Ninguno existía. Cero de 35.365. No eran identificadores de CFDI: eran cadenas con el formato correcto y contenido inventado, generadas en algún momento para rellenar la hoja.

Migrarla habría metido 12.436 documentos falsos a un sistema con obligaciones fiscales, mezclados con los reales y prácticamente indistinguibles una vez dentro. El costo de descubrirlo después —en una auditoría, por ejemplo— no se compara con el de descubrirlo antes.

La hoja se descartó por completo y el histórico se reconstruyó únicamente desde comprobantes verificables. El sistema quedó con menos datos de los que prometía el Excel, y con todos ellos ciertos.

La lección que me llevo: cuando una fuente de datos parece demasiado completa, esa es exactamente la razón para validarla contra algo externo antes de confiar en ella.

Decisiones técnicas y por qué

Validar archivos por números mágicos, no por extensión
La extensión la controla quien sube el archivo; los primeros bytes, no. Renombrar un ejecutable a .xml deja de servir de nada.
Derivar el nombre almacenado del RFC
El nombre original del archivo nunca toca el sistema de archivos, lo que elimina de raíz el path traversal y los choques de nombres entre empresas.
Sesión por cookie en lugar de autenticación básica
Basic Auth reenvía la credencial en cada petición y no se puede cerrar sesión. La cookie de sesión es revocable.
SQL explícito con SQLAlchemy Core, no ORM completo
En un dominio fiscal quiero poder leer la consulta exacta que se ejecuta y auditarla. Un ORM que genera SQL por mí es más cómodo y menos verificable.
Entornos reproducibles desde el primer día
docker-compose, devcontainer de Codespaces y scripts de arranque para Windows y Linux, para que levantar el sistema no dependa de la máquina de nadie.

Todo esto vive en 15 especificaciones por módulo y una bitácora de decisiones dentro del repositorio. Documentar por qué se eligió algo es lo que permite revisarlo meses después sin repetir el análisis.

Cómo trabajé: la IA como herramienta de desarrollo

Este sistema lo construí usando Claude Code como herramienta de desarrollo, y no tengo interés en disimularlo: es precisamente lo que me permitió ampliar mi alcance técnico. Un año antes mi terreno era HTML, CSS y JavaScript. Aquí levanté un backend en FastAPI, diseñé un esquema de 22 tablas en PostgreSQL con consultas recursivas y triggers, y monté 15 pantallas en Next.js 16. Trabajando solo.

El método importa más que la herramienta. Todo arranca de un archivo CLAUDE.md que fija por escrito las reglas de negocio y las restricciones fiscales del dominio, porque el criterio legal no puede quedarse en una conversación: tiene que estar en el repositorio, versionado y revisable como cualquier otro artefacto. A partir de ahí dirijo, reviso cada decisión y verifico contra datos reales.

Ese último paso es el que marca la diferencia. Ninguna herramienta me habría dicho que BDRemisiones era una hoja de datos fabricados; eso salió de desconfiar de una fuente demasiado limpia y cruzarla contra los comprobantes reales. La IA acelera la ejecución, pero el criterio sobre el dominio y la decisión de qué se migra y qué se descarta siguen siendo míos.

Uso el mismo patrón en mis otros proyectos. En la calculadora de nómina, el documento de contexto con el marco legal colombiano se escribió antes que el primer cálculo.

Resultados

Estado actual: el sistema está construido y probado con los datos reales del periodo 2021–2025. Quedan pendientes el despliegue en producción, la suite de pruebas automatizadas y la autenticación por usuario individual.

¿Tienes un Excel que ya no aguanta?

Es el problema que más me gusta resolver.