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

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

Adjust exporter name.

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