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

90 lines
8.5 KiB
Markdown

# 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
```bash
# 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.