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

Last change on this file since 18135 was 18135, checked in by Henrik Bettermann, 10 hours ago

Minor modifications requested in #252

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