## $Id: utils.py 18037 2025-03-12 14:32:29Z henrik $
##
## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
import grok
from time import time
from copy import deepcopy
from zope.component import createObject, getUtility
from waeup.kofa.interfaces import (IKofaUtils,
    ADMITTED, CLEARANCE, REQUESTED, CLEARED, RETURNING, PAID,
    REGISTERED, VALIDATED)
from kofacustom.nigeria.students.utils import NigeriaStudentsUtils
from kofacustom.iuokada.interfaces import MessageFactory as _

class CustomStudentsUtils(NigeriaStudentsUtils):
    """A collection of customized methods.

    """

    @property
    def STUDENT_ID_PREFIX(self):
        if grok.getSite().__name__ == 'iuokada-cdl':
            return u'F'
        return u'I'


    SKIP_UPLOAD_VIEWLETS = (
        'acceptanceletterupload', 'certificateupload'
        )
    # Maximum size of upload files in kB
    MAX_KB = 500

    #: A tuple containing the names of registration states in which changing of
    #: passport pictures is allowed.

    PORTRAIT_CHANGE_STATES = (ADMITTED, CLEARANCE,)

    REQUIRED_PAYMENTS_FRESH_SCIENCE = {
        'registration_fresh': 'Registration Fee (Fresh)',
        'book': 'Book Deposit',
        'develop': 'Development Fee',
        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
        'municipal_fresh': 'Fresh Students Municipal Fee',
        'matric': 'Matriculation Fee',
        'waecneco': 'WAEC/NECO Verification',
        'jambver': 'JAMB Verification Fee',
        'health_insurance': 'Student Health Insurance',
        'id_card': 'I.D. Card',
        'medical_screening': 'Medical Screening Fees',
        'science': 'Science Bench Fee',
        }

    REQUIRED_PAYMENTS_FRESH_NON_SCIENCE = {
        'registration_fresh': 'Registration Fee (Fresh)',
        'book': 'Book Deposit',
        'develop': 'Development Fee',
        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
        'municipal_fresh': 'Fresh Students Municipal Fee',
        'matric': 'Matriculation Fee',
        'waecneco': 'WAEC/NECO Verification',
        'jambver': 'JAMB Verification Fee',
        'health_insurance': 'Student Health Insurance',
        'id_card': 'I.D. Card',
        'medical_screening': 'Medical Screening Fees',
        }

    # all students (except PHM) returning
    REQUIRED_PAYMENTS_RETURNING = {
        'registration_return': 'Registration Fee (Returning)',
        'book': 'Book Deposit',
        'develop': 'Development Fee',
        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
        'municipal_returning': 'Returning Students Municipal Fee',
        'health_insurance': 'Student Health Insurance',
        }


    # all stdents (except PHM) final year
    REQUIRED_PAYMENTS_FINAL = {
        'registration_return': 'Registration Fee (Returning)',
        'book': 'Book Deposit',
        'develop': 'Development Fee',
        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
        'municipal_returning': 'Returning Students Municipal Fee',
        'health_insurance': 'Student Health Insurance',
        'alumni': 'Alumni Fees',
        'conv': 'Convocation Fees',
        'grad_clearance': 'Clearance Fees',
        }

    # PHM returning students
    REQUIRED_PAYMENTS_RETURNING_PHARMACY = {
        'registration_return': 'Registration Fee (Returning)',
        'book': 'Book Deposit',
        'develop': 'Development Fee',
        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
        'municipal_returning': 'Returning Students Municipal Fee',
        'health_insurance': 'Student Health Insurance',
        'lab_support': 'Lab Support',
        }

    # PHM students final year
    REQUIRED_PAYMENTS_FINAL_PHARMACY = {
        'registration_return': 'Registration Fee (Returning)',
        'book': 'Book Deposit',
        'develop': 'Development Fee',
        'parentsconsult': 'Parents Consultative Forum (PCF) Fee',
        'municipal_returning': 'Returning Students Municipal Fee',
        'health_insurance': 'Student Health Insurance',
        'alumni': 'Alumni Fees',
        'conv': 'Convocation Fees',
        'grad_clearance': 'Clearance Fees',
        'lab_support': 'Lab Support',
        }

    REQUIRED_PAYMENTS_PG = {
        'pg_other': 'PG Other Charges',
        }

    def warnCreditsOOR(self, studylevel, course=None):
        """Return message if credits are out of range. In the base
        package only maximum credits is set.
        """
        max_credits = 60
        end_level = getattr(studylevel.__parent__.certificate, 'end_level', None)
        if end_level and studylevel.level >= end_level:
            max_credits = 80
        #if studylevel.certcode == 'LLB':
        #    max_credits = 50
        if studylevel.certcode == 'MBBSMED' and studylevel.level == 200:
            max_credits = 100
        if course and studylevel.total_credits + course.credits > max_credits:
            return _('Maximum credits exceeded.')
        elif studylevel.total_credits > max_credits:
            return _('Maximum credits exceeded.')
        return

    def _is_payment_for_final(self, student):
        studycourse = student['studycourse']
        certificate = getattr(studycourse,'certificate',None)
        current_level = studycourse.current_level
        if None in (current_level, certificate):
            return False
        end_level = certificate.end_level
        if current_level >= end_level-100:
            return True
        return False

    def _collect_required_payment_items(self, student):
        if student.is_postgrad:
            rp = self.REQUIRED_PAYMENTS_PG
        elif student.is_fresh and student.faccode in ('ENG', 'HSC', 'NAS', 'PHM'):
            rp = self.REQUIRED_PAYMENTS_FRESH_SCIENCE
        elif student.is_fresh:
            rp = self.REQUIRED_PAYMENTS_FRESH_NON_SCIENCE
        elif student.faccode == 'PHM' and self._is_payment_for_final(student):
            rp = self.REQUIRED_PAYMENTS_FINAL_PHARMACY
        elif student.faccode == 'PHM':
            rp = self.REQUIRED_PAYMENTS_RETURNING_PHARMACY
        elif self._is_payment_for_final(student):
            rp = self.REQUIRED_PAYMENTS_FINAL
        else:
            rp = self.REQUIRED_PAYMENTS_RETURNING
        return rp

    def _requiredPaymentsMissing(self, student, session):
        # Part time, jupeb, medical, and PG students do not pay sundry.
        if student.is_jupeb or student.current_mode.endswith('_pt') \
            or student.is_postgrad \
            or student.certcode in ('MBBSMED',):
            return
        # Has the required combi payment been made?
        for ticket in student['payments'].values():
            if ticket.p_category == 'required_combi'and \
                ticket.p_session == session and \
                ticket.p_state == 'paid':
                return
        # If not, check single payments
        rp = self._collect_required_payment_items(student)
        cats_missing = deepcopy(rp)
        if len(student['payments']):
            for category in rp.keys():
                for ticket in student['payments'].values():
                    if ticket.p_state == 'paid' and \
                        ticket.p_category == category and \
                        ticket.p_session == session:
                        del cats_missing[category]
                if not cats_missing:
                    return
        return "%s must be paid before Tution Fee. Make either single payments or make a 'Required Combi Payment'." % ', '.join(
            cats_missing.values())

    @property
    def _all_required_payments(self):
        return set(
            self.REQUIRED_PAYMENTS_PG.keys()
            + self.REQUIRED_PAYMENTS_FRESH_SCIENCE.keys()
            + self.REQUIRED_PAYMENTS_FRESH_NON_SCIENCE.keys()
            + self.REQUIRED_PAYMENTS_FINAL_PHARMACY.keys()
            + self.REQUIRED_PAYMENTS_RETURNING_PHARMACY.keys()
            + self.REQUIRED_PAYMENTS_FINAL.keys()
            + self.REQUIRED_PAYMENTS_RETURNING.keys()
            )

    def samePaymentMade(self, student, category, p_item, p_session):
        if category.startswith('resit'):
            return False
        if category == 'combi':
            return False
        for key in student['payments'].keys():
            ticket = student['payments'][key]
            if ticket.p_state == 'paid' and\
               ticket.p_category == category and \
               ticket.p_item != 'Balance' and \
               ticket.p_item == p_item and \
               ticket.p_session == p_session:
                  return True
        return False

    def setPaymentDetails(self, category, student,
            previous_session=None, previous_level=None, combi=[]):
        """Create a payment ticket and set the payment data of a
        student for the payment category specified.
        """
        if grok.getSite().__name__ == 'iuokada-cdl':
            return self.setCDLPaymentDetails(category, student,
                previous_session, previous_level, combi)
        p_item = u''
        amount = 0.0
        if previous_session:
            if previous_session < student['studycourse'].entry_session:
                return _('The previous session must not fall below '
                         'your entry session.'), None
            if category == 'schoolfee':
                # School fee is always paid for the following session
                if previous_session > student['studycourse'].current_session:
                    return _('This is not a previous session.'), None
            else:
                if previous_session > student['studycourse'].current_session - 1:
                    return _('This is not a previous session.'), None
            p_session = previous_session
            p_level = previous_level
            p_current = False
        else:
            p_session = student['studycourse'].current_session
            p_level = student['studycourse'].current_level
            p_current = True
            if category in list(self._all_required_payments) + ['required_combi',] \
                and student.state == RETURNING:
                # In case of school fee or required sundry fee payments the
                # payment session is always next session if students are in
                # state returning.
                p_session, p_level = self.getReturningData(student)
        academic_session = self._getSessionConfiguration(p_session)
        if academic_session == None:
            return _(u'Session configuration object is not available.'), None
        # Determine fee.
        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
            rpm = self._requiredPaymentsMissing(student, p_session)
            if rpm:
                return rpm, None
            try:
                certificate = student['studycourse'].certificate
                p_item = certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
            if previous_session:
                # Students can pay for previous sessions in all
                # workflow states.  Fresh students are excluded by the
                # update method of the PreviousPaymentAddFormPage.
                if previous_level == 100:
                    amount = getattr(certificate, 'school_fee_1', 0.0)
                else:
                    amount = getattr(certificate, 'school_fee_2', 0.0)
                if category == 'schoolfee40':
                    amount = 0.4*amount
                elif category == 'secondinstal':
                    amount = 0.6*amount
            else:
                if category == 'secondinstal':
                    if student.is_fresh:
                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
                    else:
                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
                else:
                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
                        amount = getattr(certificate, 'school_fee_1', 0.0)
                    elif student.state == RETURNING:
                        amount = getattr(certificate, 'school_fee_2', 0.0)
                    elif student.is_postgrad and student.state == PAID:
                        # Returning postgraduate students also pay for the
                        # next session but their level always remains the
                        # same.
                        p_session += 1
                        academic_session = self._getSessionConfiguration(p_session)
                        if academic_session == None:
                            return _(
                                u'Session configuration object is not available.'
                                ), None
                        amount = getattr(certificate, 'school_fee_2', 0.0)
                    if amount and category == 'schoolfee40':
                        amount = 0.4*amount
        elif category == 'clearance':
            try:
                p_item = student['studycourse'].certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
            amount = academic_session.clearance_fee
            if student.is_postgrad:
                amount *= 0.5
        elif category.startswith('resit'):
            amount = academic_session.resit_fee
            number = int(category.strip('resit'))
            amount *= number
        #elif category == 'bed_allocation':
        #    p_item = self.getAccommodationDetails(student)['bt']
        #    amount = academic_session.booking_fee
        #elif category == 'hostel_maintenance':
        #    amount = 0.0
        #    bedticket = student['accommodation'].get(
        #        str(student.current_session), None)
        #    if bedticket is not None and bedticket.bed is not None:
        #        p_item = bedticket.bed_coordinates
        #        if bedticket.bed.__parent__.maint_fee > 0:
        #            amount = bedticket.bed.__parent__.maint_fee
        #        else:
        #            # fallback
        #            amount = academic_session.maint_fee
        #    else:
        #        return _(u'No bed allocated.'), None
        elif category == 'combi' and combi:
            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
            for cat in combi:
                fee_name = cat + '_fee'
                cat_amount = getattr(academic_session, fee_name, 0.0)
                if not cat_amount:
                    return _('%s undefined.' % categories[cat]), None
                amount += cat_amount
                p_item += u'%s + ' % categories[cat]
            p_item = p_item.strip(' + ')

        elif category == 'required_combi':
            rp = self._collect_required_payment_items(student)
            for cat in rp.keys():
                fee_name = cat + '_fee'
                cat_amount = getattr(academic_session, fee_name, 0.0)
                if not cat_amount:
                    return _('%s undefined.' % rp[cat]), None
                amount += cat_amount
                p_item += u'%s + ' % rp[cat]
            p_item = p_item.strip(' + ')

        else:
            fee_name = category + '_fee'
            amount = getattr(academic_session, fee_name, 0.0)
        if amount in (0.0, None):
            return _('Amount could not be determined.'), None
        if self.samePaymentMade(student, category, p_item, p_session):
            return _('This type of payment has already been made.'), None
        if self._isPaymentDisabled(p_session, category, student):
            return _('This category of payments has been disabled.'), None
        payment = createObject(u'waeup.StudentOnlinePayment')
        timestamp = ("%d" % int(time()*10000))[1:]
        if category in (
            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
            payment.provider_amt = 5000.0
        if category in (
            'schoolfee', 'schoolfee40') and student.is_jupeb:
            payment.provider_amt = 5000.0
        payment.p_id = "p%s" % timestamp
        payment.p_category = category
        payment.p_item = p_item
        payment.p_session = p_session
        payment.p_level = p_level
        payment.p_current = p_current
        payment.amount_auth = amount
        payment.p_combi = combi
        return None, payment

    def setCDLPaymentDetails(self, category, student,
            previous_session=None, previous_level=None, combi=[]):
        """Create a payment ticket and set the payment data of a
        student for the payment category specified.
        """
        p_item = u''
        amount = 0.0
        if previous_session:
            if previous_session < student['studycourse'].entry_session:
                return _('The previous session must not fall below '
                         'your entry session.'), None
            if category == 'schoolfee':
                # School fee is always paid for the following session
                if previous_session > student['studycourse'].current_session:
                    return _('This is not a previous session.'), None
            else:
                if previous_session > student['studycourse'].current_session - 1:
                    return _('This is not a previous session.'), None
            p_session = previous_session
            p_level = previous_level
            p_current = False
        else:
            p_session = student['studycourse'].current_session
            p_level = student['studycourse'].current_level
            p_current = True
            if category in self.REQUIRED_PAYMENTS_FRESH.keys() \
                + self.REQUIRED_PAYMENTS_RETURNING.keys() \
                + ['schoolfee','schoolfee40','secondinstal'] \
                and student.state == RETURNING:
                # In case of school fee or required sundry fee payments the
                # payment session is always next session if students are in
                # state returning.
                p_session, p_level = self.getReturningData(student)
        academic_session = self._getSessionConfiguration(p_session)
        if academic_session == None:
            return _(u'Session configuration object is not available.'), None
        # Determine fee.
        if category in ('schoolfee', 'schoolfee40', 'secondinstal'):
            rpm = self._requiredPaymentsMissing(student, p_session)
            if rpm:
                return rpm, None
            try:
                certificate = student['studycourse'].certificate
                p_item = certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
            if previous_session:
                # Students can pay for previous sessions in all
                # workflow states.  Fresh students are excluded by the
                # update method of the PreviousPaymentAddFormPage.
                if previous_level == 100:
                    amount = getattr(certificate, 'school_fee_1', 0.0)
                else:
                    amount = getattr(certificate, 'school_fee_2', 0.0)
                if category == 'schoolfee40':
                    amount = 0.4*amount
                elif category == 'secondinstal':
                    amount = 0.6*amount
            else:
                if category == 'secondinstal':
                    if student.is_fresh:
                        amount = 0.6 * getattr(certificate, 'school_fee_1', 0.0)
                    else:
                        amount = 0.6 * getattr(certificate, 'school_fee_2', 0.0)
                else:
                    if student.state in (CLEARANCE, REQUESTED, CLEARED):
                        amount = getattr(certificate, 'school_fee_1', 0.0)
                    elif student.state == RETURNING:
                        amount = getattr(certificate, 'school_fee_2', 0.0)
                    elif student.is_postgrad and student.state == PAID:
                        # Returning postgraduate students also pay for the
                        # next session but their level always remains the
                        # same.
                        p_session += 1
                        academic_session = self._getSessionConfiguration(p_session)
                        if academic_session == None:
                            return _(
                                u'Session configuration object is not available.'
                                ), None
                        amount = getattr(certificate, 'school_fee_2', 0.0)
                    if amount and category == 'schoolfee40':
                        amount = 0.4*amount
        elif category == 'clearance':
            try:
                p_item = student['studycourse'].certificate.code
            except (AttributeError, TypeError):
                return _('Study course data are incomplete.'), None
            amount = academic_session.clearance_fee
        elif category.startswith('cdlcourse'):
            amount = academic_session.course_fee
            number = int(category.strip('cdlcourse'))
            amount *= number
        elif category == 'combi' and combi:
            categories = getUtility(IKofaUtils).COMBI_PAYMENT_CATEGORIES
            for cat in combi:
                fee_name = cat + '_fee'
                cat_amount = getattr(academic_session, fee_name, 0.0)
                if not cat_amount:
                    return _('%s undefined.' % categories[cat]), None
                amount += cat_amount
                p_item += u'%s + ' % categories[cat]
            p_item = p_item.strip(' + ')
        else:
            fee_name = category + '_fee'
            amount = getattr(academic_session, fee_name, 0.0)
        if amount in (0.0, None):
            return _('Amount could not be determined.'), None
        if self.samePaymentMade(student, category, p_item, p_session):
            return _('This type of payment has already been made.'), None
        if self._isPaymentDisabled(p_session, category, student):
            return _('This category of payments has been disabled.'), None
        payment = createObject(u'waeup.StudentOnlinePayment')
        timestamp = ("%d" % int(time()*10000))[1:]
        if category in (
            'registration', 'required_combi', 'pg_other', 'jupeb_reg'):
            payment.provider_amt = 5000.0
        if category in (
            'schoolfee', 'schoolfee40') and student.is_jupeb:
            payment.provider_amt = 5000.0
        payment.p_id = "p%s" % timestamp
        payment.p_category = category
        payment.p_item = p_item
        payment.p_session = p_session
        payment.p_level = p_level
        payment.p_current = p_current
        payment.amount_auth = amount
        payment.p_combi = combi
        return None, payment

    def setBalanceDetails(self, category, student,
            balance_session, balance_level, balance_amount):
        """Create a balance payment ticket and set the payment data
        as selected by the student.
        """
        if category in ('schoolfee', 'schoolfee40', 'secondinstal') \
            and balance_session > 2019:
            rpm = self._requiredPaymentsMissing(student, balance_session)
            if rpm:
                return rpm, None
        return super(
            CustomStudentsUtils, self).setBalanceDetails(category, student,
            balance_session, balance_level, balance_amount)

    def constructMatricNumber(self, student):
        """Fetch the matric number counter which fits the student and
        construct the new matric number of the student.
        """
        next_integer = grok.getSite()['configuration'].next_matric_integer
        if next_integer == 0:
            return _('Matriculation number cannot be set.'), None
        if not student.state in (
            RETURNING, CLEARED, PAID, REGISTERED, VALIDATED):
            return _('Matriculation number cannot be set.'), None
        year = unicode(student.entry_session)[2:]
        if grok.getSite().__name__ == 'iuokada-cdl':
            return None, "%s/%04d/%s/CDL" % (year, next_integer, student.faccode)
        return None, "%s/%06d/%s" % (year, next_integer, student.faccode)
