Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/authentication.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/authentication.py	(revision 9169)
@@ -102,4 +102,6 @@
     def checkPassword(self, password):
         """Check whether the given `password` matches the one stored.
+
+        We additionally check if student account has been suspended.
         """
         if not isinstance(password, basestring):
@@ -107,4 +109,6 @@
         if not getattr(self.context, 'password', None):
             # unset/empty passwords do never match
+            return False
+        if self.context.suspended == True:
             return False
         passwordmanager = getUtility(IPasswordManager, 'SSHA')
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/batching.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/batching.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/batching.py	(revision 9169)
@@ -26,4 +26,5 @@
 import grok
 import csv
+from time import time
 from zope.interface import Interface
 from zope.schema import getFields
@@ -42,5 +43,6 @@
     IStudentOnlinePayment, IStudentVerdictUpdate)
 from waeup.kofa.students.workflow import  (
-    IMPORTABLE_STATES, IMPORTABLE_TRANSITIONS)
+    IMPORTABLE_STATES, IMPORTABLE_TRANSITIONS,
+    FORBIDDEN_POSTGRAD_TRANS, FORBIDDEN_POSTGRAD_STATES)
 from waeup.kofa.utils.batching import BatchProcessor
 
@@ -170,4 +172,12 @@
             if transition not in allowed_transitions:
                 return 'Transition not allowed.'
+            if transition in FORBIDDEN_POSTGRAD_TRANS and \
+                obj.is_postgrad:
+                return 'Transition not allowed (pg student).'
+        state = row.get('state', IGNORE_MARKER)
+        if state not in (IGNORE_MARKER, ''):
+            if state in FORBIDDEN_POSTGRAD_STATES and \
+                obj.is_postgrad:
+                return 'State not allowed (pg student).'
         return None
 
@@ -183,4 +193,5 @@
 
         # Update password
+        # XXX: Tale DELETION_MARKER into consideration
         if row.has_key('password'):
             passwd = row.get('password', IGNORE_MARKER)
@@ -222,5 +233,6 @@
         parent = self.getParent(row, site)
         if hasattr(obj,'student_id'):
-            # Update mode: the student exists and we can get the student_id
+            # Update mode: the student exists and we can get the student_id.
+            # Create mode: the record contains the student_id
             parent.logger.info(
                 '%s - Student record updated: %s'
@@ -259,17 +271,16 @@
         errs, inv_errs, conv_dict =  converter.fromStringDict(
             row, self.factory_name, mode=mode)
-        if row.has_key('transition') and \
-            not row['transition'] in IMPORTABLE_TRANSITIONS:
-            if row['transition'] not in (IGNORE_MARKER, ''):
-                errs.append(('transition','not allowed'))
-        if row.has_key('state') and \
-            not row['state'] in IMPORTABLE_STATES:
-            if row['state'] not in (IGNORE_MARKER, ''):
-                errs.append(('state','not allowed'))
-            else:
-                # state is an attribute of Student and must not
-                # be changed if empty
-                conv_dict['state'] = IGNORE_MARKER
-
+        if row.has_key('transition'):
+            if row['transition'] not in IMPORTABLE_TRANSITIONS:
+                if row['transition'] not in (IGNORE_MARKER, ''):
+                    errs.append(('transition','not allowed'))
+        if row.has_key('state'):
+            if row['state'] not in IMPORTABLE_STATES:
+                if row['state'] not in (IGNORE_MARKER, ''):
+                    errs.append(('state','not allowed'))
+                else:
+                    # State is an attribute of Student and must not
+                    # be changed if empty.
+                    conv_dict['state'] = IGNORE_MARKER
         try:
             # Correct stud_id counter. As the IConverter for students
@@ -291,8 +302,9 @@
     grok.baseclass()
 
-    #: required fields beside 'student_id', 'reg_number' and 'matric_number'
+    # additional available  fields
+    # beside 'student_id', 'reg_number' and 'matric_number'
     additional_fields = []
 
-    #: header fields additional required
+    #: header fields additionally required
     additional_headers = []
 
@@ -361,4 +373,21 @@
         return errs, inv_errs, conv_dict
 
+    def getMapping(self, path, headerfields, mode):
+        """Get a mapping from CSV file headerfields to actually used fieldnames.
+        """
+        result = dict()
+        reader = csv.reader(open(path, 'rb'))
+        raw_header = reader.next()
+        for num, field in enumerate(headerfields):
+            if field not in ['student_id', 'reg_number', 'matric_number',
+                             'p_id', 'code', 'level'
+                             ] and mode == 'remove':
+                continue
+            if field == u'--IGNORE--':
+                # Skip ignored columns in failed and finished data files.
+                continue
+            result[raw_header[num]] = field
+        return result
+
 
 class StudentStudyCourseProcessor(StudentProcessorBase):
@@ -406,10 +435,23 @@
             StudentStudyCourseProcessor, self).checkConversion(row, mode=mode)
         # We have to check if current_level is in range of certificate.
-        if conv_dict.has_key('certificate'):
-          cert = conv_dict['certificate']
-          if conv_dict['current_level'] < cert.start_level or \
-              conv_dict['current_level'] > cert.end_level+120:
-              errs.append(('current_level','not in range'))
+        if conv_dict.has_key('certificate') and \
+            conv_dict.has_key('current_level'):
+            cert = conv_dict['certificate']
+            level = conv_dict['current_level']
+            if level < cert.start_level or level > cert.end_level+120:
+                errs.append(('current_level','not in range'))
         return errs, inv_errs, conv_dict
+
+    def checkUpdateRequirements(self, obj, row, site):
+        """Checks requirements the object must fulfill when being updated.
+
+        Returns error messages as strings in case of requirement
+        problems.
+        """
+        current_level = row.get('current_level', None)
+        if current_level == 999 and \
+            obj.__parent__.state in FORBIDDEN_POSTGRAD_STATES:
+            return 'Not a pg student.'
+        return None
 
 class StudentStudyLevelProcessor(StudentProcessorBase):
@@ -427,4 +469,5 @@
 
     location_fields = []
+
     additional_fields = ['level']
     additional_headers = ['level']
@@ -509,8 +552,9 @@
         items_changed = super(CourseTicketProcessor, self).updateEntry(
             obj, row, site)
+        parent = self.getParent(row, site)
         student = self.getParent(row, site).__parent__.__parent__
         student.__parent__.logger.info(
-            '%s - Course ticket updated: %s'
-            % (student.student_id, items_changed))
+            '%s - Course ticket in %s updated: %s'
+            % (student.student_id,  parent.level, items_changed))
         return
 
@@ -528,4 +572,14 @@
         return
 
+    def delEntry(self, row, site):
+        ticket = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if ticket is not None:
+            student = self._getStudent(row, site)
+            student.__parent__.logger.info('%s - Course ticket in %s removed: %s'
+                % (student.student_id, parent.level, ticket.code))
+            del parent[ticket.code]
+        return
+
     def checkConversion(self, row, mode='ignore'):
         """Validates all values in row.
@@ -552,5 +606,5 @@
     grok.name(util_name)
 
-    name = u'Payment Processor'
+    name = u'Student Payment Processor'
     iface = IStudentOnlinePayment
     factory_name = 'waeup.StudentOnlinePayment'
@@ -558,5 +612,12 @@
     location_fields = []
     additional_fields = ['p_id']
-    additional_headers = ['p_id']
+    additional_headers = []
+
+    def checkHeaders(self, headerfields, mode='ignore'):
+        super(StudentOnlinePaymentProcessor, self).checkHeaders(headerfields)
+        if mode in ('update', 'remove') and not 'p_id' in headerfields:
+            raise FatalCSVError(
+                "Need p_id for import in update and remove modes!")
+        return True
 
     def parentsExist(self, row, site):
@@ -573,13 +634,15 @@
         if payments is None:
             return None
+        p_id = row.get('p_id', None)
+        if p_id is None:
+            return None
         # We can use the hash symbol at the end of p_id in import files
         # to avoid annoying automatic number transformation
         # by Excel or Calc
-        p_id = row['p_id'].strip('#')
-        if p_id.startswith('p'):
-            entry = payments.get(p_id)
-        else:
-            # For data migration from old SRP
-            entry = payments.get('p' + p_id[6:])
+        p_id = p_id.strip('#')
+        if not p_id.startswith('p'):
+            # For data migration from old SRP only
+            p_id = 'p' + p_id[7:] + '0'
+        entry = payments.get(p_id)
         return entry
 
@@ -600,5 +663,5 @@
         if not p_id.startswith('p'):
             # For data migration from old SRP
-            obj.p_id = 'p' + p_id[6:]
+            obj.p_id = 'p' + p_id[7:] + '0'
             parent[obj.p_id] = obj
         else:
@@ -606,4 +669,14 @@
         return
 
+    def delEntry(self, row, site):
+        payment = self.getEntry(row, site)
+        parent = self.getParent(row, site)
+        if payment is not None:
+            student = self._getStudent(row, site)
+            student.__parent__.logger.info('%s - Payment ticket removed: %s'
+                % (student.student_id, payment.p_id))
+            del parent[payment.p_id]
+        return
+
     def checkConversion(self, row, mode='ignore'):
         """Validates all values in row.
@@ -613,5 +686,12 @@
 
         # We have to check p_id.
-        p_id = row['p_id'].strip('#')
+        p_id = row.get('p_id', None)
+        if not p_id:
+            timestamp = ("%d" % int(time()*10000))[1:]
+            p_id = "p%s" % timestamp
+            conv_dict['p_id'] = p_id
+            return errs, inv_errs, conv_dict
+        else:
+            p_id = p_id.strip('#')
         if p_id.startswith('p'):
             if not len(p_id) == 14:
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser.py	(revision 9169)
@@ -43,5 +43,5 @@
 from waeup.kofa.interfaces import (
     IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
-    IKofaUtils, IUniversity)
+    IKofaUtils, IUniversity, IObjectHistory)
 from waeup.kofa.interfaces import MessageFactory as _
 from waeup.kofa.widgets.datewidget import (
@@ -53,11 +53,14 @@
     IUGStudentClearance,IPGStudentClearance,
     IStudentPersonal, IStudentBase, IStudentStudyCourse,
+    IStudentStudyCourseTransfer,
     IStudentAccommodation, IStudentStudyLevel,
     ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
-    IStudentOnlinePayment, IBedTicket, IStudentsUtils, IStudentRequestPW
+    IStudentOnlinePayment, IStudentPreviousPayment,
+    IBedTicket, IStudentsUtils, IStudentRequestPW
     )
 from waeup.kofa.students.catalog import search
 from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
-    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
+    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
+    FORBIDDEN_POSTGRAD_TRANS)
 from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
 from waeup.kofa.students.vocabularies import StudyLevelSource
@@ -65,4 +68,5 @@
 from waeup.kofa.hostels.hostel import NOT_OCCUPIED
 from waeup.kofa.utils.helpers import get_current_principal, to_timezone
+from waeup.kofa.mandates.mandate import PasswordMandate
 
 grok.context(IKofaObject) # Make IKofaObject the default context
@@ -91,4 +95,17 @@
     return
 
+def translated_values(view):
+    lang = view.request.cookies.get('kofa.language')
+    for value in view.context.values():
+        value_dict = dict([i for i in value.__dict__.items()])
+        value_dict['mandatory_bool'] = value.mandatory
+        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
+            target_language=lang)
+        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
+            target_language=lang)
+        value_dict['automatic'] = translate(str(value.automatic), 'zope',
+            target_language=lang)
+        yield value_dict
+
 class StudentsBreadcrumb(Breadcrumb):
     """A breadcrumb for the students container.
@@ -116,5 +133,10 @@
     """
     grok.context(IStudentStudyCourse)
-    title = _('Study Course')
+
+    def title(self):
+        if self.context.is_current:
+            return _('Study Course')
+        else:
+            return _('Previous Study Course')
 
 class PaymentsBreadcrumb(Breadcrumb):
@@ -279,10 +301,13 @@
     grok.require('waeup.viewStudent')
     grok.template('basepage')
-    form_fields = grok.AutoFields(IStudentBase).omit('password')
+    form_fields = grok.AutoFields(IStudentBase).omit('password', 'suspended')
     pnav = 4
 
     @property
     def label(self):
-        return _('${a}: Base Data',
+        if self.context.suspended:
+            return _('${a}: Base Data (account deactivated)',
+                mapping = {'a':self.context.display_fullname})
+        return  _('${a}: Base Data',
             mapping = {'a':self.context.display_fullname})
 
@@ -292,4 +317,10 @@
             return _('set')
         return _('unset')
+
+class StudentBasePDFFormPage(KofaDisplayFormPage):
+    """ Page to display student base data in pdf files.
+    """
+    form_fields = grok.AutoFields(IStudentBase).omit(
+        'password', 'suspended', 'phone', 'adm_code', 'sex')
 
 class ContactStudentForm(ContactAdminForm):
@@ -336,5 +367,6 @@
     grok.name('manage_base')
     grok.require('waeup.manageStudent')
-    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
+    form_fields = grok.AutoFields(IStudentBase).omit(
+        'student_id', 'adm_code', 'suspended')
     grok.template('basemanagepage')
     label = _('Manage base data')
@@ -362,4 +394,7 @@
         allowed_transitions = [t for t in self.wf_info.getManualTransitions()
             if not t[0].startswith('pay')]
+        if self.context.is_postgrad:
+            allowed_transitions = [t for t in allowed_transitions
+                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
         return [dict(name='', title=_('No transition'))] +[
             dict(name=x, title=y) for x, y in allowed_transitions]
@@ -396,4 +431,42 @@
         return
 
+class StudentActivatePage(UtilityView, grok.View):
+    """ Activate student account
+    """
+    grok.context(IStudent)
+    grok.name('activate')
+    grok.require('waeup.manageStudent')
+
+    def update(self):
+        self.context.suspended = False
+        self.context.writeLogMessage(self, 'account activated')
+        history = IObjectHistory(self.context)
+        history.addMessage('Student account activated')
+        self.flash(_('Student account has been activated.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
+class StudentDeactivatePage(UtilityView, grok.View):
+    """ Deactivate student account
+    """
+    grok.context(IStudent)
+    grok.name('deactivate')
+    grok.require('waeup.manageStudent')
+
+    def update(self):
+        self.context.suspended = True
+        self.context.writeLogMessage(self, 'account deactivated')
+        history = IObjectHistory(self.context)
+        history.addMessage('Student account deactivated')
+        self.flash(_('Student account has been deactivated.'))
+        self.redirect(self.url(self.context))
+        return
+
+    def render(self):
+        return
+
 class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
     """ Page to display student clearance data
@@ -411,7 +484,9 @@
     def form_fields(self):
         if self.context.is_postgrad:
-            form_fields = grok.AutoFields(IPGStudentClearance).omit('clearance_locked')
-        else:
-            form_fields = grok.AutoFields(IUGStudentClearance).omit('clearance_locked')
+            form_fields = grok.AutoFields(
+                IPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                IUGStudentClearance).omit('clearance_locked')
         return form_fields
 
@@ -432,7 +507,9 @@
     def form_fields(self):
         if self.context.is_postgrad:
-            form_fields = grok.AutoFields(IPGStudentClearance).omit('clearance_locked')
-        else:
-            form_fields = grok.AutoFields(IUGStudentClearance).omit('clearance_locked')
+            form_fields = grok.AutoFields(
+                IPGStudentClearance).omit('clearance_locked')
+        else:
+            form_fields = grok.AutoFields(
+                IUGStudentClearance).omit('clearance_locked')
         return form_fields
 
@@ -446,15 +523,20 @@
     def label(self):
         portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
-        return translate(_('Clearance Slip of '),
+        return translate(_('Clearance Slip of'),
             'waeup.kofa', target_language=portal_language) \
             + ' %s' % self.context.display_fullname
 
+    def _signatures(self):
+        if self.context.state == CLEARED:
+            return (_('Student Signature'), _('Clearance Officer Signature'))
+        return
+
     def render(self):
-        studentview = StudentBaseDisplayFormPage(self.context.student,
+        studentview = StudentBasePDFFormPage(self.context.student,
             self.request)
         students_utils = getUtility(IStudentsUtils)
         return students_utils.renderPDF(
             self, 'clearance.pdf',
-            self.context.student, studentview)
+            self.context.student, studentview, signatures=self._signatures())
 
 class StudentClearanceManageFormPage(KofaEditFormPage):
@@ -475,7 +557,7 @@
     def form_fields(self):
         if self.context.is_postgrad:
-            form_fields = grok.AutoFields(IPGStudentClearance)
-        else:
-            form_fields = grok.AutoFields(IUGStudentClearance)
+            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
+        else:
+            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
         return form_fields
 
@@ -557,12 +639,12 @@
             mapping = {'a':self.context.display_fullname})
 
-class StudentPersonalEditFormPage(KofaEditFormPage):
-    """ Page to edit personal data
+class StudentPersonalManageFormPage(KofaEditFormPage):
+    """ Page to manage personal data
     """
     grok.context(IStudent)
-    grok.name('edit_personal')
-    grok.require('waeup.handleStudent')
+    grok.name('manage_personal')
+    grok.require('waeup.manageStudent')
     form_fields = grok.AutoFields(IStudentPersonal)
-    label = _('Edit personal data')
+    label = _('Manage personal data')
     pnav = 4
 
@@ -572,4 +654,12 @@
         return
 
+class StudentPersonalEditFormPage(StudentPersonalManageFormPage):
+    """ Page to edit personal data
+    """
+    grok.name('edit_personal')
+    grok.require('waeup.handleStudent')
+    label = _('Edit personal data')
+    pnav = 4
+
 class StudyCourseDisplayFormPage(KofaDisplayFormPage):
     """ Page to display the student study course data
@@ -578,12 +668,24 @@
     grok.name('index')
     grok.require('waeup.viewStudent')
-    form_fields = grok.AutoFields(IStudentStudyCourse)
     grok.template('studycoursepage')
     pnav = 4
 
     @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
+                'current_verdict', 'previous_verdict')
+        else:
+            form_fields = grok.AutoFields(IStudentStudyCourse)
+        return form_fields
+
+    @property
     def label(self):
-        return _('${a}: Study Course',
-            mapping = {'a':self.context.__parent__.display_fullname})
+        if self.context.is_current:
+            return _('${a}: Study Course',
+                mapping = {'a':self.context.__parent__.display_fullname})
+        else:
+            return _('${a}: Previous Study Course',
+                mapping = {'a':self.context.__parent__.display_fullname})
 
     @property
@@ -606,4 +708,21 @@
         return
 
+    @property
+    def prev_studycourses(self):
+        if self.context.is_current:
+            if self.context.__parent__.get('studycourse_2', None) is not None:
+                return (
+                        {'href':self.url(self.context.student) + '/studycourse_1',
+                        'title':_('First Study Course, ')},
+                        {'href':self.url(self.context.student) + '/studycourse_2',
+                        'title':_('Second Study Course')}
+                        )
+            if self.context.__parent__.get('studycourse_1', None) is not None:
+                return (
+                        {'href':self.url(self.context.student) + '/studycourse_1',
+                        'title':_('First Study Course')},
+                        )
+        return
+
 class StudyCourseManageFormPage(KofaEditFormPage):
     """ Page to edit the student study course data
@@ -613,5 +732,4 @@
     grok.require('waeup.manageStudent')
     grok.template('studycoursemanagepage')
-    form_fields = grok.AutoFields(IStudentStudyCourse)
     label = _('Manage study course')
     pnav = 4
@@ -620,5 +738,17 @@
     tabthreeactions = [_('Add study level')]
 
+    @property
+    def form_fields(self):
+        if self.context.is_postgrad:
+            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
+                'current_verdict', 'previous_verdict')
+        else:
+            form_fields = grok.AutoFields(IStudentStudyCourse)
+        return form_fields
+
     def update(self):
+        if not self.context.is_current:
+            emit_lock_message(self)
+            return
         super(StudyCourseManageFormPage, self).update()
         tabs.need()
@@ -685,4 +815,33 @@
         return
 
+class StudentTransferFormPage(KofaAddFormPage):
+    """Page to transfer the student.
+    """
+    grok.context(IStudent)
+    grok.name('transfer')
+    grok.require('waeup.manageStudent')
+    label = _('Transfer student')
+    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
+        'entry_mode', 'entry_session')
+    pnav = 4
+
+    def update(self):
+        super(StudentTransferFormPage, self).update()
+        warning.need()
+        return
+
+    @jsaction(_('Transfer'))
+    def transferStudent(self, **data):
+        error = self.context.transfer(**data)
+        if error == -1:
+            self.flash(_('Current level does not match certificate levels.'))
+        elif error == -2:
+            self.flash(_('Former study course record incomplete.'))
+        elif error == -3:
+            self.flash(_('Maximum number of transfers exceeded.'))
+        else:
+            self.flash(_('Successfully transferred.'))
+        return
+
 class StudyLevelDisplayFormPage(KofaDisplayFormPage):
     """ Page to display student study levels
@@ -692,4 +851,6 @@
     grok.require('waeup.viewStudent')
     form_fields = grok.AutoFields(IStudentStudyLevel)
+    form_fields[
+        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
     grok.template('studylevelpage')
     pnav = 4
@@ -702,14 +863,5 @@
     @property
     def translated_values(self):
-        lang = self.request.cookies.get('kofa.language')
-        for value in self.context.values():
-            value_dict = dict([i for i in value.__dict__.items()])
-            value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
-                target_language=lang)
-            value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
-                target_language=lang)
-            value_dict['automatic'] = translate(str(value.automatic), 'zope',
-                target_language=lang)
-            yield value_dict
+        return translated_values(self)
 
     @property
@@ -771,5 +923,5 @@
         Mand = translate(_('Mand.'), 'waeup.kofa', target_language=portal_language)
         Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
-        studentview = StudentBaseDisplayFormPage(self.context.student,
+        studentview = StudentBasePDFFormPage(self.context.student,
             self.request)
         students_utils = getUtility(IStudentsUtils)
@@ -796,5 +948,6 @@
     grok.require('waeup.manageStudent')
     grok.template('studylevelmanagepage')
-    form_fields = grok.AutoFields(IStudentStudyLevel)
+    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
+        'validation_date', 'validated_by')
     pnav = 4
     taboneactions = [_('Save'),_('Cancel')]
@@ -803,4 +956,7 @@
 
     def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
         super(StudyLevelManageFormPage, self).update()
         tabs.need()
@@ -813,4 +969,8 @@
         datatable.need()
         return
+
+    @property
+    def translated_values(self):
+        return translated_values(self)
 
     @property
@@ -861,4 +1021,7 @@
 
     def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
         if str(self.context.__parent__.current_level) != self.context.__name__:
             self.flash(_('This level does not correspond current level.'))
@@ -883,4 +1046,7 @@
 
     def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
         if str(self.context.__parent__.current_level) != self.context.__name__:
             self.flash(_('This level does not correspond current level.'))
@@ -918,4 +1084,11 @@
     pnav = 4
 
+    def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
+        super(CourseTicketAddFormPage, self).update()
+        return
+
     @action(_('Add course ticket'))
     def addCourseTicket(self, **data):
@@ -924,13 +1097,6 @@
         ticket.automatic = False
         ticket.carry_over = False
-        ticket.code = course.code
-        ticket.title = course.title
-        ticket.fcode = course.__parent__.__parent__.__parent__.code
-        ticket.dcode = course.__parent__.__parent__.code
-        ticket.credits = course.credits
-        ticket.passmark = course.passmark
-        ticket.semester = course.semester
         try:
-            self.context.addCourseTicket(ticket)
+            self.context.addCourseTicket(ticket, course)
         except KeyError:
             self.flash(_('The ticket exists.'))
@@ -1030,5 +1196,5 @@
                 mapping = {'a': ', '.join(deleted)}))
             self.context.writeLogMessage(
-                self,'removed: % s' % ', '.join(deleted))
+                self,'removed: %s' % ', '.join(deleted))
         self.redirect(self.url(self.context))
         return
@@ -1052,4 +1218,6 @@
     def createTicket(self, **data):
         p_category = data['p_category']
+        previous_session = data.get('p_session', None)
+        previous_level = data.get('p_level', None)
         student = self.context.__parent__
         if p_category == 'bed_allocation' and student[
@@ -1062,7 +1230,11 @@
                 return
         students_utils = getUtility(IStudentsUtils)
-        error, payment = students_utils.setPaymentDetails(p_category, student)
+        error, payment = students_utils.setPaymentDetails(
+            p_category, student, previous_session, previous_level)
         if error is not None:
             self.flash(error)
+            if 'Would you like' in error:
+                self.redirect(self.url(self.context) + '/@@addpp')
+                return
             self.redirect(self.url(self.context))
             return
@@ -1071,4 +1243,15 @@
         self.redirect(self.url(self.context))
         return
+
+class PreviousPaymentAddFormPage(OnlinePaymentAddFormPage):
+    """ Page to add an online payment ticket for previous sessions
+    """
+    grok.context(IStudentPaymentsContainer)
+    grok.name('addpp')
+    grok.require('waeup.payStudent')
+    form_fields = grok.AutoFields(IStudentPreviousPayment).select(
+        'p_category', 'p_session', 'p_level')
+    label = _('Add previous session online payment')
+    pnav = 4
 
 class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
@@ -1149,5 +1332,5 @@
         #    self.redirect(self.url(self.context))
         #    return
-        studentview = StudentBaseDisplayFormPage(self.context.student,
+        studentview = StudentBasePDFFormPage(self.context.student,
             self.request)
         students_utils = getUtility(IStudentsUtils)
@@ -1369,5 +1552,5 @@
 
     def render(self):
-        studentview = StudentBaseDisplayFormPage(self.context.student,
+        studentview = StudentBasePDFFormPage(self.context.student,
             self.request)
         students_utils = getUtility(IStudentsUtils)
@@ -1569,17 +1752,16 @@
             self.flash(_('Activation code is invalid.'))
             return
+        if code.state == USED:
+            self.flash(_('Activation code has already been used.'))
+            return
         # Mark pin as used (this also fires a pin related transition)
         # and fire transition start_clearance
-        if code.state == USED:
-            self.flash(_('Activation code has already been used.'))
-            return
-        else:
-            comment = _(u"invalidated")
-            # Here we know that the ac is in state initialized so we do not
-            # expect an exception, but the owner might be different
-            if not invalidate_accesscode(pin,comment,self.context.student_id):
-                self.flash(_('You are not the owner of this access code.'))
-                return
-            self.context.clr_code = pin
+        comment = _(u"invalidated")
+        # Here we know that the ac is in state initialized so we do not
+        # expect an exception, but the owner might be different
+        if not invalidate_accesscode(pin, comment, self.context.student_id):
+            self.flash(_('You are not the owner of this access code.'))
+            return
+        self.context.clr_code = pin
         IWorkflowInfo(self.context).fireTransition('start_clearance')
         self.flash(_('Clearance process has been started.'))
@@ -1598,7 +1780,9 @@
     def form_fields(self):
         if self.context.is_postgrad:
-            form_fields = grok.AutoFields(IPGStudentClearance).omit('clearance_locked')
-        else:
-            form_fields = grok.AutoFields(IUGStudentClearance).omit('clearance_locked')
+            form_fields = grok.AutoFields(IPGStudentClearance).omit(
+                'clearance_locked', 'clr_code')
+        else:
+            form_fields = grok.AutoFields(IUGStudentClearance).omit(
+                'clearance_locked', 'clr_code')
         return form_fields
 
@@ -1627,5 +1811,19 @@
             return
         self.flash(_('Clearance form has been saved.'))
-        self.redirect(self.url(self.context,'request_clearance'))
+        if self.context.clr_code:
+            self.redirect(self.url(self.context, 'request_clearance'))
+        else:
+            # We bypass the request_clearance page if student
+            # has been imported in state 'clearance started' and
+            # no clr_code was entered before.
+            state = IWorkflowState(self.context).getState()
+            if state != CLEARANCE:
+                # This shouldn't happen, but the application officer
+                # might have forgotten to lock the form after changing the state
+                self.flash(_('This form cannot be submitted. Wrong state!'))
+                return
+            IWorkflowInfo(self.context).fireTransition('request_clearance')
+            self.flash(_('Clearance has been requested.'))
+            self.redirect(self.url(self.context))
         return
 
@@ -1647,11 +1845,11 @@
             return
         pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
-        if self.context.clr_code != pin:
+        if self.context.clr_code and self.context.clr_code != pin:
             self.flash(_("This isn't your CLR access code."))
             return
         state = IWorkflowState(self.context).getState()
-        # This shouldn't happen, but the application officer
-        # might have forgotten to lock the form after changing the state
         if state != CLEARANCE:
+            # This shouldn't happen, but the application officer
+            # might have forgotten to lock the form after changing the state
             self.flash(_('This form cannot be submitted. Wrong state!'))
             return
@@ -1673,4 +1871,8 @@
 
     def update(self, SUBMIT=None):
+        if not self.context.is_current:
+            emit_lock_message(self)
+            return
+        super(StartSessionPage, self).update()
         if not self.context.next_session_allowed:
             self.flash(_("You are not entitled to start session."))
@@ -1730,4 +1932,7 @@
 
     def update(self):
+        if not self.context.is_current:
+            emit_lock_message(self)
+            return
         if self.context.student.state != PAID:
             emit_lock_message(self)
@@ -1762,4 +1967,7 @@
 
     def update(self):
+        if not self.context.__parent__.is_current:
+            emit_lock_message(self)
+            return
         if self.context.student.state != PAID:
             emit_lock_message(self)
@@ -1776,5 +1984,5 @@
         level_title = translate(self.context.level_title, 'waeup.kofa',
             target_language=lang)
-        return _('Add and remove course tickets of study level ${a}',
+        return _('Edit course list of ${a}',
             mapping = {'a':level_title})
 
@@ -1785,4 +1993,8 @@
             total_credits += val.credits
         return total_credits
+
+    @property
+    def translated_values(self):
+        return translated_values(self)
 
     @action(_('Add course ticket'))
@@ -1847,9 +2059,8 @@
         ticket = createObject(u'waeup.CourseTicket')
         course = data['course']
-        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
-            setattr(ticket, name, getattr(course, name))
         ticket.automatic = False
+        ticket.carry_over = False
         try:
-            self.context.addCourseTicket(ticket)
+            self.context.addCourseTicket(ticket, course)
         except KeyError:
             self.flash(_('The ticket exists.'))
@@ -1927,5 +2138,5 @@
     grok.template('requestpw')
     form_fields = grok.AutoFields(IStudentRequestPW).select(
-        'firstname','reg_number','email')
+        'firstname','number','email')
     label = _('Request password for first-time login')
 
@@ -1948,5 +2159,5 @@
         return True
 
-    @action(_('Get login credentials'), style='primary')
+    @action(_('Send login credentials to email address'), style='primary')
     def get_credentials(self, **data):
         if not self.captcha_result.is_valid:
@@ -1954,9 +2165,12 @@
             # No need to flash something.
             return
-        reg_number = data.get('reg_number','')
+        number = data.get('number','')
         firstname = data.get('firstname','')
         cat = getUtility(ICatalog, name='students_catalog')
         results = list(
-            cat.searchResults(reg_number=(reg_number, reg_number)))
+            cat.searchResults(reg_number=(number, number)))
+        if not results:
+            results = list(
+                cat.searchResults(matric_number=(number, number)))
         if results:
             student = results[0]
@@ -1984,10 +2198,16 @@
         kofa_utils = getUtility(IKofaUtils)
         password = kofa_utils.genPassword()
-        IUserAccount(student).setPassword(password)
+        mandate = PasswordMandate()
+        mandate.params['password'] = password
+        mandate.params['user'] = student
+        site = grok.getSite()
+        site['mandates'].addMandate(mandate)
         # Send email with credentials
-        login_url = self.url(grok.getSite(), 'login')
+        args = {'mandate_id':mandate.mandate_id}
+        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
+        url_info = u'Confirmation link: %s' % mandate_url
         msg = _('You have successfully requested a password for the')
         if kofa_utils.sendCredentials(IUserAccount(student),
-            password, login_url, msg):
+            password, url_info, msg):
             email_sent = student.email
         else:
@@ -1995,4 +2215,7 @@
         self._redirect(email=email_sent, password=password,
             student_id=student.student_id)
+        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
+        self.context.logger.info(
+            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
         return
 
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/requestpw.pt
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/requestpw.pt	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/requestpw.pt	(revision 9169)
@@ -17,6 +17,7 @@
   </table>
   <p i18n:translate="">
-    Your student record will be looked up and an email with your login
-    credentials will be sent to the address provided.
+    Your student record will be looked up and
+    your login credentials will be sent to the email address given above.
+    To be able to proceed you must provide a valid email address!
   </p>
   <div tal:condition="view/availableActions">
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/requestpwmailsent.pt	(revision 9169)
@@ -29,9 +29,10 @@
     <tr>
   </table>
-  <p i18n:translate="">
-    Print this page and proceed to the
+  <p>
+    <span i18n:translate="">Print this page and proceed to the</span>
     <a tal:attributes="href python: view.url(layout.site, 'login')">login form</a>.
-    Please note that passwords are case-sensitive,
+    <span i18n:translate="">Please note that passwords are case-sensitive,
     <br />when entering your credentials, and keep your password secret!
+    </span>
   </p>
   <p tal:condition = "view/email">
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studentpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studentpage.pt	(revision 9168)
+++ 	(revision )
@@ -1,24 +1,0 @@
-<table class="form-table">
-  <tbody>
-    <tal:block repeat="widget view/widgets">
-      <tr>
-        <td class="fieldname">
-          <tal:block content="widget/label"/>:
-        </td>
-        <td>
-          <input tal:replace="structure widget" />
-        </td>
-      </tr>
-    </tal:block>
-  </tbody>
-  <tfoot>
-    <tr>
-      <td colspan="2" class="align-right">
-        <input tal:repeat="action view/actions"
-               tal:replace="structure action/render" />
-      </td>
-    </tr>
-  </tfoot>
-</table>
-
-
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studycoursepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studycoursepage.pt	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studycoursepage.pt	(revision 9169)
@@ -47,4 +47,16 @@
         </td>
       </tr>
+      <tr tal:condition="view/prev_studycourses">
+        <td i18n:translate="">
+            Previous Study Courses:
+        </td>
+        <td>
+          <span tal:repeat="value view/prev_studycourses">
+            <a tal:attributes="href value/href" tal:content="value/title">
+              FACULTY
+            </a>
+          </span>
+        </td>
+      </tr>
   </tbody>
 </table>
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studyleveleditpage.pt	(revision 9169)
@@ -22,9 +22,9 @@
   </thead>
   <tbody>
-    <tr tal:repeat="value context/values" class="gradeC">
+    <tr tal:repeat="value view/translated_values" class="gradeC">
        <td>
         <input type="checkbox" name="val_id"
   		  tal:attributes="value value/__name__"
-        tal:condition="not: value/mandatory" />
+        tal:condition="not: value/mandatory_bool" />
       </td>
       <td tal:content="value/semester">SEMESTER</td>
@@ -36,5 +36,5 @@
       <td tal:content="value/fcode">FACULTY</td>
       <td tal:content="value/credits">CREDITS</td>
-      <td tal:content="value/score">SCORE</td>
+      <td tal:content="value/score|nothing">SCORE</td>
       <td tal:content="value/carry_over">CO</td>
     </tr>
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studylevelmanagepage.pt	(revision 9169)
@@ -46,5 +46,5 @@
       </thead>
       <tbody>
-        <tr tal:repeat="value context/values" class="gradeC">
+        <tr tal:repeat="value view/translated_values" class="gradeC">
           <td>
             <input type="checkbox" name="val_id"
@@ -60,5 +60,5 @@
           <td tal:content="value/credits">CREDITS</td>
           <td tal:content="value/mandatory">MANDATORY</td>
-          <td tal:content="value/score">SCORE</td>
+          <td tal:content="value/score|nothing">SCORE</td>
           <td tal:content="value/carry_over">CO</td>
           <td tal:content="value/automatic">AUTO</td>
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studylevelpage.pt
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studylevelpage.pt	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/browser_templates/studylevelpage.pt	(revision 9169)
@@ -34,6 +34,6 @@
       <th i18n:translate="">Code</th>
       <th i18n:translate="">Title</th>
-      <th i18n:translate="">Dept.</th>
-      <th i18n:translate="">Fact.</th>
+      <th i18n:translate="">Dep.</th>
+      <th i18n:translate="">Fac.</th>
       <th i18n:translate="">Cred.</th>
       <th i18n:translate="">Mand.</th>
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/dynamicroles.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/dynamicroles.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/dynamicroles.py	(revision 9169)
@@ -51,35 +51,6 @@
         """Get roles for principal with id `principal_id`.
 
-        Different to the default implementation, this method also
-        takes into account local roles set on any department connected
-        to the context student.
-
-        If the given principal has at least one of the
-        `external_rolenames` roles granted for the external object, it
-        additionally gets `additional_rolename` role for the context
-        student.
-
-        For the additional roles the `extra_attrib` and all its parent
-        objects are looked up, because 'role inheritance' does not
-        work on that basic level of permission handling.
-
-        Some advantages of this approach:
-
-        - we don't have to store extra local roles for clearance
-          officers in ZODB for each student
-
-        - when local roles on a department change, we don't have to
-          update thousands of students; the local role is assigned
-          dynamically.
-
-        Disadvantage:
-
-        - More expensive role lookups when a clearance officer wants
-          to see an student form.
-
-        This implementation is designed to be usable also for other
-        contexts than students. You can inherit from it and set
-        different role names to lookup/set easily via the static class
-        attributes.
+        See waeup.kofa.applicants.dynamicroles.ApplicantPrincipalRoleManager
+        for further information.
         """
         apr_manager = AnnotationPrincipalRoleManager(self._context)
@@ -118,4 +89,20 @@
                         result.append(
                             ('waeup.StudentsOfficer', setting))
+                elif 'UGClearanceOfficer' in role_id:
+                    if not self._context.is_postgrad:
+                        result.append(
+                            ('waeup.StudentsClearanceOfficer', setting))
+                    else:
+                        # Otherwise grant at least view permissions.
+                        result.append(
+                            ('waeup.StudentsOfficer', setting))
+                elif 'PGClearanceOfficer' in role_id:
+                    if self._context.is_postgrad:
+                        result.append(
+                            ('waeup.StudentsClearanceOfficer', setting))
+                    else:
+                        # Otherwise grant at least view permissions.
+                        result.append(
+                            ('waeup.StudentsOfficer', setting))
                 elif role_id in self.rolename_mapping.keys():
                     # Grant additional role
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/export.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/export.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/export.py	(revision 9169)
@@ -113,4 +113,8 @@
         if name == 'history':
             value = value.messages
+        if name == 'phone' and value is not None:
+            # Append hash '#' to phone numbers to circumvent
+            # unwanted excel automatic
+            value = str('%s#' % value)
         return super(
             StudentsExporter, self).mangle_value(
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/interfaces.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/interfaces.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/interfaces.py	(revision 9169)
@@ -43,5 +43,5 @@
     def getValues(self, context):
         verdicts_dict = getUtility(IStudentsUtils).VERDICTS_DICT
-        return verdicts_dict.keys()
+        return sorted(verdicts_dict.keys())
 
     def getToken(self, context, value):
@@ -50,4 +50,6 @@
     def getTitle(self, context, value):
         verdicts_dict = getUtility(IStudentsUtils).VERDICTS_DICT
+        if value != '0':
+            return verdicts_dict[value] + ' (%s)' % value
         return verdicts_dict[value]
 
@@ -65,5 +67,6 @@
         """
 
-    def setPaymentDetails(category, student):
+    def setPaymentDetails(category, student, previous_session=None,
+            previous_level=None,):
         """Create Payment object and set the payment data of a student for
         the payment category specified.
@@ -151,7 +154,15 @@
     faccode = Attribute('The faculty code of any chosen study course')
     current_session = Attribute('The current session of the student')
+    current_level = Attribute('The current level of the student')
     current_mode = Attribute('The current mode of the student')
     fullname = Attribute('All name parts separated by hyphens')
     display_fullname = Attribute('The fullname of an applicant')
+    is_postgrad = Attribute('True if postgraduate student')
+
+    suspended = schema.Bool(
+        title = _(u'Account suspended'),
+        default = False,
+        required = False,
+        )
 
     student_id = schema.TextLine(
@@ -198,5 +209,5 @@
         title = _(u'PWD Activation Code'),
         required = False,
-        readonly = True,
+        readonly = False,
         )
 
@@ -212,4 +223,10 @@
         )
 
+    def transfer(certificate, current_session,
+        current_level, current_verdict):
+        """ Creates a new studycourse and backups the old one.
+
+        """
+
 class IUGStudentClearance(IKofaObject):
     """Representation of undergraduate student clearance data.
@@ -225,4 +242,5 @@
         title = _(u'Clearance form locked'),
         default = False,
+        required = False,
         )
 
@@ -230,5 +248,5 @@
         title = _(u'CLR Activation Code'),
         required = False,
-        readonly = True,
+        readonly = False,
         )
 
@@ -288,6 +306,6 @@
     login for the the first time.
     """
-    reg_number = schema.TextLine(
-        title = u'Registration Number',
+    number = schema.TextLine(
+        title = _(u'Registr. or Matric. Number'),
         required = True,
         )
@@ -346,5 +364,5 @@
         title = _(u'Current Verdict'),
         source = VerdictSource(),
-        default = 'NY',
+        default = '0',
         required = False,
         )
@@ -353,7 +371,31 @@
         title = _(u'Previous Verdict'),
         source = VerdictSource(),
-        default = 'NY',
-        required = False,
-        )
+        default = '0',
+        required = False,
+        )
+
+class IStudentStudyCourseTransfer(IStudentStudyCourse):
+    """An student transfers.
+
+    """
+
+    certificate = schema.Choice(
+        title = _(u'Certificate'),
+        source = CertificateSource(),
+        required = True,
+        )
+
+    current_level = schema.Choice(
+        title = _(u'Current Level'),
+        source = StudyLevelSource(),
+        required = True,
+        readonly = False,
+        )
+
+
+IStudentStudyCourseTransfer['certificate'].order = IStudentStudyCourse[
+    'certificate'].order
+IStudentStudyCourseTransfer['current_level'].order = IStudentStudyCourse[
+    'current_level'].order
 
 class IStudentVerdictUpdate(IKofaObject):
@@ -385,11 +427,9 @@
     """
     level = Attribute('The level code')
-    validation_date = Attribute('The date of validation')
-    validated_by = Attribute('User Id of course adviser')
 
     level_session = schema.Choice(
         title = _(u'Session'),
         source = academic_sessions_vocab,
-        required = True,
+        required = False,
         )
 
@@ -397,9 +437,21 @@
         title = _(u'Verdict'),
         source = VerdictSource(),
-        default = 'NY',
-        required = False,
-        )
-
-    def addCourseTicket(courseticket):
+        default = '0',
+        required = False,
+        )
+
+    validated_by = schema.TextLine(
+        title = _(u'Validated by'),
+        default = None,
+        required = False,
+        )
+
+    validation_date = schema.Datetime(
+        title = _(u'Validation Date'),
+        required = False,
+        readonly = False,
+        )
+
+    def addCourseTicket(ticket, course):
         """Add a course ticket object.
         """
@@ -520,4 +572,10 @@
     """
 
+    p_current = schema.Bool(
+        title = _(u'Current Session Payment'),
+        default = True,
+        required = False,
+        )
+
     p_level = schema.Int(
         title = _(u'Payment Level'),
@@ -543,4 +601,21 @@
 IStudentOnlinePayment['p_level'].order = IStudentOnlinePayment[
     'p_session'].order
+
+class IStudentPreviousPayment(IOnlinePayment):
+    """An interface for adding previous session payments.
+
+    """
+
+    p_session = schema.Choice(
+        title = _(u'Payment Session'),
+        source = academic_sessions_vocab,
+        required = True,
+        )
+
+    p_level = schema.Choice(
+        title = _(u'Payment Level'),
+        source = StudyLevelSource(),
+        required = True,
+        )
 
 class ICSVStudentExporter(ICSVExporter):
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/payments.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/payments.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/payments.py	(revision 9169)
@@ -97,7 +97,8 @@
         """Process student after payment was made.
         """
-        error = self._createActivationCodes()
-        if error is not None:
-            return False, error, error
+        if self.p_current:
+            error = self._createActivationCodes()
+            if error is not None:
+                return False, error, error
         log = 'successful payment: %s' % self.p_id
         msg = _('Successful payment')
@@ -107,7 +108,8 @@
         """Process student after payment was approved.
         """
-        error = self._createActivationCodes()
-        if error is not None:
-            return False, error, error
+        if self.p_current:
+            error = self._createActivationCodes()
+            if error is not None:
+                return False, error, error
         log = 'payment approved: %s' % self.p_id
         msg = _('Payment approved')
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/student.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/student.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/student.py	(revision 9169)
@@ -28,4 +28,5 @@
 from zope.interface import implementedBy
 from zope.securitypolicy.interfaces import IPrincipalRoleManager
+from zope.schema.interfaces import ConstraintNotSatisfied
 
 from waeup.kofa.image import KofaImageFile
@@ -130,4 +131,10 @@
 
     @property
+    def current_level(self):
+        level = getattr(
+            self.get('studycourse', None), 'current_level', None)
+        return level
+
+    @property
     def current_mode(self):
         certificate = getattr(
@@ -142,4 +149,41 @@
             self.get('studycourse', None), 'is_postgrad', False)
         return is_postgrad
+
+    def transfer(self, certificate, current_session=None,
+        current_level=None, current_verdict=None, previous_verdict=None):
+        """ Creates a new studycourse and backups the old one.
+
+        """
+        studycourse = createObject(u'waeup.StudentStudyCourse')
+        try:
+            studycourse.certificate = certificate
+            studycourse.entry_mode = 'transfer'
+            studycourse.current_session = current_session
+            studycourse.current_level = current_level
+            studycourse.current_verdict = current_verdict
+            studycourse.previous_verdict = previous_verdict
+        except ConstraintNotSatisfied:
+            return -1
+        old = self['studycourse']
+        if getattr(old, 'entry_session', None) is None or\
+            getattr(old, 'certificate', None) is None:
+            return -2
+        studycourse.entry_session = old.entry_session
+        # Students can be transferred only two times.
+        if 'studycourse_1' in self.keys():
+            if 'studycourse_2' in self.keys():
+                return -3
+            self['studycourse_2'] = old
+        else:
+            self['studycourse_1'] = old
+        del self['studycourse']
+        self['studycourse'] = studycourse
+        self.__parent__.logger.info(
+            '%s - transferred from %s to %s' % (
+            self.student_id, old.certificate.code, studycourse.certificate.code))
+        history = IObjectHistory(self)
+        history.addMessage('Transferred from %s to %s' % (
+            old.certificate.code, studycourse.certificate.code))
+        return
 
 
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/studycourse.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/studycourse.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/studycourse.py	(revision 9169)
@@ -61,6 +61,13 @@
     @property
     def is_postgrad(self):
+        if self.certificate is None:
+            return False
         return self.certificate.study_mode.startswith('pg')
-        #return cert.start_level == 999 or cert.end_level == 999
+
+    @property
+    def is_current(self):
+        if '_' in self.__name__:
+            return False
+        return True
 
     def addStudentStudyLevel(self, cert, studylevel):
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/studylevel.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/studylevel.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/studylevel.py	(revision 9169)
@@ -37,6 +37,4 @@
         super(StudentStudyLevel, self).__init__()
         self.level = None
-        self.validation_date = None
-        self.validated_by = None
         return
 
@@ -56,11 +54,18 @@
         return studylevelsource.factory.getTitle(self.__parent__, self.level)
 
-    def addCourseTicket(self, courseticket):
+    def addCourseTicket(self, ticket, course):
         """Add a course ticket object.
         """
-        if not ICourseTicket.providedBy(courseticket):
+        if not ICourseTicket.providedBy(ticket):
             raise TypeError(
                 'StudentStudyLeves contain only ICourseTicket instances')
-        self[courseticket.code] = courseticket
+        ticket.code = course.code
+        ticket.title = course.title
+        ticket.fcode = course.__parent__.__parent__.__parent__.code
+        ticket.dcode = course.__parent__.__parent__.code
+        ticket.credits = course.credits
+        ticket.passmark = course.passmark
+        ticket.semester = course.semester
+        self[ticket.code] = ticket
         return
 
@@ -87,6 +92,7 @@
 
     A course ticket contains a copy of the original course and
-    course referrer data. If the courses and/or their referrers are removed, the
-    corresponding tickets remain unchanged. So we do not need any event
+    certificate course data. If the courses and/or the referrin certificate
+    courses are removed, the corresponding tickets remain unchanged.
+    So we do not need any event
     triggered actions on course tickets.
     """
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/sample_create_payment_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/sample_create_payment_data.csv	(revision 9169)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/sample_create_payment_data.csv	(revision 9169)
@@ -0,0 +1,51 @@
+p_state,p_category,reg_number,session_id,r_amount_approved,p_item,amount_auth,r_card_num,r_code,creation_date,type
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
+paid,schoolfee,1,2010,19500,BTECHBDT,19500,942,0,2010/11/26 19:59:33.744 GMT+1,online
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/sample_payment_data.csv
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/sample_payment_data.csv	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/sample_payment_data.csv	(revision 9169)
@@ -1,4 +1,4 @@
-matric_number,p_state,p_category,p_id,reg_number,session_id,r_amount_approved,p_item,amount_auth,r_card_num,r_code,creation_date,type
-,paid,schoolfee,3816951290797973744#,1,2010,19500,BTECHBDT,19500,0942,00,2010/11/26 19:59:33.744 GMT+1,online
-100001,unpaid,schoolfee,3816951290712593757,,2010,0,BTECHBDT,19500,0942,Z0,2010-11-25 20:16:33.757 GMT+1,online
-,paid,schoolfee,p1266236341955,3,2009,19500,BTECHBDT,19500,0615,00,2010/02/15 13:19:01,online
+matric_number,p_state,p_category,p_id,reg_number,session_id,r_amount_approved,p_item,amount_auth,r_card_num,r_code,creation_date,type,p_current
+,paid,schoolfee,3816951290797973744#,1,2010,19500,BTECHBDT,19500,0942,00,2010/11/26 19:59:33.744 GMT+1,online,1
+100001,unpaid,schoolfee,3816951290712593757,,2010,0,BTECHBDT,19500,0942,Z0,2010-11-25 20:16:33.757 WAT,online,0
+,paid,schoolfee,p1266236341955,3,2009,19500,BTECHBDT,19500,0615,00,2010/02/15 13:19:01,online,1
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_authentication.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_authentication.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_authentication.py	(revision 9169)
@@ -65,4 +65,5 @@
     email = None
     phone = None
+    suspended = False
 
 
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_batching.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_batching.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_batching.py	(revision 9169)
@@ -23,8 +23,11 @@
 import unittest
 import datetime
+import grok
 from time import time
+from zope.event import notify
 from zope.component import createObject
 from zope.component.hooks import setSite, clearSite
 from zope.interface.verify import verifyClass, verifyObject
+from hurry.workflow.interfaces import IWorkflowState
 
 from waeup.kofa.app import University
@@ -126,4 +129,11 @@
     '\n')[0].split(',')
 
+PAYMENT_CREATE_SAMPLE_DATA = open(
+    os.path.join(os.path.dirname(__file__), 'sample_create_payment_data.csv'),
+    'rb').read()
+
+PAYMENT_CREATE_HEADER_FIELDS = PAYMENT_CREATE_SAMPLE_DATA.split(
+    '\n')[0].split(',')
+
 class StudentImportExportSetup(FunctionalTestCase):
 
@@ -171,11 +181,10 @@
     def setup_student(self, student):
         # set predictable values for `student`
-        student.matric_number = u'M123456'
+        student.matric_number = u'234'
         student.adm_code = u'my adm code'
         student.clearance_locked = False
         student.clr_code = u'my clr code'
         student.perm_address = u'Studentroad 21\nLagos 123456\n'
-        student.reg_number = u'123456'
-        student.student_id = u'A111111'
+        student.reg_number = u'123'
         student.firstname = u'Anna'
         student.lastname = u'Tester'
@@ -211,5 +220,5 @@
         ticket.passmark = 100
         ticket.semester = 2
-        study_level.addCourseTicket(ticket)
+        study_level[ticket.code] = ticket
         self.add_payment(student)
         return student
@@ -232,6 +241,5 @@
 
 
-
-class StudentProcessorTest(FunctionalTestCase):
+class StudentProcessorTest(StudentImportExportSetup):
 
     layer = FunctionalLayer
@@ -239,27 +247,12 @@
     def setUp(self):
         super(StudentProcessorTest, self).setUp()
-        # Setup a sample site for each test
-        app = University()
-        self.dc_root = tempfile.mkdtemp()
-        app['datacenter'].setStoragePath(self.dc_root)
-
-        # Prepopulate the ZODB...
-        self.getRootFolder()['app'] = app
-        # we add the site immediately after creation to the
-        # ZODB. Catalogs and other local utilities are not setup
-        # before that step.
-        self.app = self.getRootFolder()['app']
-        # Set site here. Some of the following setup code might need
-        # to access grok.getSite() and should get our new app then
-        setSite(app)
 
         # Add student with subobjects
         student = Student()
-        student.firstname = u'Anna'
-        student.lastname = u'Tester'
-        student.reg_number = u'123'
-        student.matric_number = u'234'
         self.app['students'].addStudent(student)
+        student = self.setup_student(student)
+        notify(grok.ObjectModifiedEvent(student))
         self.student = self.app['students'][student.student_id]
+
         self.processor = StudentProcessor()
         self.workdir = tempfile.mkdtemp()
@@ -285,11 +278,4 @@
         open(self.csv_file_duplicates, 'wb').write(STUDENT_SAMPLE_DATA_DUPLICATES)
 
-    def tearDown(self):
-        super(StudentProcessorTest, self).tearDown()
-        shutil.rmtree(self.workdir)
-        shutil.rmtree(self.dc_root)
-        clearSite()
-        return
-
     def test_interface(self):
         # Make sure we fulfill the interface contracts.
@@ -343,4 +329,22 @@
         self.assertEqual(initial_stud_id, new_stud_id)
         return
+
+    def test_checkUpdateRequirements(self):
+        # Make sure that pg students can't be updated with wrong transition.
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', state='returning'), self.app)
+        self.assertTrue(err is None)
+        self.certificate.study_mode = 'pg_ft'
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', state='returning'), self.app)
+        self.assertEqual(err, 'State not allowed (pg student).')
+        IWorkflowState(self.student).setState('school fee paid')
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', transition='reset6'), self.app)
+        self.assertEqual(err, 'Transition not allowed (pg student).')
+        err = self.processor.checkUpdateRequirements(self.student,
+            dict(reg_number='1', transition='register_courses'), self.app)
+        self.assertEqual(err, 'Transition not allowed (pg student).')
+
 
     def test_delEntry(self):
@@ -486,4 +490,11 @@
         super(StudentStudyCourseProcessorTest, self).setUp()
 
+        # Add student with subobjects
+        student = Student()
+        self.app['students'].addStudent(student)
+        student = self.setup_student(student)
+        notify(grok.ObjectModifiedEvent(student))
+        self.student = self.app['students'][student.student_id]
+
         # Import students with subobjects
         student_file = os.path.join(self.workdir, 'sample_student_data.csv')
@@ -532,4 +543,18 @@
             dict(reg_number='1', current_level='100'))
         self.assertEqual(len(errs),0)
+
+    def test_checkUpdateRequirements(self):
+        # Make sure that pg students can't be updated with wrong transition.
+        err = self.processor.checkUpdateRequirements(self.student['studycourse'],
+            dict(reg_number='1', current_level='100'), self.app)
+        self.assertTrue(err is None)
+        # Since row has passed the converter, current_level is an integer.
+        err = self.processor.checkUpdateRequirements(self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertTrue(err is None)
+        IWorkflowState(self.student).setState('returning')
+        err = self.processor.checkUpdateRequirements(self.student['studycourse'],
+            dict(reg_number='1', current_level=999), self.app)
+        self.assertEqual(err, 'Not a pg student.')
 
     def test_import(self):
@@ -667,5 +692,4 @@
         self.assertEqual(num_warns,2)
         shutil.rmtree(os.path.dirname(fin_file))
-        
 
 class CourseTicketProcessorTest(StudentImportExportSetup):
@@ -681,5 +705,5 @@
         shutil.rmtree(os.path.dirname(fin_file))
 
-        # Add course and course referrer
+        # Add course and certificate course
         self.course = createObject('waeup.Course')
         self.course.code = 'COURSE1'
@@ -689,5 +713,5 @@
         self.app['faculties']['fac1']['dep1'].courses.addCourse(
             self.course)
-        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCourseRef(
+        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCertCourse(
             self.course, level=100)
 
@@ -755,5 +779,5 @@
         # Logging message from updateEntry,
         self.assertTrue(
-            'INFO - system - K1000000 - Course ticket updated: code=COURSE1, '
+            'INFO - system - K1000000 - Course ticket in 100 updated: code=COURSE1, '
             'mandatory=False, score=3'
             in logcontent)
@@ -769,4 +793,23 @@
         self.assertEqual(num_warns,2)
         shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        # We perform the same import twice,
+        # the second time in remove mode. The number
+        # of warnings must be the same.
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'create')
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is True
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, COURSETICKET_HEADER_FIELDS,'remove')
+        self.assertEqual(num_warns,2)
+        assert self.processor.entryExists(
+            dict(reg_number='1', level='100', code='COURSE1'), self.app) is False
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - K1000000 - Course ticket in 100 removed: COURSE1'
+            in logcontent)
 
 class PaymentProcessorTest(StudentImportExportSetup):
@@ -784,5 +827,5 @@
         self.student = self.app['students'][student.student_id]
         payment = createObject(u'waeup.StudentOnlinePayment')
-        payment.p_id = 'p123'
+        payment.p_id = 'p120'
         self.student['payments'][payment.p_id] = payment
 
@@ -798,4 +841,7 @@
             self.workdir, 'sample_payment_data.csv')
         open(self.csv_file, 'wb').write(PAYMENT_SAMPLE_DATA)
+        self.csv_file2 = os.path.join(
+            self.workdir, 'sample_create_payment_data.csv')
+        open(self.csv_file2, 'wb').write(PAYMENT_CREATE_SAMPLE_DATA)
 
     def test_interface(self):
@@ -809,9 +855,22 @@
             dict(student_id='ID_NONE', p_id='nonsense'), self.app) is None
         assert self.processor.getEntry(
-            dict(student_id=self.student.student_id, p_id='p123'),
-            self.app) is self.student['payments']['p123']
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app) is self.student['payments']['p120']
         assert self.processor.getEntry(
-            dict(student_id=self.student.student_id, p_id='XXXXXX123'),
-            self.app) is self.student['payments']['p123']
+            dict(student_id=self.student.student_id, p_id='XXXXXX112'),
+            self.app) is self.student['payments']['p120']
+
+    def test_delEntry(self):
+        assert self.processor.getEntry(
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app) is self.student['payments']['p120']
+        self.assertEqual(len(self.student['payments'].keys()),1)
+        self.processor.delEntry(
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app)
+        assert self.processor.getEntry(
+            dict(student_id=self.student.student_id, p_id='p120'),
+            self.app) is None
+        self.assertEqual(len(self.student['payments'].keys()),0)
 
     def test_addEntry(self):
@@ -831,20 +890,20 @@
             self.app)
         self.assertEqual(len(self.student['payments'].keys()),3)
-        self.assertEqual(self.student['payments']['p456'].p_id, 'p456')
+        self.assertEqual(self.student['payments']['p560'].p_id, 'p560')
 
     def test_checkConversion(self):
         errs, inv_errs, conv_dict = self.processor.checkConversion(
-            dict(reg_number='1', p_id='3816951266236341955'))
+            dict(p_id='3816951266236341955'))
         self.assertEqual(len(errs),0)
         errs, inv_errs, conv_dict = self.processor.checkConversion(
-            dict(reg_number='1', p_id='p1266236341955'))
+            dict(p_id='p1266236341955'))
         self.assertEqual(len(errs),0)
         errs, inv_errs, conv_dict = self.processor.checkConversion(
-            dict(reg_number='1', p_id='nonsense'))
+            dict(p_id='nonsense'))
         self.assertEqual(len(errs),1)
-        timestamp = "%d" % int(time()*1000)
+        timestamp = ("%d" % int(time()*10000))[1:]
         p_id = "p%s" % timestamp
         errs, inv_errs, conv_dict = self.processor.checkConversion(
-            dict(reg_number='1', p_id=p_id))
+            dict(p_id=p_id))
         self.assertEqual(len(errs),0)
 
@@ -854,19 +913,29 @@
         self.assertEqual(num_warns,0)
         payment = self.processor.getEntry(dict(reg_number='1',
-            p_id='p1290797973744'), self.app)
-        self.assertEqual(payment.p_id, 'p1290797973744')
+            p_id='p2907979737440'), self.app)
+        self.assertEqual(payment.p_id, 'p2907979737440')
+        self.assertTrue(payment.p_current)
         cdate = payment.creation_date.strftime("%Y-%m-%d %H:%M:%S")
         self.assertEqual(cdate, "2010-11-26 18:59:33")
         self.assertEqual(str(payment.creation_date.tzinfo),'UTC')
-        shutil.rmtree(os.path.dirname(fin_file))
-
+        payment = self.processor.getEntry(dict(matric_number='100001',
+            p_id='p2907125937570'), self.app)
+        self.assertEqual(payment.p_id, 'p2907125937570')
+        self.assertFalse(payment.p_current)
+        cdate = payment.creation_date.strftime("%Y-%m-%d %H:%M:%S")
+        # Ooooh, still the old problem, see
+        # http://mail.dzug.org/mailman/archives/zope/2006-August/001153.html.
+        # WAT is interpreted as GMT-1 and not GMT+1
+        self.assertEqual(cdate, "2010-11-25 21:16:33")
+        self.assertEqual(str(payment.creation_date.tzinfo),'UTC')
+        shutil.rmtree(os.path.dirname(fin_file))
         logcontent = open(self.logfile).read()
-        # Logging message from updateEntry,
+        # Logging message from updateEntry
         self.assertTrue(
             'INFO - system - K1000001 - Payment ticket updated: '
-            'p_id=p1266236341955, p_item=BTECHBDT, '
-            'creation_date=2010-02-15 13:19:01+00:00, r_code=00, '
-            'r_amount_approved=19500.0, p_category=schoolfee, '
-            'amount_auth=19500.0, p_state=paid'
+            'p_item=BTECHBDT, creation_date=2010-02-15 13:19:01+00:00, '
+            'p_category=schoolfee, amount_auth=19500.0, p_current=True, '
+            'p_id=p1266236341955, r_code=00, r_amount_approved=19500.0, '
+            'p_state=paid'
             in logcontent)
 
@@ -881,4 +950,24 @@
         self.assertEqual(num_warns,0)
         shutil.rmtree(os.path.dirname(fin_file))
+
+    def test_import_remove(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'create')
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file, PAYMENT_HEADER_FIELDS,'remove')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'INFO - system - K1000001 - Payment ticket removed: p1266236341955'
+            in logcontent)
+
+    def test_import_wo_pid(self):
+        num, num_warns, fin_file, fail_file = self.processor.doImport(
+            self.csv_file2, PAYMENT_CREATE_HEADER_FIELDS,'create')
+        self.assertEqual(num_warns,0)
+        shutil.rmtree(os.path.dirname(fin_file))
+        self.assertEqual(len(self.app['students']['X666666']['payments']), 50)
+
 
 def test_suite():
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_browser.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_browser.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_browser.py	(revision 9169)
@@ -118,4 +118,5 @@
         self.manage_clearance_path = self.student_path + '/manage_clearance'
         self.edit_personal_path = self.student_path + '/edit_personal'
+        self.manage_personal_path = self.student_path + '/manage_personal'
         self.studycourse_path = self.student_path + '/studycourse'
         self.payments_path = self.student_path + '/payments'
@@ -170,5 +171,5 @@
         self.app['faculties']['fac1']['dep1'].courses.addCourse(
             self.course)
-        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCourseRef(
+        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCertCourse(
             self.course, level=100)
 
@@ -454,4 +455,8 @@
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
         self.assertEqual(self.browser.url, self.personal_path)
+        self.browser.getLink("Manage").click()
+        self.assertEqual(self.browser.headers['Status'], '200 Ok')
+        self.assertEqual(self.browser.url, self.manage_personal_path)
+        self.browser.open(self.personal_path)
         self.browser.getLink("Edit").click()
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
@@ -474,5 +479,5 @@
         self.assertEqual(self.browser.headers['Status'], '200 Ok')
         self.assertEqual(self.browser.url, self.history_path)
-        self.assertMatches('...Student admitted by Manager...',
+        self.assertMatches('...Admitted by Manager...',
                            self.browser.contents)
         # Only the Application Slip does not exist
@@ -754,5 +759,5 @@
         self.assertEqual(student['studycourse'].current_session, 2005) # +1
         self.assertEqual(student['studycourse'].current_level, 200) # +100
-        self.assertEqual(student['studycourse'].current_verdict, 'NY') # NY = not set
+        self.assertEqual(student['studycourse'].current_verdict, '0') # 0 = Zero = not set
         self.assertEqual(student['studycourse'].previous_verdict, 'A')
         self.browser.getControl(name="transition").value = ['register_courses']
@@ -762,4 +767,20 @@
         self.browser.getControl(name="transition").value = ['return']
         self.browser.getControl("Save").click()
+        return
+
+    def test_manage_pg_workflow(self):
+        # Managers can pass through the whole workflow
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        student = self.app['students'][self.student_id]
+        self.browser.open(self.manage_student_path)
+        self.assertTrue('<option value="reset6">' in self.browser.contents)
+        self.assertTrue('<option value="register_courses">' in self.browser.contents)
+        self.assertTrue('<option value="reset5">' in self.browser.contents)
+        self.certificate.study_mode = 'pg_ft'
+        self.browser.open(self.manage_student_path)
+        self.assertFalse('<option value="reset6">' in self.browser.contents)
+        self.assertFalse('<option value="register_courses">' in self.browser.contents)
+        self.assertTrue('<option value="reset5">' in self.browser.contents)
         return
 
@@ -776,10 +797,10 @@
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
         self.browser.open(datacenter_path)
-        self.browser.getLink('Upload CSV file').click()
+        self.browser.getLink('Upload data').click()
         filecontents = StringIO(open('students.csv', 'rb').read())
         filewidget = self.browser.getControl(name='uploadfile:file')
         filewidget.add_file(filecontents, 'text/plain', 'students.csv')
         self.browser.getControl(name='SUBMIT').click()
-        self.browser.getLink('Batch processing').click()
+        self.browser.getLink('Process data').click()
         button = lookup_submit_value(
             'select', 'students_zope.mgr.csv', self.browser)
@@ -802,10 +823,10 @@
 """)
         self.browser.open(datacenter_path)
-        self.browser.getLink('Upload CSV file').click()
+        self.browser.getLink('Upload data').click()
         filecontents = StringIO(open('studycourses.csv', 'rb').read())
         filewidget = self.browser.getControl(name='uploadfile:file')
         filewidget.add_file(filecontents, 'text/plain', 'studycourses.csv')
         self.browser.getControl(name='SUBMIT').click()
-        self.browser.getLink('Batch processing').click()
+        self.browser.getLink('Process data').click()
         button = lookup_submit_value(
             'select', 'studycourses_zope.mgr.csv', self.browser)
@@ -917,5 +938,5 @@
         self.assertTrue('clearance started' in self.browser.contents)
         self.browser.open(self.history_path)
-        self.assertTrue("Reset to 'clearance' by My Public Name" in
+        self.assertTrue("Reset to 'clearance started' by My Public Name" in
             self.browser.contents)
         IWorkflowInfo(self.student).fireTransition('request_clearance')
@@ -946,5 +967,5 @@
         self.app['users'].addUser('mrsadvise', 'mrsadvisesecret')
         self.app['users']['mrsadvise'].email = 'mradvise@foo.ng'
-        self.app['users']['mrsadvise'].title = 'Helen Procter'
+        self.app['users']['mrsadvise'].title = u'Helen Procter'
         # Assign local CourseAdviser100 role for a certificate
         cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
@@ -997,11 +1018,11 @@
         # the 100L CA does see the 'Validate' button
         self.browser.open(L110_student_path)
-        self.assertFalse('Validate' in self.browser.contents)
+        self.assertFalse('Validate courses' in self.browser.contents)
         IWorkflowInfo(self.student).fireTransition('register_courses')
         self.browser.open(L110_student_path)
-        self.assertFalse('Validate' in self.browser.contents)
+        self.assertFalse('Validate courses' in self.browser.contents)
         self.student['studycourse'].current_level = 110
         self.browser.open(L110_student_path)
-        self.assertTrue('Validate' in self.browser.contents)
+        self.assertTrue('Validate courses' in self.browser.contents)
         # ... but a 100L CA does not see the button on other levels
         studylevel2 = StudentStudyLevel()
@@ -1011,9 +1032,15 @@
         L200_student_path = self.studycourse_path + '/200'
         self.browser.open(L200_student_path)
-        self.assertFalse('Validate' in self.browser.contents)
+        self.assertFalse('Validate courses' in self.browser.contents)
         self.browser.open(L110_student_path)
         self.browser.getLink("Validate courses").click()
         self.assertTrue('Course list has been validated' in self.browser.contents)
         self.assertTrue('courses validated' in self.browser.contents)
+        self.assertEqual(self.student['studycourse']['110'].validated_by,
+            'Helen Procter')
+        self.assertMatches(
+            '<YYYY-MM-DD hh:mm:ss>',
+            self.student['studycourse']['110'].validation_date.strftime(
+                "%Y-%m-%d %H:%M:%S"))
         self.browser.getLink("Reject courses").click()
         self.assertTrue('Course list request has been annulled.'
@@ -1023,4 +1050,6 @@
             '/contactstudent?subject=%s' % urlmessage)
         self.assertTrue('school fee paid' in self.browser.contents)
+        self.assertTrue(self.student['studycourse']['110'].validated_by is None)
+        self.assertTrue(self.student['studycourse']['110'].validation_date is None)
         IWorkflowInfo(self.student).fireTransition('register_courses')
         self.browser.open(L110_student_path)
@@ -1142,8 +1171,16 @@
         # and can perform actions
         IWorkflowInfo(self.student).fireTransition('admit')
+        # Students can't login if their account is suspended/deactivated
+        self.student.suspended = True
         self.browser.open(self.login_path)
         self.browser.getControl(name="form.login").value = self.student_id
         self.browser.getControl(name="form.password").value = 'spwd'
         self.browser.getControl("Login").click()
+        self.assertTrue(
+            'Your account has been deactivated.' in self.browser.contents)
+        self.student.suspended = False
+        self.browser.getControl("Login").click()
+        self.assertTrue(
+            'You logged in.' in self.browser.contents)
         # Student can upload a passport picture
         self.browser.open(self.student_path + '/change_portrait')
@@ -1170,5 +1207,7 @@
         self.assertMatches('...Not all required fields filled...',
                            self.browser.contents)
-        self.student.email = 'aa@aa.ng'
+        self.browser.open(self.student_path + '/edit_base')
+        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
+        self.browser.getControl("Save").click()
         self.browser.open(self.student_path + '/start_clearance')
         self.browser.getControl(name="ac_series").value = '3'
@@ -1185,4 +1224,7 @@
         # Set the correct owner
         self.existing_clrac.owner = self.student_id
+        # clr_code might be set (and thus returns None) due importing
+        # an empty clr_code column.
+        self.student.clr_code = None
         self.browser.getControl("Start clearance now").click()
         self.assertMatches('...Clearance process has been started...',
@@ -1235,5 +1277,5 @@
         self.browser.getControl("Create course list now").click()
         self.browser.getLink("100").click()
-        self.browser.getLink("Add and remove courses").click()
+        self.browser.getLink("Edit course list").click()
         self.browser.getControl("Add course ticket").click()
         self.browser.getControl(name="form.course").value = ['COURSE1']
@@ -1248,5 +1290,5 @@
         self.browser.getControl("Create course list now").click()
         self.browser.getLink("200").click()
-        self.browser.getLink("Add and remove courses").click()
+        self.browser.getLink("Edit course list").click()
         self.browser.getControl("Add course ticket").click()
         self.browser.getControl(name="form.course").value = ['COURSE1']
@@ -1286,4 +1328,17 @@
         self.assertEqual(self.student.state, 'courses registered')
         return
+
+    def test_student_clearance_wo_clrcode(self):
+        IWorkflowState(self.student).setState('clearance started')
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+        self.student.clearance_locked = False
+        self.browser.open(self.edit_clearance_path)
+        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
+        self.browser.getControl("Save and request clearance").click()
+        self.assertMatches('...Clearance has been requested...',
+                           self.browser.contents)
 
     def test_manage_payments(self):
@@ -1516,5 +1571,5 @@
         # The new SFE-0 pin can be used for starting new session
         self.browser.open(self.studycourse_path)
-        self.browser.getLink('Start session').click()
+        self.browser.getLink('Start new session').click()
         pin = self.app['accesscodes']['SFE-0'].keys()[0]
         parts = pin.split('-')[1:]
@@ -1528,5 +1583,57 @@
         return
 
-    def test_postgraduate_payments(self):
+    def test_student_previous_payments(self):
+        # Login
+        self.browser.open(self.login_path)
+        self.browser.getControl(name="form.login").value = self.student_id
+        self.browser.getControl(name="form.password").value = 'spwd'
+        self.browser.getControl("Login").click()
+
+        # Students can add previous school fee payment tickets in any state.
+        IWorkflowState(self.student).setState('courses registered')
+        self.browser.open(self.payments_path)
+        self.browser.getControl("Add online payment ticket").click()
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl("Create ticket").click()
+
+        # Amount cannot be determined since the state is not
+        # 'cleared' or 'returning'
+        self.assertMatches('...Amount could not be determined...',
+                           self.browser.contents)
+        self.assertMatches('...Would you like to pay for a previous session?...',
+                           self.browser.contents)
+
+        # Previous session payment form is provided
+        self.browser.getControl(name="form.p_category").value = ['schoolfee']
+        self.browser.getControl(name="form.p_session").value = ['2004']
+        self.browser.getControl(name="form.p_level").value = ['300']
+        self.browser.getControl("Create ticket").click()
+        self.assertMatches('...ticket created...',
+                           self.browser.contents)
+        ctrl = self.browser.getControl(name='val_id')
+        value = ctrl.options[0]
+        self.browser.getLink(value).click()
+        self.assertMatches('...Amount Authorized...',
+                           self.browser.contents)
+        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
+
+        # Payment session is properly set
+        self.assertEqual(self.student['payments'][value].p_session, 2004)
+        self.assertEqual(self.student['payments'][value].p_level, 300)
+
+        # We simulate the approval
+        self.browser.open(self.browser.url + '/fake_approve')
+        self.assertMatches('...Payment approved...',
+                          self.browser.contents)
+
+        # No AC has been created
+        self.assertEqual(len(self.app['accesscodes']['SFE-0'].keys()), 0)
+        self.assertTrue(self.student['payments'][value].ac is None)
+
+        # Current payment flag is set False
+        self.assertFalse(self.student['payments'][value].p_current)
+        return
+
+    def test_student_postgraduate_payments(self):
         self.certificate.study_mode = 'pg_ft'
         self.certificate.start_level = 999
@@ -1552,5 +1659,5 @@
                            self.browser.contents)
         # Payment session and level are current ones.
-        # Postgrads have to school_fee_1.
+        # Postgrads have to pay school_fee_1.
         self.assertEqual(self.student['payments'][value].amount_auth, 40000.0)
         self.assertEqual(self.student['payments'][value].p_session, 2004)
@@ -1565,5 +1672,5 @@
         # The new SFE-0 pin can be used for starting session
         self.browser.open(self.studycourse_path)
-        self.browser.getLink('Start session').click()
+        self.browser.getLink('Start new session').click()
         pin = self.app['accesscodes']['SFE-0'].keys()[0]
         parts = pin.split('-')[1:]
@@ -1605,5 +1712,5 @@
         # The new SFE-1 pin can be used for starting new session
         self.browser.open(self.studycourse_path)
-        self.browser.getLink('Start session').click()
+        self.browser.getLink('Start new session').click()
         self.browser.getControl(name="ac_series").value = sfeseries
         self.browser.getControl(name="ac_number").value = sfenumber
@@ -1850,21 +1957,4 @@
         self.assertTrue('An email with' in self.browser.contents)
 
-    def test_reindex(self):
-        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
-        self.browser.open('http://localhost/app/reindex')
-        self.assertTrue('No catalog name provided' in self.browser.contents)
-        self.browser.open('http://localhost/app/reindex?ctlg=xyz')
-        self.assertTrue('xyz_catalog does not exist' in self.browser.contents)
-        cat = queryUtility(ICatalog, name='students_catalog')
-        results = cat.searchResults(student_id=(None, None))
-        self.assertEqual(len(results),1)
-        cat.clear()
-        results = cat.searchResults(student_id=(None, None))
-        self.assertEqual(len(results),0)
-        self.browser.open('http://localhost/app/reindex?ctlg=students')
-        self.assertTrue('1 students re-indexed' in self.browser.contents)
-        results = cat.searchResults(student_id=(None, None))
-        self.assertEqual(len(results),1)
-
     def test_change_current_mode(self):
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
@@ -1888,4 +1978,112 @@
         self.assertTrue('Employer' in self.browser.contents)
 
+    def test_activate_deactivate_buttons(self):
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path)
+        self.browser.getLink("Deactivate").click()
+        self.assertTrue(
+            'Student account has been deactivated.' in self.browser.contents)
+        self.assertTrue(
+            'Base Data (account deactivated)' in self.browser.contents)
+        self.assertTrue(self.student.suspended)
+        self.browser.getLink("Activate").click()
+        self.assertTrue(
+            'Student account has been activated.' in self.browser.contents)
+        self.assertFalse(
+            'Base Data (account deactivated)' in self.browser.contents)
+        self.assertFalse(self.student.suspended)
+        # History messages have been added ...
+        self.browser.getLink("History").click()
+        self.assertTrue(
+            'Student account deactivated by Manager<br />' in self.browser.contents)
+        self.assertTrue(
+            'Student account activated by Manager<br />' in self.browser.contents)
+        # ... and actions have been logged.
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.mgr - students.browser.StudentDeactivatePage - '
+                        'K1000000 - account deactivated' in logcontent)
+        self.assertTrue('zope.mgr - students.browser.StudentActivatePage - '
+                        'K1000000 - account activated' in logcontent)
+
+    def test_student_transfer(self):
+        # Add second certificate
+        self.certificate2 = createObject('waeup.Certificate')
+        self.certificate2.code = u'CERT2'
+        self.certificate2.study_mode = 'ug_ft'
+        self.certificate2.start_level = 999
+        self.certificate2.end_level = 999
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate2)
+
+        # Add study level to old study course
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 200
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        self.browser.open(self.student_path)
+        self.browser.getLink("Transfer").click()
+        self.browser.getControl(name="form.certificate").value = ['CERT2']
+        self.browser.getControl(name="form.current_session").value = ['2011']
+        self.browser.getControl(name="form.current_level").value = ['200']
+        self.browser.getControl("Transfer").click()
+        self.assertTrue(
+            'Current level does not match certificate levels'
+            in self.browser.contents)
+        self.browser.getControl(name="form.current_level").value = ['999']
+        self.browser.getControl("Transfer").click()
+        self.assertTrue('Successfully transferred' in self.browser.contents)
+
+        # Add study level to new study course
+        studylevel = createObject(u'waeup.StudentStudyLevel')
+        studylevel.level = 200
+        self.student['studycourse'].addStudentStudyLevel(
+            self.certificate, studylevel)
+
+        # Edit and add pages are locked for old study courses
+        self.browser.open(self.student_path + '/studycourse/manage')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/manage')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/start_session')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/start_session')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        IWorkflowState(self.student).setState('school fee paid')
+        self.browser.open(self.student_path + '/studycourse/add')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/add')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/200/manage')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/200/manage')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/200/validate_courses')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/200/validate_courses')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/200/reject_courses')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/200/reject_courses')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/200/add')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/200/add')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
+        self.browser.open(self.student_path + '/studycourse/200/edit')
+        self.assertFalse('The requested form is locked' in self.browser.contents)
+        self.browser.open(self.student_path + '/studycourse_1/200/edit')
+        self.assertTrue('The requested form is locked' in self.browser.contents)
+
 class StudentRequestPWTests(StudentsFullSetup):
     # Tests for student registration
@@ -1894,10 +2092,10 @@
 
     def test_request_pw(self):
-        # Student with wrong reg_number can't be found.
+        # Student with wrong number can't be found.
         self.browser.open('http://localhost/app/requestpw')
         self.browser.getControl(name="form.firstname").value = 'Anna'
-        self.browser.getControl(name="form.reg_number").value = 'anynumber'
+        self.browser.getControl(name="form.number").value = 'anynumber'
         self.browser.getControl(name="form.email").value = 'xx@yy.zz'
-        self.browser.getControl("Get login credentials").click()
+        self.browser.getControl("Send login credentials").click()
         self.assertTrue('No student record found.'
             in self.browser.contents)
@@ -1906,7 +2104,7 @@
         self.browser.open('http://localhost/app/requestpw')
         self.browser.getControl(name="form.firstname").value = 'Johnny'
-        self.browser.getControl(name="form.reg_number").value = '123'
+        self.browser.getControl(name="form.number").value = '123'
         self.browser.getControl(name="form.email").value = 'xx@yy.zz'
-        self.browser.getControl("Get login credentials").click()
+        self.browser.getControl("Send login credentials").click()
         self.assertTrue('No student record found.'
             in self.browser.contents)
@@ -1914,6 +2112,6 @@
         # password has been set and used.
         self.browser.getControl(name="form.firstname").value = 'Anna'
-        self.browser.getControl(name="form.reg_number").value = '123'
-        self.browser.getControl("Get login credentials").click()
+        self.browser.getControl(name="form.number").value = '123'
+        self.browser.getControl("Send login credentials").click()
         self.assertTrue('Your password has already been set and used.'
             in self.browser.contents)
@@ -1922,8 +2120,16 @@
         # The firstname field, used for verification, is not case-sensitive.
         self.browser.getControl(name="form.firstname").value = 'aNNa'
-        self.browser.getControl(name="form.reg_number").value = '123'
+        self.browser.getControl(name="form.number").value = '123'
         self.browser.getControl(name="form.email").value = 'new@yy.zz'
-        self.browser.getControl("Get login credentials").click()
+        self.browser.getControl("Send login credentials").click()
         # Yeah, we succeded ...
+        self.assertTrue('Your password request was successful.'
+            in self.browser.contents)
+        # We can also use the matric_number instead.
+        self.browser.open('http://localhost/app/requestpw')
+        self.browser.getControl(name="form.firstname").value = 'aNNa'
+        self.browser.getControl(name="form.number").value = '234'
+        self.browser.getControl(name="form.email").value = 'new@yy.zz'
+        self.browser.getControl("Send login credentials").click()
         self.assertTrue('Your password request was successful.'
             in self.browser.contents)
@@ -1934,3 +2140,8 @@
             email=('new@yy.zz', 'new@yy.zz')))
         self.assertEqual(self.student,results[0])
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'main.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('zope.anybody - students.browser.StudentRequestPasswordPage - '
+                        '234 (K1000000) - new@yy.zz' in logcontent)
         return
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_dynamicroles.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_dynamicroles.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_dynamicroles.py	(revision 9169)
@@ -25,4 +25,5 @@
     Test as APRMTest, Manageable)
 from waeup.kofa.testing import FunctionalLayer
+from waeup.kofa.app import University
 from waeup.kofa.students.tests.test_browser import StudentsFullSetup
 from waeup.kofa.students import Student, StudentPrincipalRoleManager
@@ -48,8 +49,9 @@
     def setUp(self):
         super(StudentPrincipalRoleManagerFunctionalTests, self).setUp()
-        self.officer_role = 'waeup.StudentsClearanceOfficer'
         # assign clearance permissions for a virtual officer
         prm = IPrincipalRoleManager(self.app['faculties']['fac1']['dep1'])
         prm.assignRoleToPrincipal('waeup.local.ClearanceOfficer', 'alice')
+        prm.assignRoleToPrincipal('waeup.local.PGClearanceOfficer', 'bob')
+        prm.assignRoleToPrincipal('waeup.local.UGClearanceOfficer', 'anne')
         return
 
@@ -74,5 +76,5 @@
         # student
         prm = IPrincipalRoleManager(self.student)
-        result = prm.getRolesForPrincipal('bob')
+        result = prm.getRolesForPrincipal('claus')
         self.assertEqual(result, [])
         return
@@ -83,4 +85,18 @@
         prm = IPrincipalRoleManager(self.student)
         result = prm.getRolesForPrincipal('alice')
-        self.assertEqual(result, [(self.officer_role, Allow)])
+        self.assertEqual(result, [('waeup.StudentsClearanceOfficer', Allow)])
+        # Student is a UG student
+        self.assertFalse(self.student.is_postgrad)
+        result = prm.getRolesForPrincipal('bob')
+        self.assertEqual(result, [('waeup.StudentsOfficer', Allow)])
+        result = prm.getRolesForPrincipal('anne')
+        self.assertEqual(result, [('waeup.StudentsClearanceOfficer', Allow)])
+        # Make student a PG student
+        self.certificate.study_mode = u'pg_ft'
+        self.assertTrue(self.student.is_postgrad)
+        result = prm.getRolesForPrincipal('bob')
+        # The dynamic roles changed
+        self.assertEqual(result, [('waeup.StudentsClearanceOfficer', Allow)])
+        result = prm.getRolesForPrincipal('anne')
+        self.assertEqual(result, [('waeup.StudentsOfficer', Allow)])
         return
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_export.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_export.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_export.py	(revision 9169)
@@ -44,10 +44,10 @@
             'adm_code,clearance_locked,clr_code,date_of_birth,email,employer,'
             'firstname,lastname,matric_number,middlename,nationality,'
-            'perm_address,phone,reg_number,sex,student_id,password,'
+            'perm_address,phone,reg_number,sex,student_id,suspended,password,'
             'state,history,certcode\r\n'
 
             'my adm code,0,my clr code,1981-02-04#,anna@sample.com,,'
-            'Anna,Tester,M123456,M.,NG,"Studentroad 21\nLagos 123456\n",'
-            '+234-123-12345,123456,f,A111111,,created'
+            'Anna,Tester,234,M.,NG,"Studentroad 21\nLagos 123456\n",'
+            '+234-123-12345#,123,f,A111111,0,,created'
             in result
             )
@@ -64,10 +64,10 @@
             'adm_code,clearance_locked,clr_code,date_of_birth,email,employer,'
             'firstname,lastname,matric_number,middlename,nationality,'
-            'perm_address,phone,reg_number,sex,student_id,password,'
+            'perm_address,phone,reg_number,sex,student_id,suspended,password,'
             'state,history,certcode\r\n'
 
             'my adm code,0,my clr code,1981-02-04#,anna@sample.com,,'
-            'Anna,Tester,M123456,M.,NG,"Studentroad 21\nLagos 123456\n",'
-            '+234-123-12345,123456,f,A111111,,created'
+            'Anna,Tester,234,M.,NG,"Studentroad 21\nLagos 123456\n",'
+            '+234-123-12345#,123,f,A111111,0,,created'
             in result
             )
@@ -83,10 +83,10 @@
             'adm_code,clearance_locked,clr_code,date_of_birth,email,employer,'
             'firstname,lastname,matric_number,middlename,nationality,'
-            'perm_address,phone,reg_number,sex,student_id,password,'
+            'perm_address,phone,reg_number,sex,student_id,suspended,password,'
             'state,history,certcode\r\n'
 
             'my adm code,0,my clr code,1981-02-04#,anna@sample.com,,'
-            'Anna,Tester,M123456,M.,NG,"Studentroad 21\nLagos 123456\n",'
-            '+234-123-12345,123456,f,A111111,,created'
+            'Anna,Tester,234,M.,NG,"Studentroad 21\nLagos 123456\n",'
+            '+234-123-12345#,123,f,A111111,0,,created'
             in result
             )
@@ -127,5 +127,5 @@
             'entry_mode,entry_session,previous_verdict,student_id\r\n'
 
-            ',,,NY,,,NY,\r\n'
+            ',,,0,,,0,\r\n'
             )
         return
@@ -144,5 +144,5 @@
             'entry_mode,entry_session,previous_verdict,student_id\r\n'
 
-            'CERT1,200,2012,NY,ug_ft,2010,NY,A111111\r\n'
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
             )
         return
@@ -160,5 +160,5 @@
             'entry_mode,entry_session,previous_verdict,student_id\r\n'
 
-            'CERT1,200,2012,NY,ug_ft,2010,NY,A111111\r\n'
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
             )
         return
@@ -175,5 +175,5 @@
             'entry_mode,entry_session,previous_verdict,student_id\r\n'
 
-            'CERT1,200,2012,NY,ug_ft,2010,NY,A111111\r\n'
+            'CERT1,200,2012,0,ug_ft,2010,0,A111111\r\n'
             )
         return
@@ -209,6 +209,7 @@
         self.assertEqual(
             result,
-            'level,level_session,level_verdict,student_id\r\n'
-            ',,NY,\r\n'
+            'level,level_session,level_verdict,validated_by,validation_date,'
+            'student_id\r\n'
+            ',,0,,,\r\n'
             )
         return
@@ -225,6 +226,7 @@
         self.assertEqual(
             result,
-            'level,level_session,level_verdict,student_id\r\n'
-            '100,2012,A,A111111\r\n'
+            'level,level_session,level_verdict,validated_by,validation_date,'
+            'student_id\r\n'
+            '100,2012,A,,,A111111\r\n'
             )
         return
@@ -239,6 +241,7 @@
         self.assertEqual(
             result,
-            'level,level_session,level_verdict,student_id\r\n'
-            '100,2012,A,A111111\r\n'
+            'level,level_session,level_verdict,validated_by,validation_date,'
+            'student_id\r\n'
+            '100,2012,A,,,A111111\r\n'
             )
         return
@@ -252,6 +255,7 @@
         self.assertEqual(
             result,
-            'level,level_session,level_verdict,student_id\r\n'
-            '100,2012,A,A111111\r\n'
+            'level,level_session,level_verdict,validated_by,validation_date,'
+            'student_id\r\n'
+            '100,2012,A,,,A111111\r\n'
             )
         return
@@ -371,9 +375,9 @@
         self.assertEqual(
             result,
-            'ac,amount_auth,creation_date,p_category,p_id,'
+            '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\r\n'
 
-            ',0.0,2012-04-01 13:12:01,schoolfee,,,,,unpaid,,0.0,,,\r\n'
+            ',0.0,2012-04-01 13:12:01,schoolfee,1,,,,,unpaid,,0.0,,,\r\n'
             )
         return
@@ -389,9 +393,9 @@
         self.assertEqual(
             result,
-            'ac,amount_auth,creation_date,p_category,p_id,'
+            '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\r\n'
 
-            '666,0.0,2012-04-01 13:12:01,schoolfee,my-id,'
+            '666,0.0,2012-04-01 13:12:01,schoolfee,1,my-id,'
             'p-item,100,2012,unpaid,2012-04-01 14:12:01,12.12,'
             'r-code,,A111111\r\n'
@@ -408,9 +412,9 @@
         self.assertEqual(
             result,
-            'ac,amount_auth,creation_date,p_category,p_id,'
+            '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\r\n'
 
-            '666,0.0,2012-04-01 13:12:01,schoolfee,my-id,'
+            '666,0.0,2012-04-01 13:12:01,schoolfee,1,my-id,'
             'p-item,100,2012,unpaid,2012-04-01 14:12:01,12.12,'
             'r-code,,A111111\r\n'
@@ -427,9 +431,9 @@
         self.assertEqual(
             result,
-            'ac,amount_auth,creation_date,p_category,p_id,'
+            '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\r\n'
 
-            '666,0.0,2012-04-01 13:12:01,schoolfee,my-id,'
+            '666,0.0,2012-04-01 13:12:01,schoolfee,1,my-id,'
             'p-item,100,2012,unpaid,2012-04-01 14:12:01,12.12,'
             'r-code,,A111111\r\n'
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_student.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_student.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/tests/test_student.py	(revision 9169)
@@ -21,9 +21,13 @@
 import re
 import unittest
+import grok
 from cStringIO import StringIO
 from datetime import tzinfo
-from zope.component import getUtility
+from zope.component import getUtility, queryUtility, createObject
+from zope.catalog.interfaces import ICatalog
 from zope.component.interfaces import IFactory
+from zope.event import notify
 from zope.interface import verify
+from zope.schema.interfaces import RequiredMissing
 from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
 from waeup.kofa.students.export import EXPORTER_NAMES
@@ -129,5 +133,5 @@
         studylevel = StudentStudyLevel()
         self.assertRaises(
-            TypeError, studylevel.addCourseTicket, department)
+            TypeError, studylevel.addCourseTicket, department, department)
 
     def test_booking_date(self):
@@ -236,4 +240,84 @@
         return
 
+class StudentTransferTests(StudentImportExportSetup):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(StudentTransferTests, self).setUp()
+
+        # Add additional certificate
+        self.certificate2 = createObject('waeup.Certificate')
+        self.certificate2.code = 'CERT2'
+        self.certificate2.application_category = 'basic'
+        self.certificate2.start_level = 200
+        self.certificate2.end_level = 500
+        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
+            self.certificate2)
+
+        # Add student with subobjects
+        student = Student()
+        self.app['students'].addStudent(student)
+        student = self.setup_student(student)
+        notify(grok.ObjectModifiedEvent(student))
+        self.student = self.app['students'][student.student_id]
+        return
+
+    def test_transfer_student(self):
+        self.assertRaises(
+            RequiredMissing, self.student.transfer, self.certificate2)
+        error = self.student.transfer(self.certificate2, current_session=1000)
+        self.assertTrue(error == -1)
+        error = self.student.transfer(self.certificate2, current_session=2013)
+        self.assertTrue(error == None)
+        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
+        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
+        self.assertEqual(self.student['studycourse_1'].current_session, 2012)
+        self.assertEqual(self.student['studycourse'].current_session, 2013)
+        self.assertEqual(self.student['studycourse'].entry_session,
+            self.student['studycourse_1'].entry_session)
+        self.assertEqual(self.student['studycourse_1'].__name__, 'studycourse_1')
+        logfile = os.path.join(
+            self.app['datacenter'].storage, 'logs', 'students.log')
+        logcontent = open(logfile).read()
+        self.assertTrue('system - K1000000 - transferred from CERT1 to CERT2'
+            in logcontent)
+        messages = ' '.join(self.student.history.messages)
+        self.assertMatches(
+            '...<YYYY-MM-DD hh:mm:ss> UTC - '
+            'Transferred from CERT1 to CERT2 by system', messages)
+
+        # The students_catalog has been updated.
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 0)
+        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+        results = cat.searchResults(current_session=(2013,2013))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+
+        # Students can be transferred (only) two times.
+        error = self.student.transfer(self.certificate,
+            current_session=2013)
+        self.assertTrue(error == None)
+        error = self.student.transfer(self.certificate2,
+            current_session=2013)
+        self.assertTrue(error == -3)
+        self.assertEqual([i for i in self.student.keys()],
+            [u'accommodation', u'payments', u'studycourse',
+             u'studycourse_1', u'studycourse_2'])
+
+        # The students_catalog has been updated again.
+        cat = queryUtility(ICatalog, name='students_catalog')
+        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
+        results = [x for x in results]
+        self.assertEqual(len(results), 1)
+        assert results[0] is self.app['students'][self.student.student_id]
+        return
 
 class StudentFactoryTest(FunctionalTestCase):
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/utils.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/utils.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/utils.py	(revision 9169)
@@ -19,27 +19,17 @@
 """
 import grok
-from random import SystemRandom as r
 from time import time
-from datetime import datetime
-from zope.i18n import translate
-from zope.component import getUtility, createObject
-from reportlab.pdfgen import canvas
 from reportlab.lib import colors
 from reportlab.lib.units import cm
-from reportlab.lib.enums import TA_RIGHT
 from reportlab.lib.pagesizes import A4
-from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
-from reportlab.platypus import (Frame, Paragraph, Image, PageBreak, Table,
-                                Spacer)
-from reportlab.platypus.tables import TableStyle
-from reportlab.platypus.flowables import PageBreak
-from zope.component import getUtility
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.platypus import Paragraph, Image, Table, Spacer
+from zope.component import getUtility, createObject
 from zope.formlib.form import setUpEditWidgets
-
+from zope.i18n import translate
 from waeup.kofa.interfaces import (
     IExtFileStore, IKofaUtils, RETURNING, PAID, CLEARED)
 from waeup.kofa.interfaces import MessageFactory as _
 from waeup.kofa.students.interfaces import IStudentsUtils
-from waeup.kofa.utils.helpers import now
 
 SLIP_STYLE = [
@@ -127,6 +117,13 @@
     #data.append([Spacer(1, 12)])
     portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
+
+    f_label = formatted_label(size=12) % _('Name')
+    f_label = Paragraph(f_label, style["Normal"])
+    f_text = formatted_text(studentview.context.display_fullname, size=12)
+    f_text = Paragraph(f_text, style["Normal"])
+    data_right.append([f_label,f_text])
+
     for widget in studentview.widgets:
-        if widget.name == 'form.adm_code':
+        if 'name' in widget.name:
             continue
         f_label = formatted_label(size=12) % translate(
@@ -137,4 +134,12 @@
         f_text = Paragraph(f_text, style["Normal"])
         data_right.append([f_label,f_text])
+
+    if hasattr(studentview.context, 'certcode'):
+        f_label = formatted_label(size=12) % _('Study Course')
+        f_label = Paragraph(f_label, style["Normal"])
+        f_text = formatted_text(studentview.context.certcode, size=12)
+        f_text = Paragraph(f_text, style["Normal"])
+        data_right.append([f_label,f_text])
+
     table_left = Table(data_left,style=SLIP_STYLE)
     table_right = Table(data_right,style=SLIP_STYLE, colWidths=[5*cm, 6*cm])
@@ -165,4 +170,47 @@
     return table
 
+def get_signature_table(signatures, lang='en'):
+    """Return a reportlab table containing signature fields (with date).
+    """
+    style = getSampleStyleSheet()
+    space_width = 0.4  # width in cm of space between signatures
+    table_width = 16.0 # supposed width of signature table in cms
+    # width of signature cells in cm...
+    sig_col_width = table_width - ((len(signatures) - 1) * space_width)
+    sig_col_width = sig_col_width / len(signatures)
+    data = []
+    col_widths = [] # widths of columns
+
+    sig_style = [
+        ('VALIGN',(0,-1),(-1,-1),'TOP'),
+        ('FONT', (0,0), (-1,-1), 'Helvetica-BoldOblique', 12),
+        ('BOTTOMPADDING', (0,0), (-1,0), 36),
+        ('TOPPADDING', (0,-1), (-1,-1), 0),
+        ]
+    for num, elem in enumerate(signatures):
+        # draw a line above each signature cell (not: empty cells in between)
+        sig_style.append(
+            ('LINEABOVE', (num*2,-1), (num*2, -1), 1, colors.black))
+
+    row = []
+    for signature in signatures:
+        row.append(trans(_('Date:'), lang))
+        row.append('')
+        if len(signatures) > 1:
+            col_widths.extend([sig_col_width*cm, space_width*cm])
+        else:
+            col_widths.extend([sig_col_width/2*cm, sig_col_width/2*cm])
+            row.append('') # empty spaceholder on right
+    data.append(row[:-1])
+    data.extend(([''],)*3) # insert 3 empty rows...
+    row = []
+    for signature in signatures:
+        row.append(Paragraph(trans(signature, lang), style["Normal"]))
+        row.append('')
+    data.append(row[:-1])
+    table = Table(data, style=sig_style, repeatRows=len(data),
+                  colWidths=col_widths)
+    return table
+
 def docs_as_flowables(view, lang='en'):
     """Create reportlab flowables out of scanned docs.
@@ -190,7 +238,7 @@
             if img_path is None:
                 pass
-            elif not img_path.endswith('.jpg'):
+            elif not img_path[-4:] in ('.jpg', '.JPG'):
                 # reportlab requires jpg images, I think.
-                f_text = Paragraph('%s (Not displayable)' % (
+                f_text = Paragraph('%s (not displayable)' % (
                     viewlet.title,), ENTRY1_STYLE)
             else:
@@ -202,23 +250,4 @@
     return data
 
-def insert_footer(pdf,width,style,text=None, number_of_pages=1):
-      """Render the whole footer frame.
-      """
-      story = []
-      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
-      tz = getUtility(IKofaUtils).tzinfo
-      timestamp = now(tz).strftime("%d/%m/%Y %H:%M:%S %Z")
-      left_text = '<font size=10>%s</font>' % timestamp
-      story.append(Paragraph(left_text, style["Normal"]))
-      frame_footer.addFromList(story,pdf)
-      story = []
-      frame_footer = Frame(1*cm,0,width-(2*cm),1*cm)
-      portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
-      right_text = translate(_('<font size=10>${a} Page ${b} of ${c}</font>',
-          mapping = {'a':text, 'b':pdf.getPageNumber(), 'c':number_of_pages}),
-          'waeup.kofa', target_language=portal_language)
-      story.append(Paragraph(right_text, style["Right"]))
-      frame_footer.addFromList(story,pdf)
-
 class StudentsUtils(grok.GlobalUtility):
     """A collection of methods subject to customization.
@@ -227,5 +256,5 @@
 
     def getReturningData(self, student):
-        """ This method defines what happens after school fee payment
+        """ Define what happens after school fee payment
         depending on the student's senate verdict.
 
@@ -238,11 +267,9 @@
 
     def setReturningData(self, student):
-        """ This method defines what happens after school fee payment
-        depending on the student's senate verdict. It folllows
-        the same algorithm as getReturningData but it also sets the new
-        values
-
-        In the base configuration current level is always increased
-        by 100 no matter which verdict has been assigned.
+        """ Define what happens after school fee payment
+        depending on the student's senate verdict.
+
+        This method folllows the same algorithm as getReturningData but
+        it also sets the new values.
         """
         new_session, new_level = self.getReturningData(student)
@@ -250,19 +277,24 @@
         student['studycourse'].current_session = new_session
         verdict = student['studycourse'].current_verdict
-        student['studycourse'].current_verdict = 'NY'
+        student['studycourse'].current_verdict = '0'
         student['studycourse'].previous_verdict = verdict
         return
 
-    def setPaymentDetails(self, category, student):
+    def setPaymentDetails(self, category, student,
+            previous_session, previous_level):
         """Create Payment object and set the payment data of a student for
         the payment category specified.
 
         """
-        details = {}
         p_item = u''
         amount = 0.0
-        error = u''
-        p_session = student['studycourse'].current_session
-        p_level = student['studycourse'].current_level
+        if previous_session:
+            p_session = previous_session
+            p_level = previous_level
+            p_current = False
+        else:
+            p_session = student['studycourse'].current_session
+            p_level = student['studycourse'].current_level
+            p_current = True
         session = str(p_session)
         try:
@@ -276,17 +308,23 @@
             except (AttributeError, TypeError):
                 return _('Study course data are incomplete.'), None
-            if student.state == CLEARED:
-                amount = getattr(certificate, 'school_fee_1', 0.0)
-            elif student.state == RETURNING:
-                # In case of returning school fee payment the payment session
-                # and level contain the values of the session the student
-                # has paid for.
-                p_session, p_level = self.getReturningData(student)
-                amount = getattr(certificate, 'school_fee_2', 0.0)
-            elif student.is_postgrad and student.state == PAID:
-                # Returning postgraduate students also pay for the next session
-                # but their level always remains the same.
-                p_session += 1
-                amount = getattr(certificate, 'school_fee_2', 0.0)
+            if previous_session:
+                if previous_level == 100:
+                    amount = getattr(certificate, 'school_fee_1', 0.0)
+                else:
+                    amount = getattr(certificate, 'school_fee_2', 0.0)
+            else:
+                if student.state == CLEARED:
+                    amount = getattr(certificate, 'school_fee_1', 0.0)
+                elif student.state == RETURNING:
+                    # In case of returning school fee payment the payment session
+                    # and level contain the values of the session the student
+                    # has paid for.
+                    p_session, p_level = self.getReturningData(student)
+                    amount = getattr(certificate, 'school_fee_2', 0.0)
+                elif student.is_postgrad and student.state == PAID:
+                    # Returning postgraduate students also pay for the next session
+                    # but their level always remains the same.
+                    p_session += 1
+                    amount = getattr(certificate, 'school_fee_2', 0.0)
         elif category == 'clearance':
             p_item = student['studycourse'].certificate.code
@@ -296,5 +334,6 @@
             amount = academic_session.booking_fee
         if amount in (0.0, None):
-            return _(u'Amount could not be determined.'), None
+            return _('Amount could not be determined.' +
+                     ' Would you like to pay for a previous session?'), None
         for key in student['payments'].keys():
             ticket = student['payments'][key]
@@ -303,7 +342,8 @@
                ticket.p_item == p_item and \
                ticket.p_session == p_session:
-                  return _('This type of payment has already been made.'), None
+                  return _('This type of payment has already been made.' +
+                           ' Would you like to pay for a previous session?'), None
         payment = createObject(u'waeup.StudentOnlinePayment')
-        timestamp = "%d" % int(time()*1000)
+        timestamp = ("%d" % int(time()*10000))[1:]
         payment.p_id = "p%s" % timestamp
         payment.p_category = category
@@ -311,4 +351,5 @@
         payment.p_session = p_session
         payment.p_level = p_level
+        payment.p_current = p_current
         payment.amount_auth = amount
         return None, payment
@@ -333,5 +374,7 @@
             return
         end_level = certificate.end_level
-        if entry_session == grok.getSite()['hostels'].accommodation_session:
+        if current_level == 10:
+            bt = 'pr'
+        elif entry_session == grok.getSite()['hostels'].accommodation_session:
             bt = 'fr'
         elif current_level >= end_level:
@@ -357,5 +400,5 @@
     def renderPDF(self, view, filename='slip.pdf', student=None,
                   studentview=None, tableheader=None, tabledata=None,
-                  note=None):
+                  note=None, signatures=None):
         """Render pdf slips for various pages.
         """
@@ -373,4 +416,8 @@
             footer_text = "%s - %s - " % (student.student_id, footer_text)
 
+        # Insert history
+        if not filename.startswith('payment'):
+            data.extend(creator.fromStringList(student.history.messages))
+
         # Insert student data table
         portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
@@ -399,4 +446,10 @@
             contenttable = render_table_data(tableheader,tabledata)
             data.append(contenttable)
+
+        # Insert signatures
+        if signatures:
+            data.append(Spacer(1, 20))
+            signaturetable = get_signature_table(signatures)
+            data.append(signaturetable)
 
         view.response.setHeader(
@@ -412,5 +465,5 @@
 
     VERDICTS_DICT = {
-        'NY': _('(not yet)'),
+        '0': _('(not yet)'),
         'A': 'Successful student',
         'B': 'Student with carryover courses',
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/viewlets.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/viewlets.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/viewlets.py	(revision 9169)
@@ -1,3 +1,3 @@
-## $Id$
+3## $Id$
 ##
 ## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
@@ -166,4 +166,53 @@
     target = 'manage_base'
 
+class StudentTransfernButton(ManageActionButton):
+    grok.order(5)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Transfer student')
+    target = 'transfer'
+    icon = 'actionicon_redo.png'
+
+class StudentDeactivateActionButton(ManageActionButton):
+    grok.order(6)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Deactivate account')
+    target = 'deactivate'
+    icon = 'actionicon_traffic_lights_red.png'
+
+    @property
+    def target_url(self):
+        if self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
+
+class StudentActivateActionButton(ManageActionButton):
+    grok.order(6)
+    grok.context(IStudent)
+    grok.view(StudentBaseDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Activate account')
+    target = 'activate'
+    icon = 'actionicon_traffic_lights_green.png'
+
+    @property
+    def target_url(self):
+        if not self.context.suspended:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
+    @property
+    def onclick(self):
+        return "return window.confirm(%s);" % _(
+            "'A history message will be added. Are you sure?'")
+
 class StudentClearanceManageActionButton(ManageActionButton):
     grok.order(1)
@@ -231,9 +280,17 @@
     target = 'view_personal'
 
+class StudentPersonalManageActionButton(ManageActionButton):
+    grok.order(1)
+    grok.context(IStudent)
+    grok.view(StudentPersonalDisplayFormPage)
+    grok.require('waeup.manageStudent')
+    text = _('Manage')
+    target = 'manage_personal'
+
 class StudentPersonalEditActionButton(ManageActionButton):
-    grok.order(1)
+    grok.order(2)
     grok.context(IStudent)
     grok.view(StudentPersonalDisplayFormPage)
-    grok.require('waeup.viewStudent')
+    grok.require('waeup.handleStudent')
     text = _('Edit')
     target = 'edit_personal'
@@ -247,4 +304,10 @@
     target = 'manage'
 
+    @property
+    def target_url(self):
+        if self.context.is_current:
+            return self.view.url(self.view.context, self.target)
+        return False
+
 class StudyLevelManageActionButton(ManageActionButton):
     grok.order(1)
@@ -254,4 +317,11 @@
     text = _('Manage')
     target = 'manage'
+
+    @property
+    def target_url(self):
+        is_current = self.context.__parent__.is_current
+        if not is_current:
+            return ''
+        return self.view.url(self.view.context, self.target)
 
 class StudentValidateCoursesActionButton(ManageActionButton):
@@ -266,6 +336,8 @@
     @property
     def target_url(self):
+        is_current = self.context.__parent__.is_current
         if self.context.student.state != REGISTERED or \
-            str(self.context.__parent__.current_level) != self.context.__name__:
+            str(self.context.__parent__.current_level) != self.context.__name__ or\
+            not is_current:
             return ''
         return self.view.url(self.view.context, self.target)
@@ -282,6 +354,8 @@
     @property
     def target_url(self):
+        is_current = self.context.__parent__.is_current
         if self.context.student.state not in (VALIDATED, REGISTERED) or \
-            str(self.context.__parent__.current_level) != self.context.__name__:
+            str(self.context.__parent__.current_level) != self.context.__name__ or\
+            not is_current:
             return ''
         return self.view.url(self.view.context, self.target)
@@ -296,4 +370,11 @@
     target = 'course_registration.pdf'
 
+    @property
+    def target_url(self):
+        is_current = self.context.__parent__.is_current
+        if not is_current:
+            return ''
+        return self.view.url(self.view.context, self.target)
+
 class CourseTicketManageActionButton(ManageActionButton):
     grok.order(1)
@@ -313,5 +394,5 @@
 
 class PaymentReceiptActionButton(ManageActionButton):
-    grok.order(1)
+    grok.order(9) # This button should always be the last one.
     grok.context(IStudentOnlinePayment)
     grok.view(OnlinePaymentDisplayFormPage)
@@ -327,7 +408,6 @@
         return self.view.url(self.view.context, self.target)
 
-
 class ApprovePaymentActionButton(ManageActionButton):
-    grok.order(2)
+    grok.order(8)
     grok.context(IStudentOnlinePayment)
     grok.view(OnlinePaymentDisplayFormPage)
@@ -437,10 +517,10 @@
     grok.require('waeup.handleStudent')
     icon = 'actionicon_start.gif'
-    text = _('Start session')
+    text = _('Start new session')
     target = 'start_session'
 
     @property
     def target_url(self):
-        if self.context.next_session_allowed:
+        if self.context.next_session_allowed and self.context.is_current:
             return self.view.url(self.view.context, self.target)
         return False
@@ -460,5 +540,6 @@
         condition2 = str(student['studycourse'].current_level) in \
             self.view.context.keys()
-        if condition1 or condition2:
+        condition3 = not self.context.is_current
+        if condition1 or condition2 or condition3:
             return ''
         return self.view.url(self.view.context, self.target)
@@ -469,5 +550,5 @@
     grok.view(StudyLevelDisplayFormPage)
     grok.require('waeup.handleStudent')
-    text = _('Add and remove courses')
+    text = _('Edit course list')
     target = 'edit'
 
@@ -478,5 +559,6 @@
         condition2 = student[
             'studycourse'].current_level != self.view.context.level
-        if condition1 or condition2:
+        is_current = self.context.__parent__.is_current
+        if condition1 or condition2 or not is_current:
             return ''
         return self.view.url(self.view.context, self.target)
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/vocabularies.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/vocabularies.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/vocabularies.py	(revision 9169)
@@ -36,8 +36,9 @@
 nats_vocab = SimpleKofaVocabulary(*COUNTRIES)
 
-def study_levels(studycourse):
-    if studycourse.certificate is not None:
-        start_level = int(studycourse.certificate.start_level)
-        end_level = int(studycourse.certificate.end_level)
+def study_levels(context):
+    certificate = getattr(context, 'certificate', None)
+    if  certificate is not None:
+        start_level = int(certificate.start_level)
+        end_level = int(certificate.end_level)
         if start_level == 999 or end_level == 999:
             levels = [999]
@@ -50,5 +51,7 @@
     else:
         # default level range
-        levels = [level for level in range(100,1000,10) if level % 100 < 30]
+        levels = [10, ]
+        levels += [level for level in range(100,1000,10) if level % 100 < 30]
+        levels.append(999)
     return levels
 
@@ -68,10 +71,11 @@
 
     def getTitle(self, context, value):
-        if context.certificate is not None:
-            start_level = int(context.certificate.start_level)
-            end_level = int(context.certificate.end_level)
+        certificate = getattr(context, 'certificate', None)
+        if certificate is not None:
+            start_level = int(certificate.start_level)
+            end_level = int(certificate.end_level)
         else:
             # default level range
-            start_level = 100
+            start_level = 10
             end_level = 1000
         if start_level == 999 or end_level == 999:
@@ -79,4 +83,5 @@
                 return _('Error: wrong level id ${value}',
                     mapping={'value': value})
+        if value == 999:
             return course_levels.by_value[999].title
         if value < start_level or value > end_level + 120:
Index: main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/workflow.py
===================================================================
--- main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/workflow.py	(revision 9168)
+++ main/waeup.kofa/branches/uli-async-update/src/waeup/kofa/students/workflow.py	(revision 9169)
@@ -2,4 +2,5 @@
 """
 import grok
+from datetime import datetime
 from zope.component import getUtility
 from hurry.workflow.workflow import Transition, WorkflowState, NullCondition
@@ -12,8 +13,11 @@
 from waeup.kofa.workflow import KofaWorkflow, KofaWorkflowInfo
 from waeup.kofa.students.interfaces import IStudent, IStudentsUtils
+from waeup.kofa.utils.helpers import get_current_principal
 
 
 IMPORTABLE_STATES = (ADMITTED, CLEARANCE, REQUESTED, CLEARED, PAID, RETURNING,
     REGISTERED, VALIDATED)
+
+FORBIDDEN_POSTGRAD_STATES = (RETURNING, REGISTERED, VALIDATED)
 
 REGISTRATION_TRANSITIONS = (
@@ -23,5 +27,5 @@
         source = None,
         condition = NullCondition,
-        msg = _('Student record created'),
+        msg = _('Record created'),
         destination = CREATED),
 
@@ -29,5 +33,5 @@
         transition_id = 'admit',
         title = _('Admit student'),
-        msg = _('Student admitted'),
+        msg = _('Admitted'),
         source = CREATED,
         destination = ADMITTED),
@@ -63,6 +67,6 @@
     Transition(
         transition_id = 'reset3',
-        title = _('Reset to clearance'),
-        msg = _("Reset to 'clearance'"),
+        title = _('Reset to clearance started'),
+        msg = _("Reset to 'clearance started'"),
         source = REQUESTED,
         destination = CLEARANCE),
@@ -77,6 +81,6 @@
     Transition(
         transition_id = 'reset4',
-        title = _('Reset to clearance'),
-        msg = _("Reset to 'clearance'"),
+        title = _('Reset to clearance started'),
+        msg = _("Reset to 'clearance started'"),
         source = CLEARED,
         destination = CLEARANCE),
@@ -85,5 +89,5 @@
         transition_id = 'pay_first_school_fee',
         title = _('Pay school fee'),
-        msg = _('School fee paid'),
+        msg = _('First school fee payment made'),
         source = CLEARED,
         destination = PAID),
@@ -92,5 +96,5 @@
         transition_id = 'approve_first_school_fee',
         title = _('Approve payment'),
-        msg = _('School fee payment approved'),
+        msg = _('First school fee payment approved'),
         source = CLEARED,
         destination = PAID),
@@ -106,5 +110,5 @@
         transition_id = 'pay_school_fee',
         title = _('Pay school fee'),
-        msg = _('Payment made'),
+        msg = _('School fee payment made'),
         source = RETURNING,
         destination = PAID),
@@ -112,6 +116,6 @@
     Transition(
         transition_id = 'pay_pg_fee',
-        title = _('Pay postgraduate school fee'),
-        msg = _('Payment made'),
+        title = _('Pay PG school fee'),
+        msg = _('PG school fee payment made'),
         source = PAID,
         destination = PAID),
@@ -119,5 +123,5 @@
     Transition(
         transition_id = 'approve_school_fee',
-        title = _('Approve payment'),
+        title = _('Approve school fee payment'),
         msg = _('School fee payment approved'),
         source = RETURNING,
@@ -126,6 +130,6 @@
     Transition(
         transition_id = 'approve_pg_fee',
-        title = _('Approve postgraduate payment'),
-        msg = _('School fee payment approved'),
+        title = _('Approve PG school fee payment'),
+        msg = _('PG school fee payment approved'),
         source = PAID,
         destination = PAID),
@@ -147,6 +151,6 @@
     Transition(
         transition_id = 'reset7',
-        title = _('Reset to paid'),
-        msg = _("Reset to 'paid'"),
+        title = _('Reset to school fee paid'),
+        msg = _("Reset to 'school fee paid'"),
         source = REGISTERED,
         destination = PAID),
@@ -161,6 +165,6 @@
     Transition(
         transition_id = 'reset8',
-        title = _('Reset to paid'),
-        msg = _("Reset to 'paid'"),
+        title = _('Reset to school fee paid'),
+        msg = _("Reset to 'school fee paid'"),
         source = VALIDATED,
         destination = PAID),
@@ -175,6 +179,6 @@
     Transition(
         transition_id = 'reset9',
-        title = _('Reset to validated'),
-        msg = _("Reset to 'validated'"),
+        title = _('Reset to courses validated'),
+        msg = _("Reset to 'courses validated'"),
         source = RETURNING,
         destination = VALIDATED),
@@ -183,4 +187,5 @@
 IMPORTABLE_TRANSITIONS = [i.transition_id for i in REGISTRATION_TRANSITIONS]
 
+FORBIDDEN_POSTGRAD_TRANS = ['reset6', 'register_courses']
 LOCK_CLEARANCE_TRANS = ('reset2', 'request_clearance')
 UNLOCK_CLEARANCE_TRANS = ('reset3', 'reset4', 'start_clearance')
@@ -212,5 +217,5 @@
 
     Lock and unlock clearance form.
-    Triger actions after school fee payment.
+    Trigger actions after school fee payment.
     """
 
@@ -231,4 +236,23 @@
         new_session = obj['studycourse'].current_session + 1
         obj['studycourse'].current_session = new_session
+    elif event.transition.transition_id == 'validate_courses':
+        current_level = obj['studycourse'].current_level
+        level_object = obj['studycourse'].get(str(current_level), None)
+        if level_object is not None:
+            user = get_current_principal()
+            if user is None:
+                usertitle = 'system'
+            else:
+                usertitle = getattr(user, 'public_name', None)
+                if not usertitle:
+                    usertitle = user.title
+            level_object.validated_by = usertitle
+            level_object.validation_date = datetime.utcnow()
+    elif event.transition.transition_id == 'reset8':
+        current_level = obj['studycourse'].current_level
+        level_object = obj['studycourse'].get(str(current_level), None)
+        if level_object is not None:
+            level_object.validated_by = None
+            level_object.validation_date = None
     # In some tests we don't have a students container
     try:
