diff --git a/modules/expenses/index.py b/modules/expenses/index.py
index a07285a..afe7ec2 100644
--- a/modules/expenses/index.py
+++ b/modules/expenses/index.py
@@ -235,10 +235,17 @@ def _list_expenses(self):
contractors = repo.get_all_contractors()
categories = repo.get_unique_categories()
+ # Module Expense model have no 'contractor' relationship (core backref
+ # live on a different mapped class), so template cannot reach
+ # expense.contractor. Build id->name map and let template look up by
+ # contractor_id — same trick report code already use.
+ contractors_map = {c.id: c.name for c in contractors}
+
return render_template('expenses.html',
expenses_by_year=expenses_by_year,
years=years,
contractors=contractors,
+ contractors_map=contractors_map,
categories=categories)
def _create_expense(self):
diff --git a/modules/expenses/templates/expenses.html b/modules/expenses/templates/expenses.html
index 6ecc264..860d439 100644
--- a/modules/expenses/templates/expenses.html
+++ b/modules/expenses/templates/expenses.html
@@ -80,7 +80,7 @@
Expenses
| {{ expense.expense_date.strftime('%d/%m/%Y') }} |
{{ expense.invoice_number or '-' }} |
- {{ expense.contractor.name if expense.contractor else '-' }} |
+ {{ contractors_map.get(expense.contractor_id, '-') }} |
{{ expense.category or '-' }} |
{{ expense.description or '-' }} |
{{ "%.2f"|format(expense.amount) }} {{ expense.currency }} |
diff --git a/tests/test_expenses_list.py b/tests/test_expenses_list.py
new file mode 100644
index 0000000..6f71d3b
--- /dev/null
+++ b/tests/test_expenses_list.py
@@ -0,0 +1,93 @@
+"""Regression test: Expenses list view must render the Contractor column.
+
+Bug: module Expense model have no 'contractor' relationship (core backref live
+on a separate mapped class), so template `expense.contractor` resolve to
+Undefined and Contractor column show empty for every row. List view now pass a
+contractor id->name map; this test guard that the saved contractor name reach
+the rendered table.
+"""
+from datetime import date
+
+import pytest
+
+from module_manager import ModuleManager
+
+
+@pytest.fixture(scope='module')
+def expenses_module():
+ """Load the Expenses module once against the app + a fresh DB.
+
+ Module-scoped so the ModuleManager (which registers the singleton
+ `module_enabled` table) is built only once — re-building it per test would
+ redefine that table in the shared metadata and error.
+ """
+ import app as appmod
+ from app import app as flask_app, db, Settings
+
+ with flask_app.app_context():
+ db.create_all()
+
+ mm = ModuleManager(flask_app, db)
+ mm.core._settings_model = Settings
+ mm.init_db()
+ mm.discover_modules()
+
+ enabled_model = mm._get_module_enabled_model()
+ db.session.add(enabled_model(module_id='expenses', enabled=True))
+ db.session.commit()
+ mm.load_enabled_modules()
+
+ # Templates reference these Jinja globals.
+ appmod.module_manager = mm
+ flask_app.jinja_env.globals.setdefault('module_manager', mm)
+ flask_app.jinja_env.globals.setdefault('app_version', 'test')
+
+ yield mm.modules['expenses']
+
+ db.session.remove()
+ db.drop_all()
+
+
+def test_list_view_renders_contractor_name(expenses_module):
+ from app import app as flask_app, db
+
+ em = expenses_module
+ contractor = em.Contractor(name='ACME Corp')
+ db.session.add(contractor)
+ db.session.flush()
+ db.session.add(em.Expense(
+ contractor_id=contractor.id,
+ amount=42.0,
+ currency='EUR',
+ category='Tools',
+ expense_date=date(2026, 1, 15),
+ ))
+ db.session.commit()
+
+ with flask_app.test_request_context('/expenses/'):
+ html = em._list_expenses()
+
+ # Assert on the table CELL, not just any mention — the contractor also
+ # shows up in the filter dropdown, so a bare substring check would pass even
+ # with the bug present.
+ assert 'ACME Corp | ' in html
+
+
+def test_list_view_handles_missing_contractor(expenses_module):
+ from app import app as flask_app, db
+
+ em = expenses_module
+ db.session.add(em.Expense(
+ contractor_id=None,
+ amount=9.0,
+ currency='EUR',
+ category='MiscNoContractor',
+ expense_date=date(2026, 2, 1),
+ ))
+ db.session.commit()
+
+ with flask_app.test_request_context('/expenses/'):
+ html = em._list_expenses()
+
+ # No contractor -> placeholder, and rendering must not raise.
+ assert 'MiscNoContractor' in html