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

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

Rework the entire schoolfee determination and split payment algorithm.

  • Property svn:keywords set to Id
File size: 18.6 KB
Line 
1## $Id: utils.py 16760 2022-01-28 08:17:50Z 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.is_fresh:
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.is_fresh:
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                penalty = getattr(academic_session, 'lsfp_penalty_fee')
191                if  penalty and not self._lsfp_penalty_paymentMade(
192                    student, student.current_session):
193                    return _('You have to pay late school fee payment penalty first.'), None
194                if student.state == CLEARED:
195                    if local(student):
196                        amount = getattr(certificate, 'school_fee_1', 0.0)
197                    else:
198                        amount = getattr(certificate, 'school_fee_3', 0.0)
199                elif student.state == RETURNING:
200                    # In case of returning school fee payment the
201                    # payment session and level contain the values of
202                    # the session the student has paid for. Payment
203                    # session is always next session.
204                    p_session, p_level = self.getReturningData(student)
205                    academic_session = self._getSessionConfiguration(p_session)
206                    if academic_session == None:
207                        return _(
208                            u'Session configuration object is not available.'
209                            ), None
210                    if p_level in (100, 110, 120, 130) or (
211                        p_level in (300, 310, 320, 330) and
212                        student.current_mode == 'hnd_ft'):
213                        # First-year probating ND students or third year
214                        # probating hnd students are treated like
215                        # fresh students.
216                        if local(student):
217                            amount = getattr(certificate, 'school_fee_1', 0.0)
218                        else:
219                            amount = getattr(certificate, 'school_fee_3', 0.0)
220                    else:
221                        if local(student):
222                            amount = getattr(certificate, 'school_fee_2', 0.0)
223                        else:
224                            amount = getattr(certificate, 'school_fee_4', 0.0)
225                    if student.current_mode.endswith('_we') and p_level in (
226                        300, 310, 320, 330, 600, 610, 620, 630):
227                        amount -= 7000
228                elif student.is_postgrad and student.state == PAID:
229                    # Returning postgraduate students also pay for the
230                    # next session but their level always remains the
231                    # same.
232                    p_session += 1
233                    academic_session = self._getSessionConfiguration(p_session)
234                    if academic_session == None:
235                        return _(
236                            u'Session configuration object is not available.'
237                            ), None
238                    if local(student):
239                        amount = getattr(certificate, 'school_fee_2', 0.0)
240                    else:
241                        amount = getattr(certificate, 'school_fee_4', 0.0)
242            amount += MICROSOFT_FEE(student)
243            amount += DEVELOP_FEE(student)
244            amount += TECH_FEE(student)
245        elif category == 'clearance':
246            try:
247                p_item = student['studycourse'].certificate.code
248            except (AttributeError, TypeError):
249                return _('Study course data are incomplete.'), None
250            if student.current_mode == 'hnd_ft':
251                if local(student):
252                    amount = academic_session.hndlocal_clearance_fee
253                else:
254                    amount = academic_session.hnd_clearance_fee
255            elif student.current_mode == 'nd_ft':
256                if local(student):
257                    amount = academic_session.ndlocal_clearance_fee
258                else:
259                    amount = academic_session.nd_clearance_fee
260            elif student.current_mode == 'hnd_pt':
261                amount = academic_session.hndpt_clearance_fee
262            elif student.current_mode == 'nd_pt':
263                amount = academic_session.ndpt_clearance_fee
264            else:
265                amount = academic_session.clearance_fee
266        elif category == 'bed_allocation':
267            p_item = self.getAccommodationDetails(student)['bt']
268            amount = academic_session.booking_fee
269        elif category == 'hostel_maintenance':
270            amount = 0.0
271            bedticket = student['accommodation'].get(
272                str(student.current_session), None)
273            if bedticket is not None and bedticket.bed is not None:
274                p_item = bedticket.bed_coordinates
275                if bedticket.bed.__parent__.maint_fee > 0:
276                    amount = bedticket.bed.__parent__.maint_fee
277                else:
278                    # fallback
279                    amount = academic_session.maint_fee
280            else:
281                return _(u'No bed allocated.'), None
282        elif category == 'dep_sug':
283            amount = 3150.0 # includes GATEWAY_AMT
284            #if student.faccode == 'SPAT':
285            #    amount = 1650.0 # includes GATEWAY_AMT
286            #    amount = 0.0
287            if student.state == RETURNING and not previous_session:
288                p_session, p_level = self.getReturningData(student)
289        elif category.startswith('gns'):
290            if student.state == RETURNING and not previous_session:
291                p_session, p_level = self.getReturningData(student)
292                fee_name = category + '_fee'
293                academic_session = self._getSessionConfiguration(p_session)
294                amount = getattr(academic_session, fee_name, 0.0)
295            else:
296                fee_name = category + '_fee'
297                amount = getattr(academic_session, fee_name, 0.0)
298        else:
299            fee_name = category + '_fee'
300            amount = getattr(academic_session, fee_name, 0.0)
301        if amount in (0.0, None):
302            return _('Amount could not be determined.'), None
303        if self.samePaymentMade(student, category, p_item, p_session):
304            return _('This type of payment has already been made.'), None
305        if self._isPaymentDisabled(p_session, category, student):
306            return _('This category of payments has been disabled.'), None
307        payment = createObject(u'waeup.StudentOnlinePayment')
308        timestamp = ("%d" % int(time()*10000))[1:]
309        payment.p_id = "p%s" % timestamp
310        payment.p_category = category
311        payment.p_item = p_item
312        payment.p_session = p_session
313        payment.p_level = p_level
314        payment.p_current = p_current
315        payment.amount_auth = amount
316        return None, payment
317
318    def warnCreditsOOR(self, studylevel, course=None):
319        """DSPG requires total credits on semester
320        basis.
321        """
322        total = [0, 0, 0]
323        for ticket in studylevel.values():
324            if ticket.outstanding:
325                continue
326            if not ticket.semester in (1, 2):
327                total[ticket.semester] += ticket.credits
328            else:
329                total[0] += ticket.credits
330        if course:
331            if course.semester == 1 and total[1] + course.credits > 40:
332                return _('Maximum credits (40) in 1st semester exceeded.')
333            if course.semester == 2 and total[2] + course.credits > 40:
334                return _('Maximum credits (40) in 2nd semester exceeded.')
335        else:
336            if total[1] > 40:
337                return _('Maximum credits (40) in 1st semester exceeded.')
338            if total[2] > 40:
339                return _('Maximum credits (40) in 2nd semester exceeded.')
340        return
341
342    #: A tuple containing names of file upload viewlets which are not shown
343    #: on the `StudentClearanceManageFormPage`. Nothing is being skipped
344    #: in the base package. This attribute makes only sense, if intermediate
345    #: custom packages are being used, like we do for all Nigerian portals.
346    SKIP_UPLOAD_VIEWLETS = ('acceptanceletterupload',
347                            'higherqualificationresultupload',
348                            'advancedlevelresultupload',
349                            'evidencenameupload',
350                            'refereeletterupload',
351                            'statutorydeclarationupload',
352                            'firstsittingresultupload',
353                            'secondsittingresultupload',
354                            'certificateupload',
355                            'resultstatementupload',
356                            )
357
358    #: A tuple containing the names of registration states in which changing of
359    #: passport pictures is allowed.
360    PORTRAIT_CHANGE_STATES = (ADMITTED, RETURNING,)
361
362    def constructMatricNumber(self, student):
363        #faccode = student.faccode
364        depcode = student.depcode
365        #certcode = student.certcode
366        year = unicode(student.entry_session)[2:]
367        if not student.state in (PAID, ) or not student.is_fresh:
368            return _('Matriculation number cannot be set.'), None
369
370        # ACC/ND/17/00001
371        if student.current_mode == 'nd_ft':
372            next_integer = grok.getSite()['configuration'].next_matric_integer
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
377        # ACC/HND/17/00001
378        if student.current_mode == 'hnd_ft':
379            next_integer = grok.getSite()['configuration'].next_matric_integer_2
380            if next_integer == 0:
381                return _('Matriculation number cannot be set.'), None
382            return None, "%s/HND/%s/%05d" % (depcode, year, next_integer)
383
384        # PT students get the same prefixes but their counter should start
385        # with 10001. This is inconsistent because after 9999 ft students
386        # the ft and pt number ranges will overlap. Issoufou pointed this
387        # out but the director said:
388        # "when the counter gets to 9999 for ft, it can start counting
389        # along where pt is, at that moment."
390
391        # ACC/ND/17/10001
392        if student.current_mode in ('nd_pt, nd_we'):
393            next_integer = grok.getSite()['configuration'].next_matric_integer_3
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        # ACC/HND/17/10001
398        if student.current_mode in ('hnd_pt, hnd_we'):
399            next_integer = grok.getSite()['configuration'].next_matric_integer_4
400            if next_integer == 0:
401                return _('Matriculation number cannot be set.'), None
402            return None, "%s/HND/%s/%05d" % (depcode, year, next_integer)
403
404        return _('Matriculation number cannot be set.'), None
405
406    def increaseMatricInteger(self, student):
407        """Increase counter for matric numbers.
408        """
409        if student.current_mode == 'nd_ft':
410            grok.getSite()['configuration'].next_matric_integer += 1
411            return
412        elif student.current_mode == 'hnd_ft':
413            grok.getSite()['configuration'].next_matric_integer_2 += 1
414            return
415        elif student.current_mode in ('nd_pt, nd_we'):
416            grok.getSite()['configuration'].next_matric_integer_3 += 1
417            return
418        elif student.current_mode in ('hnd_pt, hnd_we'):
419            grok.getSite()['configuration'].next_matric_integer_4 += 1
420            return
421        return
Note: See TracBrowser for help on using the repository browser.