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

Last change on this file since 18108 was 18108, checked in by Henrik Bettermann, 6 hours ago

Reenable sundry payments requirement.

  • Property svn:keywords set to Id
File size: 24.7 KB
Line 
1## $Id: utils.py 18108 2025-07-08 11:25:14Z 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        elif category.startswith('resit'):
333            amount = academic_session.resit_fee
334            number = int(category.strip('resit'))
335            amount *= number
336        #elif category == 'bed_allocation':
337        #    p_item = self.getAccommodationDetails(student)['bt']
338        #    amount = academic_session.booking_fee
339        #elif category == 'hostel_maintenance':
340        #    amount = 0.0
341        #    bedticket = student['accommodation'].get(
342        #        str(student.current_session), None)
343        #    if bedticket is not None and bedticket.bed is not None:
344        #        p_item = bedticket.bed_coordinates
345        #        if bedticket.bed.__parent__.maint_fee > 0:
346        #            amount = bedticket.bed.__parent__.maint_fee
347        #        else:
348        #            # fallback
349        #            amount = academic_session.maint_fee
350        #    else:
351        #        return _(u'No bed allocated.'), None
352        elif category == 'combi' and combi:
353            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
354            for cat in combi:
355                fee_name = cat + '_fee'
356                cat_amount = getattr(academic_session, fee_name, 0.0)
357                if not cat_amount:
358                    return _('%s undefined.' % categories[cat]), None
359                amount += cat_amount
360                p_item += u'%s + ' % categories[cat]
361            p_item = p_item.strip(' + ')
362
363        elif category == 'required_combi':
364            rp = self._collect_required_payment_items(student)
365            for cat in rp.keys():
366                fee_name = cat + '_fee'
367                cat_amount = getattr(academic_session, fee_name, 0.0)
368                if not cat_amount:
369                    return _('%s undefined.' % rp[cat]), None
370                amount += cat_amount
371                p_item += u'%s + ' % rp[cat]
372            p_item = p_item.strip(' + ')
373
374        else:
375            fee_name = category + '_fee'
376            amount = getattr(academic_session, fee_name, 0.0)
377        if amount in (0.0, None):
378            return _('Amount could not be determined.'), None
379        if self.samePaymentMade(student, category, p_item, p_session):
380            return _('This type of payment has already been made.'), None
381        if self._isPaymentDisabled(p_session, category, student):
382            return _('This category of payments has been disabled.'), None
383        payment = createObject(u'waeup.StudentOnlinePayment')
384        timestamp = ("%d" % int(time()*10000))[1:]
385        if category in (
386            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
387            payment.provider_amt = 5000.0
388        if category in (
389            'schoolfee', 'schoolfee40') and student.is_jupeb:
390            payment.provider_amt = 5000.0
391        payment.p_id = "p%s" % timestamp
392        payment.p_category = category
393        payment.p_item = p_item
394        payment.p_session = p_session
395        payment.p_level = p_level
396        payment.p_current = p_current
397        payment.amount_auth = amount
398        payment.p_combi = combi
399        return None, payment
400
401    def setCDLPaymentDetails(self, category, student,
402            previous_session=None, previous_level=None, combi=[]):
403        """Create a payment ticket and set the payment data of a
404        student for the payment category specified.
405        """
406        p_item = u''
407        amount = 0.0
408        if previous_session:
409            if previous_session < student['studycourse'].entry_session:
410                return _('The previous session must not fall below '
411                         'your entry session.'), None
412            if category == 'schoolfee':
413                # School fee is always paid for the following session
414                if previous_session > student['studycourse'].current_session:
415                    return _('This is not a previous session.'), None
416            else:
417                if previous_session > student['studycourse'].current_session - 1:
418                    return _('This is not a previous session.'), None
419            p_session = previous_session
420            p_level = previous_level
421            p_current = False
422        else:
423            p_session = student['studycourse'].current_session
424            p_level = student['studycourse'].current_level
425            p_current = True
426            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
427                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
428                + ['schoolfee','schoolfee40','secondinstal'] \
429                and student.state == RETURNING:
430                # In case of school fee or required sundry fee payments the
431                # payment session is always next session if students are in
432                # state returning.
433                p_session, p_level = self.getReturningData(student)
434        academic_session = self._getSessionConfiguration(p_session)
435        if academic_session == None:
436            return _(u'Session configuration object is not available.'), None
437        # Determine fee.
438        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
439            rpm = self._requiredPaymentsMissing(student, p_session)
440            if rpm:
441                return rpm, None
442            try:
443                certificate = student['studycourse'].certificate
444                p_item = certificate.code
445            except (AttributeError, TypeError):
446                return _('Study course data are incomplete.'), None
447            if previous_session:
448                # Students can pay for previous sessions in all
449                # workflow states.  Fresh students are excluded by the
450                # update method of the PreviousPaymentAddFormPage.
451                if previous_level == 100:
452                    amount = getattr(certificate, 'school_fee_1', 0.0)
453                else:
454                    amount = getattr(certificate, 'school_fee_2', 0.0)
455                if category == 'schoolfee40':
456                    amount = 0.4*amount
457                elif category == 'secondinstal':
458                    amount = 0.6*amount
459            else:
460                if category == 'secondinstal':
461                    if student.is_fresh:
462                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
463                    else:
464                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
465                else:
466                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
467                        amount = getattr(certificate, 'school_fee_1', 0.0)
468                    elif student.state == RETURNING:
469                        amount = getattr(certificate, 'school_fee_2', 0.0)
470                    elif student.is_postgrad and student.state == PAID:
471                        # Returning postgraduate students also pay for the
472                        # next session but their level always remains the
473                        # same.
474                        p_session += 1
475                        academic_session = self._getSessionConfiguration(p_session)
476                        if academic_session == None:
477                            return _(
478                                u'Session configuration object is not available.'
479                                ), None
480                        amount = getattr(certificate, 'school_fee_2', 0.0)
481                    if amount and category == 'schoolfee40':
482                        amount = 0.4*amount
483        elif category == 'clearance':
484            try:
485                p_item = student['studycourse'].certificate.code
486            except (AttributeError, TypeError):
487                return _('Study course data are incomplete.'), None
488            amount = academic_session.clearance_fee
489        elif category.startswith('cdlcourse'):
490            amount = academic_session.course_fee
491            number = int(category.strip('cdlcourse'))
492            amount *= number
493        elif category == 'combi' and combi:
494            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
495            for cat in combi:
496                fee_name = cat + '_fee'
497                cat_amount = getattr(academic_session, fee_name, 0.0)
498                if not cat_amount:
499                    return _('%s undefined.' % categories[cat]), None
500                amount += cat_amount
501                p_item += u'%s + ' % categories[cat]
502            p_item = p_item.strip(' + ')
503        else:
504            fee_name = category + '_fee'
505            amount = getattr(academic_session, fee_name, 0.0)
506        if amount in (0.0, None):
507            return _('Amount could not be determined.'), None
508        if self.samePaymentMade(student, category, p_item, p_session):
509            return _('This type of payment has already been made.'), None
510        if self._isPaymentDisabled(p_session, category, student):
511            return _('This category of payments has been disabled.'), None
512        payment = createObject(u'waeup.StudentOnlinePayment')
513        timestamp = ("%d" % int(time()*10000))[1:]
514        if category in (
515            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
516            payment.provider_amt = 5000.0
517        if category in (
518            'schoolfee', 'schoolfee40') and student.is_jupeb:
519            payment.provider_amt = 5000.0
520        payment.p_id = "p%s" % timestamp
521        payment.p_category = category
522        payment.p_item = p_item
523        payment.p_session = p_session
524        payment.p_level = p_level
525        payment.p_current = p_current
526        payment.amount_auth = amount
527        payment.p_combi = combi
528        return None, payment
529
530    def setBalanceDetails(self, category, student,
531            balance_session, balance_level, balance_amount):
532        """Create a balance payment ticket and set the payment data
533        as selected by the student.
534        """
535        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
536            and balance_session > 2019:
537            rpm = self._requiredPaymentsMissing(student, balance_session)
538            if rpm:
539                return rpm, None
540        return super(
541            CustomStudentsUtils, self).setBalanceDetails(category, student,
542            balance_session, balance_level, balance_amount)
543
544    def constructMatricNumber(self, student):
545        """Fetch the matric number counter which fits the student and
546        construct the new matric number of the student.
547        """
548        next_integer = grok.getSite()['configuration'].next_matric_integer
549        if next_integer == 0:
550            return _('Matriculation number cannot be set.'), None
551        if not student.state in (
552            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
553            return _('Matriculation number cannot be set.'), None
554        year = unicode(student.entry_session)[2:]
555        if grok.getSite().__name__ == 'iuokada-cdl':
556            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
557        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.