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

Last change on this file since 17808 was 17664, checked in by Henrik Bettermann, 9 months ago

Enable combo payments.

  • Property svn:keywords set to Id
File size: 20.1 KB
RevLine 
[10765]1## $Id: utils.py 17664 2024-01-11 16:33:46Z 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
[16223]51    REQUIRED_PAYMENTS_FRESH = {
[16136]52        'registration': 'Registration Fee',
53        'book': 'Book Deposit',
54        'develop': 'Development Fee',
55        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
[16223]56        'municipal_fresh': 'Fresh Students Municipal Fee',
[16136]57        }
58
[16223]59    REQUIRED_PAYMENTS_RETURNING = {
60        'registration': 'Registration Fee',
61        'book': 'Book Deposit',
62        'develop': 'Development Fee',
63        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
64        'municipal_returning': 'Returning Students Municipal Fee',
65        }
66
[16233]67    REQUIRED_PAYMENTS_PG = {
68        'pg_other': 'PG Other Charges',
69        }
70
[15660]71    def warnCreditsOOR(self, studylevel, course=None):
72        """Return message if credits are out of range. In the base
73        package only maximum credits is set.
74        """
[16377]75        max_credits = 60
76        end_level = getattr(studylevel.__parent__.certificate, 'end_level', None)
77        if end_level and studylevel.level >= end_level:
78            max_credits = 80
79        if course and studylevel.total_credits + course.credits > max_credits:
[15660]80            return _('Maximum credits exceeded.')
[16377]81        elif studylevel.total_credits > max_credits:
[15660]82            return _('Maximum credits exceeded.')
83        return
84
[16136]85    def _requiredPaymentsMissing(self, student, session):
[16257]86        # Deactivated on 29/09/20 (don't know why)
87        return
88
[16233]89        if student.is_postgrad:
90            rp = self.REQUIRED_PAYMENTS_PG
91        elif student.is_fresh:
[16223]92            rp = self.REQUIRED_PAYMENTS_FRESH
93        else:
94            rp = self.REQUIRED_PAYMENTS_RETURNING
95        for ticket in student['payments'].values():
96            if ticket.p_category == 'required_combi'and \
97                ticket.p_session == session and \
98                ticket.p_state == 'paid':
99                return
100        cats_missing = deepcopy(rp)
[16136]101        if len(student['payments']):
[16223]102            for category in rp.keys():
[16136]103                for ticket in student['payments'].values():
104                    if ticket.p_state == 'paid' and \
[16223]105                        ticket.p_category == category and \
[16136]106                        ticket.p_session == session:
107                        del cats_missing[category]
[16222]108                if not cats_missing:
109                    return
110        return "%s must be paid before Tution Fee. Make either single payments or make a 'Required Combi Payment'." % ', '.join(
111            cats_missing.values())
[16136]112
[16583]113    def samePaymentMade(self, student, category, p_item, p_session):
114        if category.startswith('resit'):
115            return False
116        for key in student['payments'].keys():
117            ticket = student['payments'][key]
118            if ticket.p_state == 'paid' and\
119               ticket.p_category == category and \
120               ticket.p_item != 'Balance' and \
121               ticket.p_item == p_item and \
122               ticket.p_session == p_session:
123                  return True
124        return False
125
[15645]126    def setPaymentDetails(self, category, student,
[16136]127            previous_session=None, previous_level=None, combi=[]):
[15645]128        """Create a payment ticket and set the payment data of a
129        student for the payment category specified.
130        """
[17661]131        if grok.getSite().__name__ == 'iuokada-cdl':
132            return self.setCDLPaymentDetails(category, student,
133                previous_session, previous_level, combi)
[15645]134        p_item = u''
135        amount = 0.0
136        if previous_session:
137            if previous_session < student['studycourse'].entry_session:
138                return _('The previous session must not fall below '
139                         'your entry session.'), None
140            if category == 'schoolfee':
141                # School fee is always paid for the following session
142                if previous_session > student['studycourse'].current_session:
143                    return _('This is not a previous session.'), None
144            else:
145                if previous_session > student['studycourse'].current_session - 1:
146                    return _('This is not a previous session.'), None
147            p_session = previous_session
148            p_level = previous_level
149            p_current = False
150        else:
151            p_session = student['studycourse'].current_session
152            p_level = student['studycourse'].current_level
153            p_current = True
[16223]154            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
155                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
156                + ['schoolfee','schoolfee40','secondinstal'] \
[16136]157                and student.state == RETURNING:
158                # In case of school fee or required sundry fee payments the
159                # payment session is always next session if students are in
160                # state returning.
161                p_session, p_level = self.getReturningData(student)
[15645]162        academic_session = self._getSessionConfiguration(p_session)
163        if academic_session == None:
164            return _(u'Session configuration object is not available.'), None
165        # Determine fee.
[16138]166        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
[16136]167            rpm = self._requiredPaymentsMissing(student, p_session)
168            if rpm:
169                return rpm, None
[15645]170            try:
171                certificate = student['studycourse'].certificate
172                p_item = certificate.code
173            except (AttributeError, TypeError):
174                return _('Study course data are incomplete.'), None
175            if previous_session:
176                # Students can pay for previous sessions in all
177                # workflow states.  Fresh students are excluded by the
178                # update method of the PreviousPaymentAddFormPage.
179                if previous_level == 100:
180                    amount = getattr(certificate, 'school_fee_1', 0.0)
181                else:
182                    amount = getattr(certificate, 'school_fee_2', 0.0)
[15780]183                if category == 'schoolfee40':
[15773]184                    amount = 0.4*amount
[15780]185                elif category == 'secondinstal':
[15773]186                    amount = 0.6*amount
[15780]187            else:
188                if category == 'secondinstal':
189                    if student.is_fresh:
190                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
191                    else:
192                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
193                else:
[16091]194                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
[15780]195                        amount = getattr(certificate, 'school_fee_1', 0.0)
196                    elif student.state == RETURNING:
197                        amount = getattr(certificate, 'school_fee_2', 0.0)
198                    elif student.is_postgrad and student.state == PAID:
199                        # Returning postgraduate students also pay for the
200                        # next session but their level always remains the
201                        # same.
202                        p_session += 1
203                        academic_session = self._getSessionConfiguration(p_session)
204                        if academic_session == None:
205                            return _(
206                                u'Session configuration object is not available.'
207                                ), None
208                        amount = getattr(certificate, 'school_fee_2', 0.0)
209                    if amount and category == 'schoolfee40':
210                        amount = 0.4*amount
[15645]211        elif category == 'clearance':
212            try:
213                p_item = student['studycourse'].certificate.code
214            except (AttributeError, TypeError):
215                return _('Study course data are incomplete.'), None
216            amount = academic_session.clearance_fee
[16464]217            if student.is_postgrad:
218                amount *= 0.5
[15937]219        elif category.startswith('resit'):
220            amount = academic_session.resit_fee
221            number = int(category.strip('resit'))
[16571]222            amount *= number
[15661]223        #elif category == 'bed_allocation':
224        #    p_item = self.getAccommodationDetails(student)['bt']
225        #    amount = academic_session.booking_fee
226        #elif category == 'hostel_maintenance':
227        #    amount = 0.0
228        #    bedticket = student['accommodation'].get(
229        #        str(student.current_session), None)
230        #    if bedticket is not None and bedticket.bed is not None:
231        #        p_item = bedticket.bed_coordinates
232        #        if bedticket.bed.__parent__.maint_fee > 0:
233        #            amount = bedticket.bed.__parent__.maint_fee
234        #        else:
235        #            # fallback
236        #            amount = academic_session.maint_fee
237        #    else:
238        #        return _(u'No bed allocated.'), None
[15676]239        elif category == 'combi' and combi:
240            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
241            for cat in combi:
242                fee_name = cat + '_fee'
243                cat_amount = getattr(academic_session, fee_name, 0.0)
244                if not cat_amount:
245                    return _('%s undefined.' % categories[cat]), None
246                amount += cat_amount
247                p_item += u'%s + ' % categories[cat]
248            p_item = p_item.strip(' + ')
[16222]249        elif category == 'required_combi':
[16233]250            if student.is_postgrad:
251                rp = self.REQUIRED_PAYMENTS_PG
252            elif student.is_fresh:
253                rp = self.REQUIRED_PAYMENTS_FRESH
[16223]254            else:
[16233]255                rp = self.REQUIRED_PAYMENTS_RETURNING
[16222]256            for cat in rp:
257                fee_name = cat + '_fee'
258                cat_amount = getattr(academic_session, fee_name, 0.0)
259                if not cat_amount:
260                    return _('%s undefined.' % rp[cat]), None
261                amount += cat_amount
262                p_item += u'%s + ' % rp[cat]
263            p_item = p_item.strip(' + ')
[15645]264        else:
265            fee_name = category + '_fee'
266            amount = getattr(academic_session, fee_name, 0.0)
267        if amount in (0.0, None):
268            return _('Amount could not be determined.'), None
269        if self.samePaymentMade(student, category, p_item, p_session):
270            return _('This type of payment has already been made.'), None
271        if self._isPaymentDisabled(p_session, category, student):
272            return _('This category of payments has been disabled.'), None
273        payment = createObject(u'waeup.StudentOnlinePayment')
274        timestamp = ("%d" % int(time()*10000))[1:]
[16270]275        if category in (
[16396]276            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
[16270]277            payment.provider_amt = 5000.0
278        if category in (
279            'schoolfee', 'schoolfee40') and student.is_jupeb:
280            payment.provider_amt = 5000.0
[15645]281        payment.p_id = "p%s" % timestamp
282        payment.p_category = category
283        payment.p_item = p_item
284        payment.p_session = p_session
285        payment.p_level = p_level
286        payment.p_current = p_current
287        payment.amount_auth = amount
[15686]288        payment.p_combi = combi
[15802]289        return None, payment
290
[17661]291    def setCDLPaymentDetails(self, category, student,
292            previous_session=None, previous_level=None, combi=[]):
293        """Create a payment ticket and set the payment data of a
294        student for the payment category specified.
295        """
296        p_item = u''
297        amount = 0.0
298        if previous_session:
299            if previous_session < student['studycourse'].entry_session:
300                return _('The previous session must not fall below '
301                         'your entry session.'), None
302            if category == 'schoolfee':
303                # School fee is always paid for the following session
304                if previous_session > student['studycourse'].current_session:
305                    return _('This is not a previous session.'), None
306            else:
307                if previous_session > student['studycourse'].current_session - 1:
308                    return _('This is not a previous session.'), None
309            p_session = previous_session
310            p_level = previous_level
311            p_current = False
312        else:
313            p_session = student['studycourse'].current_session
314            p_level = student['studycourse'].current_level
315            p_current = True
316            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
317                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
318                + ['schoolfee','schoolfee40','secondinstal'] \
319                and student.state == RETURNING:
320                # In case of school fee or required sundry fee payments the
321                # payment session is always next session if students are in
322                # state returning.
323                p_session, p_level = self.getReturningData(student)
324        academic_session = self._getSessionConfiguration(p_session)
325        if academic_session == None:
326            return _(u'Session configuration object is not available.'), None
327        # Determine fee.
328        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
329            rpm = self._requiredPaymentsMissing(student, p_session)
330            if rpm:
331                return rpm, None
332            try:
333                certificate = student['studycourse'].certificate
334                p_item = certificate.code
335            except (AttributeError, TypeError):
336                return _('Study course data are incomplete.'), None
337            if previous_session:
338                # Students can pay for previous sessions in all
339                # workflow states.  Fresh students are excluded by the
340                # update method of the PreviousPaymentAddFormPage.
341                if previous_level == 100:
342                    amount = getattr(certificate, 'school_fee_1', 0.0)
343                else:
344                    amount = getattr(certificate, 'school_fee_2', 0.0)
345                if category == 'schoolfee40':
346                    amount = 0.4*amount
347                elif category == 'secondinstal':
348                    amount = 0.6*amount
349            else:
350                if category == 'secondinstal':
351                    if student.is_fresh:
352                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
353                    else:
354                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
355                else:
356                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
357                        amount = getattr(certificate, 'school_fee_1', 0.0)
358                    elif student.state == RETURNING:
359                        amount = getattr(certificate, 'school_fee_2', 0.0)
360                    elif student.is_postgrad and student.state == PAID:
361                        # Returning postgraduate students also pay for the
362                        # next session but their level always remains the
363                        # same.
364                        p_session += 1
365                        academic_session = self._getSessionConfiguration(p_session)
366                        if academic_session == None:
367                            return _(
368                                u'Session configuration object is not available.'
369                                ), None
370                        amount = getattr(certificate, 'school_fee_2', 0.0)
371                    if amount and category == 'schoolfee40':
372                        amount = 0.4*amount
373        elif category == 'clearance':
374            try:
375                p_item = student['studycourse'].certificate.code
376            except (AttributeError, TypeError):
377                return _('Study course data are incomplete.'), None
378            amount = academic_session.clearance_fee
379        elif category.startswith('cdlcourse'):
380            amount = academic_session.course_fee
381            number = int(category.strip('cdlcourse'))
382            amount *= number
383        elif category == 'combi' and combi:
[17664]384            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
[17661]385            for cat in combi:
386                fee_name = cat + '_fee'
387                cat_amount = getattr(academic_session, fee_name, 0.0)
388                if not cat_amount:
389                    return _('%s undefined.' % categories[cat]), None
390                amount += cat_amount
391                p_item += u'%s + ' % categories[cat]
392            p_item = p_item.strip(' + ')
393        else:
394            fee_name = category + '_fee'
395            amount = getattr(academic_session, fee_name, 0.0)
396        if amount in (0.0, None):
397            return _('Amount could not be determined.'), None
398        if self.samePaymentMade(student, category, p_item, p_session):
399            return _('This type of payment has already been made.'), None
400        if self._isPaymentDisabled(p_session, category, student):
401            return _('This category of payments has been disabled.'), None
402        payment = createObject(u'waeup.StudentOnlinePayment')
403        timestamp = ("%d" % int(time()*10000))[1:]
404        if category in (
405            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
406            payment.provider_amt = 5000.0
407        if category in (
408            'schoolfee', 'schoolfee40') and student.is_jupeb:
409            payment.provider_amt = 5000.0
410        payment.p_id = "p%s" % timestamp
411        payment.p_category = category
412        payment.p_item = p_item
413        payment.p_session = p_session
414        payment.p_level = p_level
415        payment.p_current = p_current
416        payment.amount_auth = amount
417        payment.p_combi = combi
418        return None, payment
419
[16138]420    def setBalanceDetails(self, category, student,
421            balance_session, balance_level, balance_amount):
422        """Create a balance payment ticket and set the payment data
423        as selected by the student.
424        """
[16139]425        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
426            and balance_session > 2019:
[16138]427            rpm = self._requiredPaymentsMissing(student, balance_session)
428            if rpm:
429                return rpm, None
430        return super(
431            CustomStudentsUtils, self).setBalanceDetails(category, student,
432            balance_session, balance_level, balance_amount)
433
[15802]434    def constructMatricNumber(self, student):
435        """Fetch the matric number counter which fits the student and
436        construct the new matric number of the student.
437        """
438        next_integer = grok.getSite()['configuration'].next_matric_integer
439        if next_integer == 0:
440            return _('Matriculation number cannot be set.'), None
[15810]441        if not student.state in (
442            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
[15805]443            return _('Matriculation number cannot be set.'), None
[15802]444        year = unicode(student.entry_session)[2:]
[16294]445        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.