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

Last change on this file since 16660 was 16652, checked in by Henrik Bettermann, 3 years ago

Adjust getDegreeClassNumber (untested)

  • Property svn:keywords set to Id
File size: 27.3 KB
Line 
1## $Id: utils.py 16652 2021-09-29 10:03:16Z 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, queryUtility
21from zope.catalog.interfaces import ICatalog
22from waeup.kofa.interfaces import (
23    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
24    academic_sessions_vocab)
25from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
26from waeup.kofa.accesscodes import create_accesscode
27from waeup.kofa.students.utils import trans
28from waeup.aaue.interswitch.browser import gateway_net_amt, GATEWAY_AMT
29from waeup.aaue.interfaces import MessageFactory as _
30
31MINIMUM_UNITS_THRESHOLD = 15
32
33class CustomStudentsUtils(NigeriaStudentsUtils):
34    """A collection of customized methods.
35
36    """
37
38    PORTRAIT_CHANGE_STATES = (ADMITTED,)
39
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, 'IRNS / NER / NYV'),
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, 'IRNS / NER / NYV'),
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'))
74
75    def getClassFromCGPA(self, gpa, student):
76        gpa_boundaries = self.GPABoundaries(student=student)
77
78        try:
79            certificate = getattr(student['studycourse'],'certificate',None)
80            end_level = getattr(certificate, 'end_level', None)
81            final_level = max([studylevel.level for studylevel in student['studycourse'].values()])
82            if end_level and final_level >= end_level:
83                dummy, repeat = divmod(final_level, 100)
84                if gpa <= 5.1 and repeat == 20:
85                    # Irrespective of the CGPA of a student, if the He/She has
86                    # 3rd Extension, such student will be graduated with a "Pass".
87                    return 1, gpa_boundaries[1][1]
88        except ValueError:
89            pass
90
91        if gpa < gpa_boundaries[0][0]:
92            # FRNS / Fail
93            return 0, gpa_boundaries[0][1]
94        if student.entry_session < 2013 or \
95            student.current_mode.startswith('dp'):
96            if gpa < gpa_boundaries[1][0]:
97                # Pass
98                return 1, gpa_boundaries[1][1]
99        else:
100            if gpa < gpa_boundaries[1][0]:
101                # FRNS
102                # Pass degree has been phased out in 2013 for non-diploma
103                # students
104                return 0, gpa_boundaries[0][1]
105        if gpa < gpa_boundaries[2][0]:
106            # 3rd / Merit
107            return 2, gpa_boundaries[2][1]
108        if gpa < gpa_boundaries[3][0]:
109            # 2nd L / Credit
110            return 3, gpa_boundaries[3][1]
111        if gpa < gpa_boundaries[4][0]:
112            # 2nd U / Distinction
113            return 4, gpa_boundaries[4][1]
114        if gpa <= gpa_boundaries[5][0]:
115            # 1st
116            return 5, gpa_boundaries[5][1]
117        return
118
119    def getDegreeClassNumber(self, level_obj):
120        """Get degree class number (used for SessionResultsPresentation
121        reports).
122        """
123        certificate = getattr(level_obj.__parent__,'certificate', None)
124        end_level = getattr(certificate, 'end_level', None)
125        if level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
126            # credits taken below limit
127            return 0
128        if end_level and level_obj.level >= end_level:
129            failed_courses = level_obj.passed_params[4]
130            not_taken_courses = level_obj.passed_params[5]
131            if '_m' in failed_courses:
132                return 0
133            if len(not_taken_courses) \
134                and not not_taken_courses == 'Nil':
135                return 0
136        if level_obj.level_verdict in ('FRNS', 'NER', 'NYV'):
137            return 0
138        # use gpa_boundaries above
139        return self.getClassFromCGPA(
140            level_obj.cumulative_params[0], level_obj.student)[0]
141
142    def increaseMatricInteger(self, student):
143        """Increase counter for matric numbers.
144        This counter can be a centrally stored attribute or an attribute of
145        faculties, departments or certificates. In the base package the counter
146        is as an attribute of the site configuration container.
147        """
148        if student.current_mode in ('ug_pt', 'de_pt', 'ug_dsh', 'de_dsh'):
149            grok.getSite()['configuration'].next_matric_integer += 1
150            return
151        elif student.is_postgrad:
152            grok.getSite()['configuration'].next_matric_integer_3 += 1
153            return
154        elif student.current_mode in ('dp_ft',):
155            grok.getSite()['configuration'].next_matric_integer_4 += 1
156            return
157        grok.getSite()['configuration'].next_matric_integer_2 += 1
158        return
159
160    def _concessionalPaymentMade(self, student):
161        if len(student['payments']):
162            for ticket in student['payments'].values():
163                if ticket.p_state == 'paid' and \
164                    ticket.p_category == 'concessional':
165                    return True
166        return False
167
168    def constructMatricNumber(self, student):
169        faccode = student.faccode
170        depcode = student.depcode
171        certcode = student.certcode
172        degree = getattr(
173            getattr(student.get('studycourse', None), 'certificate', None),
174                'degree', None)
175        year = unicode(student.entry_session)[2:]
176        if not student.state in (PAID, ) or not student.is_fresh or \
177            student.current_mode in ('found', 'ijmbe'):
178            return _('Matriculation number cannot be set.'), None
179        #if student.current_mode not in ('mug_ft', 'mde_ft') and \
180        #    not self._concessionalPaymentMade(student):
181        #    return _('Matriculation number cannot be set.'), None
182        if student.is_postgrad:
183            next_integer = grok.getSite()['configuration'].next_matric_integer_3
184            if not degree or next_integer == 0:
185                return _('Matriculation number cannot be set.'), None
186            if student.faccode in ('IOE'):
187                return None, "AAU/SPS/%s/%s/%s/%05d" % (
188                    faccode, year, degree, next_integer)
189            return None, "AAU/SPS/%s/%s/%s/%s/%05d" % (
190                faccode, depcode, year, degree, next_integer)
191        if student.current_mode in ('ug_dsh', 'de_dsh'):
192            next_integer = grok.getSite()['configuration'].next_matric_integer
193            if next_integer == 0:
194                return _('Matriculation number cannot be set.'), None
195            return None, "DSH/%s/%s/%s/%05d" % (
196                faccode, depcode, year, next_integer)
197        if student.current_mode in ('ug_pt', 'de_pt'):
198            next_integer = grok.getSite()['configuration'].next_matric_integer
199            if next_integer == 0:
200                return _('Matriculation number cannot be set.'), None
201            return None, "PTP/%s/%s/%s/%05d" % (
202                faccode, depcode, year, next_integer)
203        if student.current_mode in ('dp_ft',):
204            next_integer = grok.getSite()['configuration'].next_matric_integer_4
205            if next_integer == 0:
206                return _('Matriculation number cannot be set.'), None
207            return None, "IOE/DIP/%s/%05d" % (year, next_integer)
208        next_integer = grok.getSite()['configuration'].next_matric_integer_2
209        if next_integer == 0:
210            return _('Matriculation number cannot be set.'), None
211        if student.faccode in ('FBM', 'FCS'):
212            return None, "CMS/%s/%s/%s/%05d" % (
213                faccode, depcode, year, next_integer)
214        return None, "%s/%s/%s/%05d" % (faccode, depcode, year, next_integer)
215
216    def getReturningData(self, student):
217        """ This method defines what happens after school fee payment
218        of returning students depending on the student's senate verdict.
219        """
220        prev_level = student['studycourse'].current_level
221        cur_verdict = student['studycourse'].current_verdict
222        if cur_verdict in ('A','B','L','M','N','Z',):
223            # Successful student
224            new_level = divmod(int(prev_level),100)[0]*100 + 100
225        elif cur_verdict == 'C':
226            # Student on probation
227            new_level = int(prev_level) + 10
228        else:
229            # Student is somehow in an undefined state.
230            # Level has to be set manually.
231            new_level = prev_level
232        new_session = student['studycourse'].current_session + 1
233        return new_session, new_level
234
235    def _isPaymentDisabled(self, p_session, category, student):
236        academic_session = self._getSessionConfiguration(p_session)
237        if category.startswith('schoolfee'):
238            if 'sf_all' in academic_session.payment_disabled:
239                return True
240            if 'sf_pg' in academic_session.payment_disabled and \
241                student.is_postgrad:
242                return True
243            if 'sf_ug_pt' in academic_session.payment_disabled and \
244                student.current_mode in ('ug_pt', 'de_pt'):
245                return True
246            if 'sf_found' in academic_session.payment_disabled and \
247                student.current_mode == 'found':
248                return True
249        if category.startswith('clearance') and \
250            'cl_regular' in academic_session.payment_disabled and \
251            student.current_mode in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
252            return True
253        if category == 'hostel_maintenance' and \
254            'maint_all' in academic_session.payment_disabled:
255            return True
256        return False
257
258    def setPaymentDetails(self, category, student,
259            previous_session=None, previous_level=None, combi=[]):
260        """Create Payment object and set the payment data of a student for
261        the payment category specified.
262
263        """
264        details = {}
265        p_item = u''
266        amount = 0.0
267        error = u''
268        if previous_session:
269            if previous_session < student['studycourse'].entry_session:
270                return _('The previous session must not fall below '
271                         'your entry session.'), None
272            if category == 'schoolfee':
273                # School fee is always paid for the following session
274                if previous_session > student['studycourse'].current_session:
275                    return _('This is not a previous session.'), None
276            else:
277                if previous_session > student['studycourse'].current_session - 1:
278                    return _('This is not a previous session.'), None
279            p_session = previous_session
280            p_level = previous_level
281            p_current = False
282        else:
283            p_session = student['studycourse'].current_session
284            p_level = student['studycourse'].current_level
285            p_current = True
286        academic_session = self._getSessionConfiguration(p_session)
287        if academic_session == None:
288            return _(u'Session configuration object is not available.'), None
289        # Determine fee.
290        if category == 'transfer':
291            amount = academic_session.transfer_fee
292        elif category == 'transcript_local':
293            amount = academic_session.transcript_fee_local
294        elif category == 'transcript_inter':
295            amount = academic_session.transcript_fee_inter
296        elif category == 'bed_allocation':
297            amount = academic_session.booking_fee
298        elif category == 'restitution':
299            if student.current_session != 2016 \
300                or student.current_mode not in ('ug_ft', 'dp_ft') \
301                or student.is_fresh:
302                return _(u'Restitution fee payment not required.'), None
303            amount = academic_session.restitution_fee
304        elif category == 'hostel_maintenance':
305            amount = 0.0
306            bedticket = student['accommodation'].get(
307                str(student.current_session), None)
308            if bedticket is not None and bedticket.bed is not None:
309                p_item = bedticket.display_coordinates
310                if bedticket.bed.__parent__.maint_fee > 0:
311                    amount = bedticket.bed.__parent__.maint_fee
312                else:
313                    # fallback
314                    amount = academic_session.maint_fee
315            else:
316                return _(u'No bed allocated.'), None
317        elif student.current_mode == 'found' and category not in (
318            'schoolfee', 'clearance', 'late_registration'):
319            return _('Not allowed.'), None
320        elif category.startswith('clearance'):
321            if student.state not in (ADMITTED, CLEARANCE, REQUESTED, CLEARED):
322                return _(u'Acceptance Fee payments not allowed.'), None
323            if student.current_mode in (
324                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
325                'transfer', 'mug_ft', 'mde_ft') \
326                and category != 'clearance_incl':
327                    return _("Additional fees must be included."), None
328            if student.current_mode == 'ijmbe':
329                amount = academic_session.clearance_fee_ijmbe
330            elif student.current_mode == 'bridge':
331                amount = academic_session.clearance_fee_bridge
332            elif student.current_mode == 'dp_ft':
333                amount = academic_session.clearance_fee_dp
334            elif student.faccode == 'FP':
335                amount = academic_session.clearance_fee_fp
336            elif student.current_mode in ('ug_pt', 'de_pt', 'ug_dsh', 'de_dsh'):
337                amount = academic_session.clearance_fee_ug_pt
338            elif student.current_mode == 'special_pg_pt':
339                amount = academic_session.clearance_fee_pg_pt
340            elif student.faccode == 'FCS':
341                # Students in clinical medical sciences pay the medical
342                # acceptance fee
343                amount = academic_session.clearance_fee_med
344            elif student.current_mode == 'special_pg_ft':
345                if category != 'clearance':
346                    return _("No additional fees required."), None
347                amount = academic_session.clearance_fee_pg
348            else:
349                amount = academic_session.clearance_fee
350            p_item = student['studycourse'].certificate.code
351            if amount in (0.0, None):
352                return _(u'Amount could not be determined.'), None
353            # Add Matric Gown Fee and Lapel Fee
354            if category == 'clearance_incl':
355                amount += gateway_net_amt(academic_session.matric_gown_fee) + \
356                    gateway_net_amt(academic_session.lapel_fee)
357        elif category == 'late_registration':
358            if student.is_postgrad:
359                amount = academic_session.late_pg_registration_fee
360            else:
361                amount = academic_session.late_registration_fee
362        elif category.startswith('schoolfee'):
363            try:
364                certificate = student['studycourse'].certificate
365                p_item = certificate.code
366            except (AttributeError, TypeError):
367                return _('Study course data are incomplete.'), None
368            if student.is_postgrad and category != 'schoolfee':
369                return _("No additional fees required."), None
370            if not previous_session and student.current_mode in (
371                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
372                'transfer', 'mug_ft', 'mde_ft') \
373                and not category in (
374                'schoolfee_incl', 'schoolfee_1', 'schoolfee_2'):
375                    return _("You must choose a payment which includes "
376                             "additional fees."), None
377            if category in ('schoolfee_1', 'schoolfee_2'):
378                if student.current_mode == 'ug_pt':
379                    return _("Part-time students are not allowed "
380                             "to pay by instalments."), None
381                if student.entry_session < 2015:
382                    return _("You are not allowed "
383                             "to pay by instalments."), None
384            if previous_session:
385                # Students can pay for previous sessions in all
386                # workflow states.  Fresh students are excluded by the
387                # update method of the PreviousPaymentAddFormPage.
388                if previous_level == 100:
389                    amount = getattr(certificate, 'school_fee_1', 0.0)
390                else:
391                    if student.entry_session in (2015, 2016):
392                        amount = getattr(certificate, 'school_fee_2', 0.0)
393                    else:
394                        amount = getattr(certificate, 'school_fee_3', 0.0)
395            elif student.state == CLEARED and category != 'schoolfee_2':
396                amount = getattr(certificate, 'school_fee_1', 0.0)
397                # Cut school fee by 50%
398                if category == 'schoolfee_1' and amount:
399                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
400            elif student.is_fresh and category == 'schoolfee_2':
401                amount = getattr(certificate, 'school_fee_1', 0.0)
402                # Cut school fee by 50%
403                if amount:
404                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
405            elif student.state == RETURNING and category != 'schoolfee_2':
406                if not student.father_name:
407                    return _("Personal data form is not properly filled."), None
408                # In case of returning school fee payment the payment session
409                # and level contain the values of the session the student
410                # has paid for.
411                p_session, p_level = self.getReturningData(student)
412                try:
413                    academic_session = grok.getSite()[
414                        'configuration'][str(p_session)]
415                except KeyError:
416                    return _(u'Session configuration object is not available.'), None
417                if student.entry_session >= 2015:
418                    amount = getattr(certificate, 'school_fee_2', 0.0)
419                else:
420                    amount = getattr(certificate, 'school_fee_3', 0.0)
421                # Cut school fee by 50%
422                if category == 'schoolfee_1' and amount:
423                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
424            elif category == 'schoolfee_2':
425                amount = getattr(certificate, 'school_fee_2', 0.0)
426                # Cut school fee by 50%
427                if amount:
428                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
429            else:
430                return _('Wrong state.'), None
431            if amount in (0.0, None):
432                return _(u'Amount could not be determined.'), None
433            # Add Student Union Fee, Student Id Card Fee, Sports Dev. Fee,
434            # Library Dev. Fee and Welfare Assurance
435            if category in ('schoolfee_incl', 'schoolfee_1') \
436                and student.current_mode != 'ijmbe':
437                amount += gateway_net_amt(academic_session.welfare_fee) + \
438                    gateway_net_amt(academic_session.union_fee)
439                if student.entry_session >= 2018 and student.is_fresh:
440                    amount += gateway_net_amt(academic_session.sports_fee)
441                    if student.is_postgrad:
442                        amount += gateway_net_amt(academic_session.library_pg_fee)
443                    else:
444                        amount += gateway_net_amt(academic_session.library_fee)
445            # Add non-indigenous fee and session specific penalty fees
446            if student.is_postgrad:
447                amount += academic_session.penalty_pg
448                if student.lga and not student.lga.startswith('edo'):
449                    amount += 20000.0
450            else:
451                amount += academic_session.penalty_ug
452        #elif category == ('fac_dep'):
453        #    if student.faccode in ('FAT', 'FED', 'FLW', 'FMS', 'FSS'):
454        #        amount = 4200.0
455        #    elif student.faccode in ('FAG', 'FBM', 'FCS',
456        #                             'FES', 'FET', 'FLS', 'FPS'):
457        #        amount = 5200.0
458        #    elif student.faccode == 'FAG' and student.current_level == 400:
459        #        amount = 10200.0
460        #    elif student.depcode == 'VTE':
461        #        amount = 5200.0
462        #    if student.entry_session >= 2019:
463        #        amount += gateway_net_amt(academic_session.ict_fee)
464        elif category == 'sports_library': # temporarily in 2020
465            if student.is_postgrad:
466                amount = academic_session.sports_fee + gateway_net_amt(
467                    academic_session.library_pg_fee)
468            else:
469                amount = academic_session.sports_fee + gateway_net_amt(
470                    academic_session.library_fee)
471        elif not student.is_postgrad:
472            fee_name = category + '_fee'
473            amount = getattr(academic_session, fee_name, 0.0)
474        if amount in (0.0, None):
475            return _(u'Amount could not be determined.'), None
476        # Create ticket.
477        for key in student['payments'].keys():
478            ticket = student['payments'][key]
479            if ticket.p_state == 'paid' and\
480               ticket.p_category == category and \
481               not ticket.p_category.startswith('transcript') and \
482               ticket.p_item == p_item and \
483               ticket.p_session == p_session:
484                  return _('This type of payment has already been made.'), None
485            # Additional condition in AAUE
486            if category in ('schoolfee', 'schoolfee_incl', 'schoolfee_1'):
487                if ticket.p_state == 'paid' and \
488                   ticket.p_category in ('schoolfee',
489                                         'schoolfee_incl',
490                                         'schoolfee_1') and \
491                   ticket.p_item == p_item and \
492                   ticket.p_session == p_session:
493                      return _(
494                          'Another school fee payment for this '
495                          'session has already been made.'), None
496
497        if self._isPaymentDisabled(p_session, category, student):
498            return _('This category of payments has been disabled.'), None
499        payment = createObject(u'waeup.StudentOnlinePayment')
500        timestamp = ("%d" % int(time()*10000))[1:]
501        payment.p_id = "p%s" % timestamp
502        payment.p_category = category
503        payment.p_item = p_item
504        payment.p_session = p_session
505        payment.p_level = p_level
506        payment.p_current = p_current
507        payment.amount_auth = amount
508        return None, payment
509
510    def _admissionText(self, student, portal_language):
511        inst_name = grok.getSite()['configuration'].name
512        entry_session = student['studycourse'].entry_session
513        entry_session = academic_sessions_vocab.getTerm(entry_session).title
514        text = trans(_(
515            'This is to inform you that you have been offered provisional'
516            ' admission into ${a} for the ${b} academic session as follows:',
517            mapping = {'a': inst_name, 'b': entry_session}),
518            portal_language)
519        return text
520
521    def warnCreditsOOR(self, studylevel, course=None):
522        studycourse = studylevel.__parent__
523        certificate = getattr(studycourse,'certificate', None)
524        current_level = studycourse.current_level
525        if None in (current_level, certificate):
526            return
527        end_level = certificate.end_level
528        if current_level >= end_level:
529            limit = 52
530        else:
531            limit = 48
532        if course and studylevel.total_credits + course.credits > limit:
533            return  _('Maximum credits exceeded.')
534        elif studylevel.total_credits > limit:
535            return _('Maximum credits exceeded.')
536        return
537
538    def getBedCoordinates(self, bedticket):
539        """Return descriptive bed coordinates.
540        This method can be used to customize the `display_coordinates`
541        property method in order to  display a
542        customary description of the bed space.
543        """
544        bc = bedticket.bed_coordinates.split(',')
545        if len(bc) == 4:
546            return bc[0]
547        return bedticket.bed_coordinates
548
549    def getAccommodationDetails(self, student):
550        """Determine the accommodation data of a student.
551        """
552        d = {}
553        d['error'] = u''
554        hostels = grok.getSite()['hostels']
555        d['booking_session'] = hostels.accommodation_session
556        d['allowed_states'] = hostels.accommodation_states
557        d['startdate'] = hostels.startdate
558        d['enddate'] = hostels.enddate
559        d['expired'] = hostels.expired
560        # Determine bed type
561        bt = 'all'
562        if student.sex == 'f':
563            sex = 'female'
564        else:
565            sex = 'male'
566        special_handling = 'regular'
567        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
568        return d
569
570    def checkAccommodationRequirements(self, student, acc_details):
571        msg = super(CustomStudentsUtils, self).checkAccommodationRequirements(
572            student, acc_details)
573        if msg:
574            return msg
575        if student.current_mode not in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
576            return _('You are not eligible to book accommodation.')
577        return
578
579    # AAUE prefix
580    STUDENT_ID_PREFIX = u'E'
581
582    STUDENT_EXPORTER_NAMES = (
583            'students',
584            'studentstudycourses',
585            'studentstudylevels',
586            'coursetickets',
587            'studentpayments',
588            'bedtickets',
589            'unpaidpayments',
590            'sfpaymentsoverview',
591            'studylevelsoverview',
592            'combocard',
593            'bursary',
594            'levelreportdata',
595            'outstandingcourses',
596            'sessionpaymentsoverview',
597            'accommodationpayments',
598            'transcriptdata',
599            'trimmedpayments',
600            'trimmed',
601            )
602
603    # Maximum size of upload files in kB
604    MAX_KB = 500
Note: See TracBrowser for help on using the repository browser.