source: main/kofacustom.dspg/trunk/src/kofacustom/dspg/students/utils.py @ 16963

Last change on this file since 16963 was 16963, checked in by Henrik Bettermann, 2 years ago

Tech fee conditions are becoming adventurous.

  • Property svn:keywords set to Id
File size: 19.5 KB
Line 
1## $Id: utils.py 16963 2022-06-14 11:34:52Z 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, getUtility
21from waeup.kofa.interfaces import (IKofaUtils,
22    ADMITTED, CLEARED, RETURNING, PAID, REGISTERED, VALIDATED)
23from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
24from kofacustom.dspg.interfaces import MessageFactory as _
25
26def local(student):
27    lga = getattr(student, 'lga')
28    if lga and lga.startswith('delta'):
29        return True
30    return False
31
32def MICROSOFT_FEE(student):
33    if student.state == CLEARED \
34        or (student.current_level in (100, 110, 120, 130)
35            and student.current_verdict == 'C') \
36        or (student.current_level in (300, 310, 320, 330) \
37            and student.current_mode == 'hnd_ft') \
38            and student.current_verdict == 'C'):
39        return 5950.0
40    return 0.0
41
42def DEVELOP_FEE(student):
43    if local(student) and student.faccode != 'SPAT':
44        return 10000.0
45    return 15000.0
46
47def TECH_FEE(student):
48    if student.state == CLEARED \
49        or (student.current_level in (100, 110, 120, 130)
50            and student.current_verdict == 'C') \
51        or (student.current_level in (300, 310, 320, 330) \
52            and student.current_mode == 'hnd_ft') \
53            and student.current_verdict == 'C'):
54        return 4250.0
55    return 1200.0
56
57class CustomStudentsUtils(NigeriaStudentsUtils):
58    """A collection of customized methods.
59    """
60
61    # prefix
62    STUDENT_ID_PREFIX = u'P'
63
64    def _dep_sug_gns_paymentMade(self, student, session):
65        if student.state == RETURNING:
66            session += 1
67        dep_sug = False
68        gns = False
69        if len(student['payments']):
70            for ticket in student['payments'].values():
71                if ticket.p_state == 'paid' and \
72                    ticket.p_category == 'dep_sug' and \
73                    ticket.p_session == session:
74                    dep_sug = True
75                if ticket.p_state == 'paid' and \
76                    ticket.p_category.startswith('gns') and \
77                    ticket.p_session == session:
78                    gns = True
79                if dep_sug and gns:
80                    return True
81        return False
82
83    def _lsfp_penalty_paymentMade(self, student, session):
84        if student.current_mode not in ('hnd_ft', 'nd_ft'):
85            return True
86        if len(student['payments']):
87            for ticket in student['payments'].values():
88                if ticket.p_state == 'paid' and \
89                    ticket.p_category == 'lsfp_penalty' and \
90                    ticket.p_session == session:
91                    return True
92        return False
93
94    def getAccommodationDetails(self, student):
95        """Determine the accommodation data of a student.
96        """
97        d = {}
98        d['error'] = u''
99        hostels = grok.getSite()['hostels']
100        d['booking_session'] = hostels.accommodation_session
101        d['allowed_states'] = hostels.accommodation_states
102        d['startdate'] = hostels.startdate
103        d['enddate'] = hostels.enddate
104        d['expired'] = hostels.expired
105        # Determine bed type
106        bt = 'all'
107        if student.sex == 'f':
108            sex = 'female'
109        else:
110            sex = 'male'
111        special_handling = 'regular'
112        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
113        return d
114
115    def getBedCoordinates(self, bedticket):
116        """Translated coordinates:
117
118        Hall Name, Block Letter, Room Number, Bed Letter =
119        Hall Name, M/F + Room Number-100, Bed Letter
120
121        Requirements: Gender must be part of bed_coordinates and all hostels
122        have only one block with only one floor
123        """
124        ### ToDo
125        return bedticket.bed_coordinates
126
127    def getReturningData(self, student):
128        """ This method defines what happens after school fee payment
129        of returning students depending on the student's senate verdict.
130        """
131        prev_level = student.current_level
132        cur_verdict = student.current_verdict
133        if cur_verdict in ('A','B','L','M','N','Z',):
134            # Successful student
135            new_level = divmod(int(prev_level),100)[0]*100 + 100
136        elif cur_verdict == 'C':
137            # Student on probation
138            new_level = int(prev_level) + 10
139        else:
140            # Student is somehow in an undefined state.
141            # Level has to be set manually.
142            new_level = prev_level
143        new_session = student['studycourse'].current_session + 1
144        return new_session, new_level
145
146    def _isPaymentDisabled(self, p_session, category, student):
147        academic_session = self._getSessionConfiguration(p_session)
148        if category == 'schoolfee':
149            if 'sf_all' in academic_session.payment_disabled:
150                return True
151            if 'sf_spat' in academic_session.payment_disabled and \
152                student.faccode == 'SPAT':
153                return True
154        return False
155
156    def setPaymentDetails(self, category, student,
157            previous_session=None, previous_level=None, combi=[]):
158        """Create a payment ticket and set the payment data of a
159        student for the payment category specified.
160        """
161        p_item = u''
162        amount = 0.0
163        if previous_session:
164            if previous_session < student['studycourse'].entry_session:
165                return _('The previous session must not fall below '
166                         'your entry session.'), None
167            if category == 'schoolfee':
168                # School fee is always paid for the following session
169                if previous_session > student['studycourse'].current_session:
170                    return _('This is not a previous session.'), None
171            else:
172                if previous_session > student['studycourse'].current_session - 1:
173                    return _('This is not a previous session.'), None
174            p_session = previous_session
175            p_level = previous_level
176            p_current = False
177        else:
178            p_session = student['studycourse'].current_session
179            p_level = student['studycourse'].current_level
180            p_current = True
181        academic_session = self._getSessionConfiguration(p_session)
182        if academic_session == None:
183            return _(u'Session configuration object is not available.'), None
184        # Determine fee.
185        if category == 'schoolfee':
186            try:
187                certificate = student['studycourse'].certificate
188                p_item = certificate.code
189            except (AttributeError, TypeError):
190                return _('Study course data are incomplete.'), None
191            if previous_session:
192                # Students can pay for previous sessions in all
193                # workflow states.  Fresh students are excluded by the
194                # update method of the PreviousPaymentAddFormPage.
195                if previous_level == 100:
196                    if local(student):
197                        amount = getattr(certificate, 'school_fee_1', 0.0)
198                    else:
199                        amount = getattr(certificate, 'school_fee_3', 0.0)
200                else:
201                    if local(student):
202                        amount = getattr(certificate, 'school_fee_2', 0.0)
203                    else:
204                        amount = getattr(certificate, 'school_fee_4', 0.0)
205            else:
206                # Students are only allowed to pay school fee
207                # if current session dep_sug_gns payment has been made.
208                if not self._dep_sug_gns_paymentMade(student, student.current_session):
209                    return _('You have to pay NADESU/SA/SUG and GNS Dues first.'), None
210                if student.state == CLEARED:
211                    if local(student):
212                        amount = getattr(certificate, 'school_fee_1', 0.0)
213                    else:
214                        amount = getattr(certificate, 'school_fee_3', 0.0)
215                elif student.state == RETURNING:
216                    # In case of returning school fee payment the
217                    # payment session and level contain the values of
218                    # the session the student has paid for. Payment
219                    # session is always next session.
220                    p_session, p_level = self.getReturningData(student)
221                    academic_session = self._getSessionConfiguration(p_session)
222                    if academic_session == None:
223                        return _(
224                            u'Session configuration object is not available.'
225                            ), None
226                    if p_level in (100, 110, 120, 130) or (
227                        p_level in (300, 310, 320, 330) and
228                        student.current_mode == 'hnd_ft'):
229                        # First-year probating ND students or third year
230                        # probating hnd students are treated like
231                        # fresh students.
232                        if local(student):
233                            amount = getattr(certificate, 'school_fee_1', 0.0)
234                        else:
235                            amount = getattr(certificate, 'school_fee_3', 0.0)
236                    else:
237                        if local(student):
238                            amount = getattr(certificate, 'school_fee_2', 0.0)
239                        else:
240                            amount = getattr(certificate, 'school_fee_4', 0.0)
241                    if student.current_mode.endswith('_we') and p_level in (
242                        300, 310, 320, 330, 600, 610, 620, 630):
243                        amount -= 7000
244                elif student.is_postgrad and student.state == PAID:
245                    # Returning postgraduate students also pay for the
246                    # next session but their level always remains the
247                    # same.
248                    p_session += 1
249                    academic_session = self._getSessionConfiguration(p_session)
250                    if academic_session == None:
251                        return _(
252                            u'Session configuration object is not available.'
253                            ), None
254                    if local(student):
255                        amount = getattr(certificate, 'school_fee_2', 0.0)
256                    else:
257                        amount = getattr(certificate, 'school_fee_4', 0.0)
258                penalty = getattr(academic_session, 'lsfp_penalty_fee')
259                if  penalty and not self._lsfp_penalty_paymentMade(
260                    student, student.current_session):
261                    return _('You have to pay late school fee payment penalty first.'), None
262            if amount:
263                amount += MICROSOFT_FEE(student)
264                amount += DEVELOP_FEE(student)
265                amount += TECH_FEE(student)
266        elif category == 'clearance':
267            try:
268                p_item = student['studycourse'].certificate.code
269            except (AttributeError, TypeError):
270                return _('Study course data are incomplete.'), None
271            if student.current_mode == 'hnd_ft':
272                if local(student):
273                    amount = academic_session.hndlocal_clearance_fee
274                else:
275                    amount = academic_session.hnd_clearance_fee
276            elif student.current_mode == 'nd_ft':
277                if local(student):
278                    amount = academic_session.ndlocal_clearance_fee
279                else:
280                    amount = academic_session.nd_clearance_fee
281            elif student.current_mode == 'hnd_pt':
282                amount = academic_session.hndpt_clearance_fee
283            elif student.current_mode == 'nd_pt':
284                amount = academic_session.ndpt_clearance_fee
285            else:
286                amount = academic_session.clearance_fee
287        elif category == 'bed_allocation':
288            p_item = self.getAccommodationDetails(student)['bt']
289            amount = academic_session.booking_fee
290        elif category == 'hostel_maintenance':
291            amount = 0.0
292            bedticket = student['accommodation'].get(
293                str(student.current_session), None)
294            if bedticket is not None and bedticket.bed is not None:
295                p_item = bedticket.bed_coordinates
296                if bedticket.bed.__parent__.maint_fee > 0:
297                    amount = bedticket.bed.__parent__.maint_fee
298                else:
299                    # fallback
300                    amount = academic_session.maint_fee
301            else:
302                return _(u'No bed allocated.'), None
303        elif category == 'dep_sug':
304            amount = 5150.0 # includes GATEWAY_AMT
305            #if student.faccode == 'SPAT':
306            #    amount = 1650.0 # includes GATEWAY_AMT
307            #    amount = 0.0
308            if student.state == RETURNING and not previous_session:
309                p_session, p_level = self.getReturningData(student)
310        elif category.startswith('gns'):
311            if student.state == RETURNING and not previous_session:
312                p_session, p_level = self.getReturningData(student)
313                fee_name = category + '_fee'
314                academic_session = self._getSessionConfiguration(p_session)
315                amount = getattr(academic_session, fee_name, 0.0)
316            else:
317                fee_name = category + '_fee'
318                amount = getattr(academic_session, fee_name, 0.0)
319        else:
320            fee_name = category + '_fee'
321            amount = getattr(academic_session, fee_name, 0.0)
322        if amount in (0.0, None):
323            return _('Amount could not be determined.'), None
324        if self.samePaymentMade(student, category, p_item, p_session):
325            return _('This type of payment has already been made.'), None
326        if self._isPaymentDisabled(p_session, category, student):
327            return _('This category of payments has been disabled.'), None
328        payment = createObject(u'waeup.StudentOnlinePayment')
329        timestamp = ("%d" % int(time()*10000))[1:]
330        payment.p_id = "p%s" % timestamp
331        payment.p_category = category
332        payment.p_item = p_item
333        payment.p_session = p_session
334        payment.p_level = p_level
335        payment.p_current = p_current
336        payment.amount_auth = amount
337        return None, payment
338
339    def warnCreditsOOR(self, studylevel, course=None):
340        """DSPG requires total credits on semester
341        basis.
342        """
343        total = [0, 0, 0]
344        for ticket in studylevel.values():
345            if ticket.outstanding:
346                continue
347            if not ticket.semester in (1, 2):
348                total[ticket.semester] += ticket.credits
349            else:
350                total[0] += ticket.credits
351        if course:
352            if course.semester == 1 and total[1] + course.credits > 40:
353                return _('Maximum credits (40) in 1st semester exceeded.')
354            if course.semester == 2 and total[2] + course.credits > 40:
355                return _('Maximum credits (40) in 2nd semester exceeded.')
356        else:
357            if total[1] > 40:
358                return _('Maximum credits (40) in 1st semester exceeded.')
359            if total[2] > 40:
360                return _('Maximum credits (40) in 2nd semester exceeded.')
361        return
362
363    #: A tuple containing names of file upload viewlets which are not shown
364    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
365    #: in the base package. This attribute makes only sense, if intermediate
366    #: custom packages are being used, like we do for all Nigerian portals.
367    SKIP_UPLOAD_VIEWLETS = ('acceptanceletterupload',
368                            'higherqualificationresultupload',
369                            'advancedlevelresultupload',
370                            'evidencenameupload',
371                            'refereeletterupload',
372                            'statutorydeclarationupload',
373                            'firstsittingresultupload',
374                            'secondsittingresultupload',
375                            'certificateupload',
376                            'resultstatementupload',
377                            )
378
379    #: A tuple containing the names of registration states in which changing of
380    #: passport pictures is allowed.
381    PORTRAIT_CHANGE_STATES = (ADMITTED, RETURNING,)
382
383    def constructMatricNumber(self, student):
384        #faccode = student.faccode
385        depcode = student.depcode
386        #certcode = student.certcode
387        year = unicode(student.entry_session)[2:]
388        if not student.state in (PAID, ) or not student.is_fresh:
389            return _('Matriculation number cannot be set.'), None
390
391        # ACC/ND/17/00001
392        if student.current_mode == 'nd_ft':
393            next_integer = grok.getSite()['configuration'].next_matric_integer
394            if next_integer == 0:
395                return _('Matriculation number cannot be set.'), None
396            return None, "%s/ND/%s/%05d" % (depcode, year, next_integer)
397
398        # ACC/HND/17/00001
399        if student.current_mode == 'hnd_ft':
400            next_integer = grok.getSite()['configuration'].next_matric_integer_2
401            if next_integer == 0:
402                return _('Matriculation number cannot be set.'), None
403            return None, "%s/HND/%s/%05d" % (depcode, year, next_integer)
404
405        # PT students get the same prefixes but their counter should start
406        # with 10001. This is inconsistent because after 9999 ft students
407        # the ft and pt number ranges will overlap. Issoufou pointed this
408        # out but the director said:
409        # "when the counter gets to 9999 for ft, it can start counting
410        # along where pt is, at that moment."
411
412        # ACC/ND/17/10001
413        if student.current_mode in ('nd_pt, nd_we'):
414            next_integer = grok.getSite()['configuration'].next_matric_integer_3
415            if next_integer == 0:
416                return _('Matriculation number cannot be set.'), None
417            return None, "%s/ND/%s/%05d" % (depcode, year, next_integer)
418        # ACC/HND/17/10001
419        if student.current_mode in ('hnd_pt, hnd_we'):
420            next_integer = grok.getSite()['configuration'].next_matric_integer_4
421            if next_integer == 0:
422                return _('Matriculation number cannot be set.'), None
423            return None, "%s/HND/%s/%05d" % (depcode, year, next_integer)
424
425        return _('Matriculation number cannot be set.'), None
426
427    def increaseMatricInteger(self, student):
428        """Increase counter for matric numbers.
429        """
430        if student.current_mode == 'nd_ft':
431            grok.getSite()['configuration'].next_matric_integer += 1
432            return
433        elif student.current_mode == 'hnd_ft':
434            grok.getSite()['configuration'].next_matric_integer_2 += 1
435            return
436        elif student.current_mode in ('nd_pt, nd_we'):
437            grok.getSite()['configuration'].next_matric_integer_3 += 1
438            return
439        elif student.current_mode in ('hnd_pt, hnd_we'):
440            grok.getSite()['configuration'].next_matric_integer_4 += 1
441            return
442        return
Note: See TracBrowser for help on using the repository browser.