source: main/waeup.kwarapoly/trunk/src/waeup/kwarapoly/students/utils.py @ 17939

Last change on this file since 17939 was 14616, checked in by Henrik Bettermann, 8 years ago

Enable balance payments. Add gateway amount to balance amount. No other surcharges.

  • Property svn:keywords set to Id
File size: 16.0 KB
Line 
1## $Id: utils.py 14616 2017-03-09 10:42:42Z 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
19import random
20from time import time
21from zope.component import createObject, getUtility, queryUtility
22from zope.catalog.interfaces import ICatalog
23from waeup.kofa.interfaces import CLEARED, RETURNING, PAID
24from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
25from waeup.kofa.accesscodes import create_accesscode
26from waeup.kofa.interfaces import (
27    CLEARED, RETURNING, ADMITTED, PAID, IKofaUtils)
28from waeup.kofa.fees import FeeTable
29from waeup.kofa.hostels.hostel import NOT_OCCUPIED
30from waeup.kwarapoly.interswitch.browser import GATEWAY_AMT
31from waeup.kwarapoly.interfaces import MessageFactory as _
32
33
34# 10  = PreND (1)
35# 100 = ND1 (2)
36# 110 = ND1R (3)
37# 200 = ND2 (4)
38# 210 = ND2R (5)
39# 300 = ND3 (6)
40# 400 = HND1 (7)
41# 410 = HND1R (8)
42# 500 = HND2 (9)
43# 510 = HND2R (10)
44# 600 = HND3 (11)
45# 999 = PGD (12)
46
47PAYMENT_LEVELS = (10, 100, 110, 200, 210, 300, 400, 410, 500, 510, 600, 999)
48
49FEES_PARAMS = (
50        ('ft', 'we'),
51        ('local', 'non-local'),
52        ('science','arts'),
53        PAYMENT_LEVELS
54    )
55
56FEES_VALUES = (
57        (
58          ( # 10       100      110      200      210     300    400     410      500      510     600   999
59            (34500.0, 55500.0, 34600.0, 32500.0, 34600.0, 0.0, 62000.0, 36600.0, 35700.0, 36600.0, 0.0, 48750.0), # science
60            (34500.0, 52500.0, 31600.0, 29500.0, 31600.0, 0.0, 59000.0, 33600.0, 32700.0, 33600.0, 0.0, 47200.0)  # arts
61          ), # local
62          ( # 10       100      110      200      210     300    400     410      500      510     600   999
63            (49600.0, 75500.0, 42100.0, 35090.0, 42100.0, 0.0, 79000.0, 46600.0, 38900.0, 46600.0, 0.0, 63180.0), # science
64            (49600.0, 72500.0, 39100.0, 32090.0, 39100.0, 0.0, 76000.0, 43600.0, 35900.0, 43600.0, 0.0, 61680.0)  # arts
65          ), # non-local
66        ), # ft
67        (
68          ( # 10    100       110    200      210      300      400        410    500     510       600     999
69            (0.0, 56000.0, 34600.0, 34400.0, 34600.0, 34400.0, 56500.0, 36600.0, 36500.0, 36600.0, 36500.0, 0.0), # science
70            (0.0, 53000.0, 31600.0, 31400.0, 31600.0, 31400.0, 53500.0, 33600.0, 33500.0, 33600.0, 33500.0, 0.0)  # arts
71          ), # local
72          ( # 10   100         110    200       210      300      400     410      500     510      600     999
73            (0.0, 73000.0, 42100.0, 37350.0, 42100.0, 37350.0, 78000.0, 46600.0, 46850.0, 46600.0, 46850.0, 0.0), # science
74            (0.0, 70000.0, 39100.0, 34350.0, 39100.0, 34350.0, 75000.0, 43600.0, 43850.0, 43600.0, 43850.0, 0.0)  # arts
75          ), # non-local
76        ), # we
77    )
78
79SCHOOL_FEES = FeeTable(FEES_PARAMS, FEES_VALUES)
80
81def local_nonlocal(student):
82    lga = getattr(student, 'lga')
83    if lga and lga.startswith('kwara'):
84        return 'local'
85    else:
86        return 'non-local'
87
88def arts_science(student):
89    if student.faccode == 'IFMS':
90        return 'arts'
91    else:
92        return 'science'
93
94def we_ft(student):
95    if student.current_mode.endswith('we'):
96        return 'we'
97    else:
98        return 'ft'
99
100class CustomStudentsUtils(NigeriaStudentsUtils):
101    """A collection of customized methods.
102
103    """
104
105    def warnCreditsOOR(self, studylevel, course=None):
106        # Students do not have any credit load limit
107        return
108
109    def selectBed(self, available_beds, desired_hostel):
110        """Randomly select a bed from a list of available beds.
111        """
112        return random.choice(available_beds)
113
114    def getReturningData(self, student):
115        """ This method defines what happens after school fee payment
116        of returning students depending on the student's senate verdict.
117        """
118        prev_level = student['studycourse'].current_level
119        cur_verdict = student['studycourse'].current_verdict
120        if cur_verdict in ('A','B','L','M','N','Z',):
121            # Successful student
122            new_level = divmod(int(prev_level),100)[0]*100 + 100
123        elif cur_verdict == 'C':
124            # Student on probation
125            new_level = int(prev_level) + 10
126        else:
127            # Student is somehow in an undefined state.
128            # Level has to be set manually.
129            new_level = prev_level
130        new_session = student['studycourse'].current_session + 1
131        return new_session, new_level
132
133    def _maintPaymentMade(self, student, session):
134        if len(student['payments']):
135            for ticket in student['payments'].values():
136                if ticket.p_category == 'hostel_maintenance' and \
137                    ticket.p_session == session and ticket.p_state == 'paid':
138                        return True
139        return False
140
141    def _bedAvailable(self, student):
142        acc_details  = self.getAccommodationDetails(student)
143        cat = getUtility(ICatalog, name='beds_catalog')
144        entries = cat.searchResults(
145            owner=(student.student_id,student.student_id))
146        if len(entries):
147            # Bed has already been booked.
148            return True
149        entries = cat.searchResults(
150            bed_type=(acc_details['bt'],acc_details['bt']))
151        available_beds = [
152            entry for entry in entries if entry.owner == NOT_OCCUPIED]
153        if available_beds:
154            # Bed has not yet been booked but beds are available.
155            return True
156        return False
157
158    def _isPaymentDisabled(self, p_session, category, student):
159        academic_session = self._getSessionConfiguration(p_session)
160        if category == 'schoolfee':
161            if 'sf_all' in academic_session.payment_disabled:
162                return True
163            if 'sf_non_pg' in academic_session.payment_disabled and \
164                not student.is_postgrad:
165                return True
166        return False
167
168    def setPaymentDetails(self, category, student,
169            previous_session=None, previous_level=None):
170        """Create Payment object and set the payment data of a student for
171        the payment category specified.
172
173        """
174        details = {}
175        p_item = u''
176        amount = 0.0
177        error = u''
178        if previous_session:
179            return _('Previous session payment not yet implemented.'), None
180        p_session = student['studycourse'].current_session
181        p_level = student['studycourse'].current_level
182        p_current = True
183        academic_session = self._getSessionConfiguration(p_session)
184        if academic_session == None:
185            return _(u'Session configuration object is not available.'), None
186        # Determine fee.
187        if category == 'transfer':
188            amount = academic_session.transfer_fee
189        elif category == 'gown':
190            amount = academic_session.gown_fee
191        elif category == 'bed_allocation':
192            amount = academic_session.booking_fee
193        elif category == 'hostel_maintenance':
194            amount = 0.0
195            bedticket = student['accommodation'].get(
196                str(student.current_session), None)
197            if bedticket is not None and bedticket.bed is not None:
198                p_item = bedticket.bed_coordinates
199                if bedticket.bed.__parent__.maint_fee > 0:
200                    amount = bedticket.bed.__parent__.maint_fee
201                else:
202                    # fallback
203                    amount = academic_session.maint_fee
204            else:
205                return _(u'No bed allocated.'), None
206        elif category == 'clearance':
207            amount = academic_session.clearance_fee
208            try:
209                p_item = student['studycourse'].certificate.code
210            except (AttributeError, TypeError):
211                return _('Study course data are incomplete.'), None
212        elif category == 'schoolfee':
213            try:
214                certificate = student['studycourse'].certificate
215                p_item = certificate.code
216            except (AttributeError, TypeError):
217                return _('Study course data are incomplete.'), None
218            if student.state == RETURNING:
219                # Override p_session and p_level
220                p_session, p_level = self.getReturningData(student)
221                academic_session = self._getSessionConfiguration(p_session)
222                if academic_session == None:
223                    return _(u'Session configuration object '
224                              'is not available.'), None
225            if student.state == CLEARED and student.current_mode in (
226                                                            'hnd_ft', 'nd_ft'):
227                # Fresh students must have booked and paid for accommodation.
228                if self._bedAvailable(student):
229                    if not student.faccode == 'IOT' and \
230                        not self._maintPaymentMade(student, p_session):
231                        return _('Book and pay for accommodation first '
232                                 'before making school fee payments.'), None
233            if student.state in (RETURNING, CLEARED):
234                if p_level in PAYMENT_LEVELS:
235                    amount = SCHOOL_FEES.get_fee(
236                        (we_ft(student),
237                         local_nonlocal(student),
238                         arts_science(student),
239                         p_level)
240                        )
241        elif category == 'carryover1':
242            amount = 6000.0
243        elif category == 'carryover2':
244            amount = 10000.0
245        elif category == 'carryover3':
246            amount = 15000.0
247
248        else:
249            fee_name = category + '_fee'
250            amount = getattr(academic_session, fee_name, 0.0)
251        if amount in (0.0, None):
252            return _(u'Amount could not be determined.'), None
253        if self.samePaymentMade(student, category, p_item, p_session):
254            return _('This type of payment has already been made.'), None
255        # Add session specific penalty fee.
256        if category == 'schoolfee' and student.is_postgrad:
257            amount += academic_session.penalty_pg
258        elif category == 'schoolfee':
259            amount += academic_session.penalty_ug
260        # Recategorize carryover fees.
261        if category.startswith('carryover'):
262            p_item = getUtility(IKofaUtils).PAYMENT_CATEGORIES[category]
263            p_item = unicode(p_item)
264            # Now we change the category to reduce the number of categories.
265            # Disabled on 2016/02/04
266            #category = 'schoolfee'
267        if self._isPaymentDisabled(p_session, category, student):
268            return _('This category of payments has been disabled.'), None
269        payment = createObject(u'waeup.StudentOnlinePayment')
270        timestamp = ("%d" % int(time()*10000))[1:]
271        payment.p_id = "p%s" % timestamp
272        payment.p_category = category
273        payment.p_item = p_item
274        payment.p_session = p_session
275        payment.p_level = p_level
276        payment.p_current = p_current
277        payment.amount_auth = float(amount)
278        return None, payment
279
280    def setBalanceDetails(self, category, student,
281            balance_session, balance_level, balance_amount):
282        """Create a balance payment ticket and set the payment data
283        as selected by the student. Kwarapoly: Add Interswitch surcharge
284        """
285        p_item = u'Balance'
286        p_session = balance_session
287        p_level = balance_level
288        p_current = False
289        amount = balance_amount
290        academic_session = self._getSessionConfiguration(p_session)
291        if academic_session == None:
292            return _(u'Session configuration object is not available.'), None
293        if amount in (0.0, None) or amount < 0:
294            return _('Amount must be greater than 0.'), None
295        payment = createObject(u'waeup.StudentOnlinePayment')
296        timestamp = ("%d" % int(time()*10000))[1:]
297        payment.p_id = "p%s" % timestamp
298        payment.p_category = category
299        payment.p_item = p_item
300        payment.p_session = p_session
301        payment.p_level = p_level
302        payment.p_current = p_current
303        payment.amount_auth = amount + GATEWAY_AMT
304        return None, payment
305
306    def getAccommodationDetails(self, student):
307        """Determine the accommodation data of a student.
308        """
309        d = {}
310        d['error'] = u''
311        hostels = grok.getSite()['hostels']
312        d['booking_session'] = hostels.accommodation_session
313        d['allowed_states'] = hostels.accommodation_states
314        d['startdate'] = hostels.startdate
315        d['enddate'] = hostels.enddate
316        d['expired'] = hostels.expired
317        # Determine bed type
318        studycourse = student['studycourse']
319        certificate = getattr(studycourse,'certificate',None)
320        current_level = studycourse.current_level
321        if None in (current_level, certificate):
322            return d
323        end_level = certificate.end_level
324        if current_level == 10:
325            bt = 'pr'
326        elif current_level in (100, 400):
327            bt = 'fr'
328        elif current_level in (300, 600):
329            bt = 'fi'
330        else:
331            bt = 're'
332        if student.sex == 'f':
333            sex = 'female'
334        else:
335            sex = 'male'
336        special_handling = 'regular'
337        if student.faccode == 'IOT':
338            special_handling = 'iot'
339        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
340        return d
341
342    def increaseMatricInteger(self, student):
343        """We don't use a persistent counter in Kwarapoly.
344        """
345        return
346
347    def constructMatricNumber(self, student):
348        """Fetch the matric number counter which fits the student and
349        construct the new matric number of the student.
350
351        A typical matriculation number is like this: ND/14/STA/FT/015
352
353        ND = Study Mode Prefix
354        14 = Year of Entry
355        STA = Department Code
356        FT = Study Mode Suffix
357        015 = Serial Number (Every programme starts from "001" every
358        session and the numbers build up arithmetically)
359        """
360        cert = getattr(student.get('studycourse', None), 'certificate', None)
361        entry_session = getattr(
362            student.get('studycourse', None), 'entry_session', None)
363        entry_mode = getattr(
364            student.get('studycourse', None), 'entry_mode', None)
365        if entry_session < 2015:
366            return _('Available from session 2015/2016'), None
367        if student.state not in (PAID, ):
368            return _('Wrong state.'), None
369        if None in (cert, entry_session, entry_mode):
370            return _('Matriculation number cannot be set.'), None
371        try:
372            (prefix, suffix) = entry_mode.split('_')
373            (prefix, suffix) = (prefix.upper(), suffix.upper())
374        except ValueError:
375            return _('Matriculation number cannot be set.'), None
376        entry_year = entry_session - 100*(entry_session/100)
377        part1 =  "%s/%s/%s/%s/" % (
378            prefix, entry_year, student.depcode, suffix)
379        cat = queryUtility(ICatalog, name='students_catalog')
380        results = cat.searchResults(matric_number=(part1+'0000', part1+'9999'))
381        if not len(results):
382            return None, part1 + '001'
383        try:
384            numbers = [int(i.matric_number.replace(part1, ''))
385                       for i in results if i.matric_number]
386        except ValueError:
387            return _('Matriculation number cannot be determined.'), None
388        counter = max(numbers) + 1
389        matric_number = "%s%03d" % (part1, counter)
390        return None, matric_number
391
392    PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARED, RETURNING)
393
394    # KwaraPoly prefix
395    STUDENT_ID_PREFIX = u'W'
Note: See TracBrowser for help on using the repository browser.