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

Last change on this file since 17958 was 17947, checked in by Henrik Bettermann, 4 weeks ago

Change max_credits.

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