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

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

Add missing imports.

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