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

Last change on this file since 13881 was 13653, checked in by Henrik Bettermann, 9 years ago

Carryover payments must not trigger workflow transitions.

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