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

Last change on this file since 18111 was 18111, checked in by Henrik Bettermann, 11 hours ago

Increase acceptance fee for DPHARM; Medicine MBBSMED; Nursing BSCNNUR; Law LAW

  • Property svn:keywords set to Id
File size: 24.8 KB
Line 
1## $Id: utils.py 18111 2025-07-09 06:57:54Z 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        # disabled on 03/04/25
181        # reenabled on 08/04/25
182        if session < 2025:
183            return
184        # Part time, jupeb, medical, and PG students do not pay sundry.
185        if student.is_jupeb or student.current_mode.endswith('_pt') \
186            or student.is_postgrad \
187            or student.certcode in ('MBBSMED',):
188            return
189        # Only in states, which allow to pay school fees, sundry payments
190        # are required
191        if student.state not in (CLEARANCE, REQUESTED, CLEARED, RETURNING):
192            return
193        # Has the required combi payment been made?
194        for ticket in student['payments'].values():
195            if ticket.p_category == 'required_combi'and \
196                ticket.p_session == session and \
197                ticket.p_state == 'paid':
198                return
199        # If not, check single payments
200        rp = self._collect_required_payment_items(student)
201        cats_missing = deepcopy(rp)
202        if len(student['payments']):
203            for category in rp.keys():
204                for ticket in student['payments'].values():
205                    if ticket.p_state == 'paid' and \
206                        ticket.p_category == category and \
207                        ticket.p_session == session:
208                        del cats_missing[category]
209                if not cats_missing:
210                    return
211        return "%s must be paid before Tution Fee. Make a 'Required Combi Payment'." % ', '.join(
212            cats_missing.values())
213
214    @property
215    def _all_required_payments(self):
216        return set(
217            self.REQUIRED_PAYMENTS_PG.keys()
218            + self.REQUIRED_PAYMENTS_FRESH_SCIENCE.keys()
219            + self.REQUIRED_PAYMENTS_FRESH_NON_SCIENCE.keys()
220            + self.REQUIRED_PAYMENTS_FINAL_PHARMACY.keys()
221            + self.REQUIRED_PAYMENTS_RETURNING_PHARMACY.keys()
222            + self.REQUIRED_PAYMENTS_FINAL.keys()
223            + self.REQUIRED_PAYMENTS_RETURNING.keys()
224            )
225
226    def samePaymentMade(self, student, category, p_item, p_session):
227        if category.startswith('resit'):
228            return False
229        if category == 'combi':
230            return False
231        for key in student['payments'].keys():
232            ticket = student['payments'][key]
233            if ticket.p_state == 'paid' and\
234               ticket.p_category == category and \
235               ticket.p_item != 'Balance' and \
236               ticket.p_item == p_item and \
237               ticket.p_session == p_session:
238                  return True
239        return False
240
241    def setPaymentDetails(self, category, student,
242            previous_session=None, previous_level=None, combi=[]):
243        """Create a payment ticket and set the payment data of a
244        student for the payment category specified.
245        """
246        if grok.getSite().__name__ == 'iuokada-cdl':
247            return self.setCDLPaymentDetails(category, student,
248                previous_session, previous_level, combi)
249        p_item = u''
250        amount = 0.0
251        if previous_session:
252            if previous_session < student['studycourse'].entry_session:
253                return _('The previous session must not fall below '
254                         'your entry session.'), None
255            if category == 'schoolfee':
256                # School fee is always paid for the following session
257                if previous_session > student['studycourse'].current_session:
258                    return _('This is not a previous session.'), None
259            else:
260                if previous_session > student['studycourse'].current_session - 1:
261                    return _('This is not a previous session.'), None
262            p_session = previous_session
263            p_level = previous_level
264            p_current = False
265        else:
266            p_session = student['studycourse'].current_session
267            p_level = student['studycourse'].current_level
268            p_current = True
269            if category in list(self._all_required_payments) + ['required_combi',] \
270                and student.state == RETURNING:
271                # In case of school fee or required sundry fee payments the
272                # payment session is always next session if students are in
273                # state returning.
274                p_session, p_level = self.getReturningData(student)
275        academic_session = self._getSessionConfiguration(p_session)
276        if academic_session == None:
277            return _(u'Session configuration object is not available.'), None
278        # Determine fee.
279        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
280            rpm = self._requiredPaymentsMissing(student, p_session)
281            if rpm:
282                return rpm, None
283            try:
284                certificate = student['studycourse'].certificate
285                p_item = certificate.code
286            except (AttributeError, TypeError):
287                return _('Study course data are incomplete.'), None
288            if previous_session:
289                # Students can pay for previous sessions in all
290                # workflow states.  Fresh students are excluded by the
291                # update method of the PreviousPaymentAddFormPage.
292                if previous_level == 100:
293                    amount = getattr(certificate, 'school_fee_1', 0.0)
294                else:
295                    amount = getattr(certificate, 'school_fee_2', 0.0)
296                if category == 'schoolfee40':
297                    amount = 0.4*amount
298                elif category == 'secondinstal':
299                    amount = 0.6*amount
300            else:
301                if category == 'secondinstal':
302                    if student.is_fresh:
303                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
304                    else:
305                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
306                else:
307                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
308                        amount = getattr(certificate, 'school_fee_1', 0.0)
309                    elif student.state == RETURNING:
310                        amount = getattr(certificate, 'school_fee_2', 0.0)
311                    elif student.is_postgrad and student.state == PAID:
312                        # Returning postgraduate students also pay for the
313                        # next session but their level always remains the
314                        # same.
315                        p_session += 1
316                        academic_session = self._getSessionConfiguration(p_session)
317                        if academic_session == None:
318                            return _(
319                                u'Session configuration object is not available.'
320                                ), None
321                        amount = getattr(certificate, 'school_fee_2', 0.0)
322                    if amount and category == 'schoolfee40':
323                        amount = 0.4*amount
324        elif category == 'clearance':
325            try:
326                p_item = student['studycourse'].certificate.code
327            except (AttributeError, TypeError):
328                return _('Study course data are incomplete.'), None
329            amount = academic_session.clearance_fee
330            if student.is_postgrad:
331                amount *= 0.5
332            if student.certcode in ('DPHARM','MBBSMED','BSCNNUR','LAW'):
333                amount += 200000
334        elif category.startswith('resit'):
335            amount = academic_session.resit_fee
336            number = int(category.strip('resit'))
337            amount *= number
338        #elif category == 'bed_allocation':
339        #    p_item = self.getAccommodationDetails(student)['bt']
340        #    amount = academic_session.booking_fee
341        #elif category == 'hostel_maintenance':
342        #    amount = 0.0
343        #    bedticket = student['accommodation'].get(
344        #        str(student.current_session), None)
345        #    if bedticket is not None and bedticket.bed is not None:
346        #        p_item = bedticket.bed_coordinates
347        #        if bedticket.bed.__parent__.maint_fee > 0:
348        #            amount = bedticket.bed.__parent__.maint_fee
349        #        else:
350        #            # fallback
351        #            amount = academic_session.maint_fee
352        #    else:
353        #        return _(u'No bed allocated.'), None
354        elif category == 'combi' and combi:
355            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
356            for cat in combi:
357                fee_name = cat + '_fee'
358                cat_amount = getattr(academic_session, fee_name, 0.0)
359                if not cat_amount:
360                    return _('%s undefined.' % categories[cat]), None
361                amount += cat_amount
362                p_item += u'%s + ' % categories[cat]
363            p_item = p_item.strip(' + ')
364
365        elif category == 'required_combi':
366            rp = self._collect_required_payment_items(student)
367            for cat in rp.keys():
368                fee_name = cat + '_fee'
369                cat_amount = getattr(academic_session, fee_name, 0.0)
370                if not cat_amount:
371                    return _('%s undefined.' % rp[cat]), None
372                amount += cat_amount
373                p_item += u'%s + ' % rp[cat]
374            p_item = p_item.strip(' + ')
375
376        else:
377            fee_name = category + '_fee'
378            amount = getattr(academic_session, fee_name, 0.0)
379        if amount in (0.0, None):
380            return _('Amount could not be determined.'), None
381        if self.samePaymentMade(student, category, p_item, p_session):
382            return _('This type of payment has already been made.'), None
383        if self._isPaymentDisabled(p_session, category, student):
384            return _('This category of payments has been disabled.'), None
385        payment = createObject(u'waeup.StudentOnlinePayment')
386        timestamp = ("%d" % int(time()*10000))[1:]
387        if category in (
388            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
389            payment.provider_amt = 5000.0
390        if category in (
391            'schoolfee', 'schoolfee40') and student.is_jupeb:
392            payment.provider_amt = 5000.0
393        payment.p_id = "p%s" % timestamp
394        payment.p_category = category
395        payment.p_item = p_item
396        payment.p_session = p_session
397        payment.p_level = p_level
398        payment.p_current = p_current
399        payment.amount_auth = amount
400        payment.p_combi = combi
401        return None, payment
402
403    def setCDLPaymentDetails(self, category, student,
404            previous_session=None, previous_level=None, combi=[]):
405        """Create a payment ticket and set the payment data of a
406        student for the payment category specified.
407        """
408        p_item = u''
409        amount = 0.0
410        if previous_session:
411            if previous_session < student['studycourse'].entry_session:
412                return _('The previous session must not fall below '
413                         'your entry session.'), None
414            if category == 'schoolfee':
415                # School fee is always paid for the following session
416                if previous_session > student['studycourse'].current_session:
417                    return _('This is not a previous session.'), None
418            else:
419                if previous_session > student['studycourse'].current_session - 1:
420                    return _('This is not a previous session.'), None
421            p_session = previous_session
422            p_level = previous_level
423            p_current = False
424        else:
425            p_session = student['studycourse'].current_session
426            p_level = student['studycourse'].current_level
427            p_current = True
428            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
429                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
430                + ['schoolfee','schoolfee40','secondinstal'] \
431                and student.state == RETURNING:
432                # In case of school fee or required sundry fee payments the
433                # payment session is always next session if students are in
434                # state returning.
435                p_session, p_level = self.getReturningData(student)
436        academic_session = self._getSessionConfiguration(p_session)
437        if academic_session == None:
438            return _(u'Session configuration object is not available.'), None
439        # Determine fee.
440        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
441            rpm = self._requiredPaymentsMissing(student, p_session)
442            if rpm:
443                return rpm, None
444            try:
445                certificate = student['studycourse'].certificate
446                p_item = certificate.code
447            except (AttributeError, TypeError):
448                return _('Study course data are incomplete.'), None
449            if previous_session:
450                # Students can pay for previous sessions in all
451                # workflow states.  Fresh students are excluded by the
452                # update method of the PreviousPaymentAddFormPage.
453                if previous_level == 100:
454                    amount = getattr(certificate, 'school_fee_1', 0.0)
455                else:
456                    amount = getattr(certificate, 'school_fee_2', 0.0)
457                if category == 'schoolfee40':
458                    amount = 0.4*amount
459                elif category == 'secondinstal':
460                    amount = 0.6*amount
461            else:
462                if category == 'secondinstal':
463                    if student.is_fresh:
464                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
465                    else:
466                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
467                else:
468                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
469                        amount = getattr(certificate, 'school_fee_1', 0.0)
470                    elif student.state == RETURNING:
471                        amount = getattr(certificate, 'school_fee_2', 0.0)
472                    elif student.is_postgrad and student.state == PAID:
473                        # Returning postgraduate students also pay for the
474                        # next session but their level always remains the
475                        # same.
476                        p_session += 1
477                        academic_session = self._getSessionConfiguration(p_session)
478                        if academic_session == None:
479                            return _(
480                                u'Session configuration object is not available.'
481                                ), None
482                        amount = getattr(certificate, 'school_fee_2', 0.0)
483                    if amount and category == 'schoolfee40':
484                        amount = 0.4*amount
485        elif category == 'clearance':
486            try:
487                p_item = student['studycourse'].certificate.code
488            except (AttributeError, TypeError):
489                return _('Study course data are incomplete.'), None
490            amount = academic_session.clearance_fee
491        elif category.startswith('cdlcourse'):
492            amount = academic_session.course_fee
493            number = int(category.strip('cdlcourse'))
494            amount *= number
495        elif category == 'combi' and combi:
496            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
497            for cat in combi:
498                fee_name = cat + '_fee'
499                cat_amount = getattr(academic_session, fee_name, 0.0)
500                if not cat_amount:
501                    return _('%s undefined.' % categories[cat]), None
502                amount += cat_amount
503                p_item += u'%s + ' % categories[cat]
504            p_item = p_item.strip(' + ')
505        else:
506            fee_name = category + '_fee'
507            amount = getattr(academic_session, fee_name, 0.0)
508        if amount in (0.0, None):
509            return _('Amount could not be determined.'), None
510        if self.samePaymentMade(student, category, p_item, p_session):
511            return _('This type of payment has already been made.'), None
512        if self._isPaymentDisabled(p_session, category, student):
513            return _('This category of payments has been disabled.'), None
514        payment = createObject(u'waeup.StudentOnlinePayment')
515        timestamp = ("%d" % int(time()*10000))[1:]
516        if category in (
517            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
518            payment.provider_amt = 5000.0
519        if category in (
520            'schoolfee', 'schoolfee40') and student.is_jupeb:
521            payment.provider_amt = 5000.0
522        payment.p_id = "p%s" % timestamp
523        payment.p_category = category
524        payment.p_item = p_item
525        payment.p_session = p_session
526        payment.p_level = p_level
527        payment.p_current = p_current
528        payment.amount_auth = amount
529        payment.p_combi = combi
530        return None, payment
531
532    def setBalanceDetails(self, category, student,
533            balance_session, balance_level, balance_amount):
534        """Create a balance payment ticket and set the payment data
535        as selected by the student.
536        """
537        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
538            and balance_session > 2019:
539            rpm = self._requiredPaymentsMissing(student, balance_session)
540            if rpm:
541                return rpm, None
542        return super(
543            CustomStudentsUtils, self).setBalanceDetails(category, student,
544            balance_session, balance_level, balance_amount)
545
546    def constructMatricNumber(self, student):
547        """Fetch the matric number counter which fits the student and
548        construct the new matric number of the student.
549        """
550        next_integer = grok.getSite()['configuration'].next_matric_integer
551        if next_integer == 0:
552            return _('Matriculation number cannot be set.'), None
553        if not student.state in (
554            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
555            return _('Matriculation number cannot be set.'), None
556        year = unicode(student.entry_session)[2:]
557        if grok.getSite().__name__ == 'iuokada-cdl':
558            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
559        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.