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

Last change on this file since 13095 was 12911, checked in by Henrik Bettermann, 10 years ago

Matric number assignment is available only from session 2015/2016.

  • Property svn:keywords set to Id
File size: 15.3 KB
Line 
1## $Id: utils.py 12911 2015-05-07 08:41:01Z 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
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', 'pt'),
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, 39400.0, 28800.0, 30500.0, 28800.0, 0.0, 40000.0, 29900.0, 33700.0, 29900.0, 0.0, 48750.0), # science
59            (34500.0, 37900.0, 27300.0, 29000.0, 27300.0, 0.0, 38500.0, 28400.0, 32200.0, 28400.0, 0.0, 47200.0)  # arts
60          ), # local
61          ( # 10       100      110      200      210     300    400     410      500      510     600   999
62            (49600.0, 53900.0, 35900.0, 33090.0, 35900.0, 0.0, 56400.0, 38600.0, 36900.0, 38600.0, 0.0, 63180.0), # science
63            (49600.0, 52400.0, 34400.0, 31590.0, 34400.0, 0.0, 54900.0, 37100.0, 35400.0, 37100.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, 40700.0, 28800.0, 30900.0, 28800.0, 30900.0, 41100.0, 29900.0, 33050.0, 29900.0, 33050.0, 0.0), # science
69            (0.0, 39200.0, 27300.0, 29400.0, 27300.0, 29400.0, 39600.0, 28400.0, 31550.0, 28400.0, 31550.0, 0.0)  # arts
70          ), # local
71          ( # 10   100         110    200       210      300      400     410      500     510      600     999
72            (0.0, 55400.0, 35900.0, 34850.0, 35900.0, 34850.0, 57800.0, 38600.0, 44350.0, 38600.0, 44350.0, 0.0), # science
73            (0.0, 53900.0, 34400.0, 33350.0, 34400.0, 33350.0, 56300.0, 37100.0, 42850.0, 37100.0, 42850.0, 0.0)  # arts
74          ), # non-local
75        ), # pt
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 pt_ft(student):
94    if student.current_mode.endswith('pt'):
95        return 'pt'
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):
109        """Randomly select a bed from a list of available beds.
110
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
135        # Maint payment checking disabled on 5th Dec 2014
136        return True
137
138        #if len(student['payments']):
139        #    for ticket in student['payments'].values():
140        #        if ticket.p_category == 'hostel_maintenance' and \
141        #            ticket.p_session == session and ticket.p_state == 'paid':
142        #                return True
143        #return False
144
145    def _bedAvailable(self, student):
146        acc_details  = self.getAccommodationDetails(student)
147        cat = getUtility(ICatalog, name='beds_catalog')
148        entries = cat.searchResults(
149            owner=(student.student_id,student.student_id))
150        if len(entries):
151            # Bed has already been booked.
152            return True
153        entries = cat.searchResults(
154            bed_type=(acc_details['bt'],acc_details['bt']))
155        available_beds = [
156            entry for entry in entries if entry.owner == NOT_OCCUPIED]
157        if available_beds:
158            # Bed has not yet been booked but beds are available.
159            return True
160        return False
161
162    def _isPaymentDisabled(self, p_session, category, student):
163        academic_session = self._getSessionConfiguration(p_session)
164        if category == 'schoolfee':
165            if 'sf_all' in academic_session.payment_disabled:
166                return True
167            if 'sf_non_pg' in academic_session.payment_disabled and \
168                not student.is_postgrad:
169                return True
170        return False
171
172    def setPaymentDetails(self, category, student,
173            previous_session=None, previous_level=None):
174        """Create Payment object and set the payment data of a student for
175        the payment category specified.
176
177        """
178        details = {}
179        p_item = u''
180        amount = 0.0
181        error = u''
182        if previous_session:
183            return _('Previous session payment not yet implemented.'), None
184        p_session = student['studycourse'].current_session
185        p_level = student['studycourse'].current_level
186        p_current = True
187        academic_session = self._getSessionConfiguration(p_session)
188        if academic_session == None:
189            return _(u'Session configuration object is not available.'), None
190        # Determine fee.
191        if category == 'transfer':
192            amount = academic_session.transfer_fee
193        elif category == 'gown':
194            amount = academic_session.gown_fee
195        elif category == 'bed_allocation':
196            amount = academic_session.booking_fee
197        elif category == 'hostel_maintenance':
198            amount = 0.0
199            bedticket = student['accommodation'].get(
200                str(student.current_session), None)
201            if bedticket:
202                p_item = bedticket.bed_coordinates
203                if bedticket.bed.__parent__.maint_fee > 0:
204                    amount = bedticket.bed.__parent__.maint_fee
205                else:
206                    # fallback
207                    amount = academic_session.maint_fee
208            else:
209                # Should not happen because this is already checked
210                # in the browser module, but anyway ...
211                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
212                p_item = trans(_('no bed allocated'), portal_language)
213        elif category == 'clearance':
214            amount = academic_session.clearance_fee
215            try:
216                p_item = student['studycourse'].certificate.code
217            except (AttributeError, TypeError):
218                return _('Study course data are incomplete.'), None
219        elif category == 'schoolfee':
220            try:
221                certificate = student['studycourse'].certificate
222                p_item = certificate.code
223            except (AttributeError, TypeError):
224                return _('Study course data are incomplete.'), None
225            if student.state == RETURNING:
226                # Override p_session and p_level
227                p_session, p_level = self.getReturningData(student)
228                academic_session = self._getSessionConfiguration(p_session)
229                if academic_session == None:
230                    return _(u'Session configuration object '
231                              'is not available.'), None
232            if student.state == CLEARED and student.current_mode in (
233                                                            'hnd_ft', 'nd_ft'):
234                # Fresh students must have booked and paid for accommodation.
235                if self._bedAvailable(student):
236                    if not student.faccode == 'IOT' and \
237                        not self._maintPaymentMade(student, p_session):
238                        return _('Book and pay for accommodation first '
239                                 'before making school fee payments.'), None
240            if student.state in (RETURNING, CLEARED):
241                if p_level in PAYMENT_LEVELS:
242                    amount = SCHOOL_FEES.get_fee(
243                        (pt_ft(student),
244                         local_nonlocal(student),
245                         arts_science(student),
246                         p_level)
247                        )
248        elif category == 'carryover1':
249            amount = 6000.0
250        elif category == 'carryover2':
251            amount = 7000.0
252        elif category == 'carryover3':
253            amount = 8000.0
254
255        else:
256            fee_name = category + '_fee'
257            amount = getattr(academic_session, fee_name, 0.0)
258        if amount in (0.0, None):
259            return _(u'Amount could not be determined.'), None
260        if self.samePaymentMade(student, category, p_item, p_session):
261            return _('This type of payment has already been made.'), None
262        # Add session specific penalty fee.
263        if category == 'schoolfee' and student.is_postgrad:
264            amount += academic_session.penalty_pg
265        elif category == 'schoolfee':
266            amount += academic_session.penalty_ug
267        # Recategorize carryover fees.
268        if category.startswith('carryover'):
269            p_item = getUtility(IKofaUtils).PAYMENT_CATEGORIES[category]
270            p_item = unicode(p_item)
271            # Now we change the category to reduce the number of categories.
272            category = 'schoolfee'
273        if self._isPaymentDisabled(p_session, category, student):
274            return _('Payment temporarily disabled.'), None
275        payment = createObject(u'waeup.StudentOnlinePayment')
276        timestamp = ("%d" % int(time()*10000))[1:]
277        payment.p_id = "p%s" % timestamp
278        payment.p_category = category
279        payment.p_item = p_item
280        payment.p_session = p_session
281        payment.p_level = p_level
282        payment.p_current = p_current
283        payment.amount_auth = float(amount)
284        return None, payment
285
286    def getAccommodationDetails(self, student):
287        """Determine the accommodation data of a student.
288        """
289        d = {}
290        d['error'] = u''
291        hostels = grok.getSite()['hostels']
292        d['booking_session'] = hostels.accommodation_session
293        d['allowed_states'] = hostels.accommodation_states
294        d['startdate'] = hostels.startdate
295        d['enddate'] = hostels.enddate
296        d['expired'] = hostels.expired
297        # Determine bed type
298        studycourse = student['studycourse']
299        certificate = getattr(studycourse,'certificate',None)
300        current_level = studycourse.current_level
301        if None in (current_level, certificate):
302            return d
303        end_level = certificate.end_level
304        if current_level == 10:
305            bt = 'pr'
306        elif current_level in (100, 400):
307            bt = 'fr'
308        elif current_level in (300, 600):
309            bt = 'fi'
310        else:
311            bt = 're'
312        if student.sex == 'f':
313            sex = 'female'
314        else:
315            sex = 'male'
316        special_handling = 'regular'
317        if student.faccode == 'IOT':
318            special_handling = 'iot'
319        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
320        return d
321
322    def increaseMatricInteger(self, student):
323        """Increase counter for matric numbers.
324        """
325        cert = getattr(student.get('studycourse', None), 'certificate', None)
326        entry_session = getattr(
327            student.get('studycourse', None), 'entry_session', None)
328        dep = cert.__parent__.__parent__
329        next_matric_dict = getattr(dep, 'next_matric_dict', None)
330        if not next_matric_dict:
331            # add next_matric_dict attribute
332            dep.next_matric_dict = {}
333        if not dep.next_matric_dict.get(entry_session, None):
334            # initialize counter element
335            dep.next_matric_dict[entry_session] = 2
336            return
337        # increase counter value of entry_session
338        dep.next_matric_dict[entry_session] += 1
339        return
340
341    def constructMatricNumber(self, student):
342        """Fetch the matric number counter which fits the student and
343        construct the new matric number of the student.
344
345        A typical matriculation number is like this: ND/14/STA/FT/015
346
347        ND = Study Mode Prefix
348        14 = Year of Entry
349        STA = Department Code
350        FT = Study Mode Suffix
351        015 = Serial Number (Every department starts from "001" every
352        session and the numbers build up arithmetically)
353        """
354        cert = getattr(student.get('studycourse', None), 'certificate', None)
355        entry_session = getattr(
356            student.get('studycourse', None), 'entry_session', None)
357        entry_mode = getattr(
358            student.get('studycourse', None), 'entry_mode', None)
359        if entry_session < 2015:
360            return _('Available from session 2015/2016'), None
361        if student.state not in (PAID, ):
362            return _('Wrong state.'), None
363        if None in (cert, entry_session, entry_mode):
364            return _('Matriculation number cannot be set.'), None
365        try:
366            (prefix, suffix) = entry_mode.split('_')
367            (prefix, suffix) = (prefix.upper(), suffix.upper())
368        except ValueError:
369            return _('Matriculation number cannot be set.'), None
370        dep = cert.__parent__.__parent__
371        next_integer = getattr(
372            dep, 'next_matric_dict', {}).get(entry_session, 1)
373        entry_year = entry_session - 100*(entry_session/100)
374        matric_number = "%s/%s/%s/%s/%03d" % (
375            prefix, entry_year, student.depcode, suffix, next_integer)
376        return None, matric_number
377
378    PWCHANGE_STATES = (ADMITTED, CLEARED, RETURNING)
379
380    # KwaraPoly prefix
381    STUDENT_ID_PREFIX = u'W'
Note: See TracBrowser for help on using the repository browser.