Index: main/waeup.kofa/trunk/src/waeup/kofa/students/reports/student_payment_statistics.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/reports/student_payment_statistics.py	(revision 12562)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/reports/student_payment_statistics.py	(revision 12562)
@@ -0,0 +1,231 @@
+## $Id$
+##
+## Copyright (C) 2015 Uli Fouquet & Henrik Bettermann
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import grok
+from zope.catalog.interfaces import ICatalog
+from zope.component import queryUtility, getUtility
+from zope.interface import implementer, Interface, Attribute
+from waeup.kofa.interfaces import (
+    IKofaUtils,
+    academic_sessions_vocab, registration_states_vocab)
+from waeup.kofa.interfaces import MessageFactory as _
+from waeup.kofa.reports import IReport
+
+
+class IStudentPaymentStatisticsReport(IReport):
+
+    session = Attribute('Session to report')
+    mode = Attribute('Study modes group to report')
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+
+def get_student_payment_stats(session, mode, breakdown):
+    """Get students in a certain session and study mode.
+
+    Returns a table ordered by code (faculty or department, one per row) and
+    various payment categories (cols). The result is a 3-tuple representing
+    ((<CODES>), (<PAYMENTCATS>), (<NUM_OF_STUDENTS>)). The
+    (<NUM_OF_STUDENTS>) is an n-tuple with each entry containing the
+    number of students found in that faculty/department and with the respective
+    payment.
+
+    Sample result:
+
+      >>> ((u'FAC1', u'FAC2'),
+      ...  ('clearance', 'gown', 'hostel_maintenance', 'schoolfee'),
+      ...  ((12, 10, 1, 14), (0, 5, 25, 16)))
+
+    This result means: there are 5 students in FAC2 in who have paid
+    gown fee while 12 students in 'FAC1' who have paid for clearance.
+    """
+    site = grok.getSite()
+    payment_cats = ('clearance', 'gown', 'hostel_maintenance', 'schoolfee')
+    payment_cats = payment_cats + (u'Total',)
+    if breakdown == 'faccode':
+        codes = tuple(sorted([x for x in site['faculties'].keys()],
+                                 key=lambda x: x.lower()))
+    elif breakdown == 'depcode':
+        faculties = site['faculties']
+        depcodes = []
+        for fac in faculties.values():
+            for dep in fac.values():
+                depcodes.append(dep.code)
+        codes = tuple(sorted([x for x in depcodes],
+                                 key=lambda x: x.lower()))
+    codes = codes + ('Total',)
+    # XXX: Here we do _one_ query and then examine the single
+    #   students. One could also do multiple queries and just look for
+    #   the result length (not introspecting the delivered students
+    #   further).
+    cat = queryUtility(ICatalog, name="students_catalog")
+    result = cat.searchResults(current_session=(session, session))
+    table = [[0 for x in xrange(2*len(payment_cats))] for y in xrange(len(codes))]
+    mode_groups = getUtility(IKofaUtils).MODE_GROUPS
+    for stud in result:
+        if mode != 'All' and stud.current_mode not in mode_groups[mode]:
+            continue
+        if getattr(stud, breakdown) not in codes:
+            # studs can have a faccode ``None``
+            continue
+        row = codes.index(getattr(stud, breakdown))
+        for key in stud['payments'].keys():
+            ticket = stud['payments'][key]
+            if ticket.p_category not in payment_cats or\
+                ticket.p_state != 'paid' or\
+                ticket.p_session != session:
+                continue
+            col = payment_cats.index(ticket.p_category)
+
+            # payments in category and fac/dep
+            table[row][2*col] += 1 # number of payments
+            table[row][2*col+1] += ticket.amount_auth
+
+            # payments in category
+            table[-1][2*col] += 1
+            table[-1][2*col+1] += ticket.amount_auth
+
+            # payments in fac/dep
+            table[row][-2] += 1
+            table[row][-1] += ticket.amount_auth
+
+            # all payments
+            table[-1][-2] += 1
+            table[-1][-1] += ticket.amount_auth
+
+    # Build table header
+    utils = getUtility(IKofaUtils)
+    table_header = ['' for x in xrange(2*len(payment_cats))]
+    for cat in payment_cats:
+        cat_title = utils.PAYMENT_CATEGORIES.get(cat, cat)
+        table_header[2*payment_cats.index(cat)] = cat_title
+    # turn lists into tuples
+    table = tuple([tuple(row) for row in table])
+    return (codes, table_header, table)
+
+from reportlab.lib import colors
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import cm
+from reportlab.platypus import Paragraph, Table, Spacer
+from waeup.kofa.reports import IReport, IReportGenerator
+from waeup.kofa.reports import Report
+from waeup.kofa.browser.interfaces import IPDFCreator
+
+from waeup.kofa.students.reports.student_statistics import (
+    tbl_data_to_table, STYLE)
+
+
+TABLE_STYLE = [
+    ('FONT', (0,0), (-1,-1), 'Helvetica', 8),
+    ('FONT', (0,0), (0,-1), 'Helvetica-Bold', 8),
+    ('FONT', (0,0), (-1,0), 'Helvetica-Bold', 8),
+    ('FONT', (0,-1), (-1,-1), 'Helvetica-Bold', 8),
+    ('FONT', (-1,0), (-1,-1), 'Helvetica-Bold', 8),
+    ('ALIGN', (1,1), (-1,-1), 'RIGHT'),
+    ('INNERGRID', (0,1), (-1,-1), 0.25, colors.black),
+    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
+    ('LINEAFTER', (-1,1), (-1,-1), 0.25, colors.black),
+    ('LINEBEFORE', (-1,1), (-1,-1), 0.25, colors.black),
+    #('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
+    #('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
+    ]
+
+@implementer(IStudentPaymentStatisticsReport)
+class StudentPaymentStatisticsReport(Report):
+    data = None
+    session = None
+    mode = None
+
+    def __init__(self, session, mode, breakdown, author='System'):
+        super(StudentPaymentStatisticsReport, self).__init__(
+            args=[session, mode, breakdown], kwargs={'author':author})
+        self.session = academic_sessions_vocab.getTerm(session).title
+        self.mode = mode
+        self.breakdown = breakdown
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.data = get_student_payment_stats(session, mode, breakdown)
+
+    def create_pdf(self):
+        creator = getUtility(IPDFCreator, name='landscape')
+        table_data = tbl_data_to_table(*self.data)
+        col_widths = [None,] + [1.6*cm] * len(self.data[1]) + [None,]
+        pdf_data = [Paragraph('<b>%s</b>' % self.creation_dt_string,
+                              STYLE["Normal"]),
+                    Spacer(1, 12),]
+        pdf_data += [
+            Table(table_data, style=TABLE_STYLE, colWidths=col_widths)]
+        doc_title = '%s Student Payments in Session %s' % (self.mode, self.session)
+        pdf = creator.create_pdf(
+            pdf_data, None, doc_title, self.author,
+            'Student Payments in Session %s' % self.session
+            )
+        return pdf
+
+
+@implementer(IReportGenerator)
+class StudentPaymentStatisticsReportGenerator(grok.GlobalUtility):
+
+    title = _('Student Payment Statistics')
+    grok.name('student_payment_stats')
+
+    def generate(self, site, session=None,
+                 mode=None, breakdown=None, author=None):
+        result = StudentPaymentStatisticsReport(
+            session=session, mode=mode, breakdown=breakdown, author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.kofa.browser.layout import KofaPage
+from waeup.kofa.interfaces import academic_sessions_vocab
+from waeup.kofa.reports import get_generators
+from waeup.kofa.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+
+from waeup.kofa.students.reports.student_statistics import (
+    StudentStatisticsReportGeneratorPage,
+    StudentStatisticsReportPDFView)
+
+class StudentPaymentStatisticsReportGeneratorPage(
+    StudentStatisticsReportGeneratorPage):
+
+    grok.context(StudentPaymentStatisticsReportGenerator)
+    grok.template('studentstatisticsreportgeneratorpage')
+
+    label = _('Create student payment statistics report')
+
+
+class StudentPaymentStatisticsReportPDFView(StudentStatisticsReportPDFView):
+
+    grok.context(IStudentPaymentStatisticsReport)
+
+    def _filename(self):
+        return 'StudentPaymentStatisticsReport_%s_%s_%s.pdf' % (
+            self.context.session, self.context.mode,
+            self.context.creation_dt_string)
+
+
+class StudentStatsBreadcrumb(Breadcrumb):
+    """A breadcrumb for reports.
+    """
+    grok.context(StudentPaymentStatisticsReportGenerator)
+    title = _(u'Student Payment Statistics')
+    target = None
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/reports/tests/test_student_payment_statistics.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/reports/tests/test_student_payment_statistics.py	(revision 12562)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/reports/tests/test_student_payment_statistics.py	(revision 12562)
@@ -0,0 +1,132 @@
+# Tests for student related reports
+import os
+from zc.async.testing import wait_for_result
+from zope.interface.verify import verifyClass, verifyObject
+from zope.component import getUtility
+from waeup.kofa.interfaces import IJobManager
+from waeup.kofa.students.reports.student_payment_statistics import (
+    get_student_payment_stats,
+    StudentPaymentStatisticsReport,
+    IStudentPaymentStatisticsReport)
+from waeup.kofa.students.tests.test_catalog import CatalogTestSetup
+from waeup.kofa.students.tests.test_browser import StudentsFullSetup
+from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
+from waeup.kofa.browser.tests.test_pdf import samples_dir
+
+class StudentPaymentStatisticsReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = StudentPaymentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 'faccode')
+        verifyClass(IStudentPaymentStatisticsReport, StudentPaymentStatisticsReport)
+        verifyObject(IStudentPaymentStatisticsReport, obj)
+        return
+
+    def test_get_student_payment_stats_session_simple(self):
+        # we can get a table with one student
+        result1 = get_student_payment_stats(2010, 'Undergraduate Full-Time', 'faccode')
+        result2 = get_student_payment_stats(2009, 'Undergraduate Full-Time', 'faccode')
+        self.assertEqual(
+            result1,
+            ((u'fac1', u'Total'),
+             ['Acceptance Fee', '', 'Gown Hire Fee', '',
+              'Hostel Maintenance Fee', '', 'School Fee', '',
+              'Total', ''],
+              ((0, 0, 0, 0, 0, 0, 1, 5.678, 1, 5.678),
+              (0, 0, 0, 0, 0, 0, 1, 5.678, 1, 5.678)))
+            )
+        self.assertEqual(
+            result2,
+            ((u'fac1', u'Total'),
+             ['Acceptance Fee', '', 'Gown Hire Fee', '',
+              'Hostel Maintenance Fee', '', 'School Fee', '',
+              'Total', ''],
+              ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
+              (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))
+            )
+        return
+
+    def test_create_pdf(self):
+        report = StudentPaymentStatisticsReport(
+            2010, 'Undergraduate Full-Time', 'faccode')
+        result = report.create_pdf()
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'student_payment_statistics.pdf')
+        open(path, 'wb').write(result)
+        print "Sample PDF student_statistics.pdf written to %s" % path
+        return
+
+class StudentPaymentStatisticsReportUITests(StudentsFullSetup, FunctionalAsyncTestCase):
+
+    layer = FunctionalLayer
+
+    def wait_for_report_job_completed(self):
+        # helper function waiting until the current export job is completed
+        manager = getUtility(IJobManager)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        job = manager.get(job_id)
+        wait_for_result(job)
+        return job_id
+
+    def stored_in_reports(self, job_id):
+        # tell whether job_id is stored in reports's running jobs list
+        for entry in list(self.app['reports'].running_report_jobs):
+            if entry[0] == job_id:
+                return True
+        return False
+
+    def trigger_report_creation(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/reports')
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.browser.getLink("Create new report").click()
+        self.browser.getControl(name="generator").value = ['student_payment_stats']
+        self.browser.getControl("Configure").click()
+        self.browser.getControl(name="breakdown").value = ['depcode']
+        self.browser.getControl(name="mode").value = ['All']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl("Create").click()
+        return
+
+    def test_report_download(self):
+        # We can download a generated report
+        self.trigger_report_creation()
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_report_job_completed()
+        self.browser.open('http://localhost/app/reports')
+        # ... the pdf file can be downloaded ...
+        self.browser.getControl("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+                         'application/pdf')
+        self.assertTrue(
+            'filename="StudentPaymentStatisticsReport_2004_2005_All_' in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 1)
+        job_id = self.app['reports'].running_report_jobs[0][0]
+        # ... and discarded
+        self.browser.open('http://localhost/app/reports')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['reports'].running_report_jobs), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.student_payment_statistics.StudentPaymentStatisticsReportGeneratorPage - '
+            'report %s created: Student Payment Statistics (session=2004, mode=All, breakdown=depcode)'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - students.reports.student_payment_statistics.StudentPaymentStatisticsReportPDFView - '
+            'report %s downloaded: StudentPaymentStatisticsReport_2004_2005_All_'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
Index: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_catalog.py
===================================================================
--- main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_catalog.py	(revision 12518)
+++ main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_catalog.py	(revision 12562)
@@ -90,4 +90,5 @@
         payment.p_session = 2010
         payment.p_category = 'schoolfee'
+        payment.amount_auth = 5.678
         payment.p_state = 'paid'
         self.student['payments'][payment.p_id] = payment
