Index: remit/finance_core/models.py
===================================================================
--- remit/finance_core/models.py	(revision 0e58ad0b68ae2408845f0b6972483df470b9f8f8)
+++ remit/finance_core/models.py	(revision dd2c3d90572fd5a82034dfa25be1b59143155a30)
@@ -12,4 +12,5 @@
     # Applicable to every term?
     always = models.BooleanField(blank=True, default=False)
+    budget_term = models.ManyToManyField('BudgetTerm', through='BudgetAreaTerm')
 
     # Contact / ACL information
Index: remit/finance_core/reporting.py
===================================================================
--- remit/finance_core/reporting.py	(revision a838ee3b61c73a143a614dfd3a66ba148a8ce34e)
+++ remit/finance_core/reporting.py	(revision a838ee3b61c73a143a614dfd3a66ba148a8ce34e)
@@ -0,0 +1,117 @@
+from decimal import Decimal
+from django.db.models import Q, Sum
+import finance_core.models
+
+def build_table_annotate(line_items, primary_field, secondary_field, primary_axis, secondary_axis, ):
+    # Setup
+    arcprimary = {}
+    arcsecondary = {}
+    table = []
+    zero = Decimal('0.00')
+    for num, (pk, label, qobj, ) in enumerate(primary_axis):
+        arcprimary[pk] = num
+        table.append([zero]*len(secondary_axis))
+    for num, (pk, label, qobj, ) in enumerate(secondary_axis):
+        arcsecondary[pk] = num
+
+    def lineitem_total(obj):
+        if obj['amount__sum'] is None: return zero
+        else: return obj['amount__sum']
+
+    # Do the real work
+    results = line_items.values(primary_field, secondary_field,).annotate(Sum('amount'))
+    for result in results:
+        pkey = arcprimary[result[primary_field]]
+        skey = arcsecondary[result[secondary_field]]
+        assert table[pkey][skey] == zero
+        table[pkey][skey] = lineitem_total(result)
+
+    return table
+
+def build_table_aggregate(line_items, primary_field, secondary_field, primary_axis, secondary_axis):
+    # This uses a simpler but probably slower method
+    zero = Decimal('0.00')
+    def total_amount(queryset):
+        amount = queryset.aggregate(Sum('amount'))['amount__sum']
+        if amount is None: return zero
+        else: return amount
+    table = [ # Primary axis
+            [ # Secondary axis
+                total_amount(line_items.filter(primary[2], secondary[2]))
+            for secondary in secondary_axis]
+        for primary in primary_axis]
+
+    return table
+
+build_table = build_table_annotate
+
+
+def get_primary_axis(slug, base_area, term, ):
+    if slug in axes and axes[slug][3]:
+        return axes[slug][0:2] + (axes[slug][2](base_area, term, ), )
+    else:
+        raise NotImplementedError
+
+def get_secondary_axis(slug, base_area, term, ):
+    if slug in axes and axes[slug][4]:
+        return axes[slug][0:2] + (axes[slug][2](base_area, term, ), )
+    else:
+        raise NotImplementedError
+
+def get_budget_areas(base_area, term, ):
+    base_area_depth = base_area.depth
+    areas = base_area.get_descendants()
+    if term:
+        areas = areas.filter(Q(always=True) | Q(budget_term=term))
+    axis = [
+        (
+            area.pk,
+            area.indented_name(base_area_depth),
+            Q(budget_area=area),
+        )
+        for area in areas
+    ]
+    return axis
+
+def get_budget_terms(base_area, term, ):
+    if term:
+        terms = finance_core.models.BudgetTerm.objects.filter(pk=term.pk)
+    else:
+        terms = finance_core.models.BudgetTerm.objects.all()
+    axis = [
+        (
+            term.pk,
+            term.name,
+            Q(budget_term=term),
+        )
+        for term in terms
+    ]
+    return axis
+
+def get_layers(base_area, term, ):
+    axis = [
+        (
+            finance_core.models.layer_num(layer),
+            finance_core.models.layer_name(layer),
+            Q(layer=finance_core.models.layer_num(layer)),
+        )
+        for layer in finance_core.models.layers
+    ]
+    return axis
+
+axes = {
+    'budget-areas': ('Budget Areas', 'budget_area', get_budget_areas, True,  True,  ),
+    'budget-terms': ('Budget Terms', 'budget_term', get_budget_terms, True,  True,  ),
+    'layers':       ('Layers',       'layer',       get_layers,       True,  True,  ),
+}
+
+def append_totals(table):
+    # Row totals
+    for row in table:
+        row.append(sum(row))
+    # Column totals
+    if len(table) > 0:
+        totalrow = [None]*len(table[0])
+        for col in xrange(len(table[0])):
+            totalrow[col] = sum([row[col] for row in table])
+        table.append(totalrow)
Index: remit/finance_core/reporting_tests.sh
===================================================================
--- remit/finance_core/reporting_tests.sh	(revision d1692e80e5a311571d00881ece31a5966043ff79)
+++ remit/finance_core/reporting_tests.sh	(revision d1692e80e5a311571d00881ece31a5966043ff79)
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+methods="annotate aggregate"
+baseurl=${1:-http://localhost:8001/finance_core/reporting/?a=b}
+baseurls="$baseurl&term=2010-spring $baseurl&area=13 $baseurl&layer=30"
+for testbaseurl in $baseurls; do
+    echo $testbaseurl
+    dirname=$(mktemp -d /tmp/remit.XXXXXX)
+    cd $dirname
+    wget -q -O default.html "$baseurl"
+    for method in $methods; do
+        wget -q -O $method.html  "$baseurl&compute_method=$method"
+    done
+    for method in $methods; do
+        diff -q --report-identical-files default.html $method.html
+    done
+done
Index: remit/finance_core/urls.py
===================================================================
--- remit/finance_core/urls.py	(revision 92ca60ed46c225b17cbff5fcda7a045dd5710fae)
+++ remit/finance_core/urls.py	(revision ed4ccc7840c928311d1f921509bbfb9c2e58e1f1)
@@ -4,3 +4,4 @@
 urlpatterns = patterns('',
     (r'^display_accounts', finance_core.views.display_tree),
+    url(r'reporting/', finance_core.views.reporting, name='reporting', ),
 )
Index: remit/finance_core/views.py
===================================================================
--- remit/finance_core/views.py	(revision 92ca60ed46c225b17cbff5fcda7a045dd5710fae)
+++ remit/finance_core/views.py	(revision ea32cd9e4e86d93b103b5d11468c8b18bf862cd2)
@@ -1,6 +1,129 @@
 from django.http import HttpResponse
-import vouchers.models
+import finance_core.models
+from django.contrib.auth.decorators import user_passes_test
+from django.shortcuts import render_to_response, get_object_or_404
+from django.template import RequestContext
+from django.http import Http404, HttpResponseRedirect
+from django.db.models import Q
+from decimal import Decimal
+import finance_core.reporting
 
 def display_tree(request):
-    root = vouchers.models.BudgetArea.get_by_path(['Accounts'])
+    root = finance_core.models.BudgetArea.get_by_path(['Accounts'])
     return HttpResponse(root.dump_to_html())
+
+def reporting(request):
+    compute_method = 'default'
+    if 'compute_method' in request.REQUEST:
+        compute_method = request.REQUEST['compute_method']
+
+    ###############################
+    # Retrieve the various limits #
+    ###############################
+    line_items = finance_core.models.LineItem.objects.all()
+
+    # Term
+    if 'term' in request.REQUEST and not request.REQUEST['term'] == 'all':
+        term_obj = get_object_or_404(finance_core.models.BudgetTerm, slug=request.REQUEST['term'])
+        term_name = term_obj.name
+        line_items = line_items.filter(budget_term=term_obj)
+    else:
+        term_obj = None
+        term_name = 'All'
+
+    # Area
+    if 'area' in request.REQUEST:
+        base_area_obj = get_object_or_404(finance_core.models.BudgetArea, pk=request.REQUEST['area'])
+    else:
+        base_area_obj = finance_core.models.BudgetArea.get_by_path(['Accounts'])
+    all_relevant_areas = base_area_obj.get_descendants()
+    if term_obj:
+        all_relevant_areas = all_relevant_areas.filter(Q(always=True) | Q(budget_term=term_obj))
+    line_items = line_items.filter(budget_area__in=all_relevant_areas)
+
+    # Layer
+    if 'layer' in request.REQUEST and request.REQUEST['layer'] != 'all':
+        try:
+            layer_id = int(request.REQUEST['layer'])
+            layer = finance_core.models.get_layer_by_num(layer_id)
+        except KeyError:
+            raise Http404("Invalid layer %s request" % request.REQUEST['layer'])
+        line_items = line_items.filter(layer=layer_id)
+    else:
+        layer = 'all'
+
+    #######################
+    # Initialize the axes #
+    #######################
+    # Primary
+    if 'primary' in request.REQUEST:
+        primary_slug = request.REQUEST['primary']
+    else:
+        primary_slug = 'budget-areas'
+    try:
+        primary_name, primary_field, primary_axis = finance_core.reporting.get_primary_axis(primary_slug, base_area_obj, term_obj, )
+    except NotImplementedError:
+        raise Http404("Primary axis %s is not implemented" % primary_slug)
+
+    # Secondary
+    if 'secondary' in request.REQUEST:
+        secondary_slug = request.REQUEST['secondary']
+    else:
+        secondary_slug = 'layers'
+    try:
+        secondary_name, secondary_field, secondary_axis = finance_core.reporting.get_secondary_axis(secondary_slug, base_area_obj, term_obj, )
+    except NotImplementedError:
+        raise Http404("Secondary axis %s is not implemented" % secondary_slug)
+
+    primary_labels = [ ]
+    for num, (pk, label, qobj, ) in enumerate(primary_axis):
+        primary_labels.append(label)
+    secondary_labels = [ secondary[1] for secondary in secondary_axis ]
+
+    ######################
+    # Do the computation #
+    ######################
+    compute_methods = {
+        'default':   finance_core.reporting.build_table,
+        'aggregate': finance_core.reporting.build_table_aggregate,
+        'annotate':  finance_core.reporting.build_table_annotate,
+    }
+    if compute_method in compute_methods:
+        build_table = compute_methods[compute_method]
+    else:
+        raise Http404("Unknown compute_method selected")
+    table = build_table(line_items, primary_field, secondary_field, primary_axis, secondary_axis, )
+    finance_core.reporting.append_totals(table)
+    primary_labels.append("Total")
+    secondary_labels.append("Total")
+
+    debug = True
+    debug = False
+    if debug:
+        from django.db import connection
+        print connection.queries
+        print "Number of queries:\t%d" % (len(connection.queries),)
+        print "Table size:\t%dx%d" % (len(primary_labels), len(secondary_labels), )
+
+    ##########
+    # Render #
+    ##########
+    term_options = finance_core.models.BudgetTerm.objects.all()
+    area_options = finance_core.models.BudgetArea.objects.filter(always=True)
+    context = {
+        'pagename':'reporting',
+        'term_name': term_name,
+        'term_options': term_options,
+        'area': base_area_obj,
+        'area_options': area_options,
+        'layer': layer,
+        'layer_options': finance_core.models.layers,
+        'axes': finance_core.reporting.axes,
+        'primary_name': primary_name,
+        'secondary_name': secondary_name,
+        'primary_labels': primary_labels,
+        'secondary_labels': secondary_labels,
+        'table': table,
+        'table_with_row_labels': zip(primary_labels, table),
+    }
+    return render_to_response('finance_core/reporting.html', context, context_instance=RequestContext(request), )
Index: remit/media/style/style.css
===================================================================
--- remit/media/style/style.css	(revision f114f8bb2e70d64e4c1d94449d637a86e1e7d924)
+++ remit/media/style/style.css	(revision ed4ccc7840c928311d1f921509bbfb9c2e58e1f1)
@@ -78,2 +78,13 @@
     background: #F88;
 }
+
+table.money-table td
+{
+    text-align: right;
+}
+table.money-table th
+{
+    white-space: pre-wrap;
+    text-align: left;
+    font-weight: normal;
+}
Index: remit/remit_templates/base.html
===================================================================
--- remit/remit_templates/base.html	(revision 3a0c51b887d800089162a9247b1e52d5948fce2b)
+++ remit/remit_templates/base.html	(revision ed4ccc7840c928311d1f921509bbfb9c2e58e1f1)
@@ -16,4 +16,5 @@
         <li{% ifequal pagename "list_requests" %} class='selected'{% endifequal %}><a href="{% url list_requests %}">List requests</a></li>
         {% endif %}
+        <li{% ifequal pagename "reporting" %} class='selected'{% endifequal %}><a href="{% url reporting %}">Reporting</a></li>
     </ul>
     <div id='content'>
Index: remit/remit_templates/finance_core/reporting.html
===================================================================
--- remit/remit_templates/finance_core/reporting.html	(revision d1692e80e5a311571d00881ece31a5966043ff79)
+++ remit/remit_templates/finance_core/reporting.html	(revision d1692e80e5a311571d00881ece31a5966043ff79)
@@ -0,0 +1,76 @@
+{% extends "base.html" %}
+{% load misc %}
+
+{% block title %}Reporting{% endblock %}
+{% block content %}
+
+<h2>Reporting</h2>
+
+<h3>Parameters</h3>
+
+<form>
+<table class='pretty-table'>
+<tr>
+    <th colspan='2'>Limits</th>
+</tr>
+<tr>
+    <th>Term</th>
+    <td><select name='term'>
+    <option {% ifequal term_name "All" %}selected {% endifequal %}value='all'>All</option>
+    {% for term_opt in term_options %}<option {% ifequal term_name term_opt.name %}selected {% endifequal %}value='{{term_opt.slug}}'>{{term_opt.name}}</option>{% endfor %}
+    </select></td>
+</tr>
+<tr>
+    <th>Budget Area</th>
+    <td><select name='area'>
+    {% for area_opt in area_options %}<option {%ifequal area area_opt %}selected {%endifequal%}value='{{area_opt.pk}}'>{{area_opt.indented_name}}</option>{%endfor%}
+    </select></td>
+</tr>
+<tr>
+    <th>Layer</th>
+    <td><select name='layer'>
+    <option {%ifequal layer "all"%}selected {%endifequal%}value='all'>All</option>
+    {% for layer_opt in layer_options %}<option {%ifequal layer layer_opt %}selected {%endifequal%}value='{{layer_opt|layer_num}}'>{{layer_opt|layer_name}}</option>{%endfor%}
+    </select></td>
+</tr>
+<tr>
+    <th colspan='2'>Axes</th>
+</tr>
+<tr>
+    <th>Primary Axis</th>
+    <td><select name='primary'>
+    {% for slug, content in axes.items %}{% if content.3 %}<option {% ifequal primary_name content.0 %}selected {% endifequal %}value='{{slug}}'>{{content.0}}</option>{% endif %}{% endfor %}
+    </select></td>
+</tr>
+<tr>
+    <th>Secondary Axis</th>
+    <td><select name='secondary'>
+    {% for slug, content in axes.items %}{% if content.4 %}<option {% ifequal secondary_name content.0 %}selected {% endifequal %}value='{{slug}}'>{{content.0}}</option>{% endif %}{% endfor %}
+    </select></td>
+</tr>
+<tr>
+    <th colspan='2'><input type='submit' name='submit' value='Submit'></th>
+</tr>
+</table>
+
+<h3>Data</h3>
+
+<table class='pretty-table money-table'>
+<tr>
+    <th></th>
+{% for label in secondary_labels %}
+    <th>{{label}}</th>
+{% endfor %}
+</tr>
+{% for label, row in table_with_row_labels %}
+<tr>
+    <th>{{label}}</th>
+    {% for cell in row %}
+    <td>{% if cell %}<strong>{% endif %}${{cell}}{% if cell %}</strong>{% endif %}</td>
+    {% endfor %}
+</tr>
+{% endfor %}
+
+</table>
+
+{% endblock %}
Index: remit/util/templatetags/misc.py
===================================================================
--- remit/util/templatetags/misc.py	(revision f114f8bb2e70d64e4c1d94449d637a86e1e7d924)
+++ remit/util/templatetags/misc.py	(revision c968555fdb383841ea0d428b329c6646c2acf034)
@@ -1,3 +1,4 @@
 from django import template
+import finance_core.models
 
 register = template.Library()
@@ -22,2 +23,5 @@
         if num == value: return name.lower()
     else: return value
+
+register.filter('layer_num', finance_core.models.layer_num)
+register.filter('layer_name', finance_core.models.layer_name)
