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

Last change on this file since 16100 was 16100, checked in by Henrik Bettermann, 5 years ago

Add 'Request Transcript' tab.

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