Plan de trabajo priorizado #29
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Plan de trabajo priorizado — expenses_manager
Junta las dos fuentes: las mejoras de producto (recorrido vista por vista,
archivos
01–10) y el análisis técnico/visual de Code (ANALISIS_code.md).Organizado en tandas, no por áreas sueltas: cada tanda agrupa cosas que se tocan
juntas, para no abrir la misma vista dos veces.
El orden es una recomendación, no una obligación. La decisión de producto es del
propietario; lo técnico va anotado con el criterio de por qué sí o por qué no.
Tanda 0 — Arreglos rápidos y documentación (bajo esfuerzo, hacer ya) ✅ COMPLETADA
Cosas de bajo riesgo y alta relación coste/beneficio. Ninguna toca la lógica de
negocio de forma peligrosa.
income_confirm_delete.htmlmostraba{{ expense.date }}en vez de{{ income.date }}(fecha vacía en la confirmación). — ya corregidoGoal.progress()concached_property(Code 2.2). Hoy serecalcula 3-5 veces por objetivo en cada render de home/dashboard/objetivos.
Cambio pequeño, riesgo casi nulo, quita muchas queries. El mejor coste/beneficio
de todo el informe. (Hecho:
progresspasó acached_property; llamadas internasen
percentage()/is_exceeded()ajustadas aself.progresssin paréntesis; testsactualizados y en verde. Sin migración.)
CLAUDE.md(Code 3.1): además de los 3 puntos de Code, se puso aldía todo lo cambiado estas sesiones (Goal con
kind/periodos/cached_property,soft-delete de cuentas, CRUD de categorías y prevención de ciclos,
fuel_delete,/finance-accounts/, settings unificado por env, convención de nombres de plantillas,workflow dev/main).
README.md(Code 3.2): reescrito de cero, fiel al proyecto real(conda + SQLite en local, tabla de variables de entorno, Dockerfile documentado y
docker-compose.ymlde ejemplo sin secretos — el real vive fuera del repo en el NAS)..env.example(Code 3.8): añadido al repo con todas las variables comoplantilla (placeholders inútiles, sin secretos).
models.py,el comentario obsoleto de sourcery en
views.pyy unificadas las comillas enGoalForm.Meta.fields.Tanda 1 — Responsive / usarlo en el móvil (alto valor personal) ✅ COMPLETADA
Motivo de prioridad: el objetivo declarado es "poder usarlo más desde el móvil", y
hoy no se puede porque no está adaptado. Esto es lo que desbloquea ese uso.
base.htmlybase_auth.html(Code 1.1) — una línea,cambia radicalmente cómo se ve en móvil. (Hecho; de paso se corrigió el
<!DOCTYPE>incompleto de
base.htmly se añadiólang="es".)overflow-x:auto(gastos, comparación deldashboard, fuel con 7 columnas) para que no desborden. (Hecho: las 8 tablas del
proyecto envueltas en
.table-wrapconwhite-space:nowrap, se ensanchan segúncontenido sin anchos fijos arbitrarios.)
base.csspara los breakpoints principales.(Hecho: breakpoints 768px y 480px; grids —kpi/dashboard/goals/settings— a 1 columna
en móvil.)
Extras que salieron al probar en móvil y se resolvieron en la misma tanda:
.chart-boxde alturafija envolvía título + canvas y desbordaba). Corregido en
dashboard.htmlyhome.html(título fuera de la caja, canvas en contenedor de altura controlada).expenses/static/expenses/js/nav.js):botón ☰ bajo 768px que despliega la nav; vanilla JS solo para el toggle, con
aria-expanded. En escritorio no cambia nada.nav.js): pasó de:hovera click en escritorio y móvil, con cierre al pulsar fuera. El toggle es un<button>real conaria-haspopup/aria-controlsy foco por teclado — estoadelanta el punto 1.4 de la Tanda 3 (dropdown accesible por teclado).
dashboard (ya no se parte/solapa en móvil).
Tanda 2 — Coherencia visual (base para que todo se vea igual) ✅ COMPLETADA
Lo que hace que la app deje de verse "un poco fea/inconsistente" (queja explícita).
:root { --color-success… --color-danger… }y sustituir los 3-4 tonos distintos de verde/rojo repartidospor
base.cssy losstyle="..."inline dedashboard.html. Base para todo lovisual posterior. (Hecho: paleta Forest Teal aplicada con variables semánticas
(
--color-bg/--color-surface/--color-text/--color-primary...), pensadas parasoportar dos modos. Hex dispersos e inline sustituidos por variables.)
.btn/.btn-primary/.btn-secondary/.btn-dangercoherentes; los "Volver"/"Cancelar" que eran enlacesmorados pasan a botón real con misma altura y separación; aplicado en formularios y
confirmaciones. (Resuelve lo que se detectó al probar la Tanda 1 en móvil.)
[data-theme="dark"], sigue la preferencia del sistema por defecto, botón sol/lunaen la topbar para forzar, elección recordada en
localStorage, con script inlineanti-flash en el
<head>. Login incluido..form-actions): los pares Guardar/Crear + Volver(y Eliminar + Cancelar en las confirmaciones) quedan en horizontal, acción principal
a la izquierda, con separación y wrap responsive. Un solo cambio en la parcial
_confirm_delete.htmlcubrió las 7 confirmaciones.fuel_editconstruía el form sin
instance=y hacíaexpense.date = form.save(...)→ editarreventaba; y el
cancel_urldefuel/confirm_delete.htmlestaba fijo afuel_listignorando el
next→ cancelar desde gastos llevaba a repostajes. Ambos corregidos ycubiertos con tests de regresión nuevos (editar guarda bien, POST inválido no
rompe,
nextse respeta y unnextexterno se ignora).idénticas pero divergen en botón/clase/encabezado. Unificar en un
{% include %}con variables. De paso previene bugs como el de
income_confirm_delete. (Hecho conherencia + bloques (
_confirm_delete.html): más robusto que include con variablesplanas porque las preguntas mezclan
<strong>e importes interpolados. Los avisosespeciales (cascada de categorías, gasto asociado de fuel) se conservan vía bloque
delete_warnings. De paso se limpiaron typos (€sobrante en tag,?faltante encuenta,
form.as_pmuerto en categorías) y se normalizó<h2>→<h1>en las 7—esto adelanta el punto 1.9 de la Tanda 3.)
.pagination,.form-errors,.form-field, etc. — revisar si la paginación y los errores deformulario se están viendo sin estilo. (Inventario real: la mayoría ya tenían
estilo de trabajo posterior al análisis de Code; los huecos reales eran los enlaces
de paginación (
.step-links a/span, sin clase → sin formato) y.checkboxdeldashboard. Estilados reutilizando el lenguaje de chips existente. De paso se
aclararon en modo oscuro los filtros de periodo (subiendo
--color-chip-bg/--color-chip-hoverbajo[data-theme="dark"]), tono final afinado a mano.)iguales (Code 1.8:
tag_listusa<ul>,categoriesusa<table>).(Parcialmente avanzado: las 7 confirmaciones de borrado se normalizaron a
<h1>enesta tanda, y
goals/list.htmlen la Tanda 3. Lo que queda: eldashboard.htmlno tiene ningún
<h1>—empieza en<h2>y el resto son<h3>—, así que arreglarlobien implica bajar todos los
h3→h2y revisar que no se muevan tamaños en CSS. Yel 1.8 sigue intacto.)
Ajuste — gráficos de Chart.js en modo oscuro ✅ COMPLETADA
Detectado al usar el modo oscuro; JS, no CSS. Los colores cambian según el tema activo
y se actualizan en vivo al cambiar de tema.
--chart-grid-color), leída desde el JS.--chart-fill-color).charts.jscongetChartColors/applyCartesianColors/registerThemedChart/refreshThemedChartsMutationObserversobredata-theme.Bugs resueltos por el camino (varios intentos):
t.startsWith is not a functionNO era por colores inválidos (verificado en consolaque las variables devolvían strings válidos). Causa real: mutar
chart.options.scalesDESPUÉS de crear un gráfico
type:'line'choca con las opciones reactivas de Chart.js.opciones del
new Chart(...)(arregló el render inicial); (2) en el refresco en vivo,reasignar
scale.grid/scale.ticksenteros conObject.assignen vez de mutarscale.grid.colordirectamente (arregló el cambio de tema en vivo). Comentado enespañol para que nadie lo "simplifique" y reintroduzca el bug.
refreshThemedChartsusachart.update('none')para no re-animar en cada cambio.Tanda 3 — Accesibilidad ✅ COMPLETADA
:hoverpuro, sin toggle por teclado → un usuario de teclado no puede llegar aCategorías/Etiquetas/Objetivos. Es una ruta bloqueada, no solo "buena práctica".
(Ya resuelto en la Tanda 1: el toggle pasó a
<button>con click + Enter/Espacioy
aria-expanded.)dashboard, 3 en gastos) no tenían ningún label asociado. Resueltos con
<label class="sr-only">+ utilidad.sr-onlynueva enbase.css. Se eligió labeloculto en vez de visible porque los selects ya llevan una primera opción que hace de
etiqueta a la vista ("Año", "Mes", "Todas las cuentas"), así que el layout de la fila
de filtros —afinado en la Tanda 2— no se toca. El select de categoría de los filtros
avanzados pasó de label implícito a explícito con
for/id.role="progressbar"+aria-valuemin/aria-valuemax/aria-valuenow+aria-label+aria-valuetextengoals/list.htmly en el widget de objetivos del dashboard.Detalle a no perder:
aria-valuenowtiene que ir con|unlocalize. Sin él, lalocalización española escribe
45,5con coma y deja de ser un número válido para elatributo. El
aria-valuetextsí lleva coma a propósito: es el texto que el lector depantalla pronuncia, y en español la coma es lo correcto.
Arreglados de paso, en el mismo pase:
goal.percentageen vez degoal.bar_width, así que un presupuesto por encima del100% se salía de la caja.
goals/list.htmlya usababar_width; el dashboard sehabía quedado atrás.
Tanda 2, destapado al probar con un objetivo al 1017%):
.progress-containeres flexy
.progress-labelteníamin-width: 90pxpero nada que le impidiera encogerse.Como
.progress-barllevabawidth: 100%, reclamaba todo el ancho y obligaba a lasetiquetas a bajar a su mínimo; el texto largo (
1017,0€ / 100,0€) se desbordaba de sucaja y la barra, con fondo sólido y pintada después, lo tapaba. Corregido con
flex-shrink: 0+white-space: nowrapen.progress-labelyflex: 1+min-width: 0en.progress-bar. Elwidth: 100%de.progress-barse conserva apropósito: en el widget del dashboard la barra NO está dentro de un flex
(
.goal-cardes un bloque normal), así que ahí mandawidth, mientras que en ellistado manda
flex-basis. Una sola regla cubre las dos disposiciones.Solo se veía con etiquetas de más de 90px, o sea importes largos o porcentajes de
tres cifras — por eso llevaba tiempo sin detectarse.
colspanincorrecto en dos estados vacíos: listado de gastos (4 → 6) y gastosrecientes del dashboard (7 → 5).
<div>a<nav aria-label="Paginación de gastos">.role="group"+aria-label, yaria-hiddenen el "Tags:" visible para que no se lea dos veces.
goals/list.htmlempezaba en<h2>→<h1>(resto del punto 1.9, ver Tanda 2).Tanda 4 — Home ✅ COMPLETADA
Ver
01_home.md.por cuenta (3 consultas fijas) en vez de llamar a
current_balance()en bucle, quereproduciría el N+1 anotado para el dashboard (Code 2.1). Ojo: no se puede
resolver con un solo
annotate()de dosSumsobreexpenseseincomes— eljoin multiplica filas e infla los totales.
Solo aparece si hay algo que señalar. Reutiliza las clases
.messagede Django(
warningpara presupuestos,errorpara cuentas) en vez de inventar clases.el mes anterior es 0 no se calcula el porcentaje (dividir por cero, y "+100%" desde
cero engaña más que informa). Lleva una nota fija de que el mes en curso está
incompleto.
como máximo. Sin enlace por fila a propósito: comprobar
expense.fuel_dataparaelegir la URL de edición dispararía una consulta por fila.
show_on_homepara el widget, los excedidos para el banner). Es deliberado:Goal.progresses unacached_property, así que compartir instancias evitarecalcular el progreso de un objetivo que aparezca en ambos sitios.
is_exceeded()cortocircuita si el tipo no es presupuesto, así que pago y ahorro no tocan
progress.Arreglados de paso:
fuera de la Tanda 3, donde solo se tocaron
goals/list.htmly el dashboard. Mismopatrón, con
aria-valuenowsobrebar_width(nopercentage) para no salirse delaria-valuemax="100"declarado cuando un presupuesto se pasa.<h3>Objetivos</h3>→<h2>: la página empieza en<h1>y el resto desecciones son
<h2>.sectionni para.btnen todo el CSS. El home antiguo lo disimulaba con<br>sueltos. Resuelto con
.home-sectiony.section-actions, sin tocar reglasglobales para no descolocar el dashboard ni el listado de gastos.
last_expensesdesaparece, sustituida pormovements.Tanda 4b — Formularios y pantallas de contraseña ✅ COMPLETADA
Detectado al terminar la Tanda 3, no estaba recogido en ninguna tanda anterior: la
Tanda 2 unificó botones y la fila de acciones (
.form-actions), pero nunca tocó loscampos en sí.
Diagnóstico de partida: no existía ni una sola regla para
input,selectotextareaenbase.css. Solo estaban.form-field(unmargin-bottom) y.form-errors(color). Por eso cada campo se veía de un tamaño distinto: sin CSS,cada control usa su ancho intrínseco del navegador.
tipografía y
:focuscon las variables semánticas existentes, así que funcionan enclaro y oscuro sin trabajo extra. Decisión clave: las reglas se acotan a
.form-fielden vez de escribirlas sobreinput/select/textareaa pelo. Asílos controles con tratamiento propio quedan fuera por construcción —las chips de
tags, el
.checkboxdel dashboard y los selects de la fila de filtros— sin dependerde una lista de excepciones que se olvida.
expenses/templates/expenses/_form_fields.html(label + widget +help_text+errores), aplicada a todas las plantillas de formulario en sustitución de
form.as_py del bucle manual. El caso especial de las chips detagsvive dentrode la parcial para no repetirlo. Las casillas invierten el orden (control antes que
etiqueta) porque a todo lo ancho se leen fatal.
lenguaje visual. Typo corregido en
password_help.html("administrados").Bug real destapado — plantillas sombreadas por el admin:
/accounts/password_change/estaba renderizando la pantalla del admin de Django,no la de la app. La causa no eran las URLs (
urls.pyestaba bien) sino el orden deINSTALLED_APPS:django.contrib.adminpublica sus propiasregistration/password_change_form.htmlypassword_change_done.html, y el cargadorde plantillas por aplicación recorre
INSTALLED_APPSen orden quedándose con laprimera coincidencia. Al ir el admin antes que
expenses, ganaba siempre.Esto explicaba una contradicción que costó desenredar: las plantillas de la app
extendían
"base.html"(ruta inexistente; la base real es"expenses/base.html"),así que si se hubieran renderizado habrían lanzado
TemplateDoesNotExist— peronunca se renderizaban, así que el error jamás salió. El login no estaba afectado
porque el admin publica el suyo en
admin/login.html, no enregistration/."expenses"movido por encima de"django.contrib.admin"enINSTALLED_APPS, con comentario explicando por qué, para que nadie lo ordenealfabéticamente y reintroduzca el bug.
"expenses/base.html".get_template().origin.nameantes ydespués: solo estaban afectadas esas dos. Ninguna plantilla de
expenses/,goals/ocategories/colisiona condjango.contrib.*.Efecto secundario aceptado:
/admin/password_change/ahora usa la plantilla dela app (navegación de la app en vez del look del admin). Funciona —
AdminPasswordChangeFormhereda los mismos tres campos, así que la parcial losrenderiza bien— y se decide dejarlo así: es un flujo interno que se usa muy de vez en
cuando, y de paso las dos rutas de cambio de contraseña llevan ahora a la misma
pantalla, lo que es más coherente, no menos. Criterio para el futuro: si algún
día hay que sobrescribir más plantillas del admin y empiezan a chocar, entonces sí
toca mover las de la app a un directorio propio en
TEMPLATES["DIRS"], que tieneprioridad sobre las de aplicación sin depender del orden. Reordenar es la solución
barata mientras solo se sombreen las de
registration/.Bugs corregidos de paso:
goals/form.htmlponía "Nuevo objetivo" siempre, también al editar.categories/list.htmltenía la columna de acciones en el<tbody>pero solodos
<th>en el<thead>. Mismo tipo de desajuste que loscolspande la Tanda 3.Sin migración, sin cambios en vistas: CSS, plantillas y una línea de
settings.py.Tanda 5 — Listado de gastos (la pieza de filtrado)
Ver
02_listado_gastos.md. Es la tanda más grande de producto.de paso la query duplicada de
Tag(Code 2.7) y añadirselect_related/prefetch_related(Code 2.5).09_transversales.md: el rango de fechas y el mecanismo dinámico debendiseñarse pensando en reutilizarlos en el dashboard.
idde los selects de filtro y losatributos ARIA añadidos en la Tanda 3 tienen que sobrevivir al fragmento nuevo.
Es fácil perderlos al generar HTML desde JS.
Tanda 6 — Dashboard (producto + el N+1)
Ver
03_dashboard.md. Es la vista más usada para análisis y la que más sebeneficia de tocar producto y rendimiento a la vez.
integrados, rango de fechas, ingresos vs gastos.
dashboard(Code 3.4): 226 líneas, 5responsabilidades, contexto de 35 claves. Extraer en
_resolve_period,_build_comparison,_build_account_charts.pendiente de la Tanda 2), que hoy no tiene
<h1>.<script>muerto declearPeriodPreset()(definido y nunca llamado) yel bloque de script comentado de los presets.
_account_balancesexiste desde la Tanda 7 y solo hay que usarlo aquí.
base.cssnunca ha definido tamaños de encabezado, así que al bajar los niveles todo pasa a
los del navegador (32/24/18.7px), que en una vista densa de datos compiten con las
propias cifras. Escala acordada:
h11.625rem,h21.25rem,h31rem, peso 600,con márgenes fijos en
rem(los del navegador van enemy escalan con la fuente,dejando huecos irregulares). Afecta a toda la app, no solo al dashboard: repasar
el resto de vistas después. Es el mismo agujero que el espaciado del home y los
controles de formulario — CSS que nunca se escribió.
Traspasos entre cuentas — los gastos totales están inflados
Problema real detectado al revisar el dashboard. Hoy un traspaso entre cuentas se
registra como un
Expenseen la cuenta origen y unIncomeen la destino. Los saldoscuadran, pero la salida cuenta como gasto en el dashboard, en la comparativa del
home y en el progreso de los presupuestos, aunque el dinero no haya salido del
patrimonio. Caso típico: al cobrar la nómina se manda una cantidad a otra cuenta.
Asimetría del modelo que condiciona la solución:
Incomeno tiene categoría (solonombre, importe, fecha y cuenta), así que "marco una categoría de traspaso y la
excluyo" solo cubre la mitad de la operación.
Decisión tomada: booleano
is_transferenExpensey enIncome. Los marcadosquedan fuera de todas las agregaciones de análisis pero siguen contando para el saldo
de la cuenta. No cambia la forma de registrar: se siguen metiendo las dos patas, ahora
marcadas.
La alternativa era un modelo
Transferpropio (origen, destino, importe, fecha), quees lo correcto de diseño: un solo registro en vez de dos sueltos que nada relaciona, y
Expense/Incomerecuperando su significado de dinero que entra o sale de verdad.Se descarta por ahora porque obliga a tocar
current_balance(),monthly_balance(),balance_until()ymonthly_net(), que es el código con mástests y del que cuelga medio dashboard. El booleano arregla el problema que molesta y
es reversible: el día que exista
Transfer, los registros ya marcados son exactamentelos que hay que migrar.
Lo que el booleano NO da: nada garantiza que se marquen las dos patas ni que los
importes coincidan. Con un solo usuario y disciplina, sirve.
is_transferenExpenseeIncome(migración), con su casilla enlos dos formularios.
del dashboard, comparativa y KPIs del home,
by_category, yGoal.progressparalos tipos pago y presupuesto. Repasar el proyecto entero, no solo el dashboard.
current_balance(),monthly_balance(),balance_until()ymonthly_net()NO deben filtrarlos. Es justo lo contrario queel punto anterior, y confundirlo descuadraría todos los saldos.
vistazo.
presupuesto, y que sí afecte al saldo de las dos cuentas.
Aviso sobre los datos históricos: los traspasos ya registrados que no llevan
categoría no se pueden identificar automáticamente — nada distingue un gasto real
de un traspaso sin marcar. Hay que repasarlos a mano o asumir que lo antiguo queda
sucio y que a partir de la migración está bien. Mirar cuántos son antes de decidir.
Conecta con la Tanda 9: meter dinero en una cuenta remunerada es un traspaso, no
un gasto. Tener esto resuelto antes le quita medio problema al módulo de inversiones.
Clasificación de categorías: comprometido vs flexible (Idea A)
Es la Idea A de
10_modulo_inversiones.md, que ese documento ya marcaba comoindependiente del módulo de inversiones y hacedera en cualquier momento. Va aquí y
no en la Tanda 9 porque no comparte nada con el patrimonio: clasificar categorías
es análisis del dinero que sale, el módulo de inversiones es el dinero que
está guardado. Además el sitio donde esto se ve es el dashboard, que ya se abre
en esta tanda.
Dos campos, no uno de tres estados. "Fijo esencial", "fijo prescindible" y
"variable" mezclan dos ejes distintos y dejan fuera la casilla de variable
esencial (compra, gasolina, farmacia), que es probablemente la mayor parte del
gasto. Con dos campos se pueden responder dos preguntas que hoy no se pueden:
is_fixed(fijo / variable) — cuánto del dinero está comprometido de antemano.is_essential(esencial / prescindible) — cuánto se podría recortar apretándoseel cinturón.
Herencia con vía de escape. El campo existe en todas las categorías y admite
estar vacío, y vacío significa "lo que diga mi padre". Así se rellenan solo los
padres y los hijos heredan, pero un hijo que se salga de la norma (el seguro del
coche dentro de "Suscripciones", que es prescindible) se puede marcar aparte.
Limitar el campo solo a las categorías padre daría el mismo ahorro sin esa salida.
Coste a saber de antemano: agregar por un atributo heredado no se puede hacer
en SQL directamente. Para el desglose del dashboard hay que resolver la herencia en
Python antes de sumar. Con pocas categorías no es problema — el árbol ya se monta en
memoria en
category_list— pero es trabajo real, no un campo y ya.Partirlo en dos fases:
columna en el listado. Lleva migración. Las categorías existentes quedan
vacías a propósito: mejor rellenarlas a mano que inventar un valor por defecto del
que luego no te fías.
resuelta en Python. Hacerla cuando ya haya categorías clasificadas y se vea si la
información resulta útil.
Tanda 7 — Vistas de gestión ✅ COMPLETADA (parcialmente)
Ver
04–07. Se acotó a propósito a lo que no necesita migración de esquema nioperaciones destructivas.
llamando a
account.current_balancedesde la plantilla: 2 consultas por cuenta.Extraído el helper
_account_balances(user, active_only=False)enviews.py, quehace 3 consultas fijas, y
home()pasa a usarlo también (era el segundo sitio conla misma lógica). Saldo negativo en rojo reutilizando
.amount-negativede laTanda 4. Disponible para la Tanda 6, que tiene el mismo N+1 pendiente.
una sola consulta (helper
_category_tree), con profundidad capada a 4 y sangríapor clases
.cat-depth-1..4. Un padre fuera del conjunto se trata como raíz paraque ninguna categoría desaparezca en silencio. La columna "Categoría padre" se
retira: la jerarquía ya se ve por la sangría. El contador cuenta solo gastos
directos, no los de descendientes (decisión fijada con un test).
<ul>a<table>. Esto cierra elpunto 1.8 del análisis de Code (patrones UI distintos para datos iguales), que
seguía abierto desde la Tanda 2.
?kind=contraKIND_CHOICES(un valor inventado se ignora en vez de romper, mismo criterio que
_get_int), conlabel
.sr-onlyy estado vacío nuevo para cuando el filtro no devuelve nada.dashboard.htmlpasa del{% if %}sobrepercentagea{{ goal.progress_state }}, como ya hacíangoals/list.htmlyhome.html. Las clases.low/.medium/.highquedaronmuertas y se retiraron de
base.css.Extras hechos después, en el mismo bloque de trabajo:
Tag,CategoryyAccount:ordering = [Lower("name")]. Antes SQLite ordenaba por valor binario (AA, MK, ZZ, mk, ms) y, lo que es peor, el orden difería entre SQLite en local y PostgreSQL enel NAS; ahora se calcula en la consulta y coincide.
goal_listpasa también a.order_by(Lower("name")). Tres sitios NO se propagaban solos y hubo quetocarlos: los
.order_by("name")explícitos detag_listyaccount_list(seconvirtieron a
Lower, no se borraron, porque elorder_byexplícito protege elannotate) y elsorted(key=lambda c: c.name)de_category_tree, que ordenabaen Python por code point igual que SQLite.
<details>nativo (sin JS,accesible por teclado de serie, mismo patrón que los filtros avanzados de gastos)
explicando los tres tipos y para qué sirve cada campo.
Migraciones:
0011(ordering deTag) y0012(los tresAlterModelOptionscon
Lower). Ninguna toca el esquema, pero hay que desplegarlas.Queda fuera, para otro momento:
Goal.progress/_period_start(Code 3.5) al tocar el modelo.category_createdel listado (Code 2.6).GoalFormmuestra "Category","Include subcategories", "Show on home"... porque los campos no tienen
verbose_name. Es la versión visible para el usuario de la deuda de "comentariosen dos idiomas". Pase aparte, con migración.
Tanda 8 — Módulo de vehículos (módulo nuevo grande)
Ver
08_repostajes_vehiculos.md. Es prácticamente un módulo nuevo; abordar entero,no a trozos sueltos. Orden interno sugerido en su propio archivo.
Tanda 9 — Módulo de inversiones / patrimonio (el más grande)
Ver
10_modulo_inversiones.md. La evolución de mayor alcance. Empezar por registromanual de los cuatro tipos; precios automáticos como capa posterior (cripto primero).
La Idea A (clasificar gasto fijo/variable) NO va aquí: se ha movido a la Tanda 6,
con dos campos (
is_fixedeis_essential) en vez del campo único de tres estadosque se planteó en la ideación. Ver el detalle allí.
Refactors grandes — decididos NO hacer por ahora
Anotados a conciencia como "no hacer salvo que duela de verdad":
20+ vistas que hoy funcionan y están cubiertas por tests. Riesgo/beneficio no
compensa en un proyecto personal. Dejar salvo reescritura por otro motivo.
views.pyen un paqueteviews/(Code 3.3): más razonable que loanterior (es mover funciones, bajo riesgo) pero no urge. Hacerlo el día que el
archivo estorbe de verdad al trabajar, y de forma incremental.
se hace a la vez que otra reestructuración.
Deuda ya conocida (de la fase anterior, no del análisis de Code)
main.Preparado, pendiente de ejecutar cuando se quiera (ver
TODO_revision.md).gunicornen requirements pero no en el venv (Code 3.6): mismo patrón quewhitenoise. Revisar todas las dependencias dev vs producción de una vez.comentarios, inglés en identificadores, que es lo mayoritario) y aplicarla a
partir de ahora sin reescribir lo viejo.
Lecciones que conviene no perder
conviven sin problema (pasó con el
t.startsWithde Chart.js). Todo lo visual ytodo lo de JS se valida a mano en DevTools, en móvil y en claro/oscuro.
de las barras de progreso llevaba desde la Tanda 2 y no se vio hasta tener un
objetivo al 1017%. Merece la pena probar las vistas con valores largos y absurdos,
no solo con datos realistas.
las barras de objetivos (
bar_widthen un sitio ypercentageen otro,progress_stateen un sitio y un{% if %}en otro). Cuando un cálculo vive en elmodelo, usarlo en todas partes en vez de replicarlo en plantilla.
llevaba los ARIA de la Tanda 3 simplemente porque en esa sesión no se abrió el
archivo. Cuando un cambio afecta a "todas las barras de progreso" o "todos los
formularios", conviene un grep del patrón antes de dar la tanda por cerrada.
sectionni.btntenían margen, y elhome antiguo lo disimulaba con
<br>sueltos hasta que llegó contenido de verdad.El mismo patrón apareció en los formularios (Tanda 4b): sin reglas para
input/select/textarea, cada control usa su ancho intrínseco del navegador.de los controles se colgaron de
.form-fielden vez de escribirlas sobreinputoselectdirectamente. Las chips, el checkbox del dashboard y los filtros quedanfuera porque no están dentro de ese contenedor, no porque alguien se acordara de
excluirlos. Una lista de excepciones se queda desfasada; una regla acotada no.
desde el principio sirviéndose desde
django.contrib.adminpor el orden deINSTALLED_APPS. El síntoma era estético, la causa era de configuración, y solosalió al empeñarse en rediseñarla. Cuando una plantilla no responde a los cambios,
confirmar con
get_template("ruta").origin.namequé archivo está usando Django deverdad antes de seguir tocándola.
git diff. En un mismo informe afirmóque el proyecto no tenía repositorio git (había comprobado
git statusen la carpetacontenedora, no en la del repo) y que
expense_form.htmltenía etiquetas rotocuando estaba bien formado. Los cambios eran correctos; la descripción no. El diff
tarda menos en leerse que en deshacer un cambio hecho sobre una premisa falsa.
Meta.orderingno llega a todas partes. Al ponerlo enTaghabía tres sitiosque lo pisaban: dos
.order_by("name")explícitos en vistas y unsorted()enPython dentro de
_category_tree, que ordenaba por code point igual que SQLite. Uncambio "en el modelo y ya" hay que verificarlo en pantalla, no darlo por propagado.
ordering = ["name"], SQLite enlocal y PostgreSQL en el NAS ordenaban distinto y nadie lo había notado. Cualquier
cosa que delegue en la base de datos (orden, colación, comparaciones de texto) puede
comportarse distinto en los dos entornos.
Lower("name")lo resuelve paramayúsculas; los acentos siguen dependiendo de la colación.
pidió en la Tanda 7, no se implementó, y se dio por cerrado sin comprobarlo en
pantalla. Repasar la lista del prompt contra la vista real antes de cerrar una tanda.