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

Implement required combi split payments.

  • Property svn:keywords set to Id
File size: 24.2 KB
Line 
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##
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        '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
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 = REQUIRED_PAYMENTS_RETURNING
177        return rp
178
179    def _requiredPaymentsMissing(self, student, session):
180        # Has the required combi payment been made?
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
186        # If not, check single payments
187        rp = self._collect_required_payment_items(student)
188        cats_missing = deepcopy(rp)
189        if len(student['payments']):
190            for category in rp.keys():
191                for ticket in student['payments'].values():
192                    if ticket.p_state == 'paid' and \
193                        ticket.p_category == category and \
194                        ticket.p_session == session:
195                        del cats_missing[category]
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())
200
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
213    def samePaymentMade(self, student, category, p_item, p_session):
214        if category.startswith('resit'):
215            return False
216        if category == 'combi':
217            return False
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
228    def setPaymentDetails(self, category, student,
229            previous_session=None, previous_level=None, combi=[]):
230        """Create a payment ticket and set the payment data of a
231        student for the payment category specified.
232        """
233        if grok.getSite().__name__ == 'iuokada-cdl':
234            return self.setCDLPaymentDetails(category, student,
235                previous_session, previous_level, combi)
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
256            if category in list(self._all_required_payments) + ['required_combi',] \
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)
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.
266        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
267            rpm = self._requiredPaymentsMissing(student, p_session)
268            if rpm:
269                return rpm, None
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)
283                if category == 'schoolfee40':
284                    amount = 0.4*amount
285                elif category == 'secondinstal':
286                    amount = 0.6*amount
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:
294                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
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
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
317            if student.is_postgrad:
318                amount *= 0.5
319        elif category.startswith('resit'):
320            amount = academic_session.resit_fee
321            number = int(category.strip('resit'))
322            amount *= number
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
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(' + ')
349
350        elif category == 'required_combi':
351            rp = self._collect_required_payment_items(student)
352            for cat in rp.keys():
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(' + ')
360
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:]
372        if category in (
373            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
374            payment.provider_amt = 5000.0
375        if category in (
376            'schoolfee', 'schoolfee40') and student.is_jupeb:
377            payment.provider_amt = 5000.0
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
385        payment.p_combi = combi
386        return None, payment
387
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:
481            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
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
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        """
522        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
523            and balance_session > 2019:
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
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
538        if not student.state in (
539            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
540            return _('Matriculation number cannot be set.'), None
541        year = unicode(student.entry_session)[2:]
542        if grok.getSite().__name__ == 'iuokada-cdl':
543            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
544        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.