Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/reports.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/reports.py	(revision 12658)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/browser/reports.py	(revision 12659)
@@ -65,5 +65,8 @@
             gen_title = getattr(generator, 'title', 'Unknown')
             args = ', '.join([str(x) for x in job.kwargs['kw'].values()])
-            descr = '%s (%s)' % (gen_title, args)
+            if args:
+                descr = '%s (%s)' % (gen_title, args)
+            else:
+                descr = gen_title
             status = job.finished and 'ready' or 'running'
             status = job.failed and 'FAILED' or status
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/__init__.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/__init__.py	(revision 12659)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/__init__.py	(revision 12659)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/browser_templates/contractstatisticsreportgeneratorpage.pt
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/browser_templates/contractstatisticsreportgeneratorpage.pt	(revision 12659)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/browser_templates/contractstatisticsreportgeneratorpage.pt	(revision 12659)
@@ -0,0 +1,11 @@
+<form  i18n:domain="waeup.kofa"  method="POST">
+  <p i18n:translate="">
+    The report data will be calculated and then made available for you
+    to view/download.
+  </p>
+  <br />
+  <input i18n:translate="" type="submit" class="btn btn-primary"
+         name="CREATE" value="Create" />
+  <a class="btn btn-default" href=""
+     tal:attributes="href view/parent_url">Cancel</a>
+</form>
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/contract_statistics.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/contract_statistics.py	(revision 12659)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/contract_statistics.py	(revision 12659)
@@ -0,0 +1,222 @@
+## $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.ikoba.interfaces import (
+    IIkobaUtils, CREATED, APPROVED, SUBMITTED, EXPIRED, REJECTED)
+from waeup.ikoba.customers.interfaces import ICustomersUtils
+from waeup.ikoba.interfaces import MessageFactory as _
+from waeup.ikoba.reports import IReport
+
+class IContractStatisticsReport(IReport):
+
+    creation_dt_string = Attribute('Human readable report creation datetime')
+
+def get_contract_stats():
+    """Get contracts
+
+    Returns a table ordered by contract type and
+    contract state (cols). The result is a 3-tuple representing
+    ((<TYPES>), (<STATES>), (<NUM_OF_CUSTOMERS>)). The
+    (<NUM_OF_CUSTOMERS>) is an n-tuple with each entry containing the
+    number of contracts found for that type and with the respective
+    state.
+
+    Sample result:
+
+      >>> ((u'Sample Contract 1', u'Sample Contract 2'),
+      ...  ('created', 'submitted for approval', 'approved'),
+      ...  ((12, 10, 1), (0, 5, 25)))
+
+    This result means: there are 5 contracts of type 'Sample Contract 2'
+    are in state 'submitted'.
+    """
+    site = grok.getSite()
+    TYPES = getUtility(ICustomersUtils).CONTYPES_DICT
+    types = TYPES.keys()
+    type_names = tuple(TYPES.values()) + (u'All',)
+    states = (CREATED, SUBMITTED, APPROVED, EXPIRED, REJECTED)
+    STATES = getUtility(ICustomersUtils).TRANSLATED_CONTRACT_STATES
+    state_names = tuple([STATES[state] for state in states]) + (u'Total',)
+    # find all contracts
+    cat = queryUtility(ICatalog, name="contracts_catalog")
+    contracts = cat.searchResults(contract_id=(None, None))
+    # create empty table
+    table = [[0 for x in xrange(len(STATES)+1)] for y in xrange(len(TYPES)+1)]
+    # fill table
+    for contract in contracts:
+        row = types.index(contract.class_name)
+        col = states.index(contract.state)
+        table[row][col] += 1
+        table[-1][col] += 1
+        table[row][-1] += 1
+        table[-1][-1] += 1
+    # turn lists into tuples
+    table = tuple([tuple(row) for row in table])
+    return (type_names, state_names, 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.ikoba.reports import IReport, IReportGenerator
+from waeup.ikoba.reports import Report
+from waeup.ikoba.browser.interfaces import IPDFCreator
+
+STYLE = getSampleStyleSheet()
+
+def tbl_data_to_table(row_names, col_names, data):
+    result = []
+    new_col_names = []
+    for name in col_names:
+        new_col_names.append(name.replace(' ', '\n'))
+    head = [''] + list(new_col_names)
+    result = [head]
+    for idx, row_name in enumerate(row_names):
+        row = [row_name] + list(data[idx])
+        result.append(row)
+    return result
+
+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,0), (-1,-1), 0.25, colors.black),
+    ('LINEBELOW', (0,-1), (-1,-1), 0.25, colors.black),
+    ('LINEAFTER', (-1,0), (-1,-1), 0.25, colors.black),
+    ('LINEBEFORE', (-1,0), (-1,-1), 1.0, colors.black),
+    #('LINEABOVE', (0,-1), (-1,-1), 1.0, colors.black),
+    #('LINEABOVE', (0,0), (-1,0), 0.25, colors.black),
+    ]
+
+@implementer(IContractStatisticsReport)
+class ContractStatisticsReport(Report):
+
+    data = None
+
+    def __init__(self, author='System'):
+        super(ContractStatisticsReport, self).__init__(
+            args=[], kwargs={'author':author})
+        self.author = author
+        self.creation_dt_string = self.creation_dt.astimezone(
+            getUtility(IIkobaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
+        self.data = get_contract_stats()
+
+    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 = 'Contracts'
+        pdf = creator.create_pdf(
+            pdf_data, None, doc_title, self.author, 'Contracts'
+            )
+        return pdf
+
+@implementer(IReportGenerator)
+class ContractStatisticsReportGenerator(grok.GlobalUtility):
+
+    title = _('Contract Statistics')
+    grok.name('contract_stats')
+
+    def generate(self, site, author=None):
+        result = ContractStatisticsReport(
+            author=author)
+        return result
+
+###############################################################
+## Browser related stuff
+##
+## XXX: move to local browser module
+###############################################################
+from waeup.ikoba.browser.layout import IkobaPage
+from waeup.ikoba.reports import get_generators
+from waeup.ikoba.browser.breadcrumbs import Breadcrumb
+grok.templatedir('browser_templates')
+class ContractStatisticsReportGeneratorPage(IkobaPage):
+
+    grok.context(ContractStatisticsReportGenerator)
+    grok.name('index.html')
+    grok.require('waeup.manageReports')
+
+    label = _('Create contract statistics report')
+
+    @property
+    def generator_name(self):
+        for name, gen in get_generators():
+            if gen == self.context:
+                return name
+        return None
+
+    def update(self, CREATE=None):
+        self.parent_url = self.url(self.context.__parent__)
+        if CREATE:
+            # create a new report job for contracts
+            container = self.context.__parent__
+            user_id = self.request.principal.id
+            kw = dict()
+            self.flash(_('New report is being created in background'))
+            job_id = container.start_report_job(
+                self.generator_name, user_id, kw=kw)
+            ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+            grok.getSite().logger.info(
+                '%s - report %s created: %s' % (
+                ob_class, job_id, self.context.title))
+            self.redirect(self.parent_url)
+            return
+        return
+
+class ContractStatisticsReportPDFView(grok.View):
+
+    grok.context(IContractStatisticsReport)
+    grok.name('pdf')
+    grok.require('waeup.Public')
+    prefix = ''
+
+    def _filename(self):
+        return 'ContractStatisticsReport.pdf'
+
+    def render(self):
+        filename = self._filename().replace(
+            '/', '_').replace(' ','_').replace(':', '-')
+        self.response.setHeader(
+            'Content-Type', 'application/pdf')
+        self.response.setHeader(
+            'Content-Disposition:', 'attachment; filename="%s' % filename)
+        pdf_stream = self.context.create_pdf()
+        ob_class = self.__implemented__.__name__.replace('waeup.ikoba.','')
+        grok.getSite().logger.info('%s - report %s downloaded: %s' % (
+            ob_class, self.context.__name__, filename))
+        return pdf_stream
+
+class ContractStatsBreadcrumb(Breadcrumb):
+    """A breadcrumb for reports.
+    """
+    grok.context(ContractStatisticsReportGenerator)
+    title = _(u'Contract Statistics')
+    target = None
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/__init__.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/__init__.py	(revision 12659)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/__init__.py	(revision 12659)
@@ -0,0 +1,1 @@
+# Make this a package.
Index: /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py
===================================================================
--- /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py	(revision 12659)
+++ /main/waeup.ikoba/trunk/src/waeup/ikoba/customers/reports/tests/test_contract_statistics.py	(revision 12659)
@@ -0,0 +1,117 @@
+# Tests for contract 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.ikoba.interfaces import IJobManager
+from waeup.ikoba.customers.reports.contract_statistics import (
+    get_contract_stats, ContractStatisticsReport, IContractStatisticsReport)
+from waeup.ikoba.customers.tests.test_catalog import CatalogTestSetup
+from waeup.ikoba.customers.tests.test_browser import CustomersFullSetup
+from waeup.ikoba.testing import FunctionalLayer
+from waeup.ikoba.tests.test_async import FunctionalAsyncTestCase
+from waeup.ikoba.browser.tests.test_pdf import samples_dir
+
+class ContractStatisticsReportTests(CatalogTestSetup):
+
+    layer = FunctionalLayer
+
+    states = ('created', 'submitted for approval',  'approved', 'expired',
+              'rejected', 'Total')
+
+    def test_iface(self):
+        # ensure we fullfill interface contracts
+        obj = ContractStatisticsReport()
+        verifyClass(IContractStatisticsReport, ContractStatisticsReport)
+        verifyObject(IContractStatisticsReport, obj)
+        return
+
+    def test_get_contract_stats(self):
+        # we can get a table with one contract
+        result1 = get_contract_stats()
+        self.assertEqual(
+            result1,
+            ((u'Sample Contract', u'All'),
+             self.states,
+             ((1, 0, 0, 0, 0, 1),
+              (1, 0, 0, 0, 0, 1),))
+            )
+        return
+
+    def test_create_pdf(self):
+        report = ContractStatisticsReport()
+        result = report.create_pdf()
+        self.assertTrue(result.startswith('%PDF-'))
+        path = os.path.join(samples_dir(), 'contract_statistics.pdf')
+        open(path, 'wb').write(result)
+        print "Sample PDF contract_statistics.pdf written to %s" % path
+        return
+
+class ContractStatisticsReportUITests(CustomersFullSetup, 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 = ['contract_stats']
+        self.browser.getControl("Configure").click()
+        #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="ContractStatisticsReport' 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 - customers.reports.contract_statistics.ContractStatisticsReportGeneratorPage - '
+            'report %s created: Contract Statistics'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - customers.reports.contract_statistics.ContractStatisticsReportPDFView - '
+            'report %s downloaded: ContractStatisticsReport'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'INFO - zope.mgr - browser.reports.ReportsContainerPage - '
+            'report %s discarded' % job_id in logcontent
+            )
+        return
