Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py	(revision 13618)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py	(revision 13620)
@@ -20,7 +20,9 @@
 from zope.component import getUtility
 from zope.i18n import translate
+from datetime import datetime
 from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
-from waeup.kofa.interfaces import IExtFileStore
-from waeup.kofa.browser.layout import action
+from waeup.kofa.interfaces import IExtFileStore, IObjectHistory
+from waeup.kofa.browser.layout import action, UtilityView
+from waeup.kofa.utils.helpers import get_current_principal
 from waeup.kofa.students.browser import (
     StudentPersonalDisplayFormPage, StudentPersonalManageFormPage,
@@ -62,4 +64,6 @@
     form_fields = grok.AutoFields(INigeriaStudentBase).omit(
         'password', 'suspended', 'suspended_comment')
+    form_fields[
+        'financial_clearance_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
 
 class NigeriaStudentBaseManageFormPage(StudentBaseManageFormPage):
@@ -67,5 +71,6 @@
     """
     form_fields = grok.AutoFields(INigeriaStudentBase).omit(
-        'student_id', 'adm_code', 'suspended')
+        'student_id', 'adm_code', 'suspended',
+        'financially_cleared_by', 'financial_clearance_date')
 
 class NigeriaStudentBaseEditFormPage(StudentBaseEditFormPage):
@@ -252,2 +257,59 @@
         super(StudentFilesUploadPage, self).update()
         return
+
+class ClearStudentFinancially(UtilityView, grok.View):
+    """ Clear student financially by bursary officer
+    """
+    grok.context(INigeriaStudent)
+    grok.name('clear_financially')
+    grok.require('waeup.clearStudentFinancially')
+
+    def update(self):
+        if self.context.financially_cleared_by:
+            self.flash(_('This student has already been financially cleared.'),
+                       type="danger")
+            self.redirect(self.url(self.context))
+            return
+        user = get_current_principal()
+        if user is None:
+            usertitle = 'system'
+        else:
+            usertitle = getattr(user, 'public_name', None)
+            if not usertitle:
+                usertitle = user.title
+        self.context.financially_cleared_by = usertitle
+        self.context.financial_clearance_date = datetime.utcnow()
+        self.context.writeLogMessage(self,'financially cleared')
+        history = IObjectHistory(self.context)
+        history.addMessage('Financially cleared')
+        self.flash(_('Student has been financially cleared.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class WithdrawFinancialClearance(UtilityView, grok.View):
+    """ Withdraw financial clearance by bursary officer
+    """
+    grok.context(INigeriaStudent)
+    grok.name('withdraw_financial_clearance')
+    grok.require('waeup.clearStudentFinancially')
+
+    def update(self):
+        if not self.context.financially_cleared_by:
+            self.flash(_('This student has not yet been financially cleared.'),
+                       type="danger")
+            self.redirect(self.url(self.context))
+            return
+        self.context.financially_cleared_by = None
+        self.context.financial_clearance_date = None
+        self.context.writeLogMessage(self,'financial clearance withdrawn')
+        history = IObjectHistory(self.context)
+        history.addMessage('Financial clearance withdrawn')
+        self.flash(_('Financial clearance withdrawn.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py	(revision 13618)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py	(revision 13620)
@@ -55,4 +55,16 @@
         )
 
+    financially_cleared_by = schema.TextLine(
+        title = _(u'Financially Cleared by'),
+        default = None,
+        required = False,
+        )
+
+    financial_clearance_date = schema.Datetime(
+        title = _(u'Financial Clearance Date'),
+        required = False,
+        readonly = False,
+        )
+
 INigeriaStudentBase['reg_number'].order = IStudentBase[
     'reg_number'].order
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/permissions.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/permissions.py	(revision 13620)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/permissions.py	(revision 13620)
@@ -0,0 +1,45 @@
+## $Id$
+##
+## Copyright (C) 2011 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
+##
+"""
+More permissions and roles for the students section.
+"""
+import grok
+
+
+class ClearStudentFinancially(grok.Permission):
+    """The ClearStudentFinancially permission is needed to clear students
+    financially or to reject financial clearance. This permission is
+    meant for bursary officers.
+    """
+    grok.name('waeup.clearStudentFinancially')
+
+
+class BursaryOfficer(grok.Role):
+    """Bursary Officers can export bursary data. They can't access the
+    Data Center but see student data export buttons in the Academic Section.
+
+    This role overwrites the same-named role in waeup.kofa. In
+    kofacustom.nigeria bursary officers are also allowed to clear
+    students financially.
+    """
+    grok.name('waeup.BursaryOfficer')
+    grok.title(u'Bursary Officer')
+    grok.permissions('waeup.showStudents',
+                     'waeup.viewAcademics',
+                     'waeup.exportBursaryData',
+                     'waeup.clearStudentFinancially')
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_browser.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_browser.py	(revision 13618)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_browser.py	(revision 13620)
@@ -25,4 +25,5 @@
 from zope.component import getUtility, createObject
 from zope.interface import verify
+from zope.securitypolicy.interfaces import IPrincipalRoleManager
 from waeup.kofa.app import University
 from waeup.kofa.students.tests.test_browser import (
@@ -347,2 +348,71 @@
         self.assertTrue(
             'Form has been saved' in self.browser.contents)
+
+    def test_financial_clearance(self):
+        self.app['users'].addUser('mrbursary', 'mrbursarysecret')
+        self.app['users']['mrbursary'].email = 'mrbursary@foo.ng'
+        self.app['users']['mrbursary'].title = u'Carlo Pitter'
+        # Clearance officers needs to get
+        # the StudentsOfficer site role
+        prmglobal = IPrincipalRoleManager(self.app)
+        prmglobal.assignRoleToPrincipal('waeup.StudentsOfficer', 'mrbursary')
+        # Assign BursaryOfficer role
+        prmglobal.assignRoleToPrincipal('waeup.BursaryOfficer', 'mrbursary')
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrbursary'
+        self.browser.getControl(name="form.password").value = 'mrbursarysecret'
+        self.browser.getControl("Login").click()
+        # BO can see his roles
+        self.browser.getLink("My Roles").click()
+        self.assertMatches(
+            '...<div>Bursary Officer</div>...',
+            self.browser.contents)
+        # BO can view student record
+        self.browser.open(self.student_path)
+        prmglobal.assignRoleToPrincipal('waeup.BursaryOfficer', 'mrbursary')
+        self.browser.open(self.student_path)
+        # BO can see clearance button ...
+        self.assertTrue(
+            'Clear student financially' in self.browser.contents)
+        # ... but not withdraw button
+        self.assertFalse(
+            'Withdraw financial clearance' in self.browser.contents)
+        # BO can clear student
+        self.browser.getLink("Clear student financially").click()
+        self.assertTrue(
+            'Student has been financially cleared' in self.browser.contents)
+        # Name of BO and date have been stored
+        self.assertEqual(self.student.financially_cleared_by, 'Carlo Pitter')
+        self.assertMatches(
+            '<YYYY-MM-DD hh:mm:ss>',
+            self.student.financial_clearance_date.strftime(
+                "%Y-%m-%d %H:%M:%S"))
+        # BO can't see clearance button ...
+        self.assertFalse(
+            'Clear student financially' in self.browser.contents)
+        # ... but withdraw button and can withdraw clearance
+        self.browser.getLink("Withdraw financial clearance").click()
+        self.assertTrue(
+            'Financial clearance withdrawn' in self.browser.contents)
+        # Name of BO and date have been deleted
+        self.assertEqual(self.student.financially_cleared_by, None)
+        self.assertEqual(self.student.financial_clearance_date, None)
+        # Clearance is logged
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue(
+            'mrbursary - kofacustom.nigeria.students.browser.ClearStudentFinancially'
+            ' - K1000000 - financially cleared' in logcontent)
+        self.assertTrue(
+            'mrbursary - kofacustom.nigeria.students.browser.WithdrawFinancialClearance'
+            ' - K1000000 - financial clearance withdrawn' in logcontent)
+        # Clearance is also stored in the history
+        self.browser.open(self.history_path)
+        self.assertMatches(
+            '...2016-01-16 15:50:48 WAT - Financially cleared by Carlo Pitter...',
+            self.browser.contents)
+        self.assertMatches(
+            '...2016-01-16 15:50:48 WAT - Financial clearance withdrawn by Carlo Pitter...',
+            self.browser.contents)
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_export.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_export.py	(revision 13618)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_export.py	(revision 13620)
@@ -68,5 +68,7 @@
             'clr_code,date_of_birth,def_adm,disabled,email,emp2_end,'
             'emp2_position,emp2_reason,emp2_start,emp_end,emp_position,'
-            'emp_reason,emp_start,employer,employer2,firstname,former_matric,'
+            'emp_reason,emp_start,employer,employer2,'
+            'financial_clearance_date,financially_cleared_by,'
+            'firstname,former_matric,'
             'fst_sit_date,fst_sit_fname,fst_sit_no,fst_sit_results,'
             'fst_sit_type,hq2_degree,hq2_disc,hq2_matric_no,hq2_school,'
@@ -82,5 +84,5 @@
             'current_level,current_session\r\nmy adm code,,,,'
             '"[(\'accounts\', \'A\')]",my clr code,1981-02-04#,,,'
-            'anna@sample.com,,,,,,,,,,,Anna,,,,,"[(\'accounts\', \'A\')]"'
+            'anna@sample.com,,,,,,,,,,,,,Anna,,,,,"[(\'accounts\', \'A\')]"'
             ',,,,,,,,,,,,,,,,Tester,,,234,M.,NG,,,,,,,,,'
             '"Studentroad 21\nLagos 123456\n",,+234-123-12345#,,123,,,,,'
Index: main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/viewlets.py
===================================================================
--- main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/viewlets.py	(revision 13618)
+++ main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/viewlets.py	(revision 13620)
@@ -19,4 +19,5 @@
 import grok
 from zope.component import getUtility
+from waeup.kofa.browser.viewlets import ManageActionButton
 from waeup.kofa.students.interfaces import IStudentsUtils
 from waeup.kofa.students.fileviewlets import (
@@ -24,4 +25,6 @@
 from waeup.kofa.students.browser import ExportPDFClearanceSlip
 
+from kofacustom.nigeria.students.interfaces import INigeriaStudent
+from kofacustom.nigeria.students.browser import NigeriaStudentBaseDisplayFormPage
 from kofacustom.nigeria.interfaces import MessageFactory as _
 
@@ -35,4 +38,37 @@
     return False
 
+# Bursery officer buttons
+
+class StudentClearActionButton(ManageActionButton):
+    grok.order(10)
+    grok.context(INigeriaStudent)
+    grok.view(NigeriaStudentBaseDisplayFormPage)
+    grok.require('waeup.clearStudentFinancially')
+    text = _('Clear student financially')
+    target = 'clear_financially'
+    icon = 'actionicon_accept.png'
+
+    @property
+    def target_url(self):
+        if self.context.financially_cleared_by:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class StudentWithdrawClearanceActionButton(ManageActionButton):
+    grok.order(11)
+    grok.context(INigeriaStudent)
+    grok.view(NigeriaStudentBaseDisplayFormPage)
+    grok.require('waeup.clearStudentFinancially')
+    text = _('Withdraw financial clearance')
+    target = 'withdraw_financial_clearance'
+    icon = 'actionicon_reject.png'
+
+    @property
+    def target_url(self):
+        if not self.context.financially_cleared_by:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+
 # Acceptance Letter
 
