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