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

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

Increase existing counters (self-repair).

  • Property svn:keywords set to Id
File size: 15.6 KB
Line 
1## $Id: utils.py 13391 2015-11-05 14:34:22Z 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', '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, 37500.0, 35600.0, 37500.0, 0.0), # science
69            (0.0, 40000.0, 31600.0, 31400.0, 31600.0, 31400.0, 40400.0, 33600.0, 35500.0, 33600.0, 35500.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):
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        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:
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                # Should not happen because this is already checked
206                # in the browser module, but anyway ...
207                portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
208                p_item = trans(_('no bed allocated'), portal_language)
209        elif category == 'clearance':
210            amount = academic_session.clearance_fee
211            try:
212                p_item = student['studycourse'].certificate.code
213            except (AttributeError, TypeError):
214                return _('Study course data are incomplete.'), None
215        elif category == 'schoolfee':
216            try:
217                certificate = student['studycourse'].certificate
218                p_item = certificate.code
219            except (AttributeError, TypeError):
220                return _('Study course data are incomplete.'), None
221            if student.state == RETURNING:
222                # Override p_session and p_level
223                p_session, p_level = self.getReturningData(student)
224                academic_session = self._getSessionConfiguration(p_session)
225                if academic_session == None:
226                    return _(u'Session configuration object '
227                              'is not available.'), None
228            if student.state == CLEARED and student.current_mode in (
229                                                            'hnd_ft', 'nd_ft'):
230                # Fresh students must have booked and paid for accommodation.
231                if self._bedAvailable(student):
232                    if not student.faccode == 'IOT' and \
233                        not self._maintPaymentMade(student, p_session):
234                        return _('Book and pay for accommodation first '
235                                 'before making school fee payments.'), None
236            if student.state in (RETURNING, CLEARED):
237                if p_level in PAYMENT_LEVELS:
238                    amount = SCHOOL_FEES.get_fee(
239                        (we_ft(student),
240                         local_nonlocal(student),
241                         arts_science(student),
242                         p_level)
243                        )
244        elif category == 'carryover1':
245            amount = 6000.0
246        elif category == 'carryover2':
247            amount = 10000.0
248        elif category == 'carryover3':
249            amount = 15000.0
250
251        else:
252            fee_name = category + '_fee'
253            amount = getattr(academic_session, fee_name, 0.0)
254        if amount in (0.0, None):
255            return _(u'Amount could not be determined.'), None
256        if self.samePaymentMade(student, category, p_item, p_session):
257            return _('This type of payment has already been made.'), None
258        # Add session specific penalty fee.
259        if category == 'schoolfee' and student.is_postgrad:
260            amount += academic_session.penalty_pg
261        elif category == 'schoolfee':
262            amount += academic_session.penalty_ug
263        # Recategorize carryover fees.
264        if category.startswith('carryover'):
265            p_item = getUtility(IKofaUtils).PAYMENT_CATEGORIES[category]
266            p_item = unicode(p_item)
267            # Now we change the category to reduce the number of categories.
268            category = 'schoolfee'
269        if self._isPaymentDisabled(p_session, category, student):
270            return _('Payment temporarily disabled.'), None
271        payment = createObject(u'waeup.StudentOnlinePayment')
272        timestamp = ("%d" % int(time()*10000))[1:]
273        payment.p_id = "p%s" % timestamp
274        payment.p_category = category
275        payment.p_item = p_item
276        payment.p_session = p_session
277        payment.p_level = p_level
278        payment.p_current = p_current
279        payment.amount_auth = float(amount)
280        return None, payment
281
282    def getAccommodationDetails(self, student):
283        """Determine the accommodation data of a student.
284        """
285        d = {}
286        d['error'] = u''
287        hostels = grok.getSite()['hostels']
288        d['booking_session'] = hostels.accommodation_session
289        d['allowed_states'] = hostels.accommodation_states
290        d['startdate'] = hostels.startdate
291        d['enddate'] = hostels.enddate
292        d['expired'] = hostels.expired
293        # Determine bed type
294        studycourse = student['studycourse']
295        certificate = getattr(studycourse,'certificate',None)
296        current_level = studycourse.current_level
297        if None in (current_level, certificate):
298            return d
299        end_level = certificate.end_level
300        if current_level == 10:
301            bt = 'pr'
302        elif current_level in (100, 400):
303            bt = 'fr'
304        elif current_level in (300, 600):
305            bt = 'fi'
306        else:
307            bt = 're'
308        if student.sex == 'f':
309            sex = 'female'
310        else:
311            sex = 'male'
312        special_handling = 'regular'
313        if student.faccode == 'IOT':
314            special_handling = 'iot'
315        d['bt'] = u'%s_%s_%s' % (special_handling,sex,bt)
316        return d
317
318    def increaseMatricInteger(self, student):
319        """Increase counter for matric numbers.
320        """
321        cert = getattr(student.get('studycourse', None), 'certificate', None)
322        entry_session = getattr(
323            student.get('studycourse', None), 'entry_session', None)
324        dep = cert.__parent__.__parent__
325        next_matric_dict = getattr(dep, 'next_matric_dict', None)
326        if not next_matric_dict:
327            # add next_matric_dict attribute
328            dep.next_matric_dict = {}
329        if not dep.next_matric_dict.get(entry_session, None):
330            # initialize counter element with 11
331            dep.next_matric_dict[entry_session] = 11
332            dep._p_changed = True
333            return
334        # increase counter value of entry_session
335        dep.next_matric_dict[entry_session] += 1
336        dep._p_changed = True
337        return
338
339    def constructMatricNumber(self, student):
340        """Fetch the matric number counter which fits the student and
341        construct the new matric number of the student.
342
343        A typical matriculation number is like this: ND/14/STA/FT/015
344
345        ND = Study Mode Prefix
346        14 = Year of Entry
347        STA = Department Code
348        FT = Study Mode Suffix
349        015 = Serial Number (Every department starts from "001" every
350        session and the numbers build up arithmetically)
351        """
352        cert = getattr(student.get('studycourse', None), 'certificate', None)
353        entry_session = getattr(
354            student.get('studycourse', None), 'entry_session', None)
355        entry_mode = getattr(
356            student.get('studycourse', None), 'entry_mode', None)
357        if entry_session < 2015:
358            return _('Available from session 2015/2016'), None
359        if student.state not in (PAID, ):
360            return _('Wrong state.'), None
361        if None in (cert, entry_session, entry_mode):
362            return _('Matriculation number cannot be set.'), None
363        try:
364            (prefix, suffix) = entry_mode.split('_')
365            (prefix, suffix) = (prefix.upper(), suffix.upper())
366        except ValueError:
367            return _('Matriculation number cannot be set.'), None
368        dep = cert.__parent__.__parent__
369        next_integer = getattr(
370            dep, 'next_matric_dict', {}).get(entry_session, 10)
371
372        #############################################
373        # self-repair, can be removed after a while
374        if next_integer == 2:
375            next_integer == 10
376            dep.next_matric_dict[entry_session] = 10
377            dep._p_changed = True
378        #############################################
379
380        entry_year = entry_session - 100*(entry_session/100)
381        matric_number = "%s/%s/%s/%s/%03d" % (
382            prefix, entry_year, student.depcode, suffix, next_integer)
383        return None, matric_number
384
385    PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARED, RETURNING)
386
387    # KwaraPoly prefix
388    STUDENT_ID_PREFIX = u'W'
Note: See TracBrowser for help on using the repository browser.