source: main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/students/utils.py @ 18037

Last change on this file since 18037 was 18037, checked in by Henrik Bettermann, 8 hours ago

Add grad clearance fee.
Part time, jupeb, medical, and PG students do not pay sundry.

  • Property svn:keywords set to Id
File size: 24.4 KB
Line 
1## $Id: utils.py 18037 2025-03-12 14:32:29Z 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
19from time import time
20from copy import deepcopy
21from zope.component import createObject, getUtility
22from waeup.kofa.interfaces import (IKofaUtils,
23    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
24    REGISTERED, VALIDATED)
25from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
26from kofacustom.iuokada.interfaces import MessageFactory as _
27
28class CustomStudentsUtils(NigeriaStudentsUtils):
29    """A collection of customized methods.
30
31    """
32
33    @property
34    def STUDENT_ID_PREFIX(self):
35        if grok.getSite().__name__ == 'iuokada-cdl':
36            return u'F'
37        return u'I'
38
39
40    SKIP_UPLOAD_VIEWLETS = (
41        'acceptanceletterupload', 'certificateupload'
42        )
43    # Maximum size of upload files in kB
44    MAX_KB = 500
45
46    #: A tuple containing the names of registration states in which changing of
47    #: passport pictures is allowed.
48
49    PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARANCE,)
50
51    REQUIRED_PAYMENTS_FRESH_SCIENCE = {
52        'registration_fresh': 'Registration Fee (Fresh)',
53        'book': 'Book Deposit',
54        'develop': 'Development Fee',
55        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
56        'municipal_fresh': 'Fresh Students Municipal Fee',
57        'matric': 'Matriculation Fee',
58        'waecneco': 'WAEC/NECO Verification',
59        'jambver': 'JAMB Verification Fee',
60        'health_insurance': 'Student Health Insurance',
61        'id_card': 'I.D. Card',
62        'medical_screening': 'Medical Screening Fees',
63        'science': 'Science Bench Fee',
64        }
65
66    REQUIRED_PAYMENTS_FRESH_NON_SCIENCE = {
67        'registration_fresh': 'Registration Fee (Fresh)',
68        'book': 'Book Deposit',
69        'develop': 'Development Fee',
70        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
71        'municipal_fresh': 'Fresh Students Municipal Fee',
72        'matric': 'Matriculation Fee',
73        'waecneco': 'WAEC/NECO Verification',
74        'jambver': 'JAMB Verification Fee',
75        'health_insurance': 'Student Health Insurance',
76        'id_card': 'I.D. Card',
77        'medical_screening': 'Medical Screening Fees',
78        }
79
80    # all students (except PHM) returning
81    REQUIRED_PAYMENTS_RETURNING = {
82        'registration_return': 'Registration Fee (Returning)',
83        'book': 'Book Deposit',
84        'develop': 'Development Fee',
85        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
86        'municipal_returning': 'Returning Students Municipal Fee',
87        'health_insurance': 'Student Health Insurance',
88        }
89
90
91    # all stdents (except PHM) final year
92    REQUIRED_PAYMENTS_FINAL = {
93        'registration_return': 'Registration Fee (Returning)',
94        'book': 'Book Deposit',
95        'develop': 'Development Fee',
96        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
97        'municipal_returning': 'Returning Students Municipal Fee',
98        'health_insurance': 'Student Health Insurance',
99        'alumni': 'Alumni Fees',
100        'conv': 'Convocation Fees',
101        'grad_clearance': 'Clearance Fees',
102        }
103
104    # PHM returning students
105    REQUIRED_PAYMENTS_RETURNING_PHARMACY = {
106        'registration_return': 'Registration Fee (Returning)',
107        'book': 'Book Deposit',
108        'develop': 'Development Fee',
109        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
110        'municipal_returning': 'Returning Students Municipal Fee',
111        'health_insurance': 'Student Health Insurance',
112        'lab_support': 'Lab Support',
113        }
114
115    # PHM students final year
116    REQUIRED_PAYMENTS_FINAL_PHARMACY = {
117        'registration_return': 'Registration Fee (Returning)',
118        'book': 'Book Deposit',
119        'develop': 'Development Fee',
120        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
121        'municipal_returning': 'Returning Students Municipal Fee',
122        'health_insurance': 'Student Health Insurance',
123        'alumni': 'Alumni Fees',
124        'conv': 'Convocation Fees',
125        'grad_clearance': 'Clearance Fees',
126        'lab_support': 'Lab Support',
127        }
128
129    REQUIRED_PAYMENTS_PG = {
130        'pg_other': 'PG Other Charges',
131        }
132
133    def warnCreditsOOR(self, studylevel, course=None):
134        """Return message if credits are out of range. In the base
135        package only maximum credits is set.
136        """
137        max_credits = 60
138        end_level = getattr(studylevel.__parent__.certificate, 'end_level', None)
139        if end_level and studylevel.level >= end_level:
140            max_credits = 80
141        #if studylevel.certcode == 'LLB':
142        #    max_credits = 50
143        if studylevel.certcode == 'MBBSMED' and studylevel.level == 200:
144            max_credits = 100
145        if course and studylevel.total_credits + course.credits > max_credits:
146            return _('Maximum credits exceeded.')
147        elif studylevel.total_credits > max_credits:
148            return _('Maximum credits exceeded.')
149        return
150
151    def _is_payment_for_final(self, student):
152        studycourse = student['studycourse']
153        certificate = getattr(studycourse,'certificate',None)
154        current_level = studycourse.current_level
155        if None in (current_level, certificate):
156            return False
157        end_level = certificate.end_level
158        if current_level >= end_level-100:
159            return True
160        return False
161
162    def _collect_required_payment_items(self, student):
163        if student.is_postgrad:
164            rp = self.REQUIRED_PAYMENTS_PG
165        elif student.is_fresh and student.faccode in ('ENG', 'HSC', 'NAS', 'PHM'):
166            rp = self.REQUIRED_PAYMENTS_FRESH_SCIENCE
167        elif student.is_fresh:
168            rp = self.REQUIRED_PAYMENTS_FRESH_NON_SCIENCE
169        elif student.faccode == 'PHM' and self._is_payment_for_final(student):
170            rp = self.REQUIRED_PAYMENTS_FINAL_PHARMACY
171        elif student.faccode == 'PHM':
172            rp = self.REQUIRED_PAYMENTS_RETURNING_PHARMACY
173        elif self._is_payment_for_final(student):
174            rp = self.REQUIRED_PAYMENTS_FINAL
175        else:
176            rp = self.REQUIRED_PAYMENTS_RETURNING
177        return rp
178
179    def _requiredPaymentsMissing(self, student, session):
180        # Part time, jupeb, medical, and PG students do not pay sundry.
181        if student.is_jupeb or student.current_mode.endswith('_pt') \
182            or student.is_postgrad \
183            or student.certcode in ('MBBSMED',):
184            return
185        # Has the required combi payment been made?
186        for ticket in student['payments'].values():
187            if ticket.p_category == 'required_combi'and \
188                ticket.p_session == session and \
189                ticket.p_state == 'paid':
190                return
191        # If not, check single payments
192        rp = self._collect_required_payment_items(student)
193        cats_missing = deepcopy(rp)
194        if len(student['payments']):
195            for category in rp.keys():
196                for ticket in student['payments'].values():
197                    if ticket.p_state == 'paid' and \
198                        ticket.p_category == category and \
199                        ticket.p_session == session:
200                        del cats_missing[category]
201                if not cats_missing:
202                    return
203        return "%s must be paid before Tution Fee. Make either single payments or make a 'Required Combi Payment'." % ', '.join(
204            cats_missing.values())
205
206    @property
207    def _all_required_payments(self):
208        return set(
209            self.REQUIRED_PAYMENTS_PG.keys()
210            + self.REQUIRED_PAYMENTS_FRESH_SCIENCE.keys()
211            + self.REQUIRED_PAYMENTS_FRESH_NON_SCIENCE.keys()
212            + self.REQUIRED_PAYMENTS_FINAL_PHARMACY.keys()
213            + self.REQUIRED_PAYMENTS_RETURNING_PHARMACY.keys()
214            + self.REQUIRED_PAYMENTS_FINAL.keys()
215            + self.REQUIRED_PAYMENTS_RETURNING.keys()
216            )
217
218    def samePaymentMade(self, student, category, p_item, p_session):
219        if category.startswith('resit'):
220            return False
221        if category == 'combi':
222            return False
223        for key in student['payments'].keys():
224            ticket = student['payments'][key]
225            if ticket.p_state == 'paid' and\
226               ticket.p_category == category and \
227               ticket.p_item != 'Balance' and \
228               ticket.p_item == p_item and \
229               ticket.p_session == p_session:
230                  return True
231        return False
232
233    def setPaymentDetails(self, category, student,
234            previous_session=None, previous_level=None, combi=[]):
235        """Create a payment ticket and set the payment data of a
236        student for the payment category specified.
237        """
238        if grok.getSite().__name__ == 'iuokada-cdl':
239            return self.setCDLPaymentDetails(category, student,
240                previous_session, previous_level, combi)
241        p_item = u''
242        amount = 0.0
243        if previous_session:
244            if previous_session < student['studycourse'].entry_session:
245                return _('The previous session must not fall below '
246                         'your entry session.'), None
247            if category == 'schoolfee':
248                # School fee is always paid for the following session
249                if previous_session > student['studycourse'].current_session:
250                    return _('This is not a previous session.'), None
251            else:
252                if previous_session > student['studycourse'].current_session - 1:
253                    return _('This is not a previous session.'), None
254            p_session = previous_session
255            p_level = previous_level
256            p_current = False
257        else:
258            p_session = student['studycourse'].current_session
259            p_level = student['studycourse'].current_level
260            p_current = True
261            if category in list(self._all_required_payments) + ['required_combi',] \
262                and student.state == RETURNING:
263                # In case of school fee or required sundry fee payments the
264                # payment session is always next session if students are in
265                # state returning.
266                p_session, p_level = self.getReturningData(student)
267        academic_session = self._getSessionConfiguration(p_session)
268        if academic_session == None:
269            return _(u'Session configuration object is not available.'), None
270        # Determine fee.
271        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
272            rpm = self._requiredPaymentsMissing(student, p_session)
273            if rpm:
274                return rpm, None
275            try:
276                certificate = student['studycourse'].certificate
277                p_item = certificate.code
278            except (AttributeError, TypeError):
279                return _('Study course data are incomplete.'), None
280            if previous_session:
281                # Students can pay for previous sessions in all
282                # workflow states.  Fresh students are excluded by the
283                # update method of the PreviousPaymentAddFormPage.
284                if previous_level == 100:
285                    amount = getattr(certificate, 'school_fee_1', 0.0)
286                else:
287                    amount = getattr(certificate, 'school_fee_2', 0.0)
288                if category == 'schoolfee40':
289                    amount = 0.4*amount
290                elif category == 'secondinstal':
291                    amount = 0.6*amount
292            else:
293                if category == 'secondinstal':
294                    if student.is_fresh:
295                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
296                    else:
297                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
298                else:
299                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
300                        amount = getattr(certificate, 'school_fee_1', 0.0)
301                    elif student.state == RETURNING:
302                        amount = getattr(certificate, 'school_fee_2', 0.0)
303                    elif student.is_postgrad and student.state == PAID:
304                        # Returning postgraduate students also pay for the
305                        # next session but their level always remains the
306                        # same.
307                        p_session += 1
308                        academic_session = self._getSessionConfiguration(p_session)
309                        if academic_session == None:
310                            return _(
311                                u'Session configuration object is not available.'
312                                ), None
313                        amount = getattr(certificate, 'school_fee_2', 0.0)
314                    if amount and category == 'schoolfee40':
315                        amount = 0.4*amount
316        elif category == 'clearance':
317            try:
318                p_item = student['studycourse'].certificate.code
319            except (AttributeError, TypeError):
320                return _('Study course data are incomplete.'), None
321            amount = academic_session.clearance_fee
322            if student.is_postgrad:
323                amount *= 0.5
324        elif category.startswith('resit'):
325            amount = academic_session.resit_fee
326            number = int(category.strip('resit'))
327            amount *= number
328        #elif category == 'bed_allocation':
329        #    p_item = self.getAccommodationDetails(student)['bt']
330        #    amount = academic_session.booking_fee
331        #elif category == 'hostel_maintenance':
332        #    amount = 0.0
333        #    bedticket = student['accommodation'].get(
334        #        str(student.current_session), None)
335        #    if bedticket is not None and bedticket.bed is not None:
336        #        p_item = bedticket.bed_coordinates
337        #        if bedticket.bed.__parent__.maint_fee > 0:
338        #            amount = bedticket.bed.__parent__.maint_fee
339        #        else:
340        #            # fallback
341        #            amount = academic_session.maint_fee
342        #    else:
343        #        return _(u'No bed allocated.'), None
344        elif category == 'combi' and combi:
345            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
346            for cat in combi:
347                fee_name = cat + '_fee'
348                cat_amount = getattr(academic_session, fee_name, 0.0)
349                if not cat_amount:
350                    return _('%s undefined.' % categories[cat]), None
351                amount += cat_amount
352                p_item += u'%s + ' % categories[cat]
353            p_item = p_item.strip(' + ')
354
355        elif category == 'required_combi':
356            rp = self._collect_required_payment_items(student)
357            for cat in rp.keys():
358                fee_name = cat + '_fee'
359                cat_amount = getattr(academic_session, fee_name, 0.0)
360                if not cat_amount:
361                    return _('%s undefined.' % rp[cat]), None
362                amount += cat_amount
363                p_item += u'%s + ' % rp[cat]
364            p_item = p_item.strip(' + ')
365
366        else:
367            fee_name = category + '_fee'
368            amount = getattr(academic_session, fee_name, 0.0)
369        if amount in (0.0, None):
370            return _('Amount could not be determined.'), None
371        if self.samePaymentMade(student, category, p_item, p_session):
372            return _('This type of payment has already been made.'), None
373        if self._isPaymentDisabled(p_session, category, student):
374            return _('This category of payments has been disabled.'), None
375        payment = createObject(u'waeup.StudentOnlinePayment')
376        timestamp = ("%d" % int(time()*10000))[1:]
377        if category in (
378            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
379            payment.provider_amt = 5000.0
380        if category in (
381            'schoolfee', 'schoolfee40') and student.is_jupeb:
382            payment.provider_amt = 5000.0
383        payment.p_id = "p%s" % timestamp
384        payment.p_category = category
385        payment.p_item = p_item
386        payment.p_session = p_session
387        payment.p_level = p_level
388        payment.p_current = p_current
389        payment.amount_auth = amount
390        payment.p_combi = combi
391        return None, payment
392
393    def setCDLPaymentDetails(self, category, student,
394            previous_session=None, previous_level=None, combi=[]):
395        """Create a payment ticket and set the payment data of a
396        student for the payment category specified.
397        """
398        p_item = u''
399        amount = 0.0
400        if previous_session:
401            if previous_session < student['studycourse'].entry_session:
402                return _('The previous session must not fall below '
403                         'your entry session.'), None
404            if category == 'schoolfee':
405                # School fee is always paid for the following session
406                if previous_session > student['studycourse'].current_session:
407                    return _('This is not a previous session.'), None
408            else:
409                if previous_session > student['studycourse'].current_session - 1:
410                    return _('This is not a previous session.'), None
411            p_session = previous_session
412            p_level = previous_level
413            p_current = False
414        else:
415            p_session = student['studycourse'].current_session
416            p_level = student['studycourse'].current_level
417            p_current = True
418            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
419                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
420                + ['schoolfee','schoolfee40','secondinstal'] \
421                and student.state == RETURNING:
422                # In case of school fee or required sundry fee payments the
423                # payment session is always next session if students are in
424                # state returning.
425                p_session, p_level = self.getReturningData(student)
426        academic_session = self._getSessionConfiguration(p_session)
427        if academic_session == None:
428            return _(u'Session configuration object is not available.'), None
429        # Determine fee.
430        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
431            rpm = self._requiredPaymentsMissing(student, p_session)
432            if rpm:
433                return rpm, None
434            try:
435                certificate = student['studycourse'].certificate
436                p_item = certificate.code
437            except (AttributeError, TypeError):
438                return _('Study course data are incomplete.'), None
439            if previous_session:
440                # Students can pay for previous sessions in all
441                # workflow states.  Fresh students are excluded by the
442                # update method of the PreviousPaymentAddFormPage.
443                if previous_level == 100:
444                    amount = getattr(certificate, 'school_fee_1', 0.0)
445                else:
446                    amount = getattr(certificate, 'school_fee_2', 0.0)
447                if category == 'schoolfee40':
448                    amount = 0.4*amount
449                elif category == 'secondinstal':
450                    amount = 0.6*amount
451            else:
452                if category == 'secondinstal':
453                    if student.is_fresh:
454                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
455                    else:
456                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
457                else:
458                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
459                        amount = getattr(certificate, 'school_fee_1', 0.0)
460                    elif student.state == RETURNING:
461                        amount = getattr(certificate, 'school_fee_2', 0.0)
462                    elif student.is_postgrad and student.state == PAID:
463                        # Returning postgraduate students also pay for the
464                        # next session but their level always remains the
465                        # same.
466                        p_session += 1
467                        academic_session = self._getSessionConfiguration(p_session)
468                        if academic_session == None:
469                            return _(
470                                u'Session configuration object is not available.'
471                                ), None
472                        amount = getattr(certificate, 'school_fee_2', 0.0)
473                    if amount and category == 'schoolfee40':
474                        amount = 0.4*amount
475        elif category == 'clearance':
476            try:
477                p_item = student['studycourse'].certificate.code
478            except (AttributeError, TypeError):
479                return _('Study course data are incomplete.'), None
480            amount = academic_session.clearance_fee
481        elif category.startswith('cdlcourse'):
482            amount = academic_session.course_fee
483            number = int(category.strip('cdlcourse'))
484            amount *= number
485        elif category == 'combi' and combi:
486            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
487            for cat in combi:
488                fee_name = cat + '_fee'
489                cat_amount = getattr(academic_session, fee_name, 0.0)
490                if not cat_amount:
491                    return _('%s undefined.' % categories[cat]), None
492                amount += cat_amount
493                p_item += u'%s + ' % categories[cat]
494            p_item = p_item.strip(' + ')
495        else:
496            fee_name = category + '_fee'
497            amount = getattr(academic_session, fee_name, 0.0)
498        if amount in (0.0, None):
499            return _('Amount could not be determined.'), None
500        if self.samePaymentMade(student, category, p_item, p_session):
501            return _('This type of payment has already been made.'), None
502        if self._isPaymentDisabled(p_session, category, student):
503            return _('This category of payments has been disabled.'), None
504        payment = createObject(u'waeup.StudentOnlinePayment')
505        timestamp = ("%d" % int(time()*10000))[1:]
506        if category in (
507            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
508            payment.provider_amt = 5000.0
509        if category in (
510            'schoolfee', 'schoolfee40') and student.is_jupeb:
511            payment.provider_amt = 5000.0
512        payment.p_id = "p%s" % timestamp
513        payment.p_category = category
514        payment.p_item = p_item
515        payment.p_session = p_session
516        payment.p_level = p_level
517        payment.p_current = p_current
518        payment.amount_auth = amount
519        payment.p_combi = combi
520        return None, payment
521
522    def setBalanceDetails(self, category, student,
523            balance_session, balance_level, balance_amount):
524        """Create a balance payment ticket and set the payment data
525        as selected by the student.
526        """
527        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
528            and balance_session > 2019:
529            rpm = self._requiredPaymentsMissing(student, balance_session)
530            if rpm:
531                return rpm, None
532        return super(
533            CustomStudentsUtils, self).setBalanceDetails(category, student,
534            balance_session, balance_level, balance_amount)
535
536    def constructMatricNumber(self, student):
537        """Fetch the matric number counter which fits the student and
538        construct the new matric number of the student.
539        """
540        next_integer = grok.getSite()['configuration'].next_matric_integer
541        if next_integer == 0:
542            return _('Matriculation number cannot be set.'), None
543        if not student.state in (
544            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
545            return _('Matriculation number cannot be set.'), None
546        year = unicode(student.entry_session)[2:]
547        if grok.getSite().__name__ == 'iuokada-cdl':
548            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
549        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.