Data modelling · ETL · Full-stack

Sistema de Gestión de Presupuestos

2025·3–4 meses·Universitat Politècnica de València
PythonSQLETLNiceGUIPandasSQLAlchemy

El problema

El punto de partida era una aplicación heredada en Microsoft Access, estructurada mediante un esquema básico de tablas maestras (Conceptos, Proveedores, Responsables) y de transacciones (Gastos, Ingresos y Compensaciones) que la secretaría alimentaba registro a registro mediante formularios manuales.

La herramienta cumplía su función básica pero había quedado obsoleta. Ya no se adaptaba a nuevas necesidades que habían ido surgiendo y su arquitectura carecía de validaciones, trazabilidad y otros mecanismos de control necesarios para sistemas contables. El resultado era operativa frustrante: errores difíciles de detectar, tareas repetitivas que consumían horas y procesos contables sin soporte que obligaban a correcciones manuales continuas.

Mi rol

Proyecto desarrollado en solitario, desde el análisis de requisitos hasta el despliegue y soporte post-lanzamiento. Como único responsable, tomé todas las decisiones: arquitectura, modelado de datos, frontend, backend, pipelines de datos, sistema de informes y empaquetado final.

Trabajé en estrecha colaboración con el equipo de secretaría a través de reuniones periódicas, haciéndolos partícipes del proceso. Esto me permitió entender no solo qué necesitaban, sino cómo trabajaban —y diseñar en consecuencia.

La solución

Diseñé y desarrollé una aplicación de escritorio completa —distribuida como un único .exe sin servidores ni dependencias externas— que centraliza todo el ciclo presupuestario, desde la ingesta de datos contables hasta el envío automatizado de informes PDF a cada responsable.

Más allá de modernizar la herramienta, el sistema incorpora lógica contable específica del departamento y funcionalidades que el sistema anterior no contemplaba:

Sustituciones de personal→ Error humano eliminado

Cuando un miembro está de baja, los gastos del sustituto deben imputarse al titular. Antes, secretaría registraba cada cargo manualmente al nombre correcto. Ahora el sistema detecta si hay una sustitución activa en la fecha del gasto y la aplica de forma automática.

Órdenes de compra→ El saldo disponible siempre refleja la realidad

Algunos gastos pasan por órganos externos para su aprobación o son estimaciones sujetas a variación, por lo que no se registraban hasta recibir el importe definitivo —con el riesgo real de gastar por encima del presupuesto disponible. Al crear una orden, el importe queda retenido del saldo de inmediato. Cuando llega el gasto confirmado, se vincula a la orden por identificador y es ese importe real el que pasa a contabilizarse.

Importación masiva desde Excel→ ~2 h reducidas a ~5 min

Algunos gastos llegan en lotes en formato Excel y antes se introducían uno a uno. Ahora un asistente guía la importación: mapea las columnas, aplica las validaciones y permite revisar cada fila antes de confirmar ningún cambio en la base de datos.

Envío automático de informes (correo SMTP)→ Ciclo cerrado sin gestión humana

Antes, un responsable que quería consultar su saldo tenía que pedírselo a secretaría, que generaba el informe y se lo enviaba. Ahora esto puede hacerse en un clic, además el sistema envía los informes automáticamente cada 30 días.

Historial y auditoría→ 100% de operaciones auditadas

Cada operación de escritura queda registrada con usuario y timestamp, lo que permite trazar el origen de cualquier error. Antes no había ningún registro.

Control de concurrencia→ Múltiples usuarios sin incidencias

La versión de Access generaba conflictos cuando varios miembros trabajaban a la vez. El sistema gestiona las escrituras con control de concurrencia para evitar que dos usuarios editen el mismo registro simultáneamente.

Arquitectura y decisiones técnicas

El flujo de datos: de la UI a la base de datos

Cada operación de escritura sigue el mismo camino: los datos capturados en la interfaz se pasan al service de la entidad correspondiente, que ejecuta la validación completa antes de persistir nada. Todos los services heredan de una clase BaseService que contiene la lógica común del pipeline; cada uno añade encima sus propias reglas de negocio.

UI (formulario / importación Excel)
         │  datos brutos (strings)
         ▼
  ┌─────────────────────────────────────────────────┐
  │  BaseService.save()                             │
  │                                                 │
  │  [1] Validadores de tipo                        │
  │      StringValidator · DateValidator · ...      │
  │         │ valores normalizados / errores        │
  │         ▼                                       │
  │  [2] Restricciones del modelo (Pydantic)        │
  │      max_length · gt/lt · nullable · default    │
  │         │ modelo validado / errores             │
  │         ▼                                       │
  │  [3] Reglas de base de datos (BusinessRule)     │
  │      UniqueFieldRule · LinkedEntityExistsRule   │
  │         │ integridad confirmada / errores       │
  │         ▼                                       │
  │  [4] Reglas de negocio  ◄── específicas         │
  │      saldo · sustituciones · órdenes de compra  │  por service
  │         │ informativos / warnings / errores     │
  │         ▼                                       │
  │  Motor de traducción de mensajes                │
  └──────────────┬──────────────────────────────────┘
                 │
        ┌────────┴──────────┐
        │ ¿hay errores      │
        │ bloqueantes?      │
        └────────┬──────────┘
           sí ◄──┴──► no
           │           │
     mostrar        ¿hay warnings?
     errores        ──┬──────────┐
                    sí │         │ no
                       ▼         ▼
                   preguntar   commit BD
                   al usuario
                       │
                  confirma ──► commit BD
                  cancela  ──► descarta

Las capas 1–2 no tocan base de datos: son rápidas y sin dependencias externas. La capa 3 consulta la BD para verificar unicidad y referencias —aislarlas aquí mantiene las anteriores rápidas y permite acumular todos los errores antes de mostrar nada, en lugar de interrumpir al primer fallo. La capa 4 es específica de cada service: GastoService detecta sustituciones activas, comprueba saldo suficiente y gestiona el desbloqueo de retenciones al vincular un gasto a su orden de compra.

A lo largo del pipeline los resultados se acumulan en tres categorías: errores bloqueantes (impiden guardar), advertencias (el sistema pregunta si quiere proceder) e informativos (confirman transformaciones automáticas, como a qué titular se imputó un gasto por sustitución). Al final, un motor de traducción convierte todos los códigos técnicos en mensajes comprensibles para el operador.

Servicios de dominio con responsabilidad única

El mismo principio de encapsulación aplica al flujo de salida. Tres servicios coordinan la generación y envío de informes, cada uno con una responsabilidad única:

SaldoService          ReportService              MailService
     │                     │                         │
  Consulta BD          Llama a SaldoService       Recibe PDF binario
  Agrega gastos,       Obtiene DTOs               Envía por SMTP
  retenciones y        (SaldoFila, SaldoResumen)   con reintentos
  presupuestos         Prepara datos para          Registra fecha
     │                 plantilla ReportLab          de último envío
     ▼                      │
  DTOs tipados              ▼
  por responsable       PDF generado

SaldoService es el único punto que sabe calcular saldos. ReportService no sabe nada de SQL —recibe los DTOs y renderiza. MailService lleva registro del último envío para el ciclo automático de 30 días sin que ningún otro servicio tenga que preocuparse por ello.

Stack tecnológico

CapaTecnologíaDecisión
FrontendNiceGUI 3.7, Quasar, TailwindCSSComponentes modernos empaquetables como .exe
BackendPython 3.11+
ORMSQLModel + SQLAlchemy 2.0Tipado fuerte, sin SQL manual
Base de datosSQLiteSin servidor, portable, auditable
Data processingPandas 2.1, OpenPyXLPipeline de importación Excel
ReportingReportLab 4.0PDFs sin dependencias externas
TestingPytest · 15 módulos de testCobertura de rutas críticas de negocio
DistribuciónPyInstaller 6.x (.exe standalone)Un fichero, sin IT
¿Por qué NiceGUI y no Flask+React u otra opción?

Para este proyecto buscaba una solución simple de desarrollar, mantener y distribuir. NiceGUI es significativamente más simple que Flask+React para un solo desarrollador y más fácil de empaquetar como un único .exe sin servidores ni dependencias externas. Frente a opciones puramente Python como Tkinter o PyQt, ofrece componentes modernos (Quasar + TailwindCSS) sin renunciar a esa simplicidad. Su comunidad es más pequeña que la de React, pero es más que suficiente para un proyecto de este alcance.

¿Por qué SQLite con múltiples usuarios?

SQLite no está recomendado en entornos de red compartida: las escrituras concurrentes pueden fallar o corromper la base de datos porque sus mecanismos de bloqueo dependen del sistema de ficheros —algo que no es fiable en shares SMB. WAL tampoco es seguro en red por la misma razón.

Aun así, fue la decisión correcta para este contexto. El cliente carece de infraestructura técnica: no hay servidor donde alojar una BD cliente-servidor como PostgreSQL, ni equipo IT que pueda mantenerla. SQLite es un fichero —igual que el .mdbde Access que ya conocen—, lo que hace que conceptos como "nueva base de datos" o "copia de seguridad" sean inmediatamente comprensibles para usuarios no técnicos.

El riesgo de corrupción se mitiga con un sistema de bloqueo externo por fichero (filelock) que serializa toda escritura antes de que SQLite llegue siquiera a intentar su propio lock, más copias periódicas automáticas. La escritura la realiza principalmente un usuario; las lecturas concurrentes, que SQLite sí soporta, son el caso mayoritario. Es una arquitectura de transición con criterios de salida definidos: si aparecen locks recurrentes o el equipo crece, la migración a PostgreSQL está prevista —SQLAlchemy soporta ambos dialectos sin cambios en el resto de la arquitectura.

¿Por qué aplicación de escritorio y no una web alojada en servidor?

Por las mismas razones que condicionaron la elección de SQLite: no hay servidor disponible ni equipo IT que lo mantenga. Una web alojada requiere infraestructura de hosting, gestión de actualizaciones del servidor y disponibilidad de red. El .exe elimina todas esas dependencias —el despliegue es copiar un fichero, no hay servicio que monitorizar ni caídas de disponibilidad que gestionar. Para un equipo de seis personas en una intranet universitaria, ese overhead sería desproporcionado al problema que se resuelve.

Lo que aprendí

La lógica de negocio contable es más profunda de lo que parece

Una de las cosas que más me sorprendió fue la cantidad de lógica implícita que hay detrás de un sistema contable, incluso uno pequeño. Son sistemas donde los errores tienen consecuencias reales y auditables. Esto me hizo entender la importancia de invertir en validación y tests desde el principio, no como algo que se añade al final; y de involucrar al usuario final en cada iteración, porque son ellos quienes conocen los casos límite que no aparecen en ningún documento de requisitos.

El impacto real es el KPI definitivo

La validación más honesta llegó al final: ver la reacción del equipo al usar la aplicación por primera vez. Habían convivido años con un sistema que les complicaba el trabajo; ver que algo que construí les iba a ahorrar tiempo real —en cada importación, en cada informe, en cada cierre de período— es el tipo de resultado que da sentido a cada decisión técnica. La ingeniería solo cobra su máximo sentido cuando el problema que resuelve es real para quien lo sufría.