Añadidas tandas: 4, 4b y 7 #33

Merged
jkuijperm merged 13 commits from dev into main 2026-09-09 14:29:29 +00:00
Owner
No description provided.
jkuijperm added 12 commits 2026-09-09 14:29:02 +00:00
Los formularios usaban form.as_p (salida generica de Django) o bucles
manuales for field in form repetidos en cada plantilla, sin estilos
propios: inputs/selects/textareas sin padding, borde ni foco visibles,
y cada plantilla resolvia los errores/ayuda de campo a su manera.

Cambios:
- base.css: nuevas clases .app-form, .form-field (label + control en
  columna), estilos de input/select/textarea (incluye estado :focus
  con el color de foco del tema), .form-field-checkbox para campos
  booleanos y .form-help para el help_text de los campos.
- Nuevo parcial expenses/_form_fields.html: itera los campos del
  formulario y renderiza label, control, help_text y errores de forma
  consistente. Incluye un caso especial para el campo "tags" de gastos
  (CheckboxSelectMultiple), que se pinta como lista de chips en vez de
  checkboxes sueltos.

Este commit solo anade el estilo y el parcial; las plantillas que lo
adoptan van en el siguiente commit.
Sustituye {{ form.as_p }} (o el bucle for field in form repetido a
mano en expense_form.html) por {% include "expenses/_form_fields.html" %}
y anade class="app-form" al <form> en:

  - categories/form.html, categories/list.html (alta de categoria)
  - expenses/account_form.html
  - expenses/expense_form.html
  - expenses/income_form.html
  - expenses/tag_form.html
  - fuel/create.html
  - goals/form.html
  - registration/login.html

De paso:
- categories/list.html: se corrige indentacion de la tabla y las
  etiquetas <a> de "Editar"/"Eliminar" (estaban dentro de una celda
  sin problema funcional, solo desalineadas) y se anade una <th> vacia
  para la columna de acciones.
- goals/form.html: el bloque title y el <h1> tenian "Nuevo objetivo"
  fijo aunque la vista goal_edit ya pasa title="Editar objetivo" en el
  contexto; ahora la plantilla usa {{ title }}, asi que el formulario
  de edicion deja de decir "Nuevo objetivo" por error.

No se toca la logica de las vistas ni los formularios en forms.py.
Ambas plantillas hacian {% extends "base.html" %}, pero en este
proyecto no existe ninguna plantilla llamada asi a nivel raiz: el
layout de la app se llama "expenses/base.html" (esta dentro de
expenses/templates/expenses/, no de expenses/templates/). Con
APP_DIRS=True y DIRS=[] en settings.py, Django busca "base.html" tal
cual en la carpeta templates/ de cada app instalada y no lo encuentra
en ninguna, asi que un {% extends %} a esa ruta lanza
TemplateDoesNotExist en cuanto se intenta renderizar la plantilla.

Esto llevaba tiempo sin detectarse porque, hasta el commit de
INSTALLED_APPS de esta misma tanda, django.contrib.admin sombreaba
estas dos plantillas con las suyas propias (ver ese commit): la
plantilla rota de la app nunca llegaba a cargarse, asi que el error
nunca saltaba. Al arreglar el orden de INSTALLED_APPS estas plantillas
pasan a usarse de verdad, así que había que arreglarlas ahora sí en el
mismo movimiento.

Cambios:
- Ambas extienden "expenses/base.html".
- Se migran al parcial expenses/_form_fields.html y a class="app-form",
  igual que el resto de formularios (ver commit anterior).
- Se anade un enlace de "Volver" a {% url 'home' %} en las dos, y un
  {% block title %} (heredaban el titulo generico "Expenses manager").
Sintoma: /accounts/password_change/ renderizaba la pantalla del admin
de Django (cabecera "Administracion de Django", breadcrumb "Inicio >
Cambio de contrasena") en vez de la plantilla de la app.

Causa: con APP_DIRS=True (settings.py) y sin DIRS explicito, Django
busca plantillas recorriendo INSTALLED_APPS en orden y usa la PRIMERA
coincidencia por ruta relativa. django.contrib.admin trae sus propias
registration/password_change_form.html y
registration/password_change_done.html. Como 'django.contrib.admin'
aparecia antes que 'expenses' en la lista, sus plantillas ganaban
siempre a las de nuestra app, que ni siquiera llegaban a evaluarse.

Verificado con el shell de Django (get_template(...).origin.name):

  Antes del cambio:
    registration/password_change_form.html -> .../django/contrib/admin/templates/registration/password_change_form.html
    registration/password_change_done.html -> .../django/contrib/admin/templates/registration/password_change_done.html

  Despues del cambio:
    registration/password_change_form.html -> .../expenses/templates/registration/password_change_form.html
    registration/password_change_done.html -> .../expenses/templates/registration/password_change_done.html

Esto tambien explica por que el bug de las plantillas rotas (extendian
"base.html", que no existe; ver commit anterior) llevaba tanto tiempo
sin dar error: al estar sombreadas, esas plantillas nunca se
renderizaban, asi que su TemplateDoesNotExist nunca saltaba. Y explica
por que el login SI funcionaba pese a la colision de app: el admin
publica su login en "admin/login.html", no en "registration/login.html",
asi que ahi no habia conflicto.

Efecto secundario esperado y comprobado (no es una regresion, pero hay
que saberlo): /admin/password_change/, que Django resuelve tambien por
la ruta "registration/password_change_form.html" cuando un admin
cambia su propia contrasena, ahora renderiza con el layout completo de
la app (menu de navegacion, etc.) en vez del look propio del admin.
Probado con un superusuario contra /admin/, /admin/login/,
/admin/password_change/ y /admin/password_change/done/: los cuatro
responden 200 y el formulario funciona (AdminPasswordChangeForm hereda
los mismos campos old_password/new_password1/new_password2 de
PasswordChangeForm, asi que el parcial generico de campos los pinta
sin problema). El resto del admin (listados, cambio de contrasena de
otros usuarios, etc.) no usa ninguna plantilla bajo registration/ ni
admin/ que la app sobrescriba, asi que no le afecta.

No se reordena alfabeticamente esta lista en el futuro sin tener en
cuenta esto: hay un comentario en el propio settings.py explicandolo.
home() (views.py) y expenses/home.html pasan de mostrar tres numeros
sueltos y un listado plano de ultimos gastos a un panel mas completo:

- Saldo total y desglose por cuenta (account_balances/total_balance),
  calculado con dos queries agregadas (values().annotate(Sum(...)))
  en vez de N+1 llamadas a account.current_balance() en un bucle.
- Comparativa del gasto del mes en curso contra el mes anterior
  (prev_total/diff_amount/diff_pct), con aviso de que el mes en curso
  esta incompleto y la comparacion no es equivalente.
- Avisos (has_alerts) cuando hay objetivos excedidos o cuentas en
  negativo, con enlace directo a objetivos/cuentas.
- "Ultimos movimientos" mezcla gastos e ingresos (antes solo se veian
  los ultimos gastos) ordenados por fecha, con icono/color segun tipo.
- Se reutiliza una unica query de Goal tanto para el widget de
  objetivos (show_on_home) como para las alertas de excedidos
  (is_exceeded()), en vez de dos queries separadas.

base.css: nuevo bloque "/* Home widgets */" con las clases que usa la
plantilla (.home-section, .kpi-grid/.kpi-card, .balance-*,
.movements-list/.movement-*, .amount-positive/.amount-negative,
.comparison), incluyendo un ajuste responsive que oculta la cuenta en
movil (<480px).

No relacionado con los formularios de los commits anteriores; se
incluye aqui porque estaba pendiente de subir en la misma rama.
Saca de home() dos ayudantes reutilizables:

- _account_balances(): calcula el saldo de cada cuenta con dos queries
  agregadas en lugar de llamar a account.current_balance() en bucle,
  que disparaba un N+1.
- _category_tree(): aplana las categorias en preorden con su profundidad,
  para poder indentarlas en la plantilla.

Con ellos, account_list pasa a mostrar el saldo calculado de cada cuenta
(antes usaba current_balance() desde la plantilla) y category_list muestra
un arbol indentado en vez de una columna "Categoria padre" que obligaba a
reconstruir la jerarquia mentalmente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El listado de etiquetas era una lista sin mas, sin forma de saber cuales
estaban en uso. Pasa a tabla, en linea con el resto de listados, y anade
una columna con el numero de gastos que usa cada etiqueta.

El contador se calcula con annotate(Count("expenses")) en la vista, en vez
de contar desde la plantilla, para no provocar una query por fila.

Incluye tambien el estado vacio, que la lista no tenia.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El listado de objetivos mezclaba pagos, presupuestos y ahorros sin forma de
separarlos. Anade un filtro por tipo, con el mismo patron que los filtros de
gastos, y ordena los objetivos por nombre. Un valor de "kind" que no este en
KIND_CHOICES se ignora en vez de romper.

De paso, la barra de progreso pasa a usar goal.progress_state en lugar de
decidir el color en la plantilla con un if sobre el porcentaje. La logica ya
vivia en el modelo, que ademas distingue presupuestos (donde acercarse al
100% es un aviso) del resto; la plantilla del dashboard seguia con las clases
antiguas low/medium/high, que se retiran del CSS.

Incluye estado vacio y thead/tbody en la tabla, que le faltaban.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tag era el unico de los tres modelos con nombre sin ordering en su Meta, asi
que las etiquetas salian en orden de insercion. Al anadirlo aparecio el
problema de fondo: con ordering = ["name"], SQLite ordena por valor binario y
en ASCII todas las mayusculas van antes que todas las minusculas, de modo que
salia AA, MK, ZZ, mk, ms en vez de AA, MK, mk, ms, ZZ.

Importa mas de lo que parece porque en produccion la base de datos es
PostgreSQL, que ordena segun la configuracion regional. El orden pasa a
calcularse en la consulta con Lower("name") en vez de depender de la colacion
del motor, asi que local y NAS coinciden.

Los tres Meta usan ahora ordering = [Lower("name")]. Django admite
expresiones ahi, pero no se propaga solo a todos los sitios: habia tres
sitios con orden explicito que lo pisaban y tambien se corrigen.

- tag_list y account_list tenian .order_by("name")
- goal_list ordenaba igual
- _category_tree reordena los hermanos en memoria, y Python compara por code
  point igual que SQLite, asi que el arbol tampoco quedaba bien

Las dos migraciones son AlterModelOptions, sin cambios de esquema, pero hay
que desplegarlas.

Lo que no resuelve: los acentos. Lower() normaliza mayusculas y minusculas,
pero como se ordenan "N", "a" o "u" sigue dependiendo de la colacion de cada
motor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El formulario tiene tres tipos de objetivo y campos que solo aplican a
algunos, sin nada en pantalla que lo explique: no habia forma de saber que
"Account" solo cuenta para ahorro o que "Period" solo cuenta para
presupuesto.

Anade un desplegable, cerrado por defecto, que describe para que sirve cada
tipo y que hace cada campo. Usa <details>/<summary> nativo en vez de
JavaScript, que es accesible por teclado de serie y ya es el patron de los
filtros avanzados de gastos.

El texto esta contrastado con Goal._period_start(), Goal.progress y
Goal.progress_state(), y los nombres de campo son los que el formulario
muestra de verdad. Esas etiquetas salen hoy en ingles porque GoalForm no
define labels y no hay i18n configurada; el panel usa las reales para que
coincida con lo que se ve, no las traducidas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El listado de categorias no daba ninguna pista de cuales estan en uso.
Anade la columna con el mismo criterio que el de etiquetas: annotate en la
vista para no disparar una query por fila.

Cuenta solo los gastos directos de cada categoria, no los de sus
descendientes. Si el numero incluyera a los hijos, un padre sin gastos
propios mostraria una cifra que no corresponde a ninguna fila suya y que no
cuadraria con lo que se ve al filtrar por esa categoria.

Con Category.Meta.ordering = [Lower("name")], Django anade la expresion al
GROUP BY del annotate. Verificado que no multiplica filas ni fusiona
categorias cuyo nombre solo difiere en mayusculas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md documenta la estructura de directorios (que tiene tres niveles
llamados expenses_manager y despista), los comandos habituales, las
variables de entorno y las decisiones de auth, para no tener que deducirlo
de settings.py cada vez.

ANALISIS_code.md es un informe de solo lectura sobre UX, deuda tecnica y
mantenibilidad, ordenado por impacto. Sirve de lista de trabajo pendiente;
varios de sus puntos ya estan resueltos en los commits anteriores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jkuijperm added 1 commit 2026-09-09 14:29:21 +00:00
jkuijperm merged commit 6c6fbbd82c into main 2026-09-09 14:29:29 +00:00
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: jkuijperm/expenses_manager#33
No description provided.