Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py	(revision 17865)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/browser.py	(revision 17866)
@@ -65,4 +65,6 @@
     form_fields[
         'financial_clearance_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
+    form_fields[
+        'final_clearance_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
 
 class NigeriaStudentBaseManageFormPage(StudentBaseManageFormPage):
@@ -71,5 +73,6 @@
     form_fields = grok.AutoFields(INigeriaStudentBase).omit(
         'student_id', 'adm_code', 'suspended',
-        'financially_cleared_by', 'financial_clearance_date')
+        'financially_cleared_by', 'financial_clearance_date',
+        'finally_cleared_by', 'final_clearance_date')
 
 class NigeriaStudentBaseEditFormPage(StudentBaseEditFormPage):
@@ -400,2 +403,65 @@
             note=self.note
             )
+
+class ClearStudentFinally(UtilityView, grok.View):
+    """ Clear student finally by final clearance officer
+    """
+    grok.context(INigeriaStudent)
+    grok.name('clear_finally')
+    grok.require('waeup.clearStudentFinally')
+
+    def update(self):
+        if self.context.finally_cleared_by:
+            self.flash(_('This student has already been finally 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.finally_cleared_by = usertitle
+        self.context.final_clearance_date = datetime.utcnow()
+        self.context.writeLogMessage(self,'finally cleared')
+        history = IObjectHistory(self.context)
+        history.addMessage('Finally cleared')
+        self.flash(_('Student has been finally cleared.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class WithdrawFinalClearance(UtilityView, grok.View):
+    """ Withdraw final clearance by final clearance officer
+    """
+    grok.context(INigeriaStudent)
+    grok.name('withdraw_final_clearance')
+    grok.require('waeup.clearStudentFinally')
+
+    def update(self):
+        if not self.context.finally_cleared_by:
+            self.flash(_('This student has not yet been finally cleared.'),
+                       type="danger")
+            self.redirect(self.url(self.context))
+            return
+        self.context.finally_cleared_by = None
+        self.context.final_clearance_date = None
+        self.context.writeLogMessage(self,'final clearance withdrawn')
+        history = IObjectHistory(self.context)
+        history.addMessage('Final clearance withdrawn')
+        self.flash(_('Final clearance withdrawn.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+cleared_note = """
+<br /><br /><br />
+<strong>Finally cleared on %s by %s.</strong>
+
+"""
Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py	(revision 17865)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/interfaces.py	(revision 17866)
@@ -67,4 +67,16 @@
         )
 
+    finally_cleared_by = schema.TextLine(
+        title = _(u'Finally Cleared by'),
+        default = None,
+        required = False,
+        )
+
+    final_clearance_date = schema.Datetime(
+        title = _(u'Final Clearance Date'),
+        required = False,
+        readonly = False,
+        )
+
     provisionally_cleared = schema.Bool(
         title = _(u'Provisionally Cleared'),
Index: /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/permissions.py
===================================================================
--- /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/permissions.py	(revision 17865)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/permissions.py	(revision 17866)
@@ -38,2 +38,19 @@
     grok.title(u'Financial Clearance Officer')
     grok.permissions('waeup.clearStudentFinancially')
+
+class ClearStudentFinally(grok.Permission):
+    """The ClearStudentFinally permission is needed to clear students
+    finally or to reject final clearance. This permission is
+    meant for final clearance officers.
+    """
+    grok.name('waeup.clearStudentFinally')
+
+
+class FinalClearanceOfficer(grok.Role):
+    """Final Clearance Officers are also allowed to clear
+    students finally. Usually this role is assigned together
+    with the BursaryOfficer role and the StudentsOfficer role.
+    """
+    grok.name('waeup.FinalClearanceOfficer')
+    grok.title(u'Final Clearance Officer')
+    grok.permissions('waeup.clearStudentFinally')
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 17865)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_browser.py	(revision 17866)
@@ -456,4 +456,74 @@
         return
 
+    def test_final_clearance(self):
+        self.app['users'].addUser('mrbursary', SECRET)
+        self.app['users']['mrbursary'].email = 'mrbursary@foo.ng'
+        self.app['users']['mrbursary'].title = u'Carl 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')
+        # and FinalClearanceOfficer role
+        prmglobal.assignRoleToPrincipal(
+            'waeup.FinalClearanceOfficer', 'mrbursary')
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = 'mrbursary'
+        self.browser.getControl(name="form.password").value = SECRET
+        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)
+        # BO can see clearance button ...
+        self.assertTrue(
+            'Clear student finally' in self.browser.contents)
+        # ... but not withdraw button
+        self.assertFalse(
+            'Withdraw final clearance' in self.browser.contents)
+        # BO can clear student
+        self.browser.getLink("Clear student finally").click()
+        self.assertTrue(
+            'Student has been finally cleared' in self.browser.contents)
+        # Name of BO and date have been stored
+        self.assertEqual(self.student.finally_cleared_by, 'Carl Pitter')
+        self.assertMatches(
+            '<YYYY-MM-DD hh:mm:ss>',
+            self.student.final_clearance_date.strftime(
+                "%Y-%m-%d %H:%M:%S"))
+        # BO can't see clearance button ...
+        self.assertFalse(
+            'Clear student finally' in self.browser.contents)
+        # ... but withdraw button and can withdraw clearance
+        self.browser.getLink("Withdraw final clearance").click()
+        self.assertTrue(
+            'Final clearance withdrawn' in self.browser.contents)
+        # Name of BO and date have been deleted
+        self.assertEqual(self.student.finally_cleared_by, None)
+        self.assertEqual(self.student.final_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.ClearStudentFinally'
+            ' - K1000000 - finally cleared' in logcontent)
+        self.assertTrue(
+            'mrbursary - kofacustom.nigeria.students.browser.WithdrawFinalClearance'
+            ' - K1000000 - final 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 - Finally cleared by Carl Pitter...',
+            self.browser.contents)
+        self.assertMatches(
+            '...2016-01-16 15:50:48 WAT - Final clearance withdrawn by Carl Pitter...',
+            self.browser.contents)
+
     def test_provisionally_cleared(self):
         # Students can edit clearance data
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 17865)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/tests/test_export.py	(revision 17866)
@@ -71,4 +71,5 @@
             'emp2_position,emp2_reason,emp2_start,emp_end,emp_position,'
             'emp_reason,emp_start,employer,employer2,'
+            'final_clearance_date,finally_cleared_by,'
             'financial_clearance_date,financially_cleared_by,'
             'firstname,flash_notice,former_matric,'
@@ -87,5 +88,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 17865)
+++ /main/kofacustom.nigeria/trunk/src/kofacustom/nigeria/students/viewlets.py	(revision 17866)
@@ -93,4 +93,36 @@
     icon = 'actionicon_pdf.png'
 
+# Final Clearance Officer buttons
+
+class StudentFinallyClearActionButton(ManageActionButton):
+    grok.order(13)
+    grok.context(INigeriaStudent)
+    grok.view(NigeriaStudentBaseDisplayFormPage)
+    grok.require('waeup.clearStudentFinally')
+    text = _('Clear student finally')
+    target = 'clear_finally'
+    icon = 'actionicon_accept.png'
+
+    @property
+    def target_url(self):
+        if self.context.finally_cleared_by:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+class StudentWithdrawFinalClearanceActionButton(ManageActionButton):
+    grok.order(14)
+    grok.context(INigeriaStudent)
+    grok.view(NigeriaStudentBaseDisplayFormPage)
+    grok.require('waeup.clearStudentFinally')
+    text = _('Withdraw final clearance')
+    target = 'withdraw_final_clearance'
+    icon = 'actionicon_reject.png'
+
+    @property
+    def target_url(self):
+        if not self.context.finally_cleared_by:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
 # Acceptance Letter
 
