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

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

Add clearance_fee_dp payment.

Change schoolfee calculation and adjust to the modifications made for 2017/2018.

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