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

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

DSPG requires total credits on semester basis.

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