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

Last change on this file since 17889 was 17889, checked in by Henrik Bettermann, 5 weeks ago

Combi payments can be paid several times per session.

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