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

Last change on this file since 18131 was 18131, checked in by Henrik Bettermann, 2 days ago

Disable sundry combi payments.

  • Property svn:keywords set to Id
File size: 25.1 KB
RevLine 
[10765]1## $Id: utils.py 18131 2025-07-19 06:41:05Z 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##
[15802]18import grok
[10765]19from time import time
[16136]20from copy import deepcopy
[10765]21from zope.component import createObject, getUtility
22from waeup.kofa.interfaces import (IKofaUtils,
[16091]23    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
24    REGISTERED, VALIDATED)
[10765]25from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
[15563]26from kofacustom.iuokada.interfaces import MessageFactory as _
[10765]27
28class CustomStudentsUtils(NigeriaStudentsUtils):
29    """A collection of customized methods.
30
31    """
32
[17661]33    @property
34    def STUDENT_ID_PREFIX(self):
35        if grok.getSite().__name__ == 'iuokada-cdl':
36            return u'F'
37        return u'I'
[15645]38
[17661]39
[15649]40    SKIP_UPLOAD_VIEWLETS = (
[15654]41        'acceptanceletterupload', 'certificateupload'
[15649]42        )
[15653]43    # Maximum size of upload files in kB
44    MAX_KB = 500
[15649]45
[15656]46    #: A tuple containing the names of registration states in which changing of
47    #: passport pictures is allowed.
[16136]48
[15657]49    PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARANCE,)
[15656]50
[18034]51    REQUIRED_PAYMENTS_FRESH_SCIENCE = {
[17900]52        'registration_fresh': 'Registration Fee (Fresh)',
[16136]53        'book': 'Book Deposit',
54        'develop': 'Development Fee',
55        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
[16223]56        'municipal_fresh': 'Fresh Students Municipal Fee',
[18034]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',
[16136]64        }
65
[18034]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
[16223]81    REQUIRED_PAYMENTS_RETURNING = {
[17900]82        'registration_return': 'Registration Fee (Returning)',
[16223]83        'book': 'Book Deposit',
84        'develop': 'Development Fee',
85        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
86        'municipal_returning': 'Returning Students Municipal Fee',
[18034]87        'health_insurance': 'Student Health Insurance',
[16223]88        }
89
[18034]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',
[18037]101        'grad_clearance': 'Clearance Fees',
[18034]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',
[18037]125        'grad_clearance': 'Clearance Fees',
[18034]126        'lab_support': 'Lab Support',
127        }
128
[16233]129    REQUIRED_PAYMENTS_PG = {
130        'pg_other': 'PG Other Charges',
131        }
132
[15660]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        """
[16377]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
[17947]141        #if studylevel.certcode == 'LLB':
142        #    max_credits = 50
143        if studylevel.certcode == 'MBBSMED' and studylevel.level == 200:
144            max_credits = 100
[16377]145        if course and studylevel.total_credits + course.credits > max_credits:
[15660]146            return _('Maximum credits exceeded.')
[16377]147        elif studylevel.total_credits > max_credits:
[15660]148            return _('Maximum credits exceeded.')
149        return
150
[18034]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
[16257]161
[18034]162    def _collect_required_payment_items(self, student):
[16233]163        if student.is_postgrad:
164            rp = self.REQUIRED_PAYMENTS_PG
[18034]165        elif student.is_fresh and student.faccode in ('ENG', 'HSC', 'NAS', 'PHM'):
166            rp = self.REQUIRED_PAYMENTS_FRESH_SCIENCE
[16233]167        elif student.is_fresh:
[18034]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
[16223]175        else:
[18036]176            rp = self.REQUIRED_PAYMENTS_RETURNING
[18034]177        return rp
178
179    def _requiredPaymentsMissing(self, student, session):
[18050]180        # disabled on 03/04/25
[18108]181        # reenabled on 08/04/25
182        if session < 2025:
183            return
[18037]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
[18043]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
[18034]193        # Has the required combi payment been made?
[16223]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
[18034]199        # If not, check single payments
200        rp = self._collect_required_payment_items(student)
[16223]201        cats_missing = deepcopy(rp)
[18131]202        combi_cats = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
[16136]203        if len(student['payments']):
[16223]204            for category in rp.keys():
[16136]205                for ticket in student['payments'].values():
206                    if ticket.p_state == 'paid' and \
[16223]207                        ticket.p_category == category and \
[16136]208                        ticket.p_session == session:
209                        del cats_missing[category]
[18131]210                    elif ticket.p_state == 'paid' and \
211                        ticket.p_category == 'combi' and \
212                        combi_cats[category] in ticket.p_item and \
213                        ticket.p_session == session:
214                        del cats_missing[category]
[16222]215                if not cats_missing:
216                    return
[18122]217        return "%s must be paid before Tution Fee." % ', '.join(
[16222]218            cats_missing.values())
[16136]219
[18034]220    @property
221    def _all_required_payments(self):
222        return set(
223            self.REQUIRED_PAYMENTS_PG.keys()
224            + self.REQUIRED_PAYMENTS_FRESH_SCIENCE.keys()
225            + self.REQUIRED_PAYMENTS_FRESH_NON_SCIENCE.keys()
226            + self.REQUIRED_PAYMENTS_FINAL_PHARMACY.keys()
227            + self.REQUIRED_PAYMENTS_RETURNING_PHARMACY.keys()
228            + self.REQUIRED_PAYMENTS_FINAL.keys()
229            + self.REQUIRED_PAYMENTS_RETURNING.keys()
230            )
231
[16583]232    def samePaymentMade(self, student, category, p_item, p_session):
233        if category.startswith('resit'):
234            return False
[17889]235        if category == 'combi':
236            return False
[16583]237        for key in student['payments'].keys():
238            ticket = student['payments'][key]
239            if ticket.p_state == 'paid' and\
240               ticket.p_category == category and \
241               ticket.p_item != 'Balance' and \
242               ticket.p_item == p_item and \
243               ticket.p_session == p_session:
244                  return True
245        return False
246
[15645]247    def setPaymentDetails(self, category, student,
[16136]248            previous_session=None, previous_level=None, combi=[]):
[15645]249        """Create a payment ticket and set the payment data of a
250        student for the payment category specified.
251        """
[17661]252        if grok.getSite().__name__ == 'iuokada-cdl':
253            return self.setCDLPaymentDetails(category, student,
254                previous_session, previous_level, combi)
[15645]255        p_item = u''
256        amount = 0.0
257        if previous_session:
258            if previous_session < student['studycourse'].entry_session:
259                return _('The previous session must not fall below '
260                         'your entry session.'), None
261            if category == 'schoolfee':
262                # School fee is always paid for the following session
263                if previous_session > student['studycourse'].current_session:
264                    return _('This is not a previous session.'), None
265            else:
266                if previous_session > student['studycourse'].current_session - 1:
267                    return _('This is not a previous session.'), None
268            p_session = previous_session
269            p_level = previous_level
270            p_current = False
271        else:
272            p_session = student['studycourse'].current_session
273            p_level = student['studycourse'].current_level
274            p_current = True
[18034]275            if category in list(self._all_required_payments) + ['required_combi',] \
[16136]276                and student.state == RETURNING:
277                # In case of school fee or required sundry fee payments the
278                # payment session is always next session if students are in
279                # state returning.
280                p_session, p_level = self.getReturningData(student)
[15645]281        academic_session = self._getSessionConfiguration(p_session)
282        if academic_session == None:
283            return _(u'Session configuration object is not available.'), None
284        # Determine fee.
[16138]285        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
[16136]286            rpm = self._requiredPaymentsMissing(student, p_session)
287            if rpm:
288                return rpm, None
[15645]289            try:
290                certificate = student['studycourse'].certificate
291                p_item = certificate.code
292            except (AttributeError, TypeError):
293                return _('Study course data are incomplete.'), None
294            if previous_session:
295                # Students can pay for previous sessions in all
296                # workflow states.  Fresh students are excluded by the
297                # update method of the PreviousPaymentAddFormPage.
298                if previous_level == 100:
299                    amount = getattr(certificate, 'school_fee_1', 0.0)
300                else:
301                    amount = getattr(certificate, 'school_fee_2', 0.0)
[15780]302                if category == 'schoolfee40':
[15773]303                    amount = 0.4*amount
[15780]304                elif category == 'secondinstal':
[15773]305                    amount = 0.6*amount
[15780]306            else:
307                if category == 'secondinstal':
308                    if student.is_fresh:
309                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
310                    else:
311                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
312                else:
[16091]313                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
[15780]314                        amount = getattr(certificate, 'school_fee_1', 0.0)
315                    elif student.state == RETURNING:
316                        amount = getattr(certificate, 'school_fee_2', 0.0)
317                    elif student.is_postgrad and student.state == PAID:
318                        # Returning postgraduate students also pay for the
319                        # next session but their level always remains the
320                        # same.
321                        p_session += 1
322                        academic_session = self._getSessionConfiguration(p_session)
323                        if academic_session == None:
324                            return _(
325                                u'Session configuration object is not available.'
326                                ), None
327                        amount = getattr(certificate, 'school_fee_2', 0.0)
328                    if amount and category == 'schoolfee40':
329                        amount = 0.4*amount
[15645]330        elif category == 'clearance':
331            try:
332                p_item = student['studycourse'].certificate.code
333            except (AttributeError, TypeError):
334                return _('Study course data are incomplete.'), None
335            amount = academic_session.clearance_fee
[16464]336            if student.is_postgrad:
337                amount *= 0.5
[18118]338            if student.certcode in ('DPHARM','MBBSMED','BSCNNUR','LLB'):
[18111]339                amount += 200000
[15937]340        elif category.startswith('resit'):
341            amount = academic_session.resit_fee
342            number = int(category.strip('resit'))
[16571]343            amount *= number
[15661]344        #elif category == 'bed_allocation':
345        #    p_item = self.getAccommodationDetails(student)['bt']
346        #    amount = academic_session.booking_fee
347        #elif category == 'hostel_maintenance':
348        #    amount = 0.0
349        #    bedticket = student['accommodation'].get(
350        #        str(student.current_session), None)
351        #    if bedticket is not None and bedticket.bed is not None:
352        #        p_item = bedticket.bed_coordinates
353        #        if bedticket.bed.__parent__.maint_fee > 0:
354        #            amount = bedticket.bed.__parent__.maint_fee
355        #        else:
356        #            # fallback
357        #            amount = academic_session.maint_fee
358        #    else:
359        #        return _(u'No bed allocated.'), None
[15676]360        elif category == 'combi' and combi:
361            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
362            for cat in combi:
363                fee_name = cat + '_fee'
364                cat_amount = getattr(academic_session, fee_name, 0.0)
365                if not cat_amount:
366                    return _('%s undefined.' % categories[cat]), None
367                amount += cat_amount
368                p_item += u'%s + ' % categories[cat]
369            p_item = p_item.strip(' + ')
[18034]370
[16222]371        elif category == 'required_combi':
[18034]372            rp = self._collect_required_payment_items(student)
373            for cat in rp.keys():
[16222]374                fee_name = cat + '_fee'
375                cat_amount = getattr(academic_session, fee_name, 0.0)
376                if not cat_amount:
377                    return _('%s undefined.' % rp[cat]), None
378                amount += cat_amount
379                p_item += u'%s + ' % rp[cat]
380            p_item = p_item.strip(' + ')
[18034]381
[15645]382        else:
383            fee_name = category + '_fee'
384            amount = getattr(academic_session, fee_name, 0.0)
385        if amount in (0.0, None):
386            return _('Amount could not be determined.'), None
387        if self.samePaymentMade(student, category, p_item, p_session):
388            return _('This type of payment has already been made.'), None
389        if self._isPaymentDisabled(p_session, category, student):
390            return _('This category of payments has been disabled.'), None
391        payment = createObject(u'waeup.StudentOnlinePayment')
392        timestamp = ("%d" % int(time()*10000))[1:]
[16270]393        if category in (
[16396]394            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
[16270]395            payment.provider_amt = 5000.0
396        if category in (
397            'schoolfee', 'schoolfee40') and student.is_jupeb:
398            payment.provider_amt = 5000.0
[15645]399        payment.p_id = "p%s" % timestamp
400        payment.p_category = category
401        payment.p_item = p_item
402        payment.p_session = p_session
403        payment.p_level = p_level
404        payment.p_current = p_current
405        payment.amount_auth = amount
[15686]406        payment.p_combi = combi
[15802]407        return None, payment
408
[17661]409    def setCDLPaymentDetails(self, category, student,
410            previous_session=None, previous_level=None, combi=[]):
411        """Create a payment ticket and set the payment data of a
412        student for the payment category specified.
413        """
414        p_item = u''
415        amount = 0.0
416        if previous_session:
417            if previous_session < student['studycourse'].entry_session:
418                return _('The previous session must not fall below '
419                         'your entry session.'), None
420            if category == 'schoolfee':
421                # School fee is always paid for the following session
422                if previous_session > student['studycourse'].current_session:
423                    return _('This is not a previous session.'), None
424            else:
425                if previous_session > student['studycourse'].current_session - 1:
426                    return _('This is not a previous session.'), None
427            p_session = previous_session
428            p_level = previous_level
429            p_current = False
430        else:
431            p_session = student['studycourse'].current_session
432            p_level = student['studycourse'].current_level
433            p_current = True
434            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
435                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
436                + ['schoolfee','schoolfee40','secondinstal'] \
437                and student.state == RETURNING:
438                # In case of school fee or required sundry fee payments the
439                # payment session is always next session if students are in
440                # state returning.
441                p_session, p_level = self.getReturningData(student)
442        academic_session = self._getSessionConfiguration(p_session)
443        if academic_session == None:
444            return _(u'Session configuration object is not available.'), None
445        # Determine fee.
446        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
447            rpm = self._requiredPaymentsMissing(student, p_session)
448            if rpm:
449                return rpm, None
450            try:
451                certificate = student['studycourse'].certificate
452                p_item = certificate.code
453            except (AttributeError, TypeError):
454                return _('Study course data are incomplete.'), None
455            if previous_session:
456                # Students can pay for previous sessions in all
457                # workflow states.  Fresh students are excluded by the
458                # update method of the PreviousPaymentAddFormPage.
459                if previous_level == 100:
460                    amount = getattr(certificate, 'school_fee_1', 0.0)
461                else:
462                    amount = getattr(certificate, 'school_fee_2', 0.0)
463                if category == 'schoolfee40':
464                    amount = 0.4*amount
465                elif category == 'secondinstal':
466                    amount = 0.6*amount
467            else:
468                if category == 'secondinstal':
469                    if student.is_fresh:
470                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
471                    else:
472                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
473                else:
474                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
475                        amount = getattr(certificate, 'school_fee_1', 0.0)
476                    elif student.state == RETURNING:
477                        amount = getattr(certificate, 'school_fee_2', 0.0)
478                    elif student.is_postgrad and student.state == PAID:
479                        # Returning postgraduate students also pay for the
480                        # next session but their level always remains the
481                        # same.
482                        p_session += 1
483                        academic_session = self._getSessionConfiguration(p_session)
484                        if academic_session == None:
485                            return _(
486                                u'Session configuration object is not available.'
487                                ), None
488                        amount = getattr(certificate, 'school_fee_2', 0.0)
489                    if amount and category == 'schoolfee40':
490                        amount = 0.4*amount
491        elif category == 'clearance':
492            try:
493                p_item = student['studycourse'].certificate.code
494            except (AttributeError, TypeError):
495                return _('Study course data are incomplete.'), None
496            amount = academic_session.clearance_fee
497        elif category.startswith('cdlcourse'):
498            amount = academic_session.course_fee
499            number = int(category.strip('cdlcourse'))
500            amount *= number
501        elif category == 'combi' and combi:
[17664]502            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
[17661]503            for cat in combi:
504                fee_name = cat + '_fee'
505                cat_amount = getattr(academic_session, fee_name, 0.0)
506                if not cat_amount:
507                    return _('%s undefined.' % categories[cat]), None
508                amount += cat_amount
509                p_item += u'%s + ' % categories[cat]
510            p_item = p_item.strip(' + ')
511        else:
512            fee_name = category + '_fee'
513            amount = getattr(academic_session, fee_name, 0.0)
514        if amount in (0.0, None):
515            return _('Amount could not be determined.'), None
516        if self.samePaymentMade(student, category, p_item, p_session):
517            return _('This type of payment has already been made.'), None
518        if self._isPaymentDisabled(p_session, category, student):
519            return _('This category of payments has been disabled.'), None
520        payment = createObject(u'waeup.StudentOnlinePayment')
521        timestamp = ("%d" % int(time()*10000))[1:]
522        if category in (
523            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
524            payment.provider_amt = 5000.0
525        if category in (
526            'schoolfee', 'schoolfee40') and student.is_jupeb:
527            payment.provider_amt = 5000.0
528        payment.p_id = "p%s" % timestamp
529        payment.p_category = category
530        payment.p_item = p_item
531        payment.p_session = p_session
532        payment.p_level = p_level
533        payment.p_current = p_current
534        payment.amount_auth = amount
535        payment.p_combi = combi
536        return None, payment
537
[16138]538    def setBalanceDetails(self, category, student,
539            balance_session, balance_level, balance_amount):
540        """Create a balance payment ticket and set the payment data
541        as selected by the student.
542        """
[16139]543        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
544            and balance_session > 2019:
[16138]545            rpm = self._requiredPaymentsMissing(student, balance_session)
546            if rpm:
547                return rpm, None
548        return super(
549            CustomStudentsUtils, self).setBalanceDetails(category, student,
550            balance_session, balance_level, balance_amount)
551
[15802]552    def constructMatricNumber(self, student):
553        """Fetch the matric number counter which fits the student and
554        construct the new matric number of the student.
555        """
556        next_integer = grok.getSite()['configuration'].next_matric_integer
557        if next_integer == 0:
558            return _('Matriculation number cannot be set.'), None
[15810]559        if not student.state in (
560            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
[15805]561            return _('Matriculation number cannot be set.'), None
[15802]562        year = unicode(student.entry_session)[2:]
[17818]563        if grok.getSite().__name__ == 'iuokada-cdl':
564            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
[16294]565        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.