Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Contribution Ledger

License: MIT Python 3.9+ Flask 3.x Tests: pytest

A lightweight, offline-first contribution and financial ledger for small organizations — clubs, church groups, associations — that need to track member dues and general income/expenses without standing up a database server or paying for hosting. Runs entirely on a local SQLite file behind a single shared admin login.

Dashboard screenshot

Table of Contents

Features

  • Member management — add, rename, and remove members
  • Dues tracking — log a member's payment against a coverage period (e.g. "October 2024 → May 2025"); adjacent periods merge automatically so a member's history stays as a clean set of contiguous ranges
  • General ledger — record any credit or debit (dues, equipment, offerings, etc.) with running-balance calculation
  • Dashboard — at-a-glance balance, total dues, credits, debits, recent transactions, and top contributors
  • Search & filter — query dues and ledger records by member, keyword, date range, or type; print or export results to CSV
  • Reports — monthly or all-time financial reports, printable
  • Single-admin auth — session-based login gated behind one password; every API endpoint, reads included, requires an authenticated session
  • Exact money math — amounts are stored as integer pesewas (cents), not floating-point, so totals never drift
  • Audit trail — every create/update/delete is recorded in audit_log with before/after values, so nothing is silently lost to an edit
  • Backups — one-command, verified SQLite backups with rotation and a deliberately separate, CLI-only restore path
  • Local SQLite database — no external database server, auto-created on first run
  • Offline-first — designed to run on a single machine on a local network, no internet dependency

Screenshots

Login Dashboard
Login Dashboard
Members Ledger
Members Ledger

Tech Stack

  • Backend: Python, Flask
  • Database: SQLite (via the standard library sqlite3 module)
  • Frontend: vanilla HTML, CSS, and JavaScript — no build step, no framework, no npm install. This is a deliberate choice: the app is meant to be cloned and run with nothing more than Python, matching its offline/zero-infrastructure goal.
  • Testing: pytest

Quick Start

Prerequisites

  • Python 3.9 or later
  • pip

1. Clone the repository

git clone https://github.com/niiomar/LEDGER.git
cd LEDGER

2. Configure environment variables

cp .env.example .env

Then edit .env and set a real SECRET_KEY and ADMIN_PASSWORD (see Configuration — do not run this in production with the example defaults).

3. Install dependencies and run

Windows: double-click run_windows.bat, or from a terminal:

.\run_windows.bat

macOS / Linux:

./run_mac_linux.sh

Either script creates a virtual environment on first run, installs requirements.txt, and starts the server. Or do it manually:

python -m venv venv
source venv/bin/activate        # venv\Scripts\activate on Windows
pip install -r requirements.txt
python app.py

Then open http://127.0.0.1:5000 and log in with admin and your ADMIN_PASSWORD. Every view and every API endpoint requires that login — there is no anonymous read access.

Configuration

All configuration lives in .env (see .env.example), loaded via python-dotenv:

Variable Description Default
SECRET_KEY Signs Flask's session cookie. Must be a long random string in any real deployment — anyone with this value can forge a valid session. dev-only-change-me
ADMIN_PASSWORD Password for the single admin account. admin
DB_PATH Path to the SQLite database file. Created automatically if it doesn't exist. gmm_media.db

Generate a strong SECRET_KEY with:

python -c "import secrets; print(secrets.token_hex(32))"

The app prints a warning on startup if SECRET_KEY or ADMIN_PASSWORD are still at their insecure defaults.

Usage

  1. Log in with admin and your configured password.
  2. Log Dues — record a member's payment against a period; the amount auto-calculates from the selected period at a fixed monthly rate, and the system prevents gaps by locking the start period to right after their last paid month.
  3. Ledger — record any other credit or debit (batteries, offerings, equipment, etc.). Every dues payment also lands here automatically as a linked credit, so the ledger is always the single source of truth for cash totals. A credit entered before that automatic linking existed shows a link icon — click it to attribute it to the member and coverage period it actually paid for.
  4. Query Records — filter dues/ledger by member, keyword, date range, or type; print the results or export them to CSV.
  5. Members — add members, click a name to rename it inline, or remove a member (this also removes their dues and linked ledger entries).
  6. Reports — generate a monthly or all-time report with summary totals, a dues coverage table, and the general ledger for that period.

All of the above — including just viewing the dashboard, ledger, dues, and reports — requires being logged in as admin. There is no public/anonymous read mode.

Project Structure

LEDGER/
├── app.py                    # Entrypoint: creates the Flask app, registers blueprints
├── db.py                     # Connection handling + schema (init_db)
├── money.py                  # Cedis ↔ pesewas conversion, amount validation
├── periods.py                # "Month YYYY" period parsing/formatting
├── audit.py                  # log_audit() — writes to audit_log
├── backup.py                 # Backup/restore (also runnable as a CLI)
├── auth.py                   # login_required decorator + CSRF check
├── config.py                 # Loads settings from .env
├── routes_members.py         # Blueprint: /api/members*
├── routes_dues.py            # Blueprint: /api/dues*, dues-period merging
├── routes_ledger.py          # Blueprint: /api/ledger*
├── routes_reports.py         # Blueprint: /api/reports/*
├── routes_session.py         # Blueprint: /api/login, /api/logout, /api/session
├── routes_admin.py           # Blueprint: /api/audit, /api/admin/backup(s)
├── seed_demo.py              # Optional fictional demo data for a fresh DB
├── requirements.txt          # Runtime dependencies
├── requirements-dev.txt      # + pytest, for running the test suite
├── pytest.ini
├── run_windows.bat           # One-click setup + run (Windows)
├── run_mac_linux.sh          # One-click setup + run (macOS/Linux)
├── migrations/
│   ├── 001_data_corrections.py
│   ├── 002_link_ledger_to_dues.py
│   └── 003_money_to_minor_units.py
├── backups/                  # Created by backup.py; gitignored
├── static/
│   ├── css/style.css
│   ├── js/app.js
│   └── images/
├── templates/
│   └── index.html            # Single-page app shell
├── tests/
│   ├── conftest.py
│   ├── test_app.py
│   └── test_admin.py
└── docs/screenshots/

Data Model

Four tables, all created automatically by init_db() on first run:

members (id, name)
   │ 1
   │
   │ *
dues (id, member_id, amount, period_from, period_to)
   │ 1
   │
   │ *
ledger (id, type, description, amount, date, note, dues_id)

audit_log (id, table_name, record_id, action, actor, ip_address, before_json, after_json)
  • members — one row per person.
  • dues — coverage records ("this member's dues are paid through this period"). Adjacent periods for the same member are automatically merged into a single contiguous range.
  • ledger — the append-only cash log (every credit and debit). A dues payment creates both a dues row and a linked ledger row (ledger.dues_id); editing or deleting either side keeps the other in sync, so the two never silently drift apart.
  • audit_log — one row per create/update/delete (and dues-period merge) across members, dues, and ledger, with the before/after values as JSON. Readable via GET /api/audit.

dues.member_id and ledger.dues_id are foreign keys with ON DELETE CASCADE, and both are indexed, along with ledger.date and audit_log(table_name, record_id).

dues.amount and ledger.amount are stored as integer pesewas (1 cedi = 100 pesewas), not floating-point, so summed totals are always exact. The HTTP API is unaffected — amounts are still sent and received as decimal cedi values (e.g. 12.34); the conversion happens at the API boundary in money.py.

API Reference

All endpoints are under /api. Endpoints marked Auth require an active admin session; those marked +CSRF additionally require a matching X-CSRF-Token header (issued at login) since they change data. /api/login, /api/logout, and /api/session are the only endpoints reachable without a session, since they're how you get one.

Method Endpoint Auth Description
GET /api/members ✅ List all members
POST /api/members ✅+CSRF Add a member
PUT /api/members/<id> ✅+CSRF Rename a member
DELETE /api/members/<id> ✅+CSRF Remove a member (cascades dues/ledger)
GET /api/dues ✅ List dues records (?member_id=)
POST /api/dues ✅+CSRF Record a dues payment
PUT /api/dues/<id> ✅+CSRF Edit a dues record
DELETE /api/dues/<id> ✅+CSRF Delete a dues record
GET /api/ledger ✅ List ledger transactions (?date_from=&date_to=&keyword=&type=)
GET /api/ledger/summary ✅ Balance, totals, and counts
POST /api/ledger ✅+CSRF Add a transaction
PUT /api/ledger/<id> ✅+CSRF Edit a transaction
PUT /api/ledger/<id>/link ✅+CSRF Link/unlink a credit to a dues record ({"dues_id": <id>|null})
DELETE /api/ledger/<id> ✅+CSRF Delete a transaction
POST /api/login Authenticate, returns a CSRF token
POST /api/logout End the session
GET /api/session Check whether the current session is authenticated
GET /api/reports/monthly ✅ Monthly report (?month=YYYY-MM)
GET /api/reports/comprehensive ✅ All-time report
GET /api/audit ✅ Audit log (?table=members|dues|ledger&limit=)
GET /api/admin/backups ✅ List existing backups
POST /api/admin/backup ✅+CSRF Trigger a new backup

Testing

pip install -r requirements-dev.txt
pytest

The suite (tests/) runs against a fresh temporary SQLite database per test (never your real gmm_media.db) and covers auth/CSRF enforcement, input validation, the dues↔ledger integrity behavior (linking, merging, syncing, and cascading deletes), exact-money arithmetic, the audit log, and backup/restore/migration behavior.

Migrations

One-off data migrations live in migrations/ and are meant to be run once, manually, in numeric order:

python migrations/002_link_ledger_to_dues.py
python migrations/003_money_to_minor_units.py

001_data_corrections.py was a one-time historical fix and has already been applied. 002_link_ledger_to_dues.py backfills the dues_id link on ledger entries created before that column existed, where a confident match can be found; it's safe to run more than once (it skips rows that are already linked). 003_money_to_minor_units.py converts dues.amount and ledger.amount from floating-point cedis to integer pesewas — back up first (python backup.py); it's idempotent and refuses to touch a database that's already been migrated.

Backups

python backup.py            # create a timestamped, verified backup
python backup.py list       # list existing backups
python backup.py restore <filename> [--yes]

Backups are written to backups/ (gitignored) using SQLite's own backup API and checked with PRAGMA integrity_check immediately after writing; the oldest backups beyond the most recent 10 are pruned automatically. Restoring first takes a safety backup of whatever is currently live, then verifies and copies in the chosen backup.

Restore is CLI-only by design — it isn't exposed over HTTP, so a network request alone can never overwrite your live data. Creating a backup and listing existing ones are available to a logged-in admin via POST /api/admin/backup and GET /api/admin/backups, for convenience (e.g. wiring a "Backup now" button into the UI later).

Security Notes

  • Change SECRET_KEY and ADMIN_PASSWORD before running this anywhere beyond your own machine — see Configuration.
  • The app is served over plain HTTP by default, intended for a trusted local network. If you put it behind HTTPS, set app.config['SESSION_COOKIE_SECURE'] = True in app.py.
  • All state-changing requests require both a valid session and a CSRF token; passwords are compared with a timing-safe comparison.
  • Every read and write requires a logged-in session — there is no anonymous access to financial or member data.
  • Every create/update/delete is recorded in audit_log (see Data Model), including the before/after values, so a bad edit or deletion is traceable and recoverable from history even though there's only one shared admin login.
  • User-supplied text is HTML-escaped before being rendered client-side.

Deployment Notes

app.run() uses Flask's built-in development server, which is appropriate for this app's intended use (one admin, a handful of viewers, on a local network) but isn't hardened for public internet exposure. If you need to expose this beyond a LAN, put it behind a production WSGI server (e.g. waitress or gunicorn) and a reverse proxy with TLS.

License

MIT © 2026 Jason

About

Financial ledger built on Flask and SQLite.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages