expenses_manager/CLAUDE.md
JKuijperM 4eb120927b Anade CLAUDE.md y el informe de analisis del proyecto
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>
2026-09-09 16:22:08 +02:00

8.5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Repository layout

This git repo has a nested directory structure: the Django project root is expenses_manager/ (contains manage.py), and inside that lives the expenses_manager/ settings package plus the expenses app. So paths look like:

expenses_manager/                  <- repo root
  expenses_manager/                <- Django project root (manage.py here)
    expenses_manager/              <- settings package (settings.py, urls.py, wsgi/asgi)
    expenses/                      <- the single Django app with all models/views/forms
    pytest.ini
    requirements.txt
    Jenkinsfile

All commands below assume you're in expenses_manager/expenses_manager/ (the directory with manage.py).

Commands

# Install deps (a conda env or any virtualenv works)
pip install -r requirements.txt

# Run the dev server
python manage.py runserver

# Run all tests (pytest, not manage.py test)
pytest

# Run a single test file / test
pytest expenses/tests/test_dashboard.py
pytest expenses/tests/test_goals.py::test_budget_is_exceeded

# Run with coverage (what CI/Jenkins runs)
pytest --cov

# Migrations
python manage.py makemigrations
python manage.py migrate

# Seed demo data (guarded: only runs with DEBUG=True)
python manage.py seed_demo

Tests use pytest-django; pytest.ini sets DJANGO_SETTINGS_MODULE=expenses_manager.settings. There is a shared expenses/tests/conftest.py with fixtures (user, auth_client, account, category). Tests needing DB access must be marked @pytest.mark.django_db (or set pytestmark = pytest.mark.django_db at module level).

Configuration & environment

Settings are driven entirely by environment variables read from a .env file (via python-dotenv) next to manage.py. A single settings.py serves both local dev and production; behaviour switches on DEBUG:

  • SECRET_KEY — required when DEBUG=False (the app raises ImproperlyConfigured and refuses to start if missing). With DEBUG=True it falls back to an insecure dev key.
  • DEBUG — defaults to False. When True, SQLite is used by default and the production hardening block (secure cookies, SSL redirect, proxy header) is skipped.
  • Database is selected by DB_ENGINE: postgresql uses Postgres with DB_NAME/DB_USER/DB_PASSWORD/DB_HOST/DB_PORT; any other value (or unset) falls back to SQLite. There is no DATABASE_URL — the DB config uses these separate variables.
  • ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS are comma-separated env vars.

See .env.example for the full list. CI (Jenkins) sets DEBUG=True and a throwaway SECRET_KEY in its environment block so tests run against SQLite without the hardening block.

Architecture

This is a single-app Django project (expenses) — there is no REST API or frontend build step; views render server-side Django templates directly. All views live in one expenses/views.py, forms in one expenses/forms.py, models in one expenses/models.py.

Ownership model: every domain model (Category, Account, Tag, Expense, Income, Goal) has an owner FK to settings.AUTH_USER_MODEL. Every view filters querysets by owner=request.user — there is no shared/global data between users. When adding a new view or form, follow this same pattern (filter querysets by owner, scope get_object_or_404 by owner too) to avoid leaking data across accounts.

Core models (expenses/models.py):

  • Account — holds initial_balance and computes running balances. current_balance() sums all incomes/expenses against the initial balance. monthly_balance(year) builds a running-balance series per month for charts, and patches the current month's entry with the live current_balance() so today's number is always exact. balance_until(date) and monthly_net(year) support other reporting views (both use aggregated queries, not month-by-month loops). Accounts are soft-deleted (active=False), never physically removed, to preserve historical balances; inactive accounts are filtered out of form querysets but shown dimmed in the account list.
  • Expense — belongs to an Account and a Category (both PROTECT on delete, so you can't delete a Category/Account that has expenses), optionally tagged with Tag (M2M).
  • Income — same shape as Expense but simpler (no category/tags).
  • FuelEntry — a OneToOneField extension of Expense for tracking car fuel fill-ups (odometer, liters); fuel_create/fuel_edit/fuel_delete views create/update/delete the paired Expense + FuelEntry together (delete cascades via the OneToOne), auto-assigning a "Gasolina" category scoped to the owner via get_or_create. FuelEntryForm is a ModelForm of Expense plus the FuelEntry-specific fields.
  • Goal — a target with a kind field (payment, budget, or saving):
    • payment (debt/repayment) and budget aggregate expenses in a Category (optionally including subcategories via include_subcategories + Category.descendant_ids()).
    • budget resets per period (month/year): only the current period counts, and is_exceeded() flags going over.
    • saving measures progress via the associated Account's balance (provisional — this is the branch to change when an investments module is added).
    • progress is a cached_property (accessed as goal.progress, no parentheses), memoized per instance because percentage()/bar_width()/progress_state()/is_exceeded() all derive from it. progress() is scoped by _period_start(), not aggregated across all time.
  • Category — supports self-referential parent for subcategories; descendant_ids() returns a category plus all its descendants. Forms must scope the parent queryset by owner and, when editing, exclude the category itself and its descendants to prevent cycles (there was a past bug where parent categories leaked across users, and another where clean_parent silently dropped the value — see git history). Full CRUD exists (category_edit/category_delete); deletion catches ProtectedError when the category has expenses.

Views/forms convention: forms that need to scope choice fields by user (category, account, tags, parent) accept a user kwarg in __init__ and filter querysets there, rather than doing it in the view. Views pass user=request.user when instantiating these forms both on GET and POST. Delete views require POST and render a confirmation template on GET (pattern established across tag_delete, goal_delete, etc.).

Dashboard view (expenses/views.py:dashboard) is the most complex view: it supports period presets (this_month/last_month/this_year), per-account filtering, and an optional prior-period comparison (compare=1) that diffs current vs. previous period totals overall and by category. It also builds per-account annual balance chart data via Account.monthly_balance(). Note: this view is large (~200+ lines) and is a known refactor target.

Templates: organized by feature under expenses/templates/expenses/ (expense/account/income/tag views + dashboard/home), plus separate categories/, fuel/, goals/, settings/, registration/ subfolders. Template naming is inconsistent by folder: files under expenses/templates/expenses/ use a resource prefix (account_list.html, tag_confirm_delete.html), while categories/, goals/, fuel/ use unprefixed names (list.html, form.html, confirm_delete.html) — check the target folder before naming a new template. UI strings are in Spanish; keep that consistent when touching UI code.

Auth: uses Django's built-in django.contrib.auth views/urls (login/logout/password change) mounted at /accounts/. The app's own financial-account CRUD lives under /finance-accounts/ (renamed from /accounts/ to avoid colliding with the auth namespace). There is no password-reset-by-email flow by design: resets are done by the admin via manage.py changepassword, and a static registration/password_help.html page (route password-help/) tells users to contact the admin. LOGIN_URL/LOGIN_REDIRECT_URL/LOGOUT_REDIRECT_URL are set in settings. All app views are decorated with @login_required.

Workflow

  • Work happens on the dev branch; main only receives tested merges. Jenkins runs the test suite on main.
  • Migrations are generated in development and committed; never run makemigrations in production (it would create migrations not in git). Only migrate runs on the NAS.