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, 16 hours ago

Disable sundry combi payments.

  • Property svn:keywords set to Id
File size: 25.1 KB
Line 
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##
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        combi_cats = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
203        if len(student['payments']):
204            for category in rp.keys():
205                for ticket in student['payments'].values():
206                    if ticket.p_state == 'paid' and \
207                        ticket.p_category == category and \
208                        ticket.p_session == session:
209                        del cats_missing[category]
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]
215                if not cats_missing:
216                    return
217        return "%s must be paid before Tution Fee." % ', '.join(
218            cats_missing.values())
219
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
232    def samePaymentMade(self, student, category, p_item, p_session):
233        if category.startswith('resit'):
234            return False
235        if category == 'combi':
236            return False
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
247    def setPaymentDetails(self, category, student,
248            previous_session=None, previous_level=None, combi=[]):
249        """Create a payment ticket and set the payment data of a
250        student for the payment category specified.
251        """
252        if grok.getSite().__name__ == 'iuokada-cdl':
253            return self.setCDLPaymentDetails(category, student,
254                previous_session, previous_level, combi)
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
275            if category in list(self._all_required_payments) + ['required_combi',] \
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)
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.
285        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
286            rpm = self._requiredPaymentsMissing(student, p_session)
287            if rpm:
288                return rpm, None
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)
302                if category == 'schoolfee40':
303                    amount = 0.4*amount
304                elif category == 'secondinstal':
305                    amount = 0.6*amount
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:
313                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
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
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
336            if student.is_postgrad:
337                amount *= 0.5
338            if student.certcode in ('DPHARM','MBBSMED','BSCNNUR','LLB'):
339                amount += 200000
340        elif category.startswith('resit'):
341            amount = academic_session.resit_fee
342            number = int(category.strip('resit'))
343            amount *= number
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
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(' + ')
370
371        elif category == 'required_combi':
372            rp = self._collect_required_payment_items(student)
373            for cat in rp.keys():
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(' + ')
381
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:]
393        if category in (
394            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
395            payment.provider_amt = 5000.0
396        if category in (
397            'schoolfee', 'schoolfee40') and student.is_jupeb:
398            payment.provider_amt = 5000.0
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
406        payment.p_combi = combi
407        return None, payment
408
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:
502            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
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
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        """
543        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
544            and balance_session > 2019:
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
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
559        if not student.state in (
560            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
561            return _('Matriculation number cannot be set.'), None
562        year = unicode(student.entry_session)[2:]
563        if grok.getSite().__name__ == 'iuokada-cdl':
564            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
565        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.