Index: /main/waeup.kofa/trunk/CHANGES.txt
===================================================================
--- /main/waeup.kofa/trunk/CHANGES.txt	(revision 11729)
+++ /main/waeup.kofa/trunk/CHANGES.txt	(revision 11730)
@@ -4,4 +4,6 @@
 1.3dev (unreleased)
 ===================
+
+* Filter payment ticket data exports by specifying the payment_date period.
 
 * Application payment slips can only be downloaded if application form is
Index: /main/waeup.kofa/trunk/layout/static/css/base.css
===================================================================
--- /main/waeup.kofa/trunk/layout/static/css/base.css	(revision 11729)
+++ /main/waeup.kofa/trunk/layout/static/css/base.css	(revision 11730)
@@ -255,4 +255,8 @@
 }
 
+.datepicker-short {
+    width: 15%;
+}
+
 .ui-widget-header .ui-icon {
     background-image: url("../img/ui-icons_222222_256x240.png");
Index: /main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_browser.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_browser.py	(revision 11729)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_browser.py	(revision 11730)
@@ -347,5 +347,6 @@
         self.assertTrue(
             'zope.mgr - students.browser.DatacenterExportJobContainerJobConfig '
-            '- exported: students (None, None, None, None, None), job_id=%s'
+            '- exported: students (None, None, None, None, None, None, None), '
+            'job_id=%s'
             % job_id in logcontent)
         self.assertTrue(
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 11729)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py	(revision 11730)
@@ -3176,4 +3176,5 @@
 
     def update(self, START=None, session=None, level=None, mode=None,
+               payments_start=None, payments_end=None,
                exporter=None):
         self._set_session_values()
@@ -3183,4 +3184,13 @@
         if START is None:
             return
+        if payments_start or payments_end:
+            date_format = '%d/%m/%Y'
+            try:
+                dummy = datetime.strptime(payments_start, date_format)
+                dummy = datetime.strptime(payments_end, date_format)
+            except ValueError:
+                self.flash(_('Payment dates do not match format d/m/Y.'),
+                           type="danger")
+                return
         if session == 'all':
             session=None
@@ -3189,9 +3199,15 @@
         if mode == 'all':
             mode = None
+        if payments_start == '':
+            payments_start = None
+        if payments_end == '':
+            payments_end = None
         if (mode, level, session,
             self.depcode, self.certcode) == (None, None, None, None, None):
             # Export all students including those without certificate
             job_id = self.context.start_export_job(exporter,
-                                          self.request.principal.id)
+                                          self.request.principal.id,
+                                          payments_start = payments_start,
+                                          payments_end = payments_end)
         else:
             job_id = self.context.start_export_job(exporter,
@@ -3201,10 +3217,12 @@
                                           current_mode=mode,
                                           depcode=self.depcode,
-                                          certcode=self.certcode)
+                                          certcode=self.certcode,
+                                          payments_start = payments_start,
+                                          payments_end = payments_end)
         ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
         self.context.logger.info(
-            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
+            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s), job_id=%s'
             % (ob_class, exporter, session, level, mode, self.depcode,
-            self.certcode, job_id))
+            self.certcode, payments_start, payments_end, job_id))
         self.flash(_('Export started for students with') +
                    ' current_session=%s, current_level=%s, study_mode=%s' % (
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/exportconfig.pt
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/exportconfig.pt	(revision 11729)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/exportconfig.pt	(revision 11730)
@@ -14,5 +14,5 @@
     Exporter (Data Type)
   </label>
-  <select name="exporter" class="form-control half">
+  <select id="exporter" name="exporter" class="form-control half" onclick="test()">
     <option tal:repeat="item view/exporters"
             tal:attributes="value python:item[1]">
@@ -50,7 +50,33 @@
     </option>
   </select>
-  <br /><br />
+  <br />
+  <span id="payment_dates" style="display: none;">
+    <label for="mode" i18n:translate="">
+      Payment Date
+    </label>
+    <br />
+    from
+    <input class="datepicker-le-year datepicker-short textType"
+           id="payments_start"
+           name="payments_start" type="text" value="" />
+    to <input class="datepicker-le-year datepicker-short textType"
+           id="payments_end"
+           name="payments_end" type="text" value="" />
+    <br />
+  </span>
+  <br />
   <input type="submit" name="START"  i18n:translate=""
     value="Create CSV file"
     class="btn btn-primary" />
 </form>
+
+<script type="text/javascript">
+  function test() {
+      if (document.getElementById('exporter').value == 'bursary' ||
+          document.getElementById('exporter').value == 'studentpayments') {
+          document.getElementById('payment_dates').style.display = 'block';
+      } else {
+          document.getElementById('payment_dates').style.display = 'none';
+      }
+  }
+</script>
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt	(revision 11729)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/browser_templates/exportconfig_certificate.pt	(revision 11730)
@@ -14,5 +14,5 @@
     Exporter (Data Type)
   </label>
-  <select name="exporter" class="form-control half">
+  <select id="exporter" name="exporter" class="form-control half" onclick="test()">
     <option tal:repeat="item view/exporters"
             tal:attributes="value python:item[1]">
@@ -40,7 +40,33 @@
     </option>
   </select>
-  <br /><br />
+  <br />
+  <span id="payment_dates" style="display: none;">
+    <label for="mode" i18n:translate="">
+      Payment Date
+    </label>
+    <br />
+    from
+    <input class="datepicker-le-year datepicker-short textType"
+           id="payments_start"
+           name="payments_start" type="text" value="" />
+    to <input class="datepicker-le-year datepicker-short textType"
+           id="payments_end"
+           name="payments_end" type="text" value="" />
+    <br />
+  </span>
+  <br />
   <input type="submit" name="START"  i18n:translate=""
     value="Create CSV file"
     class="btn btn-primary" />
 </form>
+
+<script type="text/javascript">
+  function test() {
+      if (document.getElementById('exporter').value == 'bursary' ||
+          document.getElementById('exporter').value == 'studentpayments') {
+          document.getElementById('payment_dates').style.display = 'block';
+      } else {
+          document.getElementById('payment_dates').style.display = 'none';
+      }
+  }
+</script>
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/export.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/export.py	(revision 11729)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/export.py	(revision 11730)
@@ -92,21 +92,44 @@
     return tickets
 
-def get_payments(students):
-    """Get all payments of `students`.
-    """
+def get_payments(students, paid=False, **kw):
+    """Get all payments of `students` within given payment_date period.
+
+    """
+    date_format = '%d/%m/%Y'
     payments = []
-    for student in students:
-        for payment in student.get('payments', {}).values():
-            payments.append(payment)
-    return payments
-
-def get_paid_payments(students):
-    """Get all paid payments of `students`.
-    """
-    payments = []
-    for student in students:
-        for payment in student.get('payments', {}).values():
-            if payment.p_state == 'paid':
-                payments.append(payment)
+    payments_start = kw.get('payments_start')
+    payments_end = kw.get('payments_end')
+    if payments_start and payments_end:
+        # Payment period given
+        payments_start = datetime.strptime(payments_start, date_format)
+        payments_end = datetime.strptime(payments_end, date_format)
+        if paid:
+            # Only paid tickets in payment period are considered
+            for student in students:
+                for payment in student.get('payments', {}).values():
+                    if payment.p_state == 'paid' and \
+                        payment.payment_date > payments_start and \
+                        payment.payment_date < payments_end:
+                        payments.append(payment)
+        else:
+            # All tickets in payment period are considered
+            for student in students:
+                for payment in student.get('payments', {}).values():
+                    if payment.payment_date > payments_start and \
+                        payment.payment_date < payments_end:
+                        payments.append(payment)
+    else:
+        # Payment period not given
+        if paid:
+            # Only paid tickets are considered
+            for student in students:
+                for payment in student.get('payments', {}).values():
+                    if payment.p_state == 'paid':
+                        payments.append(payment)
+        else:
+            # All tickets are considered
+            for student in students:
+                for payment in student.get('payments', {}).values():
+                    payments.append(payment)
     return payments
 
@@ -156,4 +179,11 @@
                 students.append(ticket.student)
             return list(set(students))
+        # Payments can be filtered by payment_date. The period boundaries
+        # are not keys of the catalog and must thus be removed from kw.
+        try:
+            del kw['payments_start']
+            del kw['payments_end']
+        except KeyError:
+            pass
         query = StudentsQuery(**kw)
         return query.query()
@@ -316,5 +346,5 @@
 
     def filter_func(self, x, **kw):
-        return get_payments(x)
+        return get_payments(x, **kw)
 
     def mangle_value(self, value, name, context=None):
@@ -336,5 +366,5 @@
 
     def filter_func(self, x, **kw):
-        return get_paid_payments(x)
+        return get_payments(x, paid=True, **kw)
 
     #: Fieldnames considered by this exporter
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 11729)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py	(revision 11730)
@@ -3233,4 +3233,65 @@
         return job_id
 
+    def test_datacenter_export(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="mode").value = ['ug_ft']
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        self.browser.getControl(name="payments_end").value = '14/12/2012'
+        self.browser.getControl("Create CSV file").click()
+
+        # When the job is finished and we reload the page...
+        job_id = self.wait_for_export_job_completed()
+        # ... the csv file can be downloaded ...
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getLink("Download").click()
+        self.assertEqual(self.browser.headers['content-type'],
+            'text/csv; charset=UTF-8')
+        self.assertTrue(
+            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
+            self.browser.headers['content-disposition'])
+        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
+        job_id = self.app['datacenter'].running_exports[0][0]
+        # ... and discarded
+        self.browser.open('http://localhost/app/datacenter/@@export')
+        self.browser.getControl("Discard").click()
+        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
+        # Creation, downloading and discarding is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'datacenter.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'zope.mgr - students.browser.DatacenterExportJobContainerJobConfig '
+            '- exported: bursary (2004, 100, ug_ft, None, None, '
+            '13/12/2012, 14/12/2012), job_id=%s'
+            % job_id in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVView '
+            '- downloaded: WAeUP.Kofa_bursary_%s.csv, job_id=%s'
+            % (job_id, job_id) in logcontent
+            )
+        self.assertTrue(
+            'zope.mgr - browser.pages.ExportCSVPage '
+            '- discarded: job_id=%s' % job_id in logcontent
+            )
+
+    def test_payment_dates(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
+        self.browser.getControl(name="exporter").value = ['bursary']
+        self.browser.getControl(name="session").value = ['2004']
+        self.browser.getControl(name="level").value = ['100']
+        self.browser.getControl(name="mode").value = ['ug_ft']
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        # If one payment date is missing, an error message appears
+        self.browser.getControl(name="payments_end").value = ''
+        self.browser.getControl("Create CSV file").click()
+        self.assertTrue('Payment dates do not match format d/m/Y'
+            in self.browser.contents)
+
     def test_faculties_export(self):
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
@@ -3243,4 +3304,6 @@
         self.browser.getControl(name="level").value = ['100']
         self.browser.getControl(name="mode").value = ['ug_ft']
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        self.browser.getControl(name="payments_end").value = '14/12/2012'
         self.browser.getControl("Create CSV file").click()
 
@@ -3267,5 +3330,6 @@
         self.assertTrue(
             'zope.mgr - students.browser.FacultiesExportJobContainerJobConfig '
-            '- exported: bursary (2004, 100, ug_ft, None, None), job_id=%s'
+            '- exported: bursary (2004, 100, ug_ft, None, None, '
+            '13/12/2012, 14/12/2012), job_id=%s'
             % job_id in logcontent
             )
@@ -3290,4 +3354,8 @@
         self.browser.getControl(name="level").value = ['100']
         self.browser.getControl(name="mode").value = ['ug_ft']
+        # The testbrowser does not hide the payment period fields, but
+        # values are ignored when using the students exporter.
+        self.browser.getControl(name="payments_start").value = '13/12/2012'
+        self.browser.getControl(name="payments_end").value = '14/12/2012'
         self.browser.getControl("Create CSV file").click()
 
@@ -3314,5 +3382,6 @@
         self.assertTrue(
             'zope.mgr - students.browser.DepartmentExportJobContainerJobConfig '
-            '- exported: students (2004, 100, ug_ft, dep1, None), job_id=%s'
+            '- exported: students (2004, 100, ug_ft, dep1, None, '
+            '13/12/2012, 14/12/2012), job_id=%s'
             % job_id in logcontent
             )
@@ -3360,5 +3429,6 @@
         self.assertTrue(
             'zope.mgr - students.browser.CertificateExportJobContainerJobConfig '
-            '- exported: students (2004, 100, None, None, CERT1), job_id=%s'
+            '- exported: students (2004, 100, None, None, CERT1, None, None), '
+            'job_id=%s'
             % job_id in logcontent
             )
Index: /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_export.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_export.py	(revision 11729)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_export.py	(revision 11730)
@@ -761,4 +761,62 @@
         return
 
+    def test_export_filtered_by_date(self):
+        # payments_start and payments_end are being ignored
+        self.setup_student(self.student)
+        self.app['students'].addStudent(self.student)
+        notify(grok.ObjectModifiedEvent(self.student))
+        exporter = StudentPaymentsExporter()
+        # A key xxx does not exist
+        self.assertRaises(
+            KeyError, exporter.export_filtered, self.app, self.outfile,
+            current_session=None,
+            current_level=None, xxx='nonsense')
+        # payments_start and payments_end do exist but must match format '%Y-%m-%d'
+        self.assertRaises(
+            ValueError, exporter.export_filtered, self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='nonsense', payments_end='nonsense')
+        # If they match the format they are ignored by get_filtered and the
+        # exporter works properly
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='01/04/2012', payments_end='02/04/2012')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+
+            '666,12.12,2012-04-01 13:12:01,schoolfee,1,my-id,'
+            'p-item,100,2012,paid,2012-04-01 14:12:01,12.12,'
+            'r-code,,A111111,created,2012\r\n'
+            )
+        # no results if payment_date is outside the given period
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='31/03/2012', payments_end='01/04/2012')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+            )
+        exporter.export_filtered(
+            self.app, self.outfile,
+            current_session=None, current_level=None,
+            payments_start='02/04/2012', payments_end='03/04/2012')
+        result = open(self.outfile, 'rb').read()
+        self.assertEqual(
+            result,
+            'ac,amount_auth,creation_date,p_category,p_current,p_id,'
+            'p_item,p_level,p_session,p_state,payment_date,r_amount_approved,'
+            'r_code,r_desc,student_id,state,current_session\r\n'
+            )
+        return
+
 class BursaryDataExporterTest(StudentImportExportSetup):
 
