source: main/waeup.aaue/trunk/src/waeup/aaue/students/utils.py @ 14954

Last change on this file since 14954 was 14954, checked in by Henrik Bettermann, 7 years ago

Add clearance_fee_dp payment.

Change schoolfee calculation and adjust to the modifications made for 2017/2018.

  • Property svn:keywords set to Id
File size: 24.8 KB
RevLine 
[7419]1## $Id: utils.py 14954 2018-02-14 07:16:54Z 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##
[7151]18import grok
[8600]19from time import time
[14918]20from zope.component import createObject, queryUtility
21from zope.catalog.interfaces import ICatalog
[10922]22from waeup.kofa.interfaces import (
[13594]23    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
24    academic_sessions_vocab)
[8823]25from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
[8247]26from waeup.kofa.accesscodes import create_accesscode
[10922]27from waeup.kofa.students.utils import trans
[13720]28from waeup.aaue.interswitch.browser import gateway_net_amt, GATEWAY_AMT
[8444]29from waeup.aaue.interfaces import MessageFactory as _
[6902]30
[14663]31MINIMUM_UNITS_THRESHOLD = 15
32
[8823]33class CustomStudentsUtils(NigeriaStudentsUtils):
[7151]34    """A collection of customized methods.
35
36    """
37
[14242]38    PORTRAIT_CHANGE_STATES = (ADMITTED,)
[13348]39
[14918]40    def GPABoundaries(self, faccode=None, depcode=None,
41                            certcode=None, student=None):
42        if student and student.current_mode.startswith('dp'):
43            return ((1.5, 'Fail'),
44                    (2.4, 'Pass'),
45                    (3.5, 'Merit'),
46                    (4.5, 'Credit'),
47                    (5, 'Distinction'))
48        elif student:
49            return ((1, 'FRNS / NER / NYV'),
50                    (1.5, 'Pass'),
51                    (2.4, '3rd Class Honours'),
52                    (3.5, '2nd Class Honours Lower Division'),
53                    (4.5, '2nd Class Honours Upper Division'),
54                    (5, '1st Class Honours'))
55        # Session Results Presentations depend on certificate
56        results = None
57        if certcode:
58            cat = queryUtility(ICatalog, name='certificates_catalog')
59            results = list(
60                cat.searchResults(code=(certcode, certcode)))
61        if results and results[0].study_mode.startswith('dp'):
62            return ((1.5, 'Fail'),
63                    (2.4, 'Pass'),
64                    (3.5, 'Merit'),
65                    (4.5, 'Credit'),
66                    (5, 'Distinction'))
67        else:
68            return ((1, 'FRNS / NER / NYV'),
69                    (1.5, 'Pass'),
70                    (2.4, '3rd Class Honours'),
71                    (3.5, '2nd Class Honours Lower Division'),
72                    (4.5, '2nd Class Honours Upper Division'),
73                    (5, '1st Class Honours'))
[14899]74
[14462]75    def getClassFromCGPA(self, gpa, student):
[14918]76        gpa_boundaries = self.GPABoundaries(student=student)
[14899]77        if gpa < gpa_boundaries[0][0]:
78            # FRNS / Fail
79            return 0, gpa_boundaries[0][1]
80        if student.entry_session < 2013 and \
81            not student.current_mode.startswith('dp'):
82            if gpa < gpa_boundaries[1][0]:
[14462]83                # Pass
[14899]84                return 1, gpa_boundaries[1][1]
[14462]85        else:
[14899]86            if gpa < gpa_boundaries[1][0]:
[14462]87                # FRNS (Pass degree has been phased out in 2013)
[14899]88                return 0, gpa_boundaries[0][1]
89        if gpa < gpa_boundaries[2][0]:
90            # 3rd / Pass
91            return 2, gpa_boundaries[2][1]
92        if gpa < gpa_boundaries[3][0]:
93            # 2nd L / Merit
94            return 3, gpa_boundaries[3][1]
95        if gpa < gpa_boundaries[4][0]:
96            # 2nd U / Credit
97            return 4, gpa_boundaries[4][1]
98        if gpa <= gpa_boundaries[5][0]:
99            # 1st / Distinction
100            return 5, gpa_boundaries[5][1]
[14462]101        return 'N/A'
102
[14160]103    def getDegreeClassNumber(self, level_obj):
[14415]104        """Get degree class number (used for SessionResultsPresentation
105        reports).
106        """
[14459]107        certificate = getattr(level_obj.__parent__,'certificate', None)
[14160]108        end_level = getattr(certificate, 'end_level', None)
[14459]109        if end_level and level_obj.level >= end_level:
[14464]110            if level_obj.level > end_level:
111                # spill-over level
[14476]112                if level_obj.gpa_params[1] == 0:
[14464]113                    # no credits taken
114                    return 0
[14663]115            elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
[14537]116                # credits taken below limit
117                return 0
[14160]118            failed_courses = level_obj.passed_params[4]
[14411]119            not_taken_courses = level_obj.passed_params[5]
[14160]120            if '_m' in failed_courses:
121                return 0
[14441]122            if len(not_taken_courses) \
[14506]123                and not not_taken_courses == 'Nil':
[14411]124                return 0
[14663]125        elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
[14464]126            # credits taken below limit
127            return 0
[14487]128        if level_obj.level_verdict in ('FRNS', 'NER', 'NYV'):
[14464]129            return 0
[14160]130        # use gpa_boundaries above
[14462]131        return self.getClassFromCGPA(
132            level_obj.cumulative_params[0], level_obj.student)[0]
[14160]133
[13359]134    def increaseMatricInteger(self, student):
135        """Increase counter for matric numbers.
136        This counter can be a centrally stored attribute or an attribute of
137        faculties, departments or certificates. In the base package the counter
138        is as an attribute of the site configuration container.
139        """
140        if student.current_mode in ('ug_pt', 'de_pt'):
141            grok.getSite()['configuration'].next_matric_integer += 1
142            return
[13609]143        elif student.is_postgrad:
144            grok.getSite()['configuration'].next_matric_integer_3 += 1
145            return
[13793]146        elif student.current_mode in ('dp_ft',):
147            grok.getSite()['configuration'].next_matric_integer_4 += 1
148            return
[13359]149        grok.getSite()['configuration'].next_matric_integer_2 += 1
150        return
151
[13749]152    def _concessionalPaymentMade(self, student):
153        if len(student['payments']):
154            for ticket in student['payments'].values():
155                if ticket.p_state == 'paid' and \
156                    ticket.p_category == 'concessional':
157                    return True
158        return False
159
[11596]160    def constructMatricNumber(self, student):
[11593]161        faccode = student.faccode
162        depcode = student.depcode
[13609]163        certcode = student.certcode
[13664]164        degree = getattr(
165            getattr(student.get('studycourse', None), 'certificate', None),
166                'degree', None)
[11593]167        year = unicode(student.entry_session)[2:]
[13359]168        if not student.state in (PAID, ) or not student.is_fresh or \
[14615]169            student.current_mode in ('found', 'ijmbe'):
[13359]170            return _('Matriculation number cannot be set.'), None
[13755]171        #if student.current_mode not in ('mug_ft', 'mde_ft') and \
172        #    not self._concessionalPaymentMade(student):
173        #    return _('Matriculation number cannot be set.'), None
[13571]174        if student.is_postgrad:
[13609]175            next_integer = grok.getSite()['configuration'].next_matric_integer_3
[13664]176            if not degree or next_integer == 0:
[13609]177                return _('Matriculation number cannot be set.'), None
[13846]178            if student.faccode in ('IOE'):
179                return None, "AAU/SPS/%s/%s/%s/%05d" % (
180                    faccode, year, degree, next_integer)
[13609]181            return None, "AAU/SPS/%s/%s/%s/%s/%05d" % (
[13664]182                faccode, depcode, year, degree, next_integer)
[13359]183        if student.current_mode in ('ug_pt', 'de_pt'):
184            next_integer = grok.getSite()['configuration'].next_matric_integer
185            if next_integer == 0:
186                return _('Matriculation number cannot be set.'), None
[12975]187            return None, "PTP/%s/%s/%s/%05d" % (
188                faccode, depcode, year, next_integer)
[13793]189        if student.current_mode in ('dp_ft',):
190            next_integer = grok.getSite()['configuration'].next_matric_integer_4
191            if next_integer == 0:
192                return _('Matriculation number cannot be set.'), None
193            return None, "IOE/DIP/%s/%05d" % (year, next_integer)
[13359]194        next_integer = grok.getSite()['configuration'].next_matric_integer_2
195        if next_integer == 0:
196            return _('Matriculation number cannot be set.'), None
197        if student.faccode in ('FBM', 'FCS'):
198            return None, "CMS/%s/%s/%s/%05d" % (
199                faccode, depcode, year, next_integer)
200        return None, "%s/%s/%s/%05d" % (faccode, depcode, year, next_integer)
[12975]201
[8270]202    def getReturningData(self, student):
203        """ This method defines what happens after school fee payment
[8319]204        of returning students depending on the student's senate verdict.
[8270]205        """
[8319]206        prev_level = student['studycourse'].current_level
207        cur_verdict = student['studycourse'].current_verdict
[14089]208        if cur_verdict in ('A','B','C', 'L','M','N','Z',):
[8319]209            # Successful student
210            new_level = divmod(int(prev_level),100)[0]*100 + 100
[14089]211        #elif cur_verdict == 'C':
212        #    # Student on probation
213        #    new_level = int(prev_level) + 10
[8319]214        else:
215            # Student is somehow in an undefined state.
216            # Level has to be set manually.
217            new_level = prev_level
[8270]218        new_session = student['studycourse'].current_session + 1
219        return new_session, new_level
220
[13454]221    def _isPaymentDisabled(self, p_session, category, student):
222        academic_session = self._getSessionConfiguration(p_session)
[14246]223        if category.startswith('schoolfee'):
224            if 'sf_all' in academic_session.payment_disabled:
225                return True
226            if 'sf_pg' in academic_session.payment_disabled and \
227                student.is_postgrad:
228                return True
[14571]229            if 'sf_ug_pt' in academic_session.payment_disabled and \
230                student.current_mode in ('ug_pt', 'de_pt'):
[14246]231                return True
232            if 'sf_found' in academic_session.payment_disabled and \
233                student.current_mode == 'found':
234                return True
[13794]235        if category.startswith('clearance') and \
236            'cl_regular' in academic_session.payment_disabled and \
237            student.current_mode in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
238            return True
[13454]239        if category == 'hostel_maintenance' and \
240            'maint_all' in academic_session.payment_disabled:
241            return True
242        return False
243
[9154]244    def setPaymentDetails(self, category, student,
245            previous_session=None, previous_level=None):
[8600]246        """Create Payment object and set the payment data of a student for
247        the payment category specified.
248
249        """
[8306]250        details = {}
[8600]251        p_item = u''
252        amount = 0.0
253        error = u''
[9154]254        if previous_session:
[14544]255            if previous_session < student['studycourse'].entry_session:
256                return _('The previous session must not fall below '
257                         'your entry session.'), None
258            if category == 'schoolfee':
259                # School fee is always paid for the following session
260                if previous_session > student['studycourse'].current_session:
261                    return _('This is not a previous session.'), None
262            else:
263                if previous_session > student['studycourse'].current_session - 1:
264                    return _('This is not a previous session.'), None
265            p_session = previous_session
266            p_level = previous_level
267            p_current = False
268        else:
269            p_session = student['studycourse'].current_session
270            p_level = student['studycourse'].current_level
271            p_current = True
[9527]272        academic_session = self._getSessionConfiguration(p_session)
273        if academic_session == None:
[8600]274            return _(u'Session configuration object is not available.'), None
[8677]275        # Determine fee.
[7151]276        if category == 'transfer':
[8600]277            amount = academic_session.transfer_fee
[14296]278        elif category == 'transcript_local':
279            amount = academic_session.transcript_fee_local
280        elif category == 'transcript_inter':
281            amount = academic_session.transcript_fee_inter
[7151]282        elif category == 'bed_allocation':
[8600]283            amount = academic_session.booking_fee
[14378]284        elif category == 'restitution':
[14660]285            if student.entry_session >= 2016 \
286                or student.current_mode not in ('ug_ft', 'dp_ft'):
[14378]287                return _(u'Restitution fee payment not required.'), None
288            amount = academic_session.restitution_fee
[7151]289        elif category == 'hostel_maintenance':
[13418]290            amount = 0.0
291            bedticket = student['accommodation'].get(
292                str(student.current_session), None)
[13502]293            if bedticket is not None and bedticket.bed is not None:
[13474]294                p_item = bedticket.display_coordinates
[13418]295                if bedticket.bed.__parent__.maint_fee > 0:
296                    amount = bedticket.bed.__parent__.maint_fee
297                else:
298                    # fallback
299                    amount = academic_session.maint_fee
300            else:
[13506]301                return _(u'No bed allocated.'), None
[13636]302        elif student.current_mode == 'found' and category not in (
303            'schoolfee', 'clearance', 'late_registration'):
304            return _('Not allowed.'), None
[13400]305        elif category.startswith('clearance'):
[13594]306            if student.state not in (ADMITTED, CLEARANCE, REQUESTED, CLEARED):
307                return _(u'Acceptance Fee payments not allowed.'), None
[13855]308            if student.current_mode in (
309                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
310                'transfer', 'mug_ft', 'mde_ft') \
311                and category != 'clearance_incl':
312                    return _("Additional fees must be included."), None
[14518]313            if student.current_mode == 'ijmbe':
314                amount = academic_session.clearance_fee_ijmbe
[14954]315            elif student.current_mode == 'dp_ft':
316                amount = academic_session.clearance_fee_dp
[14518]317            elif student.faccode == 'FP':
[11653]318                amount = academic_session.clearance_fee_fp
[13377]319            elif student.current_mode.endswith('_pt'):
[13678]320                if student.is_postgrad:
321                    amount = academic_session.clearance_fee_pg_pt
322                else:
323                    amount = academic_session.clearance_fee_ug_pt
[13466]324            elif student.faccode == 'FCS':
325                # Students in clinical medical sciences pay the medical
326                # acceptance fee
[13377]327                amount = academic_session.clearance_fee_med
[13678]328            elif student.is_postgrad:  # and not part-time
[13853]329                if category != 'clearance':
[13854]330                    return _("No additional fees required."), None
[13526]331                amount = academic_session.clearance_fee_pg
[11653]332            else:
333                amount = academic_session.clearance_fee
[8753]334            p_item = student['studycourse'].certificate.code
[13689]335            if amount in (0.0, None):
336                return _(u'Amount could not be determined.'), None
[14239]337            # Add Matric Gown Fee and Lapel Fee
[13689]338            if category == 'clearance_incl':
[13414]339                amount += gateway_net_amt(academic_session.matric_gown_fee) + \
340                    gateway_net_amt(academic_session.lapel_fee)
[11004]341        elif category == 'late_registration':
[14117]342            if student.is_postgrad:
343                amount = academic_session.late_pg_registration_fee
344            else:
345                amount = academic_session.late_registration_fee
[13400]346        elif category.startswith('schoolfee'):
[8600]347            try:
[8753]348                certificate = student['studycourse'].certificate
349                p_item = certificate.code
[8600]350            except (AttributeError, TypeError):
351                return _('Study course data are incomplete.'), None
[13853]352            if student.is_postgrad and category != 'schoolfee':
[13854]353                return _("No additional fees required."), None
[14544]354            if not previous_session and student.current_mode in (
[13855]355                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
356                'transfer', 'mug_ft', 'mde_ft') \
357                and not category in (
358                'schoolfee_incl', 'schoolfee_1', 'schoolfee_2'):
[14244]359                    return _("You must choose a payment which includes "
[13855]360                             "additional fees."), None
[13780]361            if category in ('schoolfee_1', 'schoolfee_2'):
362                if student.current_mode == 'ug_pt':
363                    return _("Part-time students are not allowed "
364                             "to pay by instalments."), None
[14241]365                if student.entry_session < 2015:
366                    return _("You are not allowed "
367                             "to pay by instalments."), None
[14544]368            if previous_session:
369                # Students can pay for previous sessions in all
370                # workflow states.  Fresh students are excluded by the
371                # update method of the PreviousPaymentAddFormPage.
372                if previous_level == 100:
373                    amount = getattr(certificate, 'school_fee_1', 0.0)
374                else:
[14954]375                    if student.entry_session in (2015, 2016):
[14544]376                        amount = getattr(certificate, 'school_fee_2', 0.0)
377                    else:
378                        amount = getattr(certificate, 'school_fee_3', 0.0)
379            elif student.state == CLEARED and category != 'schoolfee_2':
[14229]380                amount = getattr(certificate, 'school_fee_1', 0.0)
[13512]381                # Cut school fee by 50%
[14241]382                if category == 'schoolfee_1' and amount:
383                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
384            elif student.is_fresh and category == 'schoolfee_2':
385                amount = getattr(certificate, 'school_fee_1', 0.0)
386                # Cut school fee by 50%
387                if amount:
388                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
[14396]389            elif student.state == RETURNING and category != 'schoolfee_2':
[13482]390                if not student.father_name:
391                    return _("Personal data form is not properly filled."), None
[13374]392                # In case of returning school fee payment the payment session
393                # and level contain the values of the session the student
394                # has paid for.
395                p_session, p_level = self.getReturningData(student)
[8961]396                try:
397                    academic_session = grok.getSite()[
398                        'configuration'][str(p_session)]
399                except KeyError:
400                    return _(u'Session configuration object is not available.'), None
[14954]401                if student.entry_session in (2015, 2016):
[14229]402                    amount = getattr(certificate, 'school_fee_2', 0.0)
[13374]403                else:
[14229]404                    amount = getattr(certificate, 'school_fee_3', 0.0)
[14954]405                # Cut school fee by 50%
406                if category == 'schoolfee_1' and amount:
407                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
[14241]408            elif category == 'schoolfee_2':
409                amount = getattr(certificate, 'school_fee_2', 0.0)
410                # Cut school fee by 50%
411                if amount:
412                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
[8600]413            else:
[8753]414                return _('Wrong state.'), None
[13417]415            if amount in (0.0, None):
416                return _(u'Amount could not be determined.'), None
[14244]417            # Add Student Union Fee , Student Id Card Fee and Welfare Assurance
[13512]418            if category in ('schoolfee_incl', 'schoolfee_1'):
[13414]419                amount += gateway_net_amt(academic_session.welfare_fee) + \
420                    gateway_net_amt(academic_session.union_fee)
[14244]421                if student.entry_session == 2016 \
422                    and student.entry_mode == 'ug_ft' \
423                    and student.state == CLEARED:
424                    amount += gateway_net_amt(academic_session.id_card_fee)
[13534]425            # Add non-indigenous fee and session specific penalty fees
426            if student.is_postgrad:
427                amount += academic_session.penalty_pg
[14246]428                if student.lga and not student.lga.startswith('edo'):
[13534]429                    amount += 20000.0
430            else:
431                amount += academic_session.penalty_ug
[14248]432        elif not student.is_postgrad:
433            fee_name = category + '_fee'
434            amount = getattr(academic_session, fee_name, 0.0)
[8600]435        if amount in (0.0, None):
436            return _(u'Amount could not be determined.'), None
[8677]437        # Create ticket.
[8600]438        for key in student['payments'].keys():
439            ticket = student['payments'][key]
440            if ticket.p_state == 'paid' and\
441               ticket.p_category == category and \
442               ticket.p_item == p_item and \
443               ticket.p_session == p_session:
444                  return _('This type of payment has already been made.'), None
[13786]445            # Additional condition in AAUE
446            if category in ('schoolfee', 'schoolfee_incl', 'schoolfee_1'):
447                if ticket.p_state == 'paid' and \
448                   ticket.p_category in ('schoolfee',
449                                         'schoolfee_incl',
450                                         'schoolfee_1') and \
451                   ticket.p_item == p_item and \
452                   ticket.p_session == p_session:
453                      return _(
454                          'Another school fee payment for this '
455                          'session has already been made.'), None
456
[11455]457        if self._isPaymentDisabled(p_session, category, student):
[13798]458            return _('This category of payments has been disabled.'), None
[8712]459        payment = createObject(u'waeup.StudentOnlinePayment')
[8954]460        timestamp = ("%d" % int(time()*10000))[1:]
[8600]461        payment.p_id = "p%s" % timestamp
462        payment.p_category = category
463        payment.p_item = p_item
464        payment.p_session = p_session
465        payment.p_level = p_level
[9154]466        payment.p_current = p_current
[8600]467        payment.amount_auth = amount
468        return None, payment
[7621]469
[10922]470    def _admissionText(self, student, portal_language):
471        inst_name = grok.getSite()['configuration'].name
472        entry_session = student['studycourse'].entry_session
473        entry_session = academic_sessions_vocab.getTerm(entry_session).title
474        text = trans(_(
[10953]475            'This is to inform you that you have been offered provisional'
476            ' admission into ${a} for the ${b} academic session as follows:',
[10922]477            mapping = {'a': inst_name, 'b': entry_session}),
478            portal_language)
479        return text
480
[14585]481    def warnCreditsOOR(self, studylevel, course=None):
[14733]482        studycourse = studylevel.__parent__
483        certificate = getattr(studycourse,'certificate', None)
484        current_level = studycourse.current_level
485        if None in (current_level, certificate):
486            return
487        end_level = certificate.end_level
488        if current_level >= end_level:
489            limit = 52
490        else:
491            limit = 48
492        if course and studylevel.total_credits + course.credits > limit:
[14585]493            return  _('Maximum credits exceeded.')
[14733]494        elif studylevel.total_credits > limit:
[14585]495            return _('Maximum credits exceeded.')
496        return
[10051]497
[13353]498    def getBedCoordinates(self, bedticket):
499        """Return descriptive bed coordinates.
500        This method can be used to customize the `display_coordinates`
501        property method in order to  display a
502        customary description of the bed space.
503        """
504        bc = bedticket.bed_coordinates.split(',')
505        if len(bc) == 4:
506            return bc[0]
507        return bedticket.bed_coordinates
508
[13415]509    def getAccommodationDetails(self, student):
510        """Determine the accommodation data of a student.
511        """
512        d = {}
513        d['error'] = u''
514        hostels = grok.getSite()['hostels']
515        d['booking_session'] = hostels.accommodation_session
516        d['allowed_states'] = hostels.accommodation_states
517        d['startdate'] = hostels.startdate
518        d['enddate'] = hostels.enddate
519        d['expired'] = hostels.expired
520        # Determine bed type
[13416]521        bt = 'all'
[13415]522        if student.sex == 'f':
523            sex = 'female'
524        else:
525            sex = 'male'
526        special_handling = 'regular'
527        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
528        return d
529
[13753]530    def checkAccommodationRequirements(self, student, acc_details):
[14238]531        msg = super(CustomStudentsUtils, self).checkAccommodationRequirements(
[13753]532            student, acc_details)
[14238]533        if msg:
534            return msg
[13753]535        if student.current_mode not in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
536            return _('You are not eligible to book accommodation.')
537        return
538
[8444]539    # AAUE prefix
[14593]540    STUDENT_ID_PREFIX = u'E'
541
542    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
543            'studentstudylevels', 'coursetickets',
544            'studentpayments', 'studentunpaidpayments',
545            'bedtickets', 'paymentsoverview',
546            'studylevelsoverview', 'combocard', 'bursary',
547            'levelreportdata')
Note: See TracBrowser for help on using the repository browser.