# finanzas-personales

> Finanzas Personales compatible con Banco Santander Chile.

- Repositorio: https://github.com/KikeMuller/finanzas-personales
- Page: https://kikemuller.dev/es/proyectos/finanzas-personales/

- Lenguajes: Python, JavaScript
- Categorías: Finanzas
- Temas: finance, financial-analysis, santander
- Licencia: MIT
- Estrellas: 1
- Última actualización: 03-08-2026
- Última versión: v1.0.0 (https://github.com/KikeMuller/finanzas-personales/releases/tag/v1.0.0)
- Clonar: `git clone https://github.com/KikeMuller/finanzas-personales.git`

## README

<div align="center">

<img src="docs/dashboard.png" alt="El dashboard de finanzas personales" width="880">

<sub>Las cifras, las escalas, los nombres de comercio y los dígitos de las tarjetas están tapados: son datos reales.</sub>

# 💵 Finanzas Personales

**Tus finanzas, construidas desde los estados de cuenta del banco.**

Un solo archivo que se abre con doble clic.<br>
Sin instalar nada, sin servidor y sin internet. Ningún dato sale de tu computador.

<br>

[![Descargar el dashboard](https://img.shields.io/badge/%E2%A4%93%20Descargar%20el%20dashboard-1,75%20MB-2ea043?style=for-the-badge)](https://github.com/KikeMuller/finanzas-personales/releases/latest/download/dashboard_vacio.html)

![Banco Santander Chile](https://img.shields.io/badge/banco-Santander_Chile-EC0000?style=flat-square)
![Funciona sin internet](https://img.shields.io/badge/funciona-sin_internet-1f6feb?style=flat-square)
![Sin instalación](https://img.shields.io/badge/instalaci%C3%B3n-ninguna-2ea043?style=flat-square)

</div>

---

## 1. Partir de cero

1. **Descarga el dashboard** con el botón de arriba. Es un archivo, `dashboard_vacio.html`.
2. **Ábrelo con doble clic.** No hay paso de instalación: se abre en tu navegador y ya funciona.
3. **Dile quién eres.** En la pestaña **Datos**, en el JSON de reglas, agrega tu nombre y tu RUT:

   ```json
   "titular": ["NOMBRE APELLIDO"],
   "titular_rut": ["12345678-9"]
   ```

   Esto va **antes** de cargar los archivos, porque el tipo de cada movimiento se decide al leerlos.
   Sin esto, las transferencias entre tus propias cuentas se cuentan como ingresos y los totales
   quedan inflados.
4. **Arrastra tus estados de cuenta** sobre la página. Se procesan en tu computador, sin subir nada.
5. **Aprieta ⤓ Guardar HTML con datos.** Esa copia, con tus datos adentro, pasa a ser tu dashboard
   de aquí en adelante. Si cierras sin guardar, se pierde lo que cargaste.

**Por ahora solo es compatible con Banco Santander Chile.** Los extractores están escritos contra el
formato exacto de sus cartolas y estados de tarjeta; documentos de otro banco no se van a leer.

---

## 2. Uso diario

1. Doble clic en `dashboard.html`.
2. Tres pestañas:
   - **Dashboard** — indicadores, gráficos, alertas y conciliación de caja.
   - **Detalle** — todos los movimientos, filtrables por mes, origen, categoría, tipo y texto.
   - **Datos** — cargar archivos, ver la verificación y editar las reglas de categorización.
3. Los filtros de arriba (desde / hasta / origen / categoría) afectan a las dos primeras pestañas.

### Agregar meses nuevos

1. Descarga los tres documentos **desde el sitio del banco**:
   - la **cartola de cuenta corriente** del mes, en `.xlsx`;
   - los **estados de las dos tarjetas**, en `.pdf`.

   **No sirven los estados que llegan por correo:** vienen protegidos con contraseña y el dashboard
   no puede abrirlos. Tienen que bajarse desde la página.
2. Arrástralos sobre la página, en la pestaña **Datos**.
3. Revisa el recuadro de verificación: debe decir *Todo cuadra*.
4. Aprieta **⤓ Guardar HTML con datos** y deja esa copia en lugar de `dashboard.html`.

Si cargas un archivo que ya estaba, **reemplaza** al anterior en lugar de duplicarlo. Eso permite
volver a cargar un mes corregido sin ensuciar los datos.

### Nada se guarda solo

Los datos viven **dentro del propio `dashboard.html`**. Lo que cargues, edites o vacíes existe
únicamente mientras la pestaña está abierta: si la cierras sin guardar, al volver a abrir el archivo
reaparece exactamente lo que había antes. Una página web no puede modificar por su cuenta un archivo
de tu disco.

Por eso, cuando hay cambios pendientes aparece arriba a la derecha el aviso **● Cambios sin guardar**
y el navegador pregunta antes de cerrar la pestaña.

Al apretar **⤓ Guardar HTML con datos**, en Chrome y Edge se abre el diálogo de guardar y puedes
elegir el propio `dashboard.html` para sobrescribirlo. En los navegadores que no lo permiten, el
archivo cae en Descargas y tienes que reemplazarlo tú.

Esto también aplica al botón **Vaciar datos de esta pestaña**: vacía la vista, pero el archivo queda
igual hasta que guardes la versión vacía encima.

### Nombres de archivo

Los PDF se identifican por su contenido, así que da lo mismo cómo se llamen.
Las cartolas **sí** dependen del nombre para saber a qué mes corresponden: debe conservar el formato
del banco, `... - <Mes> <Año>.xlsx` (por ejemplo `Cartola de cuenta Corriente - Marzo 2026.xlsx`).

---

## 3. Cómo funciona

```
PDF / XLSX  ─►  parser.js  ─►  movimientos  ─►  agregados.js  ─►  gráficos y tablas
                    │                                 │
              verificación                     dashboard.html
           contra totales del banco        (todo esto va adentro)
```

| Pieza | Rol |
|---|---|
| `dashboard.html` | **El entregable.** ~2,1 MB. Lleva adentro los datos, la interfaz y las librerías. |
| `dashboard_vacio.html` | La misma página sin un solo dato. Es el punto de partida para alguien nuevo. |
| `scripts/parser.js` | Lee cartolas y estados de tarjeta, clasifica cada movimiento y verifica los totales. |
| `scripts/agregados.js` | Convierte los movimientos en KPIs, series mensuales, recurrentes, alertas y conciliación. |
| `scripts/reglas_categorias.json` | Reglas de categorización, tipo de cambio y cuentas propias. |
| `scripts/vendor/` | PDF.js, fflate y Chart.js, que se incrustan en el HTML. Licencias en `LICENSES.txt`. |
| `scripts/build_dashboard.py` | Rearma `dashboard.html` desde cero. Solo para desarrollo. |
| `scripts/actualizar_dolar.py` | Trae el dólar observado del SII y actualiza el tipo de cambio. |
| `scripts/extract.py` | Extractor original en Python. Respaldo y control cruzado. |
| `scripts/test_parser.mjs` | Compara el parser JS contra el de Python, movimiento a movimiento. |
| `data/transacciones.csv` | Los movimientos en CSV, para abrir en Excel. |
| `data/resumen.json` | Dataset completo con los metadatos de cada documento. |

PDF.js normalmente descarga en segundo plano un archivo aparte para procesar los PDF; acá viene
incrustado y se ejecuta en el hilo principal, por eso funciona sin internet y al abrir el archivo
directamente desde el disco. Procesar un año entero de documentos toma menos de un segundo.

---

## 4. Criterios de cálculo

Vale la pena entenderlos para leer bien los números.

### Gasto real = compras de las tarjetas

Los cargos `Traspaso Internet a T. Crédito` de la cuenta corriente se marcan como `pago_tarjeta`
y **no** se cuentan como gasto. Si se contaran, cada compra aparecería dos veces: una en el detalle
de la tarjeta y otra al pagar la factura a fin de mes.

### Dos vistas de resultado, las dos correctas

- **Balance devengado** (el KPI): ingresos − gastos del mes en que ocurrieron. Sirve para saber
  en qué se va la plata.
- **Conciliación de caja** (la tabla): lo que efectivamente entró y salió de la cuenta corriente.
  Cuadra peso a peso con el saldo inicial y final de cada cartola.

La diferencia entre ambas es deuda de tarjeta (compras que se pagan el mes siguiente o se refinancian
en cuotas) más plata movida entre cuentas propias. Por eso el balance devengado puede ser negativo
mientras el saldo de la cuenta sube.

### Movimientos internacionales

Si el estado internacional trae el monto original en pesos, se usa ese monto tal cual. Si viene en
otra moneda, se convierte desde USD con el `tipo_cambio_usd_clp` de las reglas. En la práctica eso
afecta a una minoría de los movimientos: la mayoría ya viene convertida por el banco.

Ese valor se refresca con el **dólar observado del SII**:

```bash
python scripts/actualizar_dolar.py            # último valor publicado
python scripts/actualizar_dolar.py --dry-run  # muestra qué haría, sin escribir
```

Es un paso de desarrollo, no del dashboard: la página corre sin red por diseño y no puede consultar
al SII sola. Después hay que regenerar con `extract.py` y `build_dashboard.py`.

Se usa **un solo valor para todo el período**, no el del día de cada compra. Mientras sean pocos
movimientos la diferencia es marginal; si algún día pesa más, conviene pasar a convertir cada
movimiento con el dólar de su fecha.

### Meses parciales

Los meses sin cartola de cuenta corriente solo tienen compras de tarjeta: aparecen marcados bajo el
gráfico de flujo y no deben leerse como meses sin ingresos. Suele pasar en el primer y el último mes
del período, cuando todavía no descargaste la cartola correspondiente.

---

## 5. Diccionario de tipos

Cada movimiento tiene un **tipo**, que determina si suma al gasto, al ingreso o a ninguno.
Los montos positivos son salidas de plata; los negativos, entradas.

| Tipo | Qué es | ¿Cuenta como gasto? |
|---|---|---|
| `gasto` | Compra en un comercio. | Sí |
| `cargo_financiero` | Intereses, comisiones, impuestos y cargos por uso internacional. | Sí |
| `reembolso` | Notas de crédito y devoluciones. Se resta del gasto bruto. | Resta |
| `ingreso` | Remuneración, devoluciones de impuestos, transferencias recibidas de terceros. | Es ingreso |
| `traspaso_interno` | Plata movida entre cuentas propias o a la línea de crédito. | No |
| `pago_tarjeta` | Pago de la factura de la tarjeta desde la cuenta corriente. | No (evita doble conteo) |
| `financiamiento` | Refinanciamientos: traspaso de deuda a cuotas, monto cancelado, traspaso a deuda nacional. | No |

Las categorías (`Supermercado y almacén`, `Delivery de comida`, etc.) son una capa aparte, editable.
Los tipos `ingreso`, `traspaso_interno`, `pago_tarjeta`, `financiamiento` y `reembolso` reciben
categoría fija; solo `gasto` y `cargo_financiero` pasan por las reglas.

---

## 6. Categorización

En la pestaña **Datos** están las reglas en JSON y, al lado, la lista de movimientos
**sin clasificar** ordenada por monto. El flujo es:

1. Buscas un comercio en la lista de la derecha.
2. Lo agregas a la categoría que corresponda en el JSON de la izquierda.
3. Aprietas **Aplicar reglas**: recategoriza al instante, sin volver a leer los PDF.
4. Exportas el HTML para conservarlo.

Las reglas se evalúan **en orden y la primera que calza gana**, así que las específicas van arriba.
Los patrones se comparan en mayúsculas y sin acentos contra la descripción completa del movimiento,
como texto contenido (no hace falta el nombre exacto).

Además de `reglas`, el JSON tiene:

| Campo | Para qué sirve |
|---|---|
| `tipo_cambio_usd_clp` | Conversión de compras internacionales cuando el estado no trae pesos. |
| `titular` | Nombres tuyos. Las transferencias a esos nombres se marcan como traspaso interno. |
| `titular_rut` | Lo mismo, pero por RUT: más confiable, porque el banco escribe el nombre de varias formas. |
| `tarjetas` | Opcional. Alias por los cuatro últimos dígitos, si no te gusta el nombre que trae el PDF. |

Un movimiento marcado como traspaso interno no cuenta ni como ingreso ni como gasto. Sin esto, mover
plata desde tu cuenta de ahorro aparecería como sueldo extra.

`titular`, `titular_rut` y `tarjetas` no están en `reglas_categorias.json` sino en
**`scripts/titular.local.json`**, que queda fuera del repositorio por ser datos personales. Los
scripts lo fusionan sobre las reglas si existe; si falta, avisan y siguen corriendo, pero esos
traspasos pasan a contarse como ingresos. Para reconstruirlo hay una plantilla en
`scripts/titular.local.example.json`.

### Nombres de las tarjetas

No hay ninguna tarjeta escrita en el código. El nombre sale del propio estado de cuenta, de la línea
`Nº DE TARJETA DE CRÉDITO XXXX XXXX XXXX 1234 VISA PLATINUM LATAM`, y queda como
`Visa Platinum Latam ...1234`. Si prefieres llamarlas de otra forma, el campo `tarjetas` de
`titular.local.json` las reemplaza:

```json
"tarjetas": { "1234": "La de los viajes" }
```

---

## 7. Verificación

Cada vez que se leen documentos, se comparan contra los totales que declara el banco:

| Documento | Qué se cuadra |
|---|---|
| Cartola | `saldo inicial + abonos − cargos = saldo final` |
| Tarjeta nacional | `1. TOTAL OPERACIONES` y `3. CARGOS, COMISIONES, IMPUESTOS Y ABONOS` |
| Tarjeta internacional | `TOTAL DE COMPRAS Y CARGOS` en USD |

El resultado sale en la pestaña **Datos** y al pie de la página.
El objetivo es siempre el mismo: **cero diferencias en todos los documentos**.

Esto es lo que hay que mirar al cargar un mes nuevo: si el banco cambia el formato de sus documentos,
la diferencia aparece de inmediato en vez de pasar como un número silenciosamente equivocado.

---

## 8. Si algo falla

| Síntoma | Causa probable y qué hacer |
|---|---|
| *"No pude leer el mes del nombre del archivo"* | La cartola fue renombrada. Devuélvele el formato `... - Marzo 2026.xlsx`. |
| Un PDF no carga o da error al abrirlo | Casi seguro es el estado que llega por correo, que viene con contraseña. Descárgalo desde la página del banco. |
| Los documentos no son de Santander Chile | No están soportados: los extractores leen el formato específico de ese banco. |
| Aparece una diferencia en la verificación | El banco cambió el formato del documento. El movimiento que falta se ve comparando el total del PDF con el de la pantalla; avísame y ajusto el parser. |
| Muchos movimientos sin clasificar | Normal al cargar meses nuevos con comercios nuevos. Agrégalos a las reglas. |
| Un mes muestra gasto pero cero ingresos | Falta la cartola de cuenta corriente de ese mes. |
| Los gráficos no aparecen | El archivo quedó a medio descargar. Vuelve a guardarlo o rearma con `build_dashboard.py`. |
| Vaciaste los datos y al reabrir siguen ahí | Es lo esperado: vaciar solo afecta a la pestaña. Hay que guardar el HTML vacío y reemplazar con él al archivo anterior. |
| Cargaste archivos y al reabrir no están | Lo mismo: faltó guardar. El aviso *● Cambios sin guardar* arriba a la derecha avisa cuando queda algo pendiente. |
| Guardaste y quedaron dos archivos | El navegador no dejó sobrescribir y lo mandó a Descargas. El bueno es el recién descargado. |

---

## 9. Trabajar el proyecto con Claude Code

```bash
cd D:\dev\Finanzas_Personales
claude
```

`CLAUDE.md` le da el contexto: arquitectura, los criterios de cálculo que no se pueden romper y por
qué existen dos implementaciones del extractor. Hay cuatro comandos propios en `.claude/commands/`:

| Comando | Qué hace |
|---|---|
| `/rebuild` | Regenera `dashboard.html` desde los estados de cuenta y verifica que todo cuadre. |
| `/verificar` | Corre la verificación contra los totales del banco y la paridad JS ↔ Python. |
| `/nuevo-mes` | Incorpora los documentos de un mes, revisa que cuadren y propone reglas para lo nuevo. |
| `/categorias` | Revisa lo que quedó sin clasificar y propone reglas. |

Ejemplos de lo que sirve pedirle: *"agrega un gráfico de gasto por día de la semana"*,
*"el estado de agosto no cuadra, revisa qué movimiento se está perdiendo"*,
*"¿en qué categoría subí más respecto al trimestre pasado?"*.

Dos cosas configuradas a propósito:

- `.gitignore` deja fuera los estados de cuenta, el CSV y el dashboard con datos. Si algún día
  inicias git, el historial no guarda información financiera.
- `.claude/settings.json` bloquea la lectura directa de `Datos Ejemplo/`, para que un estado de
  cuenta completo no termine en el contexto sin querer. Para depurar un documento se usa
  `pdfplumber` desde la terminal. Si te estorba, borra esa línea del archivo.

## 10. Los scripts de Python (opcionales)

El pipeline en Python fue el original y se mantiene como respaldo y control cruzado.
Ya no hace falta para usar el dashboard.

```bash
python scripts/extract.py                    # PDF/XLSX -> data/transacciones.csv y data/resumen.json
python scripts/build_dashboard.py            # rearma dashboard.html desde cero
python scripts/build_dashboard.py --vacio    # dashboard_vacio.html, sin datos, para compartir
python scripts/actualizar_dolar.py           # refresca el tipo de cambio desde el SII
```

`extract.py` requiere `openpyxl` y `pdfplumber`; `actualizar_dolar.py` no necesita nada instalado,
pero sí internet. `build_dashboard.py` corre aunque no exista `data/resumen.json`: en ese caso genera
el dashboard vacío.

**`--vacio` no incrusta `titular.local.json`**, así que esa copia no lleva tu nombre, tu RUT ni los
alias de tus tarjetas. Es la que se puede publicar.

El botón de descarga del principio apunta al **último release**, no al archivo del repositorio:
GitHub sirve los `.html` como texto plano, así que un enlace al archivo lo abriría en pantalla en vez
de descargarlo. Eso significa que el asset del release es una copia congelada: al regenerar
`dashboard_vacio.html` hay que publicarlo de nuevo, o la gente se sigue bajando el anterior.

```bash
gh release create v1.1.0 dashboard_vacio.html --title "..." --notes "..."
```

Para comprobar que el parser del navegador sigue dando exactamente lo mismo que Python:

```bash
cd scripts && npm i && node test_parser.mjs
```

`scripts/package.json` fija `pdfjs-dist` en la versión **exacta** que hay en `scripts/vendor/`, sin
`^`. Si npm instalara una más nueva, la prueba dejaría de comparar contra la librería que realmente
corre dentro del dashboard.

Hoy da **paridad total**: los dos extractores producen exactamente los mismos movimientos, las
mismas categorías y los mismos totales, con cero diferencias contra lo que declara cada documento.
Si algún día se toca el parser, esta prueba es la red de seguridad.

Node solo hace falta para esta prueba: `parser.js` está escrito para el navegador, y compararlo
contra `extract.py` exige ejecutarlo fuera de él. El dashboard no lo necesita nunca. En Windows sin
Node, sirve WSL:

```bash
wsl -e bash -lc "cd /mnt/d/dev/Finanzas_Personales && node scripts/test_parser.mjs"
```

Los avisos de `DOMMatrix` y `Path2D` al arrancar son normales: son el polyfill de `canvas` que PDF.js
usa para *dibujar* páginas, y acá solo se extrae texto.
