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

Last change on this file since 18034 was 18034, checked in by Henrik Bettermann, 8 hours ago

Implement required combi split payments.

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