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

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

Enable matriculation number assignment.

  • Property svn:keywords set to Id
File size: 14.8 KB
Line 
1## $Id: utils.py 13536 2015-12-07 14:38: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
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            category = 'schoolfee'
265        if self._isPaymentDisabled(p_session, category, student):
266            return _('Payment temporarily disabled.'), None
267        payment = createObject(u'waeup.StudentOnlinePayment')
268        timestamp = ("%d" % int(time()*10000))[1:]
269        payment.p_id = "p%s" % timestamp
270        payment.p_category = category
271        payment.p_item = p_item
272        payment.p_session = p_session
273        payment.p_level = p_level
274        payment.p_current = p_current
275        payment.amount_auth = float(amount)
276        return None, payment
277
278    def getAccommodationDetails(self, student):
279        """Determine the accommodation data of a student.
280        """
281        d = {}
282        d['error'] = u''
283        hostels = grok.getSite()['hostels']
284        d['booking_session'] = hostels.accommodation_session
285        d['allowed_states'] = hostels.accommodation_states
286        d['startdate'] = hostels.startdate
287        d['enddate'] = hostels.enddate
288        d['expired'] = hostels.expired
289        # Determine bed type
290        studycourse = student['studycourse']
291        certificate = getattr(studycourse,'certificate',None)
292        current_level = studycourse.current_level
293        if None in (current_level, certificate):
294            return d
295        end_level = certificate.end_level
296        if current_level == 10:
297            bt = 'pr'
298        elif current_level in (100, 400):
299            bt = 'fr'
300        elif current_level in (300, 600):
301            bt = 'fi'
302        else:
303            bt = 're'
304        if student.sex == 'f':
305            sex = 'female'
306        else:
307            sex = 'male'
308        special_handling = 'regular'
309        if student.faccode == 'IOT':
310            special_handling = 'iot'
311        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
312        return d
313
314    def increaseMatricInteger(self, student):
315        """We don't use a persistent counter in Kwarapoly.
316        """
317        return
318
319    def constructMatricNumber(self, student):
320        """Fetch the matric number counter which fits the student and
321        construct the new matric number of the student.
322
323        A typical matriculation number is like this: ND/14/STA/FT/015
324
325        ND = Study Mode Prefix
326        14 = Year of Entry
327        STA = Department Code
328        FT = Study Mode Suffix
329        015 = Serial Number (Every programme starts from "001" every
330        session and the numbers build up arithmetically)
331        """
332        cert = getattr(student.get('studycourse', None), 'certificate', None)
333        entry_session = getattr(
334            student.get('studycourse', None), 'entry_session', None)
335        entry_mode = getattr(
336            student.get('studycourse', None), 'entry_mode', None)
337        if entry_session < 2015:
338            return _('Available from session 2015/2016'), None
339        if student.state not in (PAID, ):
340            return _('Wrong state.'), None
341        if None in (cert, entry_session, entry_mode):
342            return _('Matriculation number cannot be set.'), None
343        try:
344            (prefix, suffix) = entry_mode.split('_')
345            (prefix, suffix) = (prefix.upper(), suffix.upper())
346        except ValueError:
347            return _('Matriculation number cannot be set.'), None
348        entry_year = entry_session - 100*(entry_session/100)
349        part1 =  "%s/%s/%s/%s/" % (
350            prefix, entry_year, student.depcode, suffix)
351        cat = queryUtility(ICatalog, name='students_catalog')
352        results = cat.searchResults(matric_number=(part1+'0000', part1+'9999'))
353        if not len(results):
354            return None, part1 + '001'
355        try:
356            numbers = [int(i.matric_number.replace(part1, ''))
357                       for i in results if i.matric_number]
358        except ValueError:
359            return _('Matriculation number cannot be determined.'), None
360        counter = max(numbers) + 1
361        matric_number = "%s%03d" % (part1, counter)
362        return None, matric_number
363
364    PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARED, RETURNING)
365
366    # KwaraPoly prefix
367    STUDENT_ID_PREFIX = u'W'
Note: See TracBrowser for help on using the repository browser.