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

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

Final year students maximum credit units = 52

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