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
Line 
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##
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        if category == 'combi':
117            return False
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
128    def setPaymentDetails(self, category, student,
129            previous_session=None, previous_level=None, combi=[]):
130        """Create a payment ticket and set the payment data of a
131        student for the payment category specified.
132        """
133        if grok.getSite().__name__ == 'iuokada-cdl':
134            return self.setCDLPaymentDetails(category, student,
135                previous_session, previous_level, combi)
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
156            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
157                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
158                + ['schoolfee','schoolfee40','secondinstal'] \
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)
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.
168        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
169            rpm = self._requiredPaymentsMissing(student, p_session)
170            if rpm:
171                return rpm, None
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)
185                if category == 'schoolfee40':
186                    amount = 0.4*amount
187                elif category == 'secondinstal':
188                    amount = 0.6*amount
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:
196                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
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
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
219            if student.is_postgrad:
220                amount *= 0.5
221        elif category.startswith('resit'):
222            amount = academic_session.resit_fee
223            number = int(category.strip('resit'))
224            amount *= number
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
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(' + ')
251        elif category == 'required_combi':
252            if student.is_postgrad:
253                rp = self.REQUIRED_PAYMENTS_PG
254            elif student.is_fresh:
255                rp = self.REQUIRED_PAYMENTS_FRESH
256            else:
257                rp = self.REQUIRED_PAYMENTS_RETURNING
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(' + ')
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:]
277        if category in (
278            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
279            payment.provider_amt = 5000.0
280        if category in (
281            'schoolfee', 'schoolfee40') and student.is_jupeb:
282            payment.provider_amt = 5000.0
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
290        payment.p_combi = combi
291        return None, payment
292
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:
386            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
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
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        """
427        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
428            and balance_session > 2019:
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
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
443        if not student.state in (
444            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
445            return _('Matriculation number cannot be set.'), None
446        year = unicode(student.entry_session)[2:]
447        if grok.getSite().__name__ == 'iuokada-cdl':
448            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
449        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
Note: See TracBrowser for help on using the repository browser.