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

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

Move penalty fee if statement.

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