/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.fp.document;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.businessobject.AccountingPeriod;
import org.kuali.kfs.coa.service.AccountingPeriodService;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.gl.service.SufficientFundsService;
import org.kuali.kfs.krad.document.Copyable;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.AccountingLine;
import org.kuali.kfs.sys.businessobject.AccountingLineBase;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.businessobject.SystemOptions;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AccountingDocumentBase;
import org.kuali.kfs.sys.document.AmountTotaling;
import org.kuali.kfs.sys.document.Correctable;
import org.kuali.kfs.sys.document.service.DebitDeterminerService;
import org.kuali.kfs.sys.service.OptionsService;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;

import java.sql.Date;
import java.util.Iterator;
import java.util.List;

import static org.kuali.kfs.fp.document.validation.impl.AuxiliaryVoucherDocumentRuleConstants.AUXILIARY_VOUCHER_ACCOUNTING_PERIOD_GRACE_PERIOD;
import static org.kuali.kfs.fp.document.validation.impl.AuxiliaryVoucherDocumentRuleConstants.GENERAL_LEDGER_PENDING_ENTRY_OFFSET_CODE;
import static org.kuali.kfs.sys.KFSConstants.AuxiliaryVoucher.ACCRUAL_DOC_TYPE;
import static org.kuali.kfs.sys.KFSConstants.AuxiliaryVoucher.ADJUSTMENT_DOC_TYPE;
import static org.kuali.kfs.sys.KFSConstants.AuxiliaryVoucher.RECODE_DOC_TYPE;
import static org.kuali.kfs.sys.KFSConstants.GL_CREDIT_CODE;
import static org.kuali.kfs.sys.KFSConstants.GL_DEBIT_CODE;

/**
 * This is the business object that represents the AuxiliaryVoucherDocument in Kuali. This is a transactional document that will
 * eventually post transactions to the G/L. It integrates with workflow and also contains two groupings of accounting lines: Expense
 * and target. Expense is the expense and target is the income lines.
 */
public class AuxiliaryVoucherDocument extends AccountingDocumentBase implements VoucherDocument, Copyable, Correctable, AmountTotaling {
    protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AuxiliaryVoucherDocument.class);

    protected String typeCode = ADJUSTMENT_DOC_TYPE;
    protected java.sql.Date reversalDate;

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocumentBase#documentPerformsSufficientFundsCheck()
     */
    @Override
    public boolean documentPerformsSufficientFundsCheck() {
        if (isRecodeType()) {
            return super.documentPerformsSufficientFundsCheck();
        } else {
            return false;
        }
    }

    /**
     * Initializes the array lists and some basic info.
     */
    public AuxiliaryVoucherDocument() {
        super();
    }

    /**
     * Read Accessor for Reversal Date
     *
     * @return java.sql.Date
     */
    public java.sql.Date getReversalDate() {
        return reversalDate;
    }

    /**
     * Write Accessor for Reversal Date
     *
     * @param reversalDate
     */
    public void setReversalDate(java.sql.Date reversalDate) {
        this.reversalDate = reversalDate;
    }

    /**
     * Read Accessor for Auxiliary Voucher Type
     *
     * @return String
     */
    public String getTypeCode() {
        return typeCode;
    }

    /**
     * Write Accessor for Auxiliary Voucher Type
     *
     * @param typeCode
     */
    public void setTypeCode(String typeCode) {
        this.typeCode = typeCode;
    }

    /**
     * A helper to test whether this document is an adjustment type AV.
     *
     * @return boolean
     */
    public boolean isAdjustmentType() {
        return ADJUSTMENT_DOC_TYPE.equals(typeCode);
    }

    /**
     * A helper to test whether this document is an recode type AV.
     *
     * @return boolean
     */
    public boolean isRecodeType() {
        return RECODE_DOC_TYPE.equals(typeCode);
    }

    /**
     * A helper to test whether this document is an accrual type AV.
     *
     * @return boolean
     */
    public boolean isAccrualType() {
        return ACCRUAL_DOC_TYPE.equals(typeCode);
    }

    /**
     * This method calculates the debit total for a JV document keying off of the debit/debit code, only summing the accounting
     * lines with a debitDebitCode that matched the debit constant, and returns the results.
     *
     * @return KualiDecimal
     */
    public KualiDecimal getDebitTotal() {
        KualiDecimal debitTotal = KualiDecimal.ZERO;
        AccountingLineBase al = null;
        Iterator<?> iter = sourceAccountingLines.iterator();
        while (iter.hasNext()) {
            al = (AccountingLineBase) iter.next();
            if (StringUtils.isNotBlank(al.getDebitCreditCode()) && al.getDebitCreditCode().equals(KFSConstants.GL_DEBIT_CODE)) {
                debitTotal = debitTotal.add(al.getAmount());
            }
        }
        return debitTotal;
    }

    /**
     * This method calculates the credit total for a JV document keying off of the debit/credit code, only summing the accounting
     * lines with a debitCreditCode that matched the debit constant, and returns the results.
     *
     * @return KualiDecimal
     */

    public KualiDecimal getCreditTotal() {
        KualiDecimal creditTotal = KualiDecimal.ZERO;
        AccountingLineBase al = null;
        Iterator<?> iter = sourceAccountingLines.iterator();
        while (iter.hasNext()) {
            al = (AccountingLineBase) iter.next();
            if (StringUtils.isNotBlank(al.getDebitCreditCode()) && al.getDebitCreditCode().equals(KFSConstants.GL_CREDIT_CODE)) {
                creditTotal = creditTotal.add(al.getAmount());
            }
        }
        return creditTotal;
    }

    /**
     * Same as default implementation but uses debit / credit totals instead. Meaning it returns either credit or if 0, debit.
     *
     * @return KualiDecimal
     * @see org.kuali.kfs.sys.document.AccountingDocumentBase#getTotalDollarAmount()
     */
    @Override
    public KualiDecimal getTotalDollarAmount() {
        return getCreditTotal().equals(KualiDecimal.ZERO) ? getDebitTotal() : getCreditTotal();
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocumentBase#toCopy()
     */
    @Override
    public void toCopy() throws WorkflowException {
        super.toCopy();
        refreshReversalDate();
    }

    /**
     * Overrides to call super and then change the reversal date if the type is accrual and the date is greater than the set
     * reversal date.
     */
    @Override
    public void doRouteStatusChange(DocumentRouteStatusChange statusChangeEvent) {
        LOG.debug("In doRouteStatusChange() for AV documents");
        super.doRouteStatusChange(statusChangeEvent);

        if (this.getDocumentHeader().getWorkflowDocument().isProcessed()) { // only do this stuff if the document has been
            // processed and approved
            // update the reversal data accoringdingly
            updateReversalDate();
        }
    }

    /**
     * This method handles updating the reversal data on the document in addition to all of the GLPEs, but only for the accrual and
     * recode types.
     */
    protected void updateReversalDate() {
        if (refreshReversalDate()) {
            // set the reversal date on each GLPE for the document too
            List<GeneralLedgerPendingEntry> glpes = getGeneralLedgerPendingEntries();
            for (GeneralLedgerPendingEntry entry : glpes) {
                if (entry.getFinancialDocumentTypeCode().equals(KFSConstants.FinancialDocumentTypeCodes.DISTRIBUTION_OF_INCOME_AND_EXPENSE)) {
                    continue;
                }
                entry.setFinancialDocumentReversalDate(getReversalDate());
            }
        }
    }

    /**
     * If the reversal date on this document is in need of refreshing, refreshes the reveral date.  THIS METHOD MAY CHANGE DOCUMENT STATE!
     *
     * @return true if the reversal date ended up getting refreshed, false otherwise
     */
    protected boolean refreshReversalDate() {
        boolean refreshed = false;
        if ((isAccrualType() || isRecodeType()) && getReversalDate() != null) {
            java.sql.Date today = SpringContext.getBean(DateTimeService.class).getCurrentSqlDateMidnight();
            if (getReversalDate().before(today)) {
                // set the reversal date on the document
                setReversalDate(today);
                refreshed = true;
            }
        }
        return refreshed;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocumentBase#toErrorCorrection()
     */
    @Override
    public void toErrorCorrection() throws WorkflowException {
        super.toErrorCorrection();
        processAuxiliaryVoucherErrorCorrections();
    }

    /**
     * KULEDOCS-1700 This method iterates over each source line and flip the sign on the amount to nullify the super's effect, then
     * flip the debit/credit code b/c an error corrected AV flips the debit/credit code.
     */
    protected void processAuxiliaryVoucherErrorCorrections() {
        Iterator<?> i = getSourceAccountingLines().iterator();

        int index = 0;
        while (i.hasNext()) {
            SourceAccountingLine sLine = (SourceAccountingLine) i.next();

            String debitCreditCode = sLine.getDebitCreditCode();

            if (StringUtils.isNotBlank(debitCreditCode)) {
                // negate the amount to to nullify the effects of the super, b/c super flipped it the first time through
                sLine.setAmount(sLine.getAmount().negated()); // offsets the effect the super

                // now just flip the debit/credit code
                if (GL_DEBIT_CODE.equals(debitCreditCode)) {
                    sLine.setDebitCreditCode(GL_CREDIT_CODE);
                } else if (GL_CREDIT_CODE.equals(debitCreditCode)) {
                    sLine.setDebitCreditCode(GL_DEBIT_CODE);
                } else {
                    throw new IllegalStateException("SourceAccountingLine at index " + index + " does not have a debit/credit " + "code associated with it.  This should never have occured. Please contact your system administrator.");

                }
                index++;
            }
        }
    }

    /**
     * Returns true if an accounting line is a debit or credit The following are credits (return false)
     * <ol>
     * <li> debitCreditCode != 'D'
     * </ol>
     * the following are debits (return true)
     * <ol>
     * <li> debitCreditCode == 'D'
     * </ol>
     * the following are invalid ( throws an <code>IllegalStateException</code>)
     * <ol>
     * <li> debitCreditCode isBlank
     * </ol>
     *
     * @param financialDocument submitted accounting document
     * @param accounttingLine   accounting line being tested if it is a debit or not
     * @see org.kuali.rice.krad.rule.AccountingLineRule#isDebit(org.kuali.rice.krad.document.FinancialDocument,
     * org.kuali.rice.krad.bo.AccountingLine)
     */
    public boolean isDebit(GeneralLedgerPendingEntrySourceDetail postable) throws IllegalStateException {
        String debitCreditCode = ((AccountingLine) postable).getDebitCreditCode();
        DebitDeterminerService isDebitUtils = SpringContext.getBean(DebitDeterminerService.class);
        if (StringUtils.isBlank(debitCreditCode)) {
            throw new IllegalStateException(isDebitUtils.getDebitCalculationIllegalStateExceptionMessage());
        }
        return isDebitUtils.isDebitCode(debitCreditCode);
    }

    /**
     * This method sets the appropriate document type and object type codes into the GLPEs based on the type of AV document chosen.
     *
     * @param document       submitted AccountingDocument
     * @param accountingLine represents accounting line where object type code is retrieved from
     * @param explicitEntry  GeneralPendingLedgerEntry object that has its document type, object type, period code, and fiscal year
     *                       set
     * @see FinancialDocumentRuleBase#customizeExplicitGeneralLedgerPendingEntry(FinancialDocument, AccountingLine,
     * GeneralLedgerPendingEntry)
     * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#processExplicitGeneralLedgerPendingEntry(org.kuali.rice.krad.document.FinancialDocument,
     * org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper, org.kuali.rice.krad.bo.AccountingLine,
     * org.kuali.module.gl.bo.GeneralLedgerPendingEntry)
     */
    @Override
    public void customizeExplicitGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail postable, GeneralLedgerPendingEntry explicitEntry) {

        java.sql.Date reversalDate = getReversalDate();
        if (reversalDate != null) {
            explicitEntry.setFinancialDocumentReversalDate(reversalDate);
        } else {
            explicitEntry.setFinancialDocumentReversalDate(null);
        }
        explicitEntry.setFinancialDocumentTypeCode(getTypeCode()); // make sure to use the accrual type as the document
        // type
        explicitEntry.setFinancialObjectTypeCode(getObjectTypeCode(postable));
        explicitEntry.setUniversityFiscalPeriodCode(getPostingPeriodCode()); // use chosen posting period code
        explicitEntry.setUniversityFiscalYear(getPostingYear()); // use chosen posting year
    }

    /**
     * Offset entries are created for recodes (AVRC) always, so this method is one of 2 offsets that get created for an AVRC. Its
     * document type is set to DI. This uses the explicit entry as its model. In addition, an offset is generated for accruals
     * (AVAE) and adjustments (AVAD), but only if the document contains accounting lines for more than one account.
     *
     * @param financialDocument submitted accounting document
     * @param accountingLine    accounting line from accounting document
     * @param explicitEntry     represents explicit entry
     * @param offsetEntry       represents offset entry
     * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#customizeOffsetGeneralLedgerPendingEntry(org.kuali.rice.krad.document.FinancialDocument,
     * org.kuali.rice.krad.bo.AccountingLine, org.kuali.module.gl.bo.GeneralLedgerPendingEntry,
     * org.kuali.module.gl.bo.GeneralLedgerPendingEntry)
     */
    @Override
    public boolean customizeOffsetGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail postable, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
        // set the document type to that of a Distrib. Of Income and Expense if it's a recode
        if (isRecodeType()) {
            offsetEntry.setFinancialDocumentTypeCode(KFSConstants.FinancialDocumentTypeCodes.DISTRIBUTION_OF_INCOME_AND_EXPENSE);

            // set the posting period
            java.sql.Date today = SpringContext.getBean(DateTimeService.class).getCurrentSqlDateMidnight();
            offsetEntry.setUniversityFiscalPeriodCode(SpringContext.getBean(AccountingPeriodService.class).getByDate(today).getUniversityFiscalPeriodCode()); // use
        }

        // now set the offset entry to the specific offset object code for the AV generated offset fund balance; only if it's an
        // accrual or adjustment
        if (isAccrualType() || isAdjustmentType()) {
            String glpeOffsetObjectCode = SpringContext.getBean(ParameterService.class).getParameterValueAsString(this.getClass(), getGeneralLedgerPendingEntryOffsetObjectCode());
            offsetEntry.setFinancialObjectCode(glpeOffsetObjectCode);

            // set the posting period
            offsetEntry.setUniversityFiscalPeriodCode(getPostingPeriodCode()); // use chosen posting period code
        }

        // set the reversal date to null
        offsetEntry.setFinancialDocumentReversalDate(null);

        // set the year to current
        offsetEntry.setUniversityFiscalYear(getPostingYear()); // use chosen posting year

        // although they are offsets, we need to set the offset indicator to false
        offsetEntry.setTransactionEntryOffsetIndicator(false);

        //KFSMI-798 - refreshNonUpdatableReferences() used instead of refresh(),
        //GeneralLedgerPendingEntry does not have any updatable references
        offsetEntry.refreshNonUpdateableReferences(); // may have changed foreign keys here; need accurate object code and account BOs at least
        offsetEntry.setAcctSufficientFundsFinObjCd(SpringContext.getBean(SufficientFundsService.class).getSufficientFundsObjectCode(offsetEntry.getFinancialObject(), offsetEntry.getAccount().getAccountSufficientFundsCode()));

        return true;
    }

    /**
     * This method examines the accounting line passed in and returns the appropriate object type code. This rule converts specific
     * objects types from an object code on an accounting line to more general values. This is specific to the AV document.
     *
     * @param line accounting line where object type code is retrieved from
     * @return object type from a accounting line ((either financial object type code, financial object type not expenditure code,
     * or financial object type income not cash code))
     */
    protected String getObjectTypeCode(GeneralLedgerPendingEntrySourceDetail line) {
        SystemOptions options = SpringContext.getBean(OptionsService.class).getCurrentYearOptions();
        String objectTypeCode = line.getObjectCode().getFinancialObjectTypeCode();

        if (options.getFinObjTypeExpenditureexpCd().equals(objectTypeCode) || options.getFinObjTypeExpendNotExpCode().equals(objectTypeCode)) {
            objectTypeCode = options.getFinObjTypeExpNotExpendCode();
        } else if (options.getFinObjectTypeIncomecashCode().equals(objectTypeCode) || options.getFinObjTypeExpendNotExpCode().equals(objectTypeCode)) {
            objectTypeCode = options.getFinObjTypeIncomeNotCashCd();
        }

        return objectTypeCode;
    }

    /**
     * Get from APC the offset object code that is used for the <code>{@link GeneralLedgerPendingEntry}</code>
     *
     * @return String returns GLPE parameter name
     */
    protected String getGeneralLedgerPendingEntryOffsetObjectCode() {
        return GENERAL_LEDGER_PENDING_ENTRY_OFFSET_CODE;
    }

    /**
     * An Accrual Voucher only generates offsets if it is a recode (AVRC). So this method overrides to do nothing more than return
     * true if it's not a recode. If it is a recode, then it is responsible for generating two offsets with a document type of DI.
     *
     * @param financialDocument  submitted accounting document
     * @param sequenceHelper     helper class which will allows us to increment a reference without using an Integer
     * @param accountingLineCopy accounting line from accounting document
     * @param explicitEntry      represents explicit entry
     * @param offsetEntry        represents offset entry
     * @return true if general ledger pending entry is processed successfully for accurals, adjustments, and recodes
     * @see org.kuali.module.financial.rules.FinancialDocumentRuleBase#processOffsetGeneralLedgerPendingEntry(org.kuali.rice.krad.document.FinancialDocument,
     * org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper, org.kuali.rice.krad.bo.AccountingLine,
     * org.kuali.module.gl.bo.GeneralLedgerPendingEntry, org.kuali.module.gl.bo.GeneralLedgerPendingEntry)
     */
    @Override
    public boolean processOffsetGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
        AccountingLine accountingLineCopy = (AccountingLine) glpeSourceDetail;
        boolean success = true;

        // do not generate an offset entry if this is a normal or adjustment AV type
        if (isAccrualType() || isAdjustmentType()) {
            success &= processOffsetGeneralLedgerPendingEntryForAccrualsAndAdjustments(sequenceHelper, accountingLineCopy, explicitEntry, offsetEntry);
        } else if (isRecodeType()) { // recodes generate offsets
            success &= processOffsetGeneralLedgerPendingEntryForRecodes(sequenceHelper, accountingLineCopy, explicitEntry, offsetEntry);
        } else {
            throw new IllegalStateException("Illegal auxiliary voucher type: " + getTypeCode());
        }
        return success;
    }

    /**
     * This method handles generating or not generating the appropriate offsets if the AV type is a recode.
     *
     * @param financialDocument  submitted accounting document
     * @param sequenceHelper     helper class which will allows us to increment a reference without using an Integer
     * @param accountingLineCopy accounting line from accounting document
     * @param explicitEntry      represents explicit entry
     * @param offsetEntry        represents offset entry
     * @return true if offset general ledger pending entry is processed
     */
    protected boolean processOffsetGeneralLedgerPendingEntryForRecodes(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, AccountingLine accountingLineCopy, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
        // the explicit entry has already been generated and added to the list, so to get the right offset, we have to set the value
        // of the document type code on the explicit
        // to the type code for a DI document so that it gets passed into the next call and we retrieve the right offset definition
        // since these offsets are
        // specific to Distrib. of Income and Expense documents - we need to do a deep copy though so we don't do this by reference
        GeneralLedgerPendingEntry explicitEntryDeepCopy = new GeneralLedgerPendingEntry(explicitEntry);
        explicitEntryDeepCopy.setFinancialDocumentTypeCode(KFSConstants.FinancialDocumentTypeCodes.DISTRIBUTION_OF_INCOME_AND_EXPENSE);

        // set the posting period to current, because DI GLPEs for recodes should post to the current period
        java.sql.Date today = SpringContext.getBean(DateTimeService.class).getCurrentSqlDateMidnight();
        explicitEntryDeepCopy.setUniversityFiscalPeriodCode(SpringContext.getBean(AccountingPeriodService.class).getByDate(today).getUniversityFiscalPeriodCode()); // use

        // call the super to process an offset entry; see the customize method below for AVRC specific attribute values
        // pass in the explicit deep copy
        boolean success = super.processOffsetGeneralLedgerPendingEntry(sequenceHelper, accountingLineCopy, explicitEntryDeepCopy, offsetEntry);

        // increment the sequence appropriately
        sequenceHelper.increment();

        // now generate the AVRC DI entry
        // pass in the explicit deep copy
        processAuxiliaryVoucherRecodeDistributionOfIncomeAndExpenseGeneralLedgerPendingEntry(sequenceHelper, explicitEntryDeepCopy);
        return success;
    }

    /**
     * This method handles generating or not generating the appropriate offsets if the AV type is accrual or adjustment.
     *
     * @param financialDocument  submitted accounting document
     * @param sequenceHelper     helper class which will allows us to increment a reference without using an Integer
     * @param accountingLineCopy accounting line from accounting document
     * @param explicitEntry      represents explicit entry
     * @param offsetEntry        represents offset entry
     * @return true if offset general ledger pending entry is processed successfully
     */
    protected boolean processOffsetGeneralLedgerPendingEntryForAccrualsAndAdjustments(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, AccountingLine accountingLineCopy, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
        boolean success = true;
        if (this.isDocumentForMultipleAccounts()) {
            success = super.processOffsetGeneralLedgerPendingEntry(sequenceHelper, accountingLineCopy, explicitEntry, offsetEntry);
        } else {
            sequenceHelper.decrement(); // the parent already increments; b/c it assumes that all documents have offset entries all
            // of the time
        }
        return success;
    }

    /**
     * This method is responsible for iterating through all of the accounting lines in the document (source only) and checking to
     * see if they are all for the same account or not. It recognizes the first account element as the base, and then it iterates
     * through the rest. If it comes across one that doesn't match, then we know it's for multiple accounts.
     *
     * @param financialDocument submitted accounting document
     * @return true if multiple accounts are being used
     */
    protected boolean isDocumentForMultipleAccounts() {
        String baseAccountNumber = "";

        int index = 0;
        List<AccountingLine> lines = this.getSourceAccountingLines();
        for (AccountingLine line : lines) {
            if (index == 0) {
                baseAccountNumber = line.getAccountNumber();
            } else if (!baseAccountNumber.equals(line.getAccountNumber())) {
                return true;
            }
            index++;
        }

        return false;
    }

    /**
     * This method creates an AV recode specific GLPE with a document type of DI. The sequence is managed outside of this method. It
     * uses the explicit entry as its model and then tweaks values appropriately.
     *
     * @param financialDocument submitted accounting document
     * @param sequenceHelper    helper class which will allows us to increment a reference without using an Integer
     * @param explicitEntry     represents explicit entry
     * @return true if recode GLPE is added to the financial document successfully
     */
    protected void processAuxiliaryVoucherRecodeDistributionOfIncomeAndExpenseGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntry explicitEntry) {
        // create a new instance based off of the explicit entry
        GeneralLedgerPendingEntry recodeGlpe = new GeneralLedgerPendingEntry(explicitEntry);

        // set the sequence number according to what was passed in - this is managed external to this method
        recodeGlpe.setTransactionLedgerEntrySequenceNumber(new Integer(sequenceHelper.getSequenceCounter()));

        // set the document type to that of a Distrib. Of Income and Expense
        recodeGlpe.setFinancialDocumentTypeCode(KFSConstants.FinancialDocumentTypeCodes.DISTRIBUTION_OF_INCOME_AND_EXPENSE);

        // set the object type code base on the value of the explicit entry
        recodeGlpe.setFinancialObjectTypeCode(getObjectTypeCodeForRecodeDistributionOfIncomeAndExpenseEntry(explicitEntry));

        // set the reversal date to null
        recodeGlpe.setFinancialDocumentReversalDate(null);

        // although this is an offsets, we need to set the offset indicator to false
        recodeGlpe.setTransactionEntryOffsetIndicator(false);

        // add the new recode offset entry to the document now
        addPendingEntry(recodeGlpe);
    }

    /**
     * This method examines the explicit entry's object type and returns the appropriate object type code. This is specific to AV
     * recodes (AVRCs).
     *
     * @param explicitEntry
     * @return object type code from explicit entry (either financial object type code, financial object type expenditure code, or
     * financial object type income cash code)
     */
    protected String getObjectTypeCodeForRecodeDistributionOfIncomeAndExpenseEntry(GeneralLedgerPendingEntry explicitEntry) {
        SystemOptions options = SpringContext.getBean(OptionsService.class).getCurrentYearOptions();
        String objectTypeCode = explicitEntry.getFinancialObjectTypeCode();

        if (options.getFinObjTypeExpNotExpendCode().equals(objectTypeCode)) {
            objectTypeCode = options.getFinObjTypeExpenditureexpCd();
        } else if (options.getFinObjTypeIncomeNotCashCd().equals(objectTypeCode)) {
            objectTypeCode = options.getFinObjectTypeIncomecashCode();
        }

        return objectTypeCode;
    }

    /**
     * This method checks if a given moment of time is within an accounting period, or its auxiliary voucher grace period.
     *
     * @param today         a date to check if it is within the period
     * @param periodToCheck the account period to check against
     * @return true if a given moment in time is within an accounting period or an auxiliary voucher grace period
     */
    public boolean calculateIfWithinGracePeriod(Date today, AccountingPeriod periodToCheck) {
        boolean result = false;
        final int todayAsComparableDate = comparableDateForm(today);
        final int periodClose = new Integer(comparableDateForm(periodToCheck.getUniversityFiscalPeriodEndDate()));
        final int periodBegin = comparableDateForm(calculateFirstDayOfMonth(periodToCheck.getUniversityFiscalPeriodEndDate()));
        final int gracePeriodClose = periodClose + new Integer(SpringContext.getBean(ParameterService.class).getParameterValueAsString(getClass(), AUXILIARY_VOUCHER_ACCOUNTING_PERIOD_GRACE_PERIOD)).intValue();
        return (todayAsComparableDate >= periodBegin && todayAsComparableDate <= gracePeriodClose);
    }

    /**
     * This method returns a date as an approximate count of days since the BCE epoch.
     *
     * @param d the date to convert
     * @return an integer count of days, very approximate
     */
    public int comparableDateForm(Date d) {
        java.util.Calendar cal = new java.util.GregorianCalendar();
        cal.setTime(d);
        return cal.get(java.util.Calendar.YEAR) * 365 + cal.get(java.util.Calendar.DAY_OF_YEAR);
    }

    /**
     * Given a day, this method calculates what the first day of that month was.
     *
     * @param d date to find first of month for
     * @return date of the first day of the month
     */
    public Date calculateFirstDayOfMonth(Date d) {
        java.util.Calendar cal = new java.util.GregorianCalendar();
        cal.setTime(d);
        int dayOfMonth = cal.get(java.util.Calendar.DAY_OF_MONTH) - 1;
        cal.add(java.util.Calendar.DAY_OF_YEAR, -1 * dayOfMonth);
        return new Date(cal.getTimeInMillis());
    }

    /**
     * This method checks if the given accounting period ends on the last day of the previous fiscal year
     *
     * @param acctPeriod accounting period to check
     * @return true if the accounting period ends with the fiscal year, false if otherwise
     */
    public boolean isEndOfPreviousFiscalYear(AccountingPeriod acctPeriod) {
        final UniversityDateService universityDateService = SpringContext.getBean(UniversityDateService.class);
        final Date firstDayOfCurrFiscalYear = new Date(universityDateService.getFirstDateOfFiscalYear(universityDateService.getCurrentFiscalYear()).getTime());
        final Date periodClose = acctPeriod.getUniversityFiscalPeriodEndDate();
        java.util.Calendar cal = new java.util.GregorianCalendar();
        cal.setTime(periodClose);
        cal.add(java.util.Calendar.DATE, 1);
        return (firstDayOfCurrFiscalYear.equals(new Date(cal.getTimeInMillis())));
    }

}
