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

Last change on this file since 16753 was 16752, checked in by Henrik Bettermann, 3 years ago

ND PT and HND PT acceptance fees differ.

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