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

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

Many customizations are needed to distinguish Diploma and Non-Diploma students in reports.

  • Property svn:keywords set to Id
File size: 24.6 KB
Line 
1## $Id: utils.py 14918 2017-11-30 12:57:05Z 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.faccode == 'FP':
316                amount = academic_session.clearance_fee_fp
317            elif student.current_mode.endswith('_pt'):
318                if student.is_postgrad:
319                    amount = academic_session.clearance_fee_pg_pt
320                else:
321                    amount = academic_session.clearance_fee_ug_pt
322            elif student.faccode == 'FCS':
323                # Students in clinical medical sciences pay the medical
324                # acceptance fee
325                amount = academic_session.clearance_fee_med
326            elif student.is_postgrad:  # and not part-time
327                if category != 'clearance':
328                    return _("No additional fees required."), None
329                amount = academic_session.clearance_fee_pg
330            else:
331                amount = academic_session.clearance_fee
332            p_item = student['studycourse'].certificate.code
333            if amount in (0.0, None):
334                return _(u'Amount could not be determined.'), None
335            # Add Matric Gown Fee and Lapel Fee
336            if category == 'clearance_incl':
337                amount += gateway_net_amt(academic_session.matric_gown_fee) + \
338                    gateway_net_amt(academic_session.lapel_fee)
339        elif category == 'late_registration':
340            if student.is_postgrad:
341                amount = academic_session.late_pg_registration_fee
342            else:
343                amount = academic_session.late_registration_fee
344        elif category.startswith('schoolfee'):
345            try:
346                certificate = student['studycourse'].certificate
347                p_item = certificate.code
348            except (AttributeError, TypeError):
349                return _('Study course data are incomplete.'), None
350            if student.is_postgrad and category != 'schoolfee':
351                return _("No additional fees required."), None
352            if not previous_session and student.current_mode in (
353                'ug_ft', 'ug_pt', 'de_ft', 'de_pt',
354                'transfer', 'mug_ft', 'mde_ft') \
355                and not category in (
356                'schoolfee_incl', 'schoolfee_1', 'schoolfee_2'):
357                    return _("You must choose a payment which includes "
358                             "additional fees."), None
359            if category in ('schoolfee_1', 'schoolfee_2'):
360                if student.current_mode == 'ug_pt':
361                    return _("Part-time students are not allowed "
362                             "to pay by instalments."), None
363                if student.entry_session < 2015:
364                    return _("You are not allowed "
365                             "to pay by instalments."), None
366            if previous_session:
367                # Students can pay for previous sessions in all
368                # workflow states.  Fresh students are excluded by the
369                # update method of the PreviousPaymentAddFormPage.
370                if previous_level == 100:
371                    amount = getattr(certificate, 'school_fee_1', 0.0)
372                else:
373                    if student.entry_session >= 2015:
374                        amount = getattr(certificate, 'school_fee_2', 0.0)
375                    else:
376                        amount = getattr(certificate, 'school_fee_3', 0.0)
377            elif student.state == CLEARED and category != 'schoolfee_2':
378                amount = getattr(certificate, 'school_fee_1', 0.0)
379                # Cut school fee by 50%
380                if category == 'schoolfee_1' and amount:
381                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
382            elif student.is_fresh and category == 'schoolfee_2':
383                amount = getattr(certificate, 'school_fee_1', 0.0)
384                # Cut school fee by 50%
385                if amount:
386                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
387            elif student.state == RETURNING and category != 'schoolfee_2':
388                if not student.father_name:
389                    return _("Personal data form is not properly filled."), None
390                # In case of returning school fee payment the payment session
391                # and level contain the values of the session the student
392                # has paid for.
393                p_session, p_level = self.getReturningData(student)
394                try:
395                    academic_session = grok.getSite()[
396                        'configuration'][str(p_session)]
397                except KeyError:
398                    return _(u'Session configuration object is not available.'), None
399                if student.entry_session >= 2015:
400                    amount = getattr(certificate, 'school_fee_2', 0.0)
401                    # Cut school fee by 50%
402                    if category == 'schoolfee_1' and amount:
403                        amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
404                else:
405                    amount = getattr(certificate, 'school_fee_3', 0.0)
406            elif category == 'schoolfee_2':
407                amount = getattr(certificate, 'school_fee_2', 0.0)
408                # Cut school fee by 50%
409                if amount:
410                    amount = gateway_net_amt(amount) / 2 + GATEWAY_AMT
411            else:
412                return _('Wrong state.'), None
413            if amount in (0.0, None):
414                return _(u'Amount could not be determined.'), None
415            # Add Student Union Fee , Student Id Card Fee and Welfare Assurance
416            if category in ('schoolfee_incl', 'schoolfee_1'):
417                amount += gateway_net_amt(academic_session.welfare_fee) + \
418                    gateway_net_amt(academic_session.union_fee)
419                if student.entry_session == 2016 \
420                    and student.entry_mode == 'ug_ft' \
421                    and student.state == CLEARED:
422                    amount += gateway_net_amt(academic_session.id_card_fee)
423            # Add non-indigenous fee and session specific penalty fees
424            if student.is_postgrad:
425                amount += academic_session.penalty_pg
426                if student.lga and not student.lga.startswith('edo'):
427                    amount += 20000.0
428            else:
429                amount += academic_session.penalty_ug
430        elif not student.is_postgrad:
431            fee_name = category + '_fee'
432            amount = getattr(academic_session, fee_name, 0.0)
433        if amount in (0.0, None):
434            return _(u'Amount could not be determined.'), None
435        # Create ticket.
436        for key in student['payments'].keys():
437            ticket = student['payments'][key]
438            if ticket.p_state == 'paid' and\
439               ticket.p_category == category and \
440               ticket.p_item == p_item and \
441               ticket.p_session == p_session:
442                  return _('This type of payment has already been made.'), None
443            # Additional condition in AAUE
444            if category in ('schoolfee', 'schoolfee_incl', 'schoolfee_1'):
445                if ticket.p_state == 'paid' and \
446                   ticket.p_category in ('schoolfee',
447                                         'schoolfee_incl',
448                                         'schoolfee_1') and \
449                   ticket.p_item == p_item and \
450                   ticket.p_session == p_session:
451                      return _(
452                          'Another school fee payment for this '
453                          'session has already been made.'), None
454
455        if self._isPaymentDisabled(p_session, category, student):
456            return _('This category of payments has been disabled.'), None
457        payment = createObject(u'waeup.StudentOnlinePayment')
458        timestamp = ("%d" % int(time()*10000))[1:]
459        payment.p_id = "p%s" % timestamp
460        payment.p_category = category
461        payment.p_item = p_item
462        payment.p_session = p_session
463        payment.p_level = p_level
464        payment.p_current = p_current
465        payment.amount_auth = amount
466        return None, payment
467
468    def _admissionText(self, student, portal_language):
469        inst_name = grok.getSite()['configuration'].name
470        entry_session = student['studycourse'].entry_session
471        entry_session = academic_sessions_vocab.getTerm(entry_session).title
472        text = trans(_(
473            'This is to inform you that you have been offered provisional'
474            ' admission into ${a} for the ${b} academic session as follows:',
475            mapping = {'a': inst_name, 'b': entry_session}),
476            portal_language)
477        return text
478
479    def warnCreditsOOR(self, studylevel, course=None):
480        studycourse = studylevel.__parent__
481        certificate = getattr(studycourse,'certificate', None)
482        current_level = studycourse.current_level
483        if None in (current_level, certificate):
484            return
485        end_level = certificate.end_level
486        if current_level >= end_level:
487            limit = 52
488        else:
489            limit = 48
490        if course and studylevel.total_credits + course.credits > limit:
491            return  _('Maximum credits exceeded.')
492        elif studylevel.total_credits > limit:
493            return _('Maximum credits exceeded.')
494        return
495
496    def getBedCoordinates(self, bedticket):
497        """Return descriptive bed coordinates.
498        This method can be used to customize the `display_coordinates`
499        property method in order to  display a
500        customary description of the bed space.
501        """
502        bc = bedticket.bed_coordinates.split(',')
503        if len(bc) == 4:
504            return bc[0]
505        return bedticket.bed_coordinates
506
507    def getAccommodationDetails(self, student):
508        """Determine the accommodation data of a student.
509        """
510        d = {}
511        d['error'] = u''
512        hostels = grok.getSite()['hostels']
513        d['booking_session'] = hostels.accommodation_session
514        d['allowed_states'] = hostels.accommodation_states
515        d['startdate'] = hostels.startdate
516        d['enddate'] = hostels.enddate
517        d['expired'] = hostels.expired
518        # Determine bed type
519        bt = 'all'
520        if student.sex == 'f':
521            sex = 'female'
522        else:
523            sex = 'male'
524        special_handling = 'regular'
525        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
526        return d
527
528    def checkAccommodationRequirements(self, student, acc_details):
529        msg = super(CustomStudentsUtils, self).checkAccommodationRequirements(
530            student, acc_details)
531        if msg:
532            return msg
533        if student.current_mode not in ('ug_ft', 'de_ft', 'mug_ft', 'mde_ft'):
534            return _('You are not eligible to book accommodation.')
535        return
536
537    # AAUE prefix
538    STUDENT_ID_PREFIX = u'E'
539
540    STUDENT_EXPORTER_NAMES = ('students', 'studentstudycourses',
541            'studentstudylevels', 'coursetickets',
542            'studentpayments', 'studentunpaidpayments',
543            'bedtickets', 'paymentsoverview',
544            'studylevelsoverview', 'combocard', 'bursary',
545            'levelreportdata')
Note: See TracBrowser for help on using the repository browser.