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

Last change on this file since 16326 was 16319, checked in by Henrik Bettermann, 4 years ago

GNS dues are no longer required.

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