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
Line 
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##
18import grok
19from time import time
20from zope.component import createObject
21from waeup.kofa.interfaces import (
22    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
23    academic_sessions_vocab)
24from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
25from waeup.kofa.accesscodes import create_accesscode
26from waeup.kofa.students.utils import trans
27from waeup.aaue.interswitch.browser import gateway_net_amt, GATEWAY_AMT
28from waeup.aaue.interfaces import MessageFactory as _
29
30MINIMUM_UNITS_THRESHOLD = 15
31
32class CustomStudentsUtils(NigeriaStudentsUtils):
33    """A collection of customized methods.
34
35    """
36
37    PORTRAIT_CHANGE_STATES = (ADMITTED,)
38
39    gpa_boundaries = ((1, 'FRNS / NER / NYV'),
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
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
72    def getDegreeClassNumber(self, level_obj):
73        """Get degree class number (used for SessionResultsPresentation
74        reports).
75        """
76        certificate = getattr(level_obj.__parent__,'certificate', None)
77        end_level = getattr(certificate, 'end_level', None)
78        if end_level and level_obj.level >= end_level:
79            if level_obj.level > end_level:
80                # spill-over level
81                if level_obj.gpa_params[1] == 0:
82                    # no credits taken
83                    return 0
84            elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
85                # credits taken below limit
86                return 0
87            failed_courses = level_obj.passed_params[4]
88            not_taken_courses = level_obj.passed_params[5]
89            if '_m' in failed_courses:
90                return 0
91            if len(not_taken_courses) \
92                and not not_taken_courses == 'Nil':
93                return 0
94        elif level_obj.gpa_params[1] < MINIMUM_UNITS_THRESHOLD:
95            # credits taken below limit
96            return 0
97        if level_obj.level_verdict in ('FRNS', 'NER', 'NYV'):
98            return 0
99        # use gpa_boundaries above
100        return self.getClassFromCGPA(
101            level_obj.cumulative_params[0], level_obj.student)[0]
102
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
112        elif student.is_postgrad:
113            grok.getSite()['configuration'].next_matric_integer_3 += 1
114            return
115        elif student.current_mode in ('dp_ft',):
116            grok.getSite()['configuration'].next_matric_integer_4 += 1
117            return
118        grok.getSite()['configuration'].next_matric_integer_2 += 1
119        return
120
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
129    def constructMatricNumber(self, student):
130        faccode = student.faccode
131        depcode = student.depcode
132        certcode = student.certcode
133        degree = getattr(
134            getattr(student.get('studycourse', None), 'certificate', None),
135                'degree', None)
136        year = unicode(student.entry_session)[2:]
137        if not student.state in (PAID, ) or not student.is_fresh or \
138            student.current_mode in ('found', 'ijmbe'):
139            return _('Matriculation number cannot be set.'), None
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
143        if student.is_postgrad:
144            next_integer = grok.getSite()['configuration'].next_matric_integer_3
145            if not degree or next_integer == 0:
146                return _('Matriculation number cannot be set.'), None
147            if student.faccode in ('IOE'):
148                return None, "AAU/SPS/%s/%s/%s/%05d" % (
149                    faccode, year, degree, next_integer)
150            return None, "AAU/SPS/%s/%s/%s/%s/%05d" % (
151                faccode, depcode, year, degree, next_integer)
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
156            return None, "PTP/%s/%s/%s/%05d" % (
157                faccode, depcode, year, next_integer)
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)
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)
170
171    def getReturningData(self, student):
172        """ This method defines what happens after school fee payment
173        of returning students depending on the student's senate verdict.
174        """
175        prev_level = student['studycourse'].current_level
176        cur_verdict = student['studycourse'].current_verdict
177        if cur_verdict in ('A','B','C', 'L','M','N','Z',):
178            # Successful student
179            new_level = divmod(int(prev_level),100)[0]*100 + 100
180        #elif cur_verdict == 'C':
181        #    # Student on probation
182        #    new_level = int(prev_level) + 10
183        else:
184            # Student is somehow in an undefined state.
185            # Level has to be set manually.
186            new_level = prev_level
187        new_session = student['studycourse'].current_session + 1
188        return new_session, new_level
189
190    def _isPaymentDisabled(self, p_session, category, student):
191        academic_session = self._getSessionConfiguration(p_session)
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
198            if 'sf_ug_pt' in academic_session.payment_disabled and \
199                student.current_mode in ('ug_pt', 'de_pt'):
200                return True
201            if 'sf_found' in academic_session.payment_disabled and \
202                student.current_mode == 'found':
203                return True
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
208        if category == 'hostel_maintenance' and \
209            'maint_all' in academic_session.payment_disabled:
210            return True
211        return False
212
213    def setPaymentDetails(self, category, student,
214            previous_session=None, previous_level=None):
215        """Create Payment object and set the payment data of a student for
216        the payment category specified.
217
218        """
219        details = {}
220        p_item = u''
221        amount = 0.0
222        error = u''
223        if previous_session:
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
241        academic_session = self._getSessionConfiguration(p_session)
242        if academic_session == None:
243            return _(u'Session configuration object is not available.'), None
244        # Determine fee.
245        if category == 'transfer':
246            amount = academic_session.transfer_fee
247        elif category == 'transcript_local':
248            amount = academic_session.transcript_fee_local
249        elif category == 'transcript_inter':
250            amount = academic_session.transcript_fee_inter
251        elif category == 'bed_allocation':
252            amount = academic_session.booking_fee
253        elif category == 'restitution':
254            if student.entry_session >= 2016 \
255                or student.current_mode not in ('ug_ft', 'dp_ft'):
256                return _(u'Restitution fee payment not required.'), None
257            amount = academic_session.restitution_fee
258        elif category == 'hostel_maintenance':
259            amount = 0.0
260            bedticket = student['accommodation'].get(
261                str(student.current_session), None)
262            if bedticket is not None and bedticket.bed is not None:
263                p_item = bedticket.display_coordinates
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:
270                return _(u'No bed allocated.'), None
271        elif student.current_mode == 'found' and category not in (
272            'schoolfee', 'clearance', 'late_registration'):
273            return _('Not allowed.'), None
274        elif category.startswith('clearance'):
275            if student.state not in (ADMITTED, CLEARANCE, REQUESTED, CLEARED):
276                return _(u'Acceptance Fee payments not allowed.'), None
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
282            if student.current_mode == 'ijmbe':
283                amount = academic_session.clearance_fee_ijmbe
284            elif student.faccode == 'FP':
285                amount = academic_session.clearance_fee_fp
286            elif student.current_mode.endswith('_pt'):
287                if student.is_postgrad:
288                    amount = academic_session.clearance_fee_pg_pt
289                else:
290                    amount = academic_session.clearance_fee_ug_pt
291            elif student.faccode == 'FCS':
292                # Students in clinical medical sciences pay the medical
293                # acceptance fee
294                amount = academic_session.clearance_fee_med
295            elif student.is_postgrad:  # and not part-time
296                if category != 'clearance':
297                    return _("No additional fees required."), None
298                amount = academic_session.clearance_fee_pg
299            else:
300                amount = academic_session.clearance_fee
301            p_item = student['studycourse'].certificate.code
302            if amount in (0.0, None):
303                return _(u'Amount could not be determined.'), None
304            # Add Matric Gown Fee and Lapel Fee
305            if category == 'clearance_incl':
306                amount += gateway_net_amt(academic_session.matric_gown_fee) + \
307                    gateway_net_amt(academic_session.lapel_fee)
308        elif category == 'late_registration':
309            if student.is_postgrad:
310                amount = academic_session.late_pg_registration_fee
311            else:
312                amount = academic_session.late_registration_fee
313        elif category.startswith('schoolfee'):
314            try:
315                certificate = student['studycourse'].certificate
316                p_item = certificate.code
317            except (AttributeError, TypeError):
318                return _('Study course data are incomplete.'), None
319            if student.is_postgrad and category != 'schoolfee':
320                return _("No additional fees required."), None
321            if not previous_session and student.current_mode in (
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'):
326                    return _("You must choose a payment which includes "
327                             "additional fees."), None
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
332                if student.entry_session < 2015:
333                    return _("You are not allowed "
334                             "to pay by instalments."), None
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':
347                amount = getattr(certificate, 'school_fee_1', 0.0)
348                # Cut school fee by 50%
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
356            elif student.state == RETURNING and category != 'schoolfee_2':
357                if not student.father_name:
358                    return _("Personal data form is not properly filled."), None
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)
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
368                if student.entry_session >= 2015:
369                    amount = getattr(certificate, 'school_fee_2', 0.0)
370                    # Cut school fee by 50%
371                    if category == 'schoolfee_1' and amount:
372                        amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
373                else:
374                    amount = getattr(certificate, 'school_fee_3', 0.0)
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
380            else:
381                return _('Wrong state.'), None
382            if amount in (0.0, None):
383                return _(u'Amount could not be determined.'), None
384            # Add Student Union Fee , Student Id Card Fee and Welfare Assurance
385            if category in ('schoolfee_incl', 'schoolfee_1'):
386                amount += gateway_net_amt(academic_session.welfare_fee) + \
387                    gateway_net_amt(academic_session.union_fee)
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)
392            # Add non-indigenous fee and session specific penalty fees
393            if student.is_postgrad:
394                amount += academic_session.penalty_pg
395                if student.lga and not student.lga.startswith('edo'):
396                    amount += 20000.0
397            else:
398                amount += academic_session.penalty_ug
399        elif not student.is_postgrad:
400            fee_name = category + '_fee'
401            amount = getattr(academic_session, fee_name, 0.0)
402        if amount in (0.0, None):
403            return _(u'Amount could not be determined.'), None
404        # Create ticket.
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
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
424        if self._isPaymentDisabled(p_session, category, student):
425            return _('This category of payments has been disabled.'), None
426        payment = createObject(u'waeup.StudentOnlinePayment')
427        timestamp = ("%d" % int(time()*10000))[1:]
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
433        payment.p_current = p_current
434        payment.amount_auth = amount
435        return None, payment
436
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(_(
442            'This is to inform you that you have been offered provisional'
443            ' admission into ${a} for the ${b} academic session as follows:',
444            mapping = {'a': inst_name, 'b': entry_session}),
445            portal_language)
446        return text
447
448    def warnCreditsOOR(self, studylevel, course=None):
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:
460            return  _('Maximum credits exceeded.')
461        elif studylevel.total_credits > limit:
462            return _('Maximum credits exceeded.')
463        return
464
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
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
488        bt = 'all'
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
497    def checkAccommodationRequirements(self, student, acc_details):
498        msg = super(CustomStudentsUtils, self).checkAccommodationRequirements(
499            student, acc_details)
500        if msg:
501            return msg
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
506    # AAUE prefix
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.