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

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

Enable combo payments.

  • Property svn:keywords set to Id
File size: 20.1 KB
Line 
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##
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 = {
52        'registration': 'Registration Fee',
53        'book': 'Book Deposit',
54        'develop': 'Development Fee',
55        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
56        'municipal_fresh': 'Fresh Students Municipal Fee',
57        }
58
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
67    REQUIRED_PAYMENTS_PG = {
68        'pg_other': 'PG Other Charges',
69        }
70
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        """
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:
80            return _('Maximum credits exceeded.')
81        elif studylevel.total_credits > max_credits:
82            return _('Maximum credits exceeded.')
83        return
84
85    def _requiredPaymentsMissing(self, student, session):
86        # Deactivated on 29/09/20 (don't know why)
87        return
88
89        if student.is_postgrad:
90            rp = self.REQUIRED_PAYMENTS_PG
91        elif student.is_fresh:
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)
101        if len(student['payments']):
102            for category in rp.keys():
103                for ticket in student['payments'].values():
104                    if ticket.p_state == 'paid' and \
105                        ticket.p_category == category and \
106                        ticket.p_session == session:
107                        del cats_missing[category]
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())
112
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
126    def setPaymentDetails(self, category, student,
127            previous_session=None, previous_level=None, combi=[]):
128        """Create a payment ticket and set the payment data of a
129        student for the payment category specified.
130        """
131        if grok.getSite().__name__ == 'iuokada-cdl':
132            return self.setCDLPaymentDetails(category, student,
133                previous_session, previous_level, combi)
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
154            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
155                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
156                + ['schoolfee','schoolfee40','secondinstal'] \
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)
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.
166        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
167            rpm = self._requiredPaymentsMissing(student, p_session)
168            if rpm:
169                return rpm, None
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)
183                if category == 'schoolfee40':
184                    amount = 0.4*amount
185                elif category == 'secondinstal':
186                    amount = 0.6*amount
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:
194                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
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
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
217            if student.is_postgrad:
218                amount *= 0.5
219        elif category.startswith('resit'):
220            amount = academic_session.resit_fee
221            number = int(category.strip('resit'))
222            amount *= number
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
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(' + ')
249        elif category == 'required_combi':
250            if student.is_postgrad:
251                rp = self.REQUIRED_PAYMENTS_PG
252            elif student.is_fresh:
253                rp = self.REQUIRED_PAYMENTS_FRESH
254            else:
255                rp = self.REQUIRED_PAYMENTS_RETURNING
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(' + ')
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:]
275        if category in (
276            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
277            payment.provider_amt = 5000.0
278        if category in (
279            'schoolfee', 'schoolfee40') and student.is_jupeb:
280            payment.provider_amt = 5000.0
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
288        payment.p_combi = combi
289        return None, payment
290
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:
384            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
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
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        """
425        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
426            and balance_session > 2019:
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
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
441        if not student.state in (
442            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
443            return _('Matriculation number cannot be set.'), None
444        year = unicode(student.entry_session)[2:]
445        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.