source: main/waeup.uniben/trunk/src/waeup/uniben/students/utils.py @ 16107

Last change on this file since 16107 was 16107, checked in by Henrik Bettermann, 4 years ago

Add missing import.

  • Property svn:keywords set to Id
File size: 27.9 KB
Line 
1## $Id: utils.py 16107 2020-06-04 07:07:44Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18import grok
19from time import time
20from zope.component import createObject, getUtility
21from reportlab.lib.styles import getSampleStyleSheet
22from waeup.kofa.interfaces import (IKofaUtils,
23    CLEARED, RETURNING, PAID, REGISTERED, VALIDATED)
24from waeup.kofa.utils.helpers import to_timezone
25from waeup.kofa.students.utils import trans, render_student_data
26from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
27from waeup.uniben.interfaces import MessageFactory as _
28
29class CustomStudentsUtils(NigeriaStudentsUtils):
30    """A collection of customized methods.
31
32    """
33
34    def getReturningData(self, student):
35        """ This method defines what happens after school fee payment
36        of returning students depending on the student's senate verdict.
37        """
38        prev_level = student['studycourse'].current_level
39        cur_verdict = student['studycourse'].current_verdict
40        if cur_verdict == 'N' and prev_level == 100:
41            new_level = prev_level
42        elif cur_verdict in ('A','B','L','M','N','Z',):
43            # Successful student
44            new_level = divmod(int(prev_level),100)[0]*100 + 100
45        elif cur_verdict == 'C':
46            # Student on probation
47            new_level = int(prev_level) + 10
48        else:
49            # Student is somehow in an undefined state.
50            # Level has to be set manually.
51            new_level = prev_level
52        new_session = student['studycourse'].current_session + 1
53        return new_session, new_level
54
55
56    def checkAccommodationRequirements(self, student, acc_details):
57        if acc_details.get('expired', False):
58            startdate = acc_details.get('startdate')
59            enddate = acc_details.get('enddate')
60            if startdate and enddate:
61                tz = getUtility(IKofaUtils).tzinfo
62                startdate = to_timezone(
63                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
64                enddate = to_timezone(
65                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
66                return _("Outside booking period: ${a} - ${b}",
67                         mapping = {'a': startdate, 'b': enddate})
68            else:
69                return _("Outside booking period.")
70        if not student.is_postgrad and student.current_mode != 'ug_ft':
71            return _("Only undergraduate full-time students are eligible to book accommodation.")
72        bt = acc_details.get('bt')
73        if not bt:
74            return _("Your data are incomplete.")
75        if not student.state in acc_details['allowed_states']:
76            return _("You are in the wrong registration state.")
77        if student['studycourse'].current_session != acc_details[
78            'booking_session']:
79            return _('Your current session does not '
80                     'match accommodation session.')
81        stage = bt.split('_')[2]
82        if not student.is_postgrad and stage != 'fr' and not student[
83            'studycourse'].previous_verdict in (
84                'A', 'B', 'F', 'J', 'L', 'M', 'C', 'Z'):
85            return _("Your are not eligible to book accommodation.")
86        bsession = str(acc_details['booking_session'])
87        if bsession in student['accommodation'].keys() \
88            and not 'booking expired' in \
89            student['accommodation'][bsession].bed_coordinates:
90            return _('You already booked a bed space in '
91                     'current accommodation session.')
92        return
93
94    def getAccommodationDetails(self, student):
95        """Determine the accommodation data of a student.
96        """
97        d = {}
98        d['error'] = u''
99        hostels = grok.getSite()['hostels']
100        d['booking_session'] = hostels.accommodation_session
101        d['allowed_states'] = hostels.accommodation_states
102        d['startdate'] = hostels.startdate
103        d['enddate'] = hostels.enddate
104        d['expired'] = hostels.expired
105        # Determine bed type
106        studycourse = student['studycourse']
107        certificate = getattr(studycourse,'certificate',None)
108        entry_session = studycourse.entry_session
109        current_level = studycourse.current_level
110        if None in (entry_session, current_level, certificate):
111            return d
112        if student.sex == 'f':
113            sex = 'female'
114        else:
115            sex = 'male'
116        if student.is_postgrad:
117            bt = 'all'
118            special_handling = 'pg'
119        else:
120            end_level = certificate.end_level
121            if current_level == 10:
122                bt = 'pr'
123            elif entry_session == grok.getSite()['hostels'].accommodation_session:
124                bt = 'fr'
125            elif current_level >= end_level:
126                bt = 'fi'
127            else:
128                bt = 're'
129            special_handling = 'regular'
130            desired_hostel = student['accommodation'].desired_hostel
131            if student.faccode in ('MED', 'DEN') and (
132                not desired_hostel or desired_hostel.startswith('clinical')):
133                special_handling = 'clinical'
134            elif student.certcode in ('BARTMAS', 'BARTTHR', 'BARTFAA',
135                                      'BAEDFAA', 'BSCEDECHED', 'BAFAA'):
136                special_handling = 'ekenwan'
137        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
138        return d
139
140    def _paymentMade(self, student, session):
141        if len(student['payments']):
142            for ticket in student['payments'].values():
143                if ticket.p_state == 'paid' and \
144                    ticket.p_category == 'schoolfee' and \
145                    ticket.p_session == session:
146                    return True
147        return False
148
149    def _isPaymentDisabled(self, p_session, category, student):
150        academic_session = self._getSessionConfiguration(p_session)
151        if category == 'schoolfee':
152            if 'sf_all' in academic_session.payment_disabled:
153                return True
154            if student.current_mode == 'found' and \
155                'sf_found' in academic_session.payment_disabled:
156                return True
157            if student.is_postgrad:
158                if 'sf_pg' in academic_session.payment_disabled:
159                    return True
160                return False
161            if student.current_mode.endswith('ft') and \
162                'sf_ft' in academic_session.payment_disabled:
163                return True
164            if student.current_mode.endswith('pt') and \
165                'sf_pt' in academic_session.payment_disabled:
166                return True
167            if student.current_mode.startswith('dp') and \
168                'sf_dp' in academic_session.payment_disabled:
169                return True
170            if student.current_mode.endswith('sw') and \
171                'sf_sw' in academic_session.payment_disabled:
172                return True
173        if category == 'hostel_maintenance' and \
174            'maint_all' in academic_session.payment_disabled:
175            return True
176        if category == 'clearance':
177            if 'cl_all' in academic_session.payment_disabled:
178                return True
179            if student.is_jupeb and \
180                'cl_jupeb' in academic_session.payment_disabled:
181                return True
182        return False
183
184    #def _hostelApplicationPaymentMade(self, student, session):
185    #    if len(student['payments']):
186    #        for ticket in student['payments'].values():
187    #            if ticket.p_state == 'paid' and \
188    #                ticket.p_category == 'hostel_application' and \
189    #                ticket.p_session == session:
190    #                return True
191    #    return False
192
193    def _pharmdInstallments(self, student):
194        installments = 0.0
195        if len(student['payments']):
196            for ticket in student['payments'].values():
197                if ticket.p_state == 'paid' and \
198                    ticket.p_category.startswith('pharmd') and \
199                    ticket.p_session == student.current_session:
200                    installments += ticket.amount_auth
201        return installments
202
203    def samePaymentMade(self, student, category, p_item, p_session):
204        if category in ('bed_allocation', 'transcript'):
205            return False
206        for key in student['payments'].keys():
207            ticket = student['payments'][key]
208            if ticket.p_state == 'paid' and\
209               ticket.p_category == category and \
210               ticket.p_item == p_item and \
211               ticket.p_session == p_session:
212                  return True
213        return False
214
215    def setPaymentDetails(self, category, student,
216            previous_session, previous_level, combi):
217        """Create Payment object and set the payment data of a student for
218        the payment category specified.
219
220        """
221        p_item = u''
222        amount = 0.0
223        if previous_session:
224            if previous_session < student['studycourse'].entry_session:
225                return _('The previous session must not fall below '
226                         'your entry session.'), None
227            if category == 'schoolfee':
228                # School fee is always paid for the following session
229                if previous_session > student['studycourse'].current_session:
230                    return _('This is not a previous session.'), None
231            else:
232                if previous_session > student['studycourse'].current_session - 1:
233                    return _('This is not a previous session.'), None
234            p_session = previous_session
235            p_level = previous_level
236            p_current = False
237        else:
238            p_session = student['studycourse'].current_session
239            p_level = student['studycourse'].current_level
240            p_current = True
241        academic_session = self._getSessionConfiguration(p_session)
242        if academic_session == None:
243            return _(u'Session configuration object is not available.'), None
244        # Determine fee.
245        if category == 'transfer':
246            amount = academic_session.transfer_fee
247        elif category == 'transcript':
248            amount = academic_session.transcript_fee
249        elif category == 'gown':
250            amount = academic_session.gown_fee
251        elif category == 'jupeb':
252            amount = academic_session.jupeb_fee
253        elif category == 'clinexam':
254            amount = academic_session.clinexam_fee
255        elif category.startswith('pharmd') \
256            and student.current_mode == 'special_ft':
257            amount = 80000.0
258        #elif category == 'develop' and student.is_postgrad:
259        #    amount = academic_session.development_fee
260        elif category == 'bed_allocation':
261            p_item = self.getAccommodationDetails(student)['bt']
262            desired_hostel = student['accommodation'].desired_hostel
263            if not desired_hostel:
264                return _(u'Select your favoured hostel first.'), None
265            if desired_hostel and desired_hostel != 'no':
266                p_item = u'%s (%s)' % (p_item, desired_hostel)
267            amount = academic_session.booking_fee
268            if student.is_postgrad:
269                amount += 500
270        elif category == 'hostel_maintenance':
271            amount = 0.0
272            bedticket = student['accommodation'].get(
273                str(student.current_session), None)
274            if bedticket is not None and bedticket.bed is not None:
275                p_item = bedticket.bed_coordinates
276                if bedticket.bed.__parent__.maint_fee > 0:
277                    amount = bedticket.bed.__parent__.maint_fee
278                else:
279                    # fallback
280                    amount = academic_session.maint_fee
281            else:
282                return _(u'No bed allocated.'), None
283        #elif category == 'hostel_application':
284        #    amount = 1000.0
285        #elif category.startswith('tempmaint'):
286        #    if not self._hostelApplicationPaymentMade(
287        #        student, student.current_session):
288        #        return _(
289        #            'You have not yet paid the hostel application fee.'), None
290        #    if category == 'tempmaint_1':
291        #        amount = 8150.0
292        #    elif category == 'tempmaint_2':
293        #        amount = 12650.0
294        #    elif category == 'tempmaint_3':
295        #        amount = 9650.0
296        elif category == 'clearance':
297            p_item = student.certcode
298            if p_item is None:
299                return _('Study course data are incomplete.'), None
300            if student.is_jupeb:
301                amount = 50000.0
302            elif student.faccode.startswith('FCETA'):
303                # ASABA and AKOKA
304                amount = 35000.0
305            elif student.faccode in ('BMS', 'MED', 'DEN'):
306            #elif p_item in ('BSCANA', 'BSCMBC', 'BMLS', 'BSCNUR', 'BSCPHS', 'BDS',
307            #    'MBBSMED', 'MBBSNDU', 'BSCPTY', 'BSCPST'):
308                amount = 80000.0
309            elif student.faccode == 'DCOEM':
310                return _('Acceptance fee payment not necessary.'), None
311            else:
312                amount = 60000.0
313        elif category == 'schoolfee':
314            try:
315                certificate = student['studycourse'].certificate
316                p_item = certificate.code
317            except (AttributeError, TypeError):
318                return _('Study course data are incomplete.'), None
319            if previous_session:
320                # Students can pay for previous sessions in all workflow states.
321                # Fresh students are excluded by the update method of the
322                # PreviousPaymentAddFormPage.
323                if previous_session == student['studycourse'].entry_session:
324                    if student.is_foreigner:
325                        amount = getattr(certificate, 'school_fee_3', 0.0)
326                    else:
327                        amount = getattr(certificate, 'school_fee_1', 0.0)
328                else:
329                    if student.is_foreigner:
330                        amount = getattr(certificate, 'school_fee_4', 0.0)
331                    else:
332                        amount = getattr(certificate, 'school_fee_2', 0.0)
333                        # Old returning students might get a discount.
334                        if student.entry_session < 2017 \
335                            and certificate.custom_float_1:
336                            amount -= certificate.custom_float_1
337            else:
338                if student.state == CLEARED:
339                    if student.is_foreigner:
340                        amount = getattr(certificate, 'school_fee_3', 0.0)
341                    else:
342                        amount = getattr(certificate, 'school_fee_1', 0.0)
343                elif student.state == PAID and student.is_postgrad:
344                    p_session += 1
345                    academic_session = self._getSessionConfiguration(p_session)
346                    if academic_session == None:
347                        return _(u'Session configuration object is not available.'), None
348
349                    # Students are only allowed to pay for the next session
350                    # if current session payment
351                    # has really been made, i.e. payment object exists.
352                    #if not self._paymentMade(
353                    #    student, student.current_session):
354                    #    return _('You have not yet paid your current/active' +
355                    #             ' session. Please use the previous session' +
356                    #             ' payment form first.'), None
357
358                    if student.is_foreigner:
359                        amount = getattr(certificate, 'school_fee_4', 0.0)
360                    else:
361                        amount = getattr(certificate, 'school_fee_2', 0.0)
362                elif student.state == RETURNING:
363                    # In case of returning school fee payment the payment session
364                    # and level contain the values of the session the student
365                    # has paid for.
366                    p_session, p_level = self.getReturningData(student)
367                    academic_session = self._getSessionConfiguration(p_session)
368                    if academic_session == None:
369                        return _(u'Session configuration object is not available.'), None
370
371                    # Students are only allowed to pay for the next session
372                    # if current session payment has really been made,
373                    # i.e. payment object exists and is paid.
374                    #if not self._paymentMade(
375                    #    student, student.current_session):
376                    #    return _('You have not yet paid your current/active' +
377                    #             ' session. Please use the previous session' +
378                    #             ' payment form first.'), None
379
380                    if student.is_foreigner:
381                        amount = getattr(certificate, 'school_fee_4', 0.0)
382                    else:
383                        amount = getattr(certificate, 'school_fee_2', 0.0)
384                        # Old returning students might get a discount.
385                        if student.entry_session < 2017 \
386                            and certificate.custom_float_1:
387                            amount -= certificate.custom_float_1
388                # PHARMD school fee amount is fixed and previously paid
389                # installments in current session are deducted.
390                if student.current_mode == 'special_ft' \
391                    and student.state in (RETURNING, CLEARED):
392                    if student.is_foreigner:
393                        amount = 260000.0 - self._pharmdInstallments(student)
394                    else:
395                        amount = 160000.0 - self._pharmdInstallments(student)
396            # Give 50% school fee discount to staff members.
397            if student.is_staff:
398                amount /= 2
399        if amount in (0.0, None):
400            return _('Amount could not be determined.'), None
401        # Add session specific penalty fee.
402        if category == 'schoolfee' and student.is_postgrad:
403            amount += academic_session.penalty_pg
404            amount += academic_session.development_fee
405        elif category == 'schoolfee' and student.current_mode == ('ug_ft'):
406            amount += academic_session.penalty_ug_ft
407        elif category == 'schoolfee' and student.current_mode == ('ug_pt'):
408            amount += academic_session.penalty_ug_pt
409        elif category == 'schoolfee' and student.current_mode == ('ug_sw'):
410            amount += academic_session.penalty_sw
411        elif category == 'schoolfee' and student.current_mode in (
412            'dp_ft', 'dp_pt'):
413            amount += academic_session.penalty_dp
414        if category.startswith('tempmaint'):
415            p_item = getUtility(IKofaUtils).PAYMENT_CATEGORIES[category]
416            p_item = unicode(p_item)
417            # Now we change the category because tempmaint payments
418            # will be obsolete when Uniben returns to Kofa bed allocation.
419            category = 'hostel_maintenance'
420        # Create ticket.
421        if self.samePaymentMade(student, category, p_item, p_session):
422            return _('This type of payment has already been made.'), None
423        if self._isPaymentDisabled(p_session, category, student):
424            return _('This category of payments has been disabled.'), None
425        payment = createObject(u'waeup.StudentOnlinePayment')
426        timestamp = ("%d" % int(time()*10000))[1:]
427        payment.p_id = "p%s" % timestamp
428        payment.p_category = category
429        payment.p_item = p_item
430        payment.p_session = p_session
431        payment.p_level = p_level
432        payment.p_current = p_current
433        payment.amount_auth = amount
434        return None, payment
435
436    def warnCreditsOOR(self, studylevel, course=None):
437        studycourse = studylevel.__parent__
438        certificate = getattr(studycourse,'certificate', None)
439        current_level = studycourse.current_level
440        if None in (current_level, certificate):
441            return
442        end_level = certificate.end_level
443        if studylevel.student.faccode in (
444            'MED', 'DEN', 'BMS') and studylevel.level == 200:
445            limit = 61
446        elif current_level >= end_level:
447            limit = 51
448        else:
449            limit = 50
450        if course and studylevel.total_credits + course.credits > limit:
451            return _('Maximum credits exceeded.')
452        elif studylevel.total_credits > limit:
453            return _('Maximum credits exceeded.')
454        return
455
456    def clearance_disabled_message(self, student):
457        if student.is_postgrad:
458            return None
459        try:
460            session_config = grok.getSite()[
461                'configuration'][str(student.current_session)]
462        except KeyError:
463            return _('Session configuration object is not available.')
464        if not session_config.clearance_enabled:
465            return _('Clearance is disabled for this session.')
466        return None
467
468    def renderPDFTranscript(self, view, filename='transcript.pdf',
469                  student=None,
470                  studentview=None,
471                  note=None,
472                  signatures=(),
473                  sigs_in_footer=(),
474                  digital_sigs=(),
475                  show_scans=True, topMargin=1.5,
476                  omit_fields=(),
477                  tableheader=None,
478                  no_passport=False,
479                  save_file=False):
480        """Render pdf slip of a transcripts.
481        """
482        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
483        # XXX: tell what the different parameters mean
484        style = getSampleStyleSheet()
485        creator = self.getPDFCreator(student)
486        data = []
487        doc_title = view.label
488        author = '%s (%s)' % (view.request.principal.title,
489                              view.request.principal.id)
490        footer_text = view.label.split('\n')
491        if len(footer_text) > 2:
492            # We can add a department in first line
493            footer_text = footer_text[1]
494        else:
495            # Only the first line is used for the footer
496            footer_text = footer_text[0]
497        if getattr(student, 'student_id', None) is not None:
498            footer_text = "%s - %s - " % (student.student_id, footer_text)
499
500        # Insert student data table
501        if student is not None:
502            #bd_translation = trans(_('Base Data'), portal_language)
503            #data.append(Paragraph(bd_translation, HEADING_STYLE))
504            data.append(render_student_data(
505                studentview, view.context,
506                omit_fields, lang=portal_language,
507                slipname=filename,
508                no_passport=no_passport))
509
510        transcript_data = view.context.getTranscriptData()
511        levels_data = transcript_data[0]
512
513        contextdata = []
514        f_label = trans(_('Course of Study:'), portal_language)
515        f_label = Paragraph(f_label, ENTRY1_STYLE)
516        f_text = formatted_text(view.context.certificate.longtitle)
517        f_text = Paragraph(f_text, ENTRY1_STYLE)
518        contextdata.append([f_label,f_text])
519
520        f_label = trans(_('Faculty:'), portal_language)
521        f_label = Paragraph(f_label, ENTRY1_STYLE)
522        f_text = formatted_text(
523            view.context.certificate.__parent__.__parent__.__parent__.longtitle)
524        f_text = Paragraph(f_text, ENTRY1_STYLE)
525        contextdata.append([f_label,f_text])
526
527        f_label = trans(_('Department:'), portal_language)
528        f_label = Paragraph(f_label, ENTRY1_STYLE)
529        f_text = formatted_text(
530            view.context.certificate.__parent__.__parent__.longtitle)
531        f_text = Paragraph(f_text, ENTRY1_STYLE)
532        contextdata.append([f_label,f_text])
533
534        f_label = trans(_('Entry Session:'), portal_language)
535        f_label = Paragraph(f_label, ENTRY1_STYLE)
536        f_text = formatted_text(
537            view.session_dict.get(view.context.entry_session))
538        f_text = Paragraph(f_text, ENTRY1_STYLE)
539        contextdata.append([f_label,f_text])
540
541        f_label = trans(_('Final Session:'), portal_language)
542        f_label = Paragraph(f_label, ENTRY1_STYLE)
543        f_text = formatted_text(
544            view.session_dict.get(view.context.current_session))
545        f_text = Paragraph(f_text, ENTRY1_STYLE)
546        contextdata.append([f_label,f_text])
547
548        f_label = trans(_('Entry Mode:'), portal_language)
549        f_label = Paragraph(f_label, ENTRY1_STYLE)
550        f_text = formatted_text(view.studymode_dict.get(
551            view.context.entry_mode))
552        f_text = Paragraph(f_text, ENTRY1_STYLE)
553        contextdata.append([f_label,f_text])
554
555        f_label = trans(_('Final Verdict:'), portal_language)
556        f_label = Paragraph(f_label, ENTRY1_STYLE)
557        f_text = formatted_text(view.studymode_dict.get(
558            view.context.current_verdict))
559        f_text = Paragraph(f_text, ENTRY1_STYLE)
560        contextdata.append([f_label,f_text])
561
562        f_label = trans(_('Cumulative GPA:'), portal_language)
563        f_label = Paragraph(f_label, ENTRY1_STYLE)
564        format_float = getUtility(IKofaUtils).format_float
565        cgpa = format_float(transcript_data[1], 3)
566        if student.state == GRADUATED:
567            f_text = formatted_text('%s (%s)' % (
568                cgpa, self.getClassFromCGPA(transcript_data[1], student)[1]))
569        else:
570            f_text = formatted_text('%s' % cgpa)
571        f_text = Paragraph(f_text, ENTRY1_STYLE)
572        contextdata.append([f_label,f_text])
573
574        contexttable = Table(contextdata,style=SLIP_STYLE)
575        data.append(contexttable)
576
577        transcripttables = render_transcript_data(
578            view, tableheader, levels_data, lang=portal_language)
579        data.extend(transcripttables)
580
581        # Insert signatures
582        # XXX: We are using only sigs_in_footer in waeup.kofa, so we
583        # do not have a test for the following lines.
584        if signatures and not sigs_in_footer:
585            data.append(Spacer(1, 20))
586            # Render one signature table per signature to
587            # get date and signature in line.
588            for signature in signatures:
589                signaturetables = get_signature_tables(signature)
590                data.append(signaturetables[0])
591
592        # Insert digital signatures
593        if digital_sigs:
594            data.append(Spacer(1, 20))
595            sigs = digital_sigs.split('\n')
596            for sig in sigs:
597                data.append(Paragraph(sig, NOTE_STYLE))
598
599        view.response.setHeader(
600            'Content-Type', 'application/pdf')
601        try:
602            pdf_stream = creator.create_pdf(
603                data, None, doc_title, author=author, footer=footer_text,
604                note=note, sigs_in_footer=sigs_in_footer, topMargin=topMargin)
605        except IOError:
606            view.flash(_('Error in image file.'))
607            return view.redirect(view.url(view.context))
608        if save_file:
609            self._saveTranscriptPDF(student, pdf_stream)
610            return
611        return pdf_stream
612
613    #: A tuple containing the names of registration states in which changing of
614    #: passport pictures is allowed.
615    PORTRAIT_CHANGE_STATES = ()
616
617    # Uniben prefix
618    STUDENT_ID_PREFIX = u'B'
619
620    STUDENT_EXPORTER_NAMES = (
621            'students',
622            'studentstudycourses',
623            'studentstudylevels',
624            'coursetickets',
625            'studentpayments',
626            'bedtickets',
627            'trimmed',
628            'outstandingcourses',
629            'unpaidpayments',
630            'sfpaymentsoverview',
631            'sessionpaymentsoverview',
632            'studylevelsoverview',
633            'combocard',
634            'bursary',
635            'accommodationpayments',
636            'transcriptdata',
637            'trimmedpayments',
638            )
Note: See TracBrowser for help on using the repository browser.