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

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

Adjust exporter name.

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