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