/*
 * 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.gl.batch.service.impl;

import org.kuali.kfs.coa.businessobject.AccountIntf;
import org.kuali.kfs.coa.businessobject.CarryForwardReversionProcessOrganizationInfo;
import org.kuali.kfs.coa.businessobject.ClosedAccountOrganizationReversion;
import org.kuali.kfs.coa.businessobject.ObjectCode;
import org.kuali.kfs.coa.businessobject.OrganizationReversion;
import org.kuali.kfs.coa.businessobject.OrganizationReversionCategory;
import org.kuali.kfs.coa.businessobject.OrganizationReversionCategoryInfo;
import org.kuali.kfs.coa.service.OrganizationReversionService;
import org.kuali.kfs.coa.service.PriorYearAccountService;
import org.kuali.kfs.coreservice.framework.parameter.ParameterService;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.batch.service.OrganizationReversionCategoryLogic;
import org.kuali.kfs.gl.batch.service.OrganizationReversionProcess;
import org.kuali.kfs.gl.batch.service.OrganizationReversionUnitOfWorkService;
import org.kuali.kfs.gl.batch.service.impl.exception.FatalErrorException;
import org.kuali.kfs.gl.businessobject.Balance;
import org.kuali.kfs.gl.businessobject.OrgReversionUnitOfWork;
import org.kuali.kfs.gl.businessobject.OrgReversionUnitOfWorkCategoryAmount;
import org.kuali.kfs.gl.businessobject.OriginEntryFull;
import org.kuali.kfs.gl.report.LedgerSummaryReport;
import org.kuali.kfs.gl.service.BalanceService;
import org.kuali.kfs.gl.service.OriginEntryService;
import org.kuali.kfs.krad.service.PersistenceService;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.businessobject.SystemOptions;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.service.FlexibleOffsetAccountService;
import org.kuali.kfs.sys.service.OptionsService;
import org.kuali.kfs.sys.service.ReportWriterService;
import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.annotation.Transactional;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * This class actually runs the year end organization reversion process
 */
@Transactional
public class OrganizationReversionProcessImpl implements OrganizationReversionProcess, InitializingBean {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(OrganizationReversionProcessImpl.class);

    // Services
    private OrganizationReversionService organizationReversionService;
    private BalanceService balanceService;
    private OriginEntryService originEntryService;
    private PersistenceService persistenceService;
    private DateTimeService dateTimeService;
    private OrganizationReversionCategoryLogic cashOrganizationReversionCategoryLogic;
    private PriorYearAccountService priorYearAccountService;
    private OrganizationReversionUnitOfWorkService orgReversionUnitOfWorkService;
    private FlexibleOffsetAccountService flexibleOffsetAccountService;
    private ParameterService parameterService;
    private ConfigurationService configurationService;

    private String batchFileDirectoryName;
    private String outputFileName;
    private OrgReversionUnitOfWork unitOfWork;
    private Map<String, OrganizationReversionCategoryLogic> categories;
    private List<OrganizationReversionCategory> categoryList;
    private CarryForwardReversionProcessOrganizationInfo organizationReversion;
    private AccountIntf account;

    private Map jobParameters;
    private Map<String, Integer> organizationReversionCounts;

    private boolean usePriorYearInformation;

    private boolean holdGeneratedOriginEntries = false;
    private List<OriginEntryFull> generatedOriginEntries;

    public String CARRY_FORWARD_OBJECT_CODE;
    public String DEFAULT_FINANCIAL_DOCUMENT_TYPE_CODE;
    public String DEFAULT_FINANCIAL_SYSTEM_ORIGINATION_CODE;
    public String DEFAULT_FINANCIAL_BALANCE_TYPE_CODE;
    public String DEFAULT_FINANCIAL_BALANCE_TYPE_CODE_YEAR_END;
    public String DEFAULT_DOCUMENT_NUMBER_PREFIX;

    private String CASH_REVERTED_TO_MESSAGE;
    private String FUND_BALANCE_REVERTED_TO_MESSAGE;
    private String CASH_REVERTED_FROM_MESSAGE;
    private String FUND_BALANCE_REVERTED_FROM_MESSAGE;
    private String FUND_CARRIED_MESSAGE;
    private String FUND_REVERTED_TO_MESSAGE;
    private String FUND_REVERTED_FROM_MESSAGE;

    private SystemOptions systemOptions;
    private Integer paramFiscalYear;

    private LedgerSummaryReport ledgerReport;

    private PrintStream outputPs;

    /**
     * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        this.CARRY_FORWARD_OBJECT_CODE = getParameterService().getParameterValueAsString(OrganizationReversion.class, GeneralLedgerConstants.OrganizationReversionProcess.CARRY_FORWARD_OBJECT_CODE);
        this.DEFAULT_FINANCIAL_DOCUMENT_TYPE_CODE = getParameterService().getParameterValueAsString(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, GeneralLedgerConstants.ANNUAL_CLOSING_DOCUMENT_TYPE);
        this.DEFAULT_FINANCIAL_SYSTEM_ORIGINATION_CODE = getParameterService().getParameterValueAsString(OrganizationReversion.class, GeneralLedgerConstants.OrganizationReversionProcess.DEFAULT_FINANCIAL_SYSTEM_ORIGINATION_CODE);
        this.DEFAULT_FINANCIAL_BALANCE_TYPE_CODE = getParameterService().getParameterValueAsString(OrganizationReversion.class, GeneralLedgerConstants.OrganizationReversionProcess.DEFAULT_FINANCIAL_BALANCE_TYPE_CODE);
        this.DEFAULT_FINANCIAL_BALANCE_TYPE_CODE_YEAR_END = getParameterService().getParameterValueAsString(OrganizationReversion.class, GeneralLedgerConstants.OrganizationReversionProcess.DEFAULT_FINANCIAL_BALANCE_TYPE_CODE_YEAR_END);
        this.DEFAULT_DOCUMENT_NUMBER_PREFIX = getParameterService().getParameterValueAsString(OrganizationReversion.class, GeneralLedgerConstants.OrganizationReversionProcess.DEFAULT_DOCUMENT_NUMBER_PREFIX);
        this.CASH_REVERTED_TO_MESSAGE = getConfigurationService().getPropertyValueAsString(KFSKeyConstants.OrganizationReversionProcess.CASH_REVERTED_TO);
        this.FUND_BALANCE_REVERTED_TO_MESSAGE = getConfigurationService().getPropertyValueAsString(KFSKeyConstants.OrganizationReversionProcess.FUND_BALANCE_REVERTED_TO);
        this.CASH_REVERTED_FROM_MESSAGE = getConfigurationService().getPropertyValueAsString(KFSKeyConstants.OrganizationReversionProcess.CASH_REVERTED_FROM);
        this.FUND_BALANCE_REVERTED_FROM_MESSAGE = getConfigurationService().getPropertyValueAsString(KFSKeyConstants.OrganizationReversionProcess.FUND_BALANCE_REVERTED_FROM);
        this.FUND_CARRIED_MESSAGE = getConfigurationService().getPropertyValueAsString(KFSKeyConstants.OrganizationReversionProcess.FUND_CARRIED);
        this.FUND_REVERTED_TO_MESSAGE = getConfigurationService().getPropertyValueAsString(KFSKeyConstants.OrganizationReversionProcess.FUND_REVERTED_TO);
        this.FUND_REVERTED_FROM_MESSAGE = getConfigurationService().getPropertyValueAsString(KFSKeyConstants.OrganizationReversionProcess.FUND_REVERTED_FROM);

        outputFileName = getBatchFileDirectoryName() + File.separator + (usePriorYearInformation ? GeneralLedgerConstants.BatchFileSystem.ORGANIZATION_REVERSION_CLOSING_FILE : GeneralLedgerConstants.BatchFileSystem.ORGANIZATION_REVERSION_PRE_CLOSING_FILE) + GeneralLedgerConstants.BatchFileSystem.EXTENSION;
    }

    /**
     * This evilly named method actually runs the organization reversion process.
     */
    public void organizationReversionProcess(Map jobParameters, Map<String, Integer> organizationReversionCounts) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("organizationReversionProcess() started");
        }
        this.jobParameters = jobParameters;
        this.organizationReversionCounts = organizationReversionCounts;

        LOG.info("Initializing the process");
        initializeProcess();

        //create files
        File outputFile = new File(outputFileName);

        try {
            outputPs = new PrintStream(outputFile);

            Iterator<Balance> balances = getBalanceService().findOrganizationReversionBalancesForFiscalYear((Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR), usePriorYearInformation);
            processBalances(balances);

            outputPs.close();
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Organization Reversion File Files doesn't exist " + outputFileName);
        }

    }

    /**
     * Given a list of balances, this method generates the origin entries for the organization reversion/carry forward process, and saves those
     * to an initialized origin entry group
     *
     * @param balances an iterator of balances to process; each balance returned by the iterator will be processed by this method
     */
    public void processBalances(Iterator<Balance> balances) {
        boolean skipToNextUnitOfWork = false;
        unitOfWork = new OrgReversionUnitOfWork();
        unitOfWork.setCategories(categoryList);

        Balance bal;
        while (balances.hasNext()) {
            bal = balances.next();
            if (LOG.isDebugEnabled()) {
                LOG.debug("BALANCE SELECTED: " + bal.getUniversityFiscalYear() + bal.getChartOfAccountsCode() + bal.getAccountNumber() + bal.getSubAccountNumber() + bal.getObjectCode() + bal.getSubObjectCode() + bal.getBalanceTypeCode() + bal.getObjectTypeCode() + " " + bal.getAccountLineAnnualBalanceAmount().add(bal.getBeginningBalanceLineAmount()));
            }

            try {
                if (!unitOfWork.isInitialized()) {
                    unitOfWork.setFields(bal.getChartOfAccountsCode(), bal.getAccountNumber(), bal.getSubAccountNumber());
                    retrieveCurrentReversionAndAccount(bal);
                } else if (!unitOfWork.wouldHold(bal)) {
                    if (!skipToNextUnitOfWork) {
                        calculateTotals();
                        List<OriginEntryFull> originEntriesToWrite = generateOutputOriginEntries();
                        summarizeOriginEntries(originEntriesToWrite);
                        if (holdGeneratedOriginEntries) {
                            generatedOriginEntries.addAll(originEntriesToWrite);
                        }
                        int recordsWritten = writeOriginEntries(originEntriesToWrite);
                        incrementCount("recordsWritten", recordsWritten);
                        getOrgReversionUnitOfWorkService().save(unitOfWork);
                    }
                    unitOfWork.setFields(bal.getChartOfAccountsCode(), bal.getAccountNumber(), bal.getSubAccountNumber());
                    retrieveCurrentReversionAndAccount(bal);
                    skipToNextUnitOfWork = false;
                }
                if (skipToNextUnitOfWork) {
                    continue; // if there is no org reversion or an org reversion detail is missing or the balances are off for
                    // this unit of work,
                    // just skip all the balances until we change unit of work
                }
                calculateBucketAmounts(bal);
            } catch (FatalErrorException fee) {
                LOG.info(fee.getMessage());
                skipToNextUnitOfWork = true;
            }
        }
        // save the final unit of work
        if (!skipToNextUnitOfWork && getBalancesSelected() > 0) {
            try {
                calculateTotals();
                List<OriginEntryFull> originEntriesToWrite = generateOutputOriginEntries();
                summarizeOriginEntries(originEntriesToWrite);
                if (holdGeneratedOriginEntries) {
                    generatedOriginEntries.addAll(originEntriesToWrite);
                }
                int recordsWritten = writeOriginEntries(originEntriesToWrite);
                incrementCount("recordsWritten", recordsWritten);
                getOrgReversionUnitOfWorkService().save(unitOfWork);
            } catch (FatalErrorException fee) {
                LOG.info(fee.getMessage());
            }
        }

    }

    /**
     * Given a balance, returns the current organization reversion record and account or prior year account for the balance; it sets them
     * to private properties
     *
     * @param bal the balance to find the account/prior year account and organization reversion record for
     * @throws FatalErrorException if an organization reversion record cannot be found in the database
     */
    protected void retrieveCurrentReversionAndAccount(Balance bal) throws FatalErrorException {
        // initialize the account
        if ((account == null) || (!bal.getChartOfAccountsCode().equals(account.getChartOfAccountsCode())) || (!bal.getAccountNumber().equals(account.getAccountNumber()))) {
            if (usePriorYearInformation) {
                account = getPriorYearAccountService().getByPrimaryKey(bal.getChartOfAccountsCode(), bal.getAccountNumber());
            } else {
                account = bal.getAccount();
            }
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Organization Reversion Service: " + getOrganizationReversionService() + "; fiscal year: " + (Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR) + "; account: " + account + "; account organization code: " + account.getOrganizationCode() + "; balance: " + bal + "; balance chart: " + bal.getChartOfAccountsCode());
        }
        organizationReversion = getOrganizationReversionService().getByPrimaryId((Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR), bal.getChartOfAccountsCode(), account.getOrganizationCode());

        if (organizationReversion == null) {
            // we can't find an organization reversion for this balance? Throw exception
            throw new FatalErrorException("No Organization Reversion found for: " + (Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR) + "-" + bal.getChartOfAccountsCode() + "-" + account.getOrganizationCode());
        }

        if (account.isClosed()) {
            organizationReversion = new ClosedAccountOrganizationReversion(organizationReversion);
        }
    }

    /**
     * This method initializes several properties needed for the process to run correctly
     */
    public void initializeProcess() {

        // clear out summary tables
        LOG.info("destroying all unit of work summaries");
        orgReversionUnitOfWorkService.destroyAllUnitOfWorkSummaries();

        categories = getOrganizationReversionService().getCategories();
        categoryList = getOrganizationReversionService().getCategoryList();

        this.paramFiscalYear = (Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR);

        organizationReversionCounts.put("balancesRead", balanceService.countBalancesForFiscalYear(paramFiscalYear));
        organizationReversionCounts.put("balancesSelected", new Integer(0));
        organizationReversionCounts.put("recordsWritten", new Integer(0));

        this.systemOptions = SpringContext.getBean(OptionsService.class).getOptions(paramFiscalYear);

        ledgerReport = new LedgerSummaryReport();
    }

    /**
     * Depending on the category that this balance belongs to, adds the balance to the appropriate bucket
     *
     * @param bal the current balance to process
     */
    protected void calculateBucketAmounts(Balance bal) {
        getPersistenceService().retrieveReferenceObject(bal, "financialObject");

        if (LOG.isDebugEnabled()) {
            LOG.debug("CONSIDERING IF TO ADD BALANCE: " + bal.getUniversityFiscalYear() + bal.getChartOfAccountsCode() + bal.getAccountNumber() + bal.getSubAccountNumber() + bal.getObjectCode() + bal.getSubObjectCode() + bal.getBalanceTypeCode() + bal.getObjectTypeCode() + " " + bal.getAccountLineAnnualBalanceAmount().add(bal.getBeginningBalanceLineAmount()));
        }

        if (getCashOrganizationReversionCategoryLogic().containsObjectCode(bal.getFinancialObject()) && bal.getBalanceTypeCode().equals(systemOptions.getActualFinancialBalanceTypeCd())) {
            unitOfWork.addTotalCash(bal.getBeginningBalanceLineAmount());
            unitOfWork.addTotalCash(bal.getAccountLineAnnualBalanceAmount());
            incrementCount("balancesSelected");
            if (LOG.isDebugEnabled()) {
                LOG.debug("ADDING BALANCE TO CASH: " + bal.getUniversityFiscalYear() + bal.getChartOfAccountsCode() + bal.getAccountNumber() + bal.getSubAccountNumber() + bal.getObjectCode() + bal.getSubObjectCode() + bal.getBalanceTypeCode() + bal.getObjectTypeCode() + " " + bal.getAccountLineAnnualBalanceAmount().add(bal.getBeginningBalanceLineAmount()) + " TO CASH, TOTAL CASH NOW = " + unitOfWork.getTotalCash());
            }
        } else {
            for (OrganizationReversionCategory cat : categoryList) {
                OrganizationReversionCategoryLogic logic = categories.get(cat.getOrganizationReversionCategoryCode());
                if (logic.containsObjectCode(bal.getFinancialObject())) {
                    if (systemOptions.getActualFinancialBalanceTypeCd().equals(bal.getBalanceTypeCode())) {
                        // Actual
                        unitOfWork.addActualAmount(cat.getOrganizationReversionCategoryCode(), bal.getBeginningBalanceLineAmount());
                        unitOfWork.addActualAmount(cat.getOrganizationReversionCategoryCode(), bal.getAccountLineAnnualBalanceAmount());
                        incrementCount("balancesSelected");
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("ADDING BALANCE TO ACTUAL: " + bal.getUniversityFiscalYear() + bal.getChartOfAccountsCode() + bal.getAccountNumber() + bal.getSubAccountNumber() + bal.getObjectCode() + bal.getSubObjectCode() + bal.getBalanceTypeCode() + bal.getObjectTypeCode() + " " + bal.getAccountLineAnnualBalanceAmount().add(bal.getBeginningBalanceLineAmount()) + " TO ACTUAL, ACTUAL FOR CATEGORY " + cat.getOrganizationReversionCategoryName() + " NOW = " + unitOfWork.getCategoryAmounts().get(cat.getOrganizationReversionCategoryCode()).getActual());
                        }
                    } else if (systemOptions.getFinObjTypeExpenditureexpCd().equals(bal.getBalanceTypeCode()) || systemOptions.getCostShareEncumbranceBalanceTypeCd().equals(bal.getBalanceTypeCode()) || systemOptions.getIntrnlEncumFinBalanceTypCd().equals(bal.getBalanceTypeCode())) {
                        // Encumbrance
                        KualiDecimal amount = bal.getBeginningBalanceLineAmount().add(bal.getAccountLineAnnualBalanceAmount());
                        if (amount.isPositive()) {
                            unitOfWork.addEncumbranceAmount(cat.getOrganizationReversionCategoryCode(), amount);
                            incrementCount("balancesSelected");
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("ADDING BALANCE TO ENCUMBRANCE: " + bal.getUniversityFiscalYear() + bal.getChartOfAccountsCode() + bal.getAccountNumber() + bal.getSubAccountNumber() + bal.getObjectCode() + bal.getSubObjectCode() + bal.getBalanceTypeCode() + bal.getObjectTypeCode() + " " + bal.getAccountLineAnnualBalanceAmount().add(bal.getBeginningBalanceLineAmount()) + " TO ENCUMBRANCE, ENCUMBRANCE FOR CATEGORY " + cat.getOrganizationReversionCategoryName() + " NOW = " + unitOfWork.getCategoryAmounts().get(cat.getOrganizationReversionCategoryCode()).getEncumbrance());
                            }
                        }
                    } else if (KFSConstants.BALANCE_TYPE_CURRENT_BUDGET.equals(bal.getBalanceTypeCode())) {
                        // Budget
                        if (!CARRY_FORWARD_OBJECT_CODE.equals(bal.getObjectCode())) {
                            unitOfWork.addBudgetAmount(cat.getOrganizationReversionCategoryCode(), bal.getBeginningBalanceLineAmount());
                            unitOfWork.addBudgetAmount(cat.getOrganizationReversionCategoryCode(), bal.getAccountLineAnnualBalanceAmount());
                            incrementCount("balancesSelected");
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("ADDING BALANCE TO BUDGET: " + bal.getUniversityFiscalYear() + bal.getChartOfAccountsCode() + bal.getAccountNumber() + bal.getSubAccountNumber() + bal.getObjectCode() + bal.getSubObjectCode() + bal.getBalanceTypeCode() + bal.getObjectTypeCode() + " " + bal.getAccountLineAnnualBalanceAmount().add(bal.getBeginningBalanceLineAmount()) + " TO CURRENT BUDGET, CURRENT BUDGET FOR CATEGORY " + cat.getOrganizationReversionCategoryName() + " NOW = " + unitOfWork.getCategoryAmounts().get(cat.getOrganizationReversionCategoryCode()).getBudget());
                            }
                        }
                    }
                    break;
                }
            }
        }
    }

    /**
     * This method determines which origin entries (reversion, cash reversion, or carry forward) need to be generated for the current unit of work,
     * and then delegates to the origin entry generation methods to create those entries
     *
     * @return a list of OriginEntries which need to be written
     * @throws FatalErrorException thrown if object codes are missing in any of the generation methods
     */
    public List<OriginEntryFull> generateOutputOriginEntries() throws FatalErrorException {
        List<OriginEntryFull> originEntriesToWrite = new ArrayList<OriginEntryFull>();
        if (unitOfWork.getTotalReversion().compareTo(KualiDecimal.ZERO) != 0) {
            generateReversions(originEntriesToWrite);
        }
        if ((unitOfWork.getTotalCarryForward().compareTo(KualiDecimal.ZERO) != 0)) {
            if (!organizationReversion.isCarryForwardByObjectCodeIndicator()) {
                generateCarryForwards(originEntriesToWrite);
            } else {
                generateMany(originEntriesToWrite);
            }
        }
        if (unitOfWork.getTotalCash().compareTo(KualiDecimal.ZERO) != 0) {
            generateCashReversions(originEntriesToWrite);
        }
        return originEntriesToWrite;
    }

    /**
     * This method writes a list of OriginEntryFulls to a given origin entry group
     *
     * @param writeGroup           the origin entry group to write to
     * @param originEntriesToWrite a list of origin entry fulls to write
     * @return the count of origin entries that were written
     */
    protected int writeOriginEntries(List<OriginEntryFull> originEntriesToWrite) {
        int originEntriesWritten = 0;

        for (OriginEntryFull originEntry : originEntriesToWrite) {
            getOriginEntryService().createEntry(originEntry, outputPs);
            originEntriesWritten += 1;
        }

        return originEntriesWritten;
    }

    /**
     * This method starts the creation of an origin entry, by setting fields that are the same in every Org Rev origin entries
     *
     * @return an OriginEntryFull partially filled out with constant information
     */
    protected OriginEntryFull getEntry() {
        OriginEntryFull entry = new OriginEntryFull();
        entry.setUniversityFiscalYear((Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR));
        entry.setUniversityFiscalPeriodCode(KFSConstants.MONTH13);
        entry.setFinancialDocumentTypeCode(DEFAULT_FINANCIAL_DOCUMENT_TYPE_CODE);
        entry.setFinancialSystemOriginationCode(DEFAULT_FINANCIAL_SYSTEM_ORIGINATION_CODE);
        entry.setTransactionLedgerEntrySequenceNumber(1);
        entry.setTransactionDebitCreditCode(KFSConstants.GL_BUDGET_CODE);
        entry.setTransactionDate((Date) jobParameters.get(KFSConstants.TRANSACTION_DT));
        entry.setProjectCode(KFSConstants.getDashProjectCode());
        return entry;
    }

    /**
     * This method generates cash reversion origin entries for the current organization reversion, and adds them to the given list
     *
     * @param originEntriesToWrite a list of OriginEntryFulls to stick generated origin entries into
     * @throws FatalErrorException thrown if an origin entry's object code can't be found
     */
    public void generateCashReversions(List<OriginEntryFull> originEntriesToWrite) throws FatalErrorException {
        int entriesWritten = 0;

        // Reversion of cash from the actual account in the fiscal year ending (balance type of NB)
        OriginEntryFull entry = getEntry();
        entry.refreshReferenceObject("option");

        entry.setChartOfAccountsCode(unitOfWork.chartOfAccountsCode);
        entry.setAccountNumber(unitOfWork.accountNumber);
        entry.setSubAccountNumber(unitOfWork.subAccountNumber);
        entry.setFinancialObjectCode(organizationReversion.getOrganizationChartCashObjectCode());
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(systemOptions.getNominalFinancialBalanceTypeCd());

        getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
        if (ObjectUtils.isNull(entry.getFinancialObject())) {
            throw new FatalErrorException("Object Code for Entry not found: " + entry);
        }

        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + entry.getAccountNumber());
        entry.setTransactionLedgerEntryDescription(CASH_REVERTED_TO_MESSAGE + " " + organizationReversion.getCashReversionAccountNumber());
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCash());
        if (unitOfWork.getTotalCash().compareTo(KualiDecimal.ZERO) > 0) {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
        } else {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCash().negated());
        }
        entry.setFinancialObjectTypeCode(entry.getFinancialObject().getFinancialObjectTypeCode());

        // 3468 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
        // 3469 WS-AMT-N.
        // 3470 MOVE WS-AMT-X TO TRN-AMT-RED-X.

        originEntriesToWrite.add(entry);

        // Reversion of fund balance, starting with the actual account, to match the cash that was reverted (balance type of NB)
        entry = getEntry();
        entry.setChartOfAccountsCode(unitOfWork.chartOfAccountsCode);
        entry.setAccountNumber(unitOfWork.accountNumber);
        entry.setSubAccountNumber(unitOfWork.subAccountNumber);
        entry.setFinancialObjectCode((String) jobParameters.get(KFSConstants.FUND_BAL_OBJECT_CD));
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(DEFAULT_FINANCIAL_BALANCE_TYPE_CODE);

        getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
        if (ObjectUtils.isNull(entry.getFinancialObject())) {
            throw new FatalErrorException("Object Code for Entry not found: " + entry);
        }

        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
        entry.setTransactionLedgerEntryDescription(FUND_BALANCE_REVERTED_TO_MESSAGE + organizationReversion.getCashReversionAccountNumber());
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCash().abs());
        if (unitOfWork.getTotalCash().compareTo(KualiDecimal.ZERO) > 0) {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
        } else {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
        }
        entry.setFinancialObjectTypeCode(entry.getFinancialObject().getFinancialObjectTypeCode());

        // 3570 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
        // 3571 WS-AMT-N.
        // 3572 MOVE WS-AMT-X TO TRN-AMT-RED-X.

        getFlexibleOffsetAccountService().updateOffset(entry);
        originEntriesToWrite.add(entry);

        // Reversion of cash to the cash reversion account in the fiscal year ending (balance type of NB)
        entry = getEntry();
        entry.setChartOfAccountsCode(organizationReversion.getCashReversionFinancialChartOfAccountsCode());
        entry.setAccountNumber(organizationReversion.getCashReversionAccountNumber());
        entry.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
        entry.setFinancialObjectCode(organizationReversion.getCashReversionChartCashObjectCode());
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(DEFAULT_FINANCIAL_BALANCE_TYPE_CODE);

        getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
        if (ObjectUtils.isNull(entry.getFinancialObject())) {
            throw new FatalErrorException("Object Code for Entry not found: " + entry);
        }

        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
        entry.setTransactionLedgerEntryDescription(CASH_REVERTED_FROM_MESSAGE + unitOfWork.accountNumber + " " + unitOfWork.subAccountNumber);
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCash());
        if (unitOfWork.getTotalCash().compareTo(KualiDecimal.ZERO) > 0) {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
        } else {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
            entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCash().negated());
        }
        entry.setFinancialObjectTypeCode(entry.getFinancialObject().getFinancialObjectTypeCode());

        // 3668 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
        // 3669 WS-AMT-N.
        // 3670 MOVE WS-AMT-X TO TRN-AMT-RED-X.

        originEntriesToWrite.add(entry);

        // Reversion of fund balance, starting with the cash reversion account, to match the cash that was reverted (balance type of NB)
        entry = getEntry();
        entry.setChartOfAccountsCode(organizationReversion.getCashReversionFinancialChartOfAccountsCode());
        entry.setAccountNumber(organizationReversion.getCashReversionAccountNumber());
        entry.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
        entry.setFinancialObjectCode((String) jobParameters.get(KFSConstants.FUND_BAL_OBJECT_CD));
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(DEFAULT_FINANCIAL_BALANCE_TYPE_CODE);

        getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
        if (ObjectUtils.isNull(entry.getFinancialObject())) {
            throw new FatalErrorException("Object Code for Entry not found: " + entry);
        }

        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
        entry.setTransactionLedgerEntryDescription(FUND_BALANCE_REVERTED_FROM_MESSAGE + unitOfWork.accountNumber + " " + unitOfWork.subAccountNumber);
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCash());
        if (unitOfWork.getTotalCash().compareTo(KualiDecimal.ZERO) > 0) {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_CREDIT_CODE);
        } else {
            entry.setTransactionDebitCreditCode(KFSConstants.GL_DEBIT_CODE);
            entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCash().negated());
        }
        entry.setFinancialObjectTypeCode(entry.getFinancialObject().getFinancialObjectTypeCode());

        // 3768 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
        // 3769 WS-AMT-N.
        // 3770 MOVE WS-AMT-X TO TRN-AMT-RED-X.

        getFlexibleOffsetAccountService().updateOffset(entry);
        originEntriesToWrite.add(entry);
    }

    /**
     * Generates carry forward origin entries on a category by category basis (if the organization reversion record asks for that), assuming carry
     * forwards are required for the current unit of work
     *
     * @param originEntriesToWrite a list of origin entries to write, which any generated origin entries should be added to
     * @throws FatalErrorException thrown if an object code cannot be found
     */
    public void generateMany(List<OriginEntryFull> originEntriesToWrite) throws FatalErrorException {
        int originEntriesCreated = 0;
        for (Iterator<OrganizationReversionCategory> iter = categoryList.iterator(); iter.hasNext(); ) {
            OrganizationReversionCategory cat = iter.next();
            OrganizationReversionCategoryInfo detail = organizationReversion.getOrganizationReversionDetail(cat.getOrganizationReversionCategoryCode());
            OrgReversionUnitOfWorkCategoryAmount amount = unitOfWork.amounts.get(cat.getOrganizationReversionCategoryCode());

            if (!amount.getCarryForward().isZero()) {
                KualiDecimal commonAmount = amount.getCarryForward();
                String commonObject = detail.getOrganizationReversionObjectCode();

                OriginEntryFull entry = getEntry();
                entry.setUniversityFiscalYear((Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR) + 1);
                entry.setChartOfAccountsCode(unitOfWork.chartOfAccountsCode);
                entry.setAccountNumber(unitOfWork.accountNumber);
                entry.setSubAccountNumber(unitOfWork.subAccountNumber);
                entry.setFinancialObjectCode((String) jobParameters.get(KFSConstants.BEG_BUD_CASH_OBJECT_CD));
                entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
                entry.setFinancialBalanceTypeCode(KFSConstants.BALANCE_TYPE_CURRENT_BUDGET);

                getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
                if (ObjectUtils.isNull(entry.getFinancialObject())) {
                    throw new FatalErrorException("Object Code for Entry not found: " + entry);
                }

                ObjectCode objectCode = entry.getFinancialObject();
                entry.setFinancialObjectTypeCode(objectCode.getFinancialObjectTypeCode());
                entry.setUniversityFiscalPeriodCode(KFSConstants.MONTH1);
                entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
                entry.setTransactionLedgerEntryDescription(FUND_CARRIED_MESSAGE + (Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR));
                entry.setTransactionLedgerEntryAmount(commonAmount);

                // 3259 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
                // 3260 WS-AMT-N.
                // 3261 MOVE WS-AMT-X TO TRN-AMT-RED-X.

                originEntriesToWrite.add(entry);

                entry = getEntry();
                entry.setUniversityFiscalYear((Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR) + 1);
                entry.setChartOfAccountsCode(unitOfWork.chartOfAccountsCode);
                entry.setAccountNumber(unitOfWork.accountNumber);
                entry.setSubAccountNumber(unitOfWork.subAccountNumber);

                entry.setFinancialObjectCode(commonObject);
                entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
                entry.setFinancialBalanceTypeCode(KFSConstants.BALANCE_TYPE_CURRENT_BUDGET);

                getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
                if (ObjectUtils.isNull(entry.getFinancialObject())) {
                    throw new FatalErrorException("Object Code for Entry not found: " + entry);
                }

                objectCode = entry.getFinancialObject();
                entry.setFinancialObjectTypeCode(objectCode.getFinancialObjectTypeCode());
                entry.setUniversityFiscalPeriodCode(KFSConstants.MONTH1);
                entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
                entry.setTransactionLedgerEntryDescription(FUND_CARRIED_MESSAGE + (Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR));
                entry.setTransactionLedgerEntryAmount(commonAmount);

                // 3343 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
                // 3344 WS-AMT-N.
                // 3345 MOVE WS-AMT-X TO TRN-AMT-RED-X.

                originEntriesToWrite.add(entry);
            }
        }
    }

    /**
     * If carry forwards need to be generated for this unit of work, this method will generate the origin entries to accomplish those object codes.
     * Note: this will only be called if the organization reversion record tells the process to munge all carry forwards for all categories
     * together; if the organization reversion record does not call for such a thing, then generateMany will be called
     *
     * @param originEntriesToWrite a list of origin entries to write, that any generated origin entries should be added to
     * @throws FatalErrorException thrown if the current object code can't be found in the database
     */
    public void generateCarryForwards(List<OriginEntryFull> originEntriesToWrite) throws FatalErrorException {
        int originEntriesWritten = 0;

        OriginEntryFull entry = getEntry();
        entry.setUniversityFiscalYear((Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR) + 1);
        entry.setChartOfAccountsCode(unitOfWork.chartOfAccountsCode);
        entry.setAccountNumber(unitOfWork.accountNumber);
        entry.setSubAccountNumber(unitOfWork.subAccountNumber);
        entry.setFinancialObjectCode((String) jobParameters.get(KFSConstants.BEG_BUD_CASH_OBJECT_CD));
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(KFSConstants.BALANCE_TYPE_CURRENT_BUDGET);

        getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
        if (ObjectUtils.isNull(entry.getFinancialObject())) {
            throw new FatalErrorException("Object Code for Entry not found: " + entry);
        }

        ObjectCode objectCode = entry.getFinancialObject();
        entry.setFinancialObjectTypeCode(objectCode.getFinancialObjectTypeCode());
        entry.setUniversityFiscalPeriodCode(KFSConstants.MONTH1);
        entry.setFinancialDocumentTypeCode(DEFAULT_FINANCIAL_DOCUMENT_TYPE_CODE);
        entry.setFinancialSystemOriginationCode(DEFAULT_FINANCIAL_SYSTEM_ORIGINATION_CODE);
        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
        entry.setTransactionLedgerEntrySequenceNumber(1);
        entry.setTransactionLedgerEntryDescription(FUND_CARRIED_MESSAGE + (Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR));
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCarryForward());
        entry.setTransactionDate((Date) jobParameters.get(KFSConstants.TRANSACTION_DT));
        entry.setProjectCode(KFSConstants.getDashProjectCode());
        // 2995 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
        // 2996 WS-AMT-N.
        // 2997 MOVE WS-AMT-X TO TRN-AMT-RED-X.

        originEntriesToWrite.add(entry);

        entry = getEntry();
        entry.setUniversityFiscalYear((Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR) + 1);
        entry.setChartOfAccountsCode(unitOfWork.chartOfAccountsCode);
        entry.setAccountNumber(unitOfWork.accountNumber);
        entry.setSubAccountNumber(unitOfWork.subAccountNumber);
        entry.setFinancialObjectCode((String) jobParameters.get(KFSConstants.UNALLOC_OBJECT_CD));

        getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
        if (ObjectUtils.isNull(entry.getFinancialObject())) {
            throw new FatalErrorException("Object Code for Entry not found: " + entry);
        }

        objectCode = entry.getFinancialObject();
        entry.setFinancialObjectTypeCode(objectCode.getFinancialObjectTypeCode());
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(KFSConstants.BALANCE_TYPE_CURRENT_BUDGET);
        entry.setUniversityFiscalPeriodCode(KFSConstants.MONTH1);
        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
        entry.setTransactionLedgerEntryDescription(FUND_CARRIED_MESSAGE + (Integer) jobParameters.get(KFSConstants.UNIV_FISCAL_YR));
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalCarryForward());

        // 3079 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
        // 3080 WS-AMT-N.
        // 3081 MOVE WS-AMT-X TO TRN-AMT-RED-X.

        originEntriesToWrite.add(entry);

    }

    /**
     * If reversions are necessary, this will generate the origin entries for those reversions
     *
     * @param originEntriesToWrite the list of origin entries to add reversions into
     * @throws FatalErrorException thrown if object code if the entry can't be found
     */
    public void generateReversions(List<OriginEntryFull> originEntriesToWrite) throws FatalErrorException {
        int originEntriesWritten = 0;

        OriginEntryFull entry = getEntry();
        entry.setChartOfAccountsCode(unitOfWork.chartOfAccountsCode);
        entry.setAccountNumber(unitOfWork.accountNumber);
        entry.setSubAccountNumber(unitOfWork.subAccountNumber);
        entry.setFinancialObjectCode((String) jobParameters.get(KFSConstants.UNALLOC_OBJECT_CD));
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(DEFAULT_FINANCIAL_BALANCE_TYPE_CODE_YEAR_END);

        getPersistenceService().retrieveReferenceObject(entry, KFSPropertyConstants.FINANCIAL_OBJECT);
        if (ObjectUtils.isNull(entry.getFinancialObject())) {
            throw new FatalErrorException("Object Code for Entry not found: " + entry);
        }

        ObjectCode objectCode = entry.getFinancialObject();
        entry.setFinancialObjectTypeCode(objectCode.getFinancialObjectTypeCode());

        entry.setUniversityFiscalPeriodCode(KFSConstants.MONTH13);

        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + entry.getAccountNumber());

        entry.setTransactionLedgerEntryDescription(FUND_REVERTED_TO_MESSAGE + organizationReversion.getBudgetReversionAccountNumber());
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalReversion().negated());

        originEntriesToWrite.add(entry);

        entry = getEntry();
        entry.setChartOfAccountsCode(organizationReversion.getBudgetReversionChartOfAccountsCode());
        entry.setAccountNumber(organizationReversion.getBudgetReversionAccountNumber());
        entry.setSubAccountNumber(KFSConstants.getDashSubAccountNumber());
        entry.setFinancialObjectCode((String) jobParameters.get(KFSConstants.UNALLOC_OBJECT_CD));
        entry.setFinancialSubObjectCode(KFSConstants.getDashFinancialSubObjectCode());
        entry.setFinancialBalanceTypeCode(DEFAULT_FINANCIAL_BALANCE_TYPE_CODE_YEAR_END);
        entry.setFinancialObjectTypeCode(objectCode.getFinancialObjectTypeCode());
        entry.setUniversityFiscalPeriodCode(KFSConstants.MONTH13);
        entry.setDocumentNumber(DEFAULT_DOCUMENT_NUMBER_PREFIX + unitOfWork.accountNumber);
        if (unitOfWork.accountNumber.equals(KFSConstants.getDashSubAccountNumber())) {
            entry.setTransactionLedgerEntryDescription(FUND_REVERTED_FROM_MESSAGE + unitOfWork.accountNumber);
        } else {
            entry.setTransactionLedgerEntryDescription(FUND_REVERTED_FROM_MESSAGE + unitOfWork.accountNumber + " " + unitOfWork.subAccountNumber);
        }
        entry.setTransactionLedgerEntryAmount(unitOfWork.getTotalReversion());

        // 2899 MOVE TRN-LDGR-ENTR-AMT TO WS-AMT-W-PERIOD
        // 2900 WS-AMT-N.
        // 2901 MOVE WS-AMT-X TO TRN-AMT-RED-X.

        originEntriesToWrite.add(entry);
    }

    /**
     * This method calculates the totals for a given unit of work's reversion
     *
     * @throws FatalErrorException
     */
    public void calculateTotals() throws FatalErrorException {
        /*
         * How this works: At the start, in the clearCalculationTotals(), both the unit of work's totalAvailable and totalReversion
         * are set to the available amounts from each of the category amounts. Then, as the logic is applied, the totalCarryForward
         * is added to and the totalReversion is subtracted from. Let's look at a simple example: Let's say you've got an amount for
         * C01, which has $2000 available, no encumbrances, that's all you've got. This means that at the end of
         * clearCalculationTotals(), there's $2000 in totalAvailable, $2000 in totalReversion, and $0 in totalCarryForward. Now, C01,
         * let's say, is for code A. So, look below at the if that catches Code A. You'll note that it adds the available amount to
         * totalCarryForward, it's own carryForward, the negated available to totalReversion, and that, done, it sets available to
         * $0. With our example, that means that $2000 is in totalCarryForward (and in the amount's carryForward), the
         * totalReversion has been knocked down to $0, and the available is $0. So, carry forward origin entries get created, and
         * reversions do not. This is also why you don't see a block about calculating R2 totals below...the process has a natural
         * inclination towards creating R2 (ie, ignore encumbrances and revert all available) entries.
         */

        // clear out the unit of work totals we're going to calculate values in, in preperation for applying rules
        clearCalculationTotals();

        // For each category, apply the rules
        for (OrganizationReversionCategory category : categoryList) {
            String categoryCode = category.getOrganizationReversionCategoryCode();
            OrganizationReversionCategoryLogic logic = categories.get(categoryCode);
            OrgReversionUnitOfWorkCategoryAmount amount = unitOfWork.amounts.get(categoryCode);

            OrganizationReversionCategoryInfo detail = organizationReversion.getOrganizationReversionDetail(categoryCode);

            if (detail == null) {
                throw new FatalErrorException("Organization Reversion " + organizationReversion.getUniversityFiscalYear() + "-" + organizationReversion.getChartOfAccountsCode() + "-" + organizationReversion.getOrganizationCode() + " does not have a detail for category " + categoryCode);
            }
            String ruleCode = detail.getOrganizationReversionCode();

            if (LOG.isDebugEnabled()) {
                LOG.debug("Unit of Work: " + unitOfWork.getChartOfAccountsCode() + unitOfWork.getAccountNumber() + unitOfWork.getSubAccountNumber() + ", category " + category.getOrganizationReversionCategoryName() + ": budget = " + amount.getBudget() + "; actual = " + amount.getActual() + "; encumbrance = " + amount.getEncumbrance() + "; available = " + amount.getAvailable() + "; apply rule code " + ruleCode);
            }

            if (KFSConstants.RULE_CODE_R1.equals(ruleCode) || KFSConstants.RULE_CODE_N1.equals(ruleCode) || KFSConstants.RULE_CODE_C1.equals(ruleCode)) {
                if (amount.getAvailable().compareTo(KualiDecimal.ZERO) > 0) { // do we have budget left?
                    if (amount.getAvailable().compareTo(amount.getEncumbrance()) > 0) { // is it more than enough to cover our
                        // encumbrances?
                        unitOfWork.addTotalCarryForward(amount.getEncumbrance());
                        amount.addCarryForward(amount.getEncumbrance());
                        unitOfWork.addTotalReversion(amount.getEncumbrance().negated());
                        amount.addAvailable(amount.getEncumbrance().negated());
                    } else {
                        // there's not enough available left to cover the encumbrances; cover what we can
                        unitOfWork.addTotalCarryForward(amount.getAvailable());
                        amount.addCarryForward(amount.getAvailable());
                        unitOfWork.addTotalReversion(amount.getAvailable().negated());
                        amount.setAvailable(KualiDecimal.ZERO);
                    }
                }
            }

            if (KFSConstants.RULE_CODE_A.equals(ruleCode)) {
                unitOfWork.addTotalCarryForward(amount.getAvailable());
                amount.addCarryForward(amount.getAvailable());
                unitOfWork.addTotalReversion(amount.getAvailable().negated());
                amount.setAvailable(KualiDecimal.ZERO);
            }

            if (KFSConstants.RULE_CODE_C1.equals(ruleCode) || KFSConstants.RULE_CODE_C2.equals(ruleCode)) {
                if (amount.getAvailable().compareTo(KualiDecimal.ZERO) > 0) {
                    unitOfWork.addTotalCarryForward(amount.getAvailable());
                    amount.addCarryForward(amount.getAvailable());
                    unitOfWork.addTotalReversion(amount.getAvailable().negated());
                    amount.setAvailable(KualiDecimal.ZERO);
                }
            }

            if (KFSConstants.RULE_CODE_N1.equals(ruleCode) || KFSConstants.RULE_CODE_N2.equals(ruleCode)) {
                if (amount.getAvailable().compareTo(KualiDecimal.ZERO) < 0) {
                    unitOfWork.addTotalCarryForward(amount.getAvailable());
                    amount.addCarryForward(amount.getAvailable());
                    unitOfWork.addTotalReversion(amount.getAvailable().negated());
                    amount.setAvailable(KualiDecimal.ZERO);
                }
            }

            if (LOG.isDebugEnabled()) {
                LOG.debug("Totals Now: " + unitOfWork.getChartOfAccountsCode() + unitOfWork.getAccountNumber() + unitOfWork.getSubAccountNumber() + ", total cash now " + unitOfWork.getTotalCash() + ": total available = " + unitOfWork.getTotalAvailable() + "; total reversion = " + unitOfWork.getTotalReversion() + "; total carry forward = " + unitOfWork.getTotalCarryForward());
            }
        }
    }

    /**
     * This method clears the unit of work's amounts to what they should be before each category bucket is calculated; specifically,
     * the total available for each category is calculated, and the total available and total reversion are set to the sum of all
     * available from each category bucket.  The total carry forward is set to 0.
     */
    protected void clearCalculationTotals() {
        // Initialize all the amounts before applying the proper rule
        KualiDecimal totalAvailable = KualiDecimal.ZERO;
        for (OrganizationReversionCategory category : categoryList) {
            OrganizationReversionCategoryLogic logic = categories.get(category.getOrganizationReversionCategoryCode());

            OrgReversionUnitOfWorkCategoryAmount amount = unitOfWork.amounts.get(category.getOrganizationReversionCategoryCode());
            if (logic.isExpense()) {
                amount.setAvailable(amount.getBudget().subtract(amount.getActual()));
            } else {
                amount.setAvailable(amount.getActual().subtract(amount.getBudget()));
            }
            totalAvailable = totalAvailable.add(amount.getAvailable());
            amount.setCarryForward(KualiDecimal.ZERO);
        }
        unitOfWork.setTotalAvailable(totalAvailable);
        unitOfWork.setTotalReversion(totalAvailable);
        unitOfWork.setTotalCarryForward(KualiDecimal.ZERO);
    }

    /**
     * Summarizes the given origin entries to the ledger report
     *
     * @param originEntries the List of originEntries to summarize
     */
    protected void summarizeOriginEntries(List<OriginEntryFull> originEntries) {
        for (OriginEntryFull originEntry : originEntries) {
            ledgerReport.summarizeEntry(originEntry);
        }
    }

    public OrgReversionUnitOfWork getUnitOfWork() {
        return unitOfWork;
    }

    public void setUnitOfWork(OrgReversionUnitOfWork unitOfWork) {
        this.unitOfWork = unitOfWork;
    }

    public List<OrganizationReversionCategory> getCategoryList() {
        return this.categoryList;
    }

    /**
     * Gets the generatedOriginEntries attribute.
     *
     * @return Returns the generatedOriginEntries.
     */
    public List<OriginEntryFull> getGeneratedOriginEntries() {
        return generatedOriginEntries;
    }

    /**
     * Sets the holdGeneratedOriginEntries attribute value.
     *
     * @param holdGeneratedOriginEntries The holdGeneratedOriginEntries to set.
     */
    public void setHoldGeneratedOriginEntries(boolean holdGeneratedOriginEntries) {
        this.holdGeneratedOriginEntries = holdGeneratedOriginEntries;
        this.generatedOriginEntries = new ArrayList<OriginEntryFull>();
    }

    /**
     * Returns the total number of balances for the previous fiscal year
     *
     * @return the total number of balances for the previous fiscal year
     */
    public int getBalancesRead() {
        return organizationReversionCounts.get("balancesRead").intValue();
    }

    /**
     * Returns the total number of balances selected for inclusion in this process
     *
     * @return the total number of balances selected for inclusion in this process
     */
    public int getBalancesSelected() {
        return organizationReversionCounts.get("balancesSelected").intValue();
    }

    /**
     * Returns the total number of origin entries written by this process
     *
     * @return the total number of origin entries written by this process
     */
    public int getRecordsWritten() {
        return organizationReversionCounts.get("recordsWritten").intValue();
    }

    /**
     * Used mainly for unit testing, this method allows a way to change the output group of a org reversion process run
     *
     * @param outputGroup
     */
    public void setOutputFileName(String outputFileName) {
        this.outputFileName = outputFileName;
    }

    /**
     * Increments one of the totals held in the count map this process uses for reported statistics
     *
     * @param countName the name of the count to increment
     */
    private void incrementCount(String countName) {
        incrementCount(countName, 1);
    }

    /**
     * Increments one of the totals held in the count map this process uses for reported statistics by a given increment
     *
     * @param countName the name of the count to increment
     * @param increment the amount to increment
     */
    protected void incrementCount(String countName, int increment) {
        Integer count = organizationReversionCounts.get(countName);
        if (countName.equals("recordsWritten")) {
            int countAsInt = count.intValue();
            // add by 1, so we're guaranteed to hit the 1000th
            for (int i = 1; i <= increment; i++) {
                countAsInt += 1;
                if (countAsInt % 1000 == 0) {
                    LOG.info(" ORIGIN ENTRIES INSERTED = " + countAsInt);
                } else if (countAsInt == 367471) {
                    LOG.info(" YOU HAVE ACHIEVED 367471 ORIGIN ENTRIES INSERTED!  TRIUMPH IS YOURS!  ");
                }
            }
            organizationReversionCounts.put(countName, new Integer(countAsInt));
        } else {
            organizationReversionCounts.put(countName, new Integer(count.intValue() + increment));
        }
    }

    /**
     * Writes out the encapsulated origin entry ledger report to the given reportWriterService
     *
     * @param reportWriterService the report to write the ledger summary report to
     */
    public void writeLedgerSummaryReport(ReportWriterService reportWriterService) {
        ledgerReport.writeReport(reportWriterService);
    }

    /**
     * Gets the organizationReversionService attribute.
     *
     * @return Returns the organizationReversionService.
     */
    public OrganizationReversionService getOrganizationReversionService() {
        return organizationReversionService;
    }

    /**
     * Sets the organizationReversionService attribute value.
     *
     * @param organizationReversionService The organizationReversionService to set.
     */
    public void setOrganizationReversionService(OrganizationReversionService organizationReversionService) {
        this.organizationReversionService = organizationReversionService;
    }

    /**
     * Gets the balanceService attribute.
     *
     * @return Returns the balanceService.
     */
    public BalanceService getBalanceService() {
        return balanceService;
    }

    /**
     * Sets the balanceService attribute value.
     *
     * @param balanceService The balanceService to set.
     */
    public void setBalanceService(BalanceService balanceService) {
        this.balanceService = balanceService;
    }

    /**
     * Gets the originEntryService attribute.
     *
     * @return Returns the originEntryService.
     */
    public OriginEntryService getOriginEntryService() {
        return originEntryService;
    }

    /**
     * Sets the originEntryService attribute value.
     *
     * @param originEntryService The originEntryService to set.
     */
    public void setOriginEntryService(OriginEntryService originEntryService) {
        this.originEntryService = originEntryService;
    }

    /**
     * Gets the persistenceService attribute.
     *
     * @return Returns the persistenceService.
     */
    public PersistenceService getPersistenceService() {
        return persistenceService;
    }

    /**
     * Sets the persistenceService attribute value.
     *
     * @param persistenceService The persistenceService to set.
     */
    public void setPersistenceService(PersistenceService persistenceService) {
        this.persistenceService = persistenceService;
    }

    /**
     * Gets the dateTimeService attribute.
     *
     * @return Returns the dateTimeService.
     */
    public DateTimeService getDateTimeService() {
        return dateTimeService;
    }

    /**
     * Sets the dateTimeService attribute value.
     *
     * @param dateTimeService The dateTimeService to set.
     */
    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    /**
     * Gets the priorYearAccountService attribute.
     *
     * @return Returns the priorYearAccountService.
     */
    public PriorYearAccountService getPriorYearAccountService() {
        return priorYearAccountService;
    }

    /**
     * Sets the priorYearAccountService attribute value.
     *
     * @param priorYearAccountService The priorYearAccountService to set.
     */
    public void setPriorYearAccountService(PriorYearAccountService priorYearAccountService) {
        this.priorYearAccountService = priorYearAccountService;
    }

    /**
     * Gets the orgReversionUnitOfWorkService attribute.
     *
     * @return Returns the orgReversionUnitOfWorkService.
     */
    public OrganizationReversionUnitOfWorkService getOrgReversionUnitOfWorkService() {
        return orgReversionUnitOfWorkService;
    }

    /**
     * Sets the orgReversionUnitOfWorkService attribute value.
     *
     * @param orgReversionUnitOfWorkService The orgReversionUnitOfWorkService to set.
     */
    public void setOrgReversionUnitOfWorkService(OrganizationReversionUnitOfWorkService orgReversionUnitOfWorkService) {
        this.orgReversionUnitOfWorkService = orgReversionUnitOfWorkService;
    }

    /**
     * Gets the flexibleOffsetAccountService attribute.
     *
     * @return Returns the flexibleOffsetAccountService.
     */
    public FlexibleOffsetAccountService getFlexibleOffsetAccountService() {
        return flexibleOffsetAccountService;
    }

    /**
     * Sets the flexibleOffsetAccountService attribute value.
     *
     * @param flexibleOffsetAccountService The flexibleOffsetAccountService to set.
     */
    public void setFlexibleOffsetAccountService(FlexibleOffsetAccountService flexibleOffsetAccountService) {
        this.flexibleOffsetAccountService = flexibleOffsetAccountService;
    }

    /**
     * Gets the parameterService attribute.
     *
     * @return Returns the parameterService.
     */
    public ParameterService getParameterService() {
        return parameterService;
    }

    /**
     * Sets the parameterService attribute value.
     *
     * @param parameterService The parameterService to set.
     */
    public void setParameterService(ParameterService parameterService) {
        this.parameterService = parameterService;
    }

    /**
     * Gets the configurationService attribute.
     *
     * @return Returns the configurationService.
     */
    public ConfigurationService getConfigurationService() {
        return configurationService;
    }

    /**
     * Sets the configurationService attribute value.
     *
     * @param configurationService The configurationService to set.
     */
    public void setConfigurationService(ConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    /**
     * Gets the usePriorYearInformation attribute.
     *
     * @return Returns the usePriorYearInformation.
     */
    public boolean isUsePriorYearInformation() {
        return usePriorYearInformation;
    }

    /**
     * Sets the usePriorYearInformation attribute value.
     *
     * @param usePriorYearInformation The usePriorYearInformation to set.
     */
    public void setUsePriorYearInformation(boolean endOfYear) {
        this.usePriorYearInformation = endOfYear;
    }

    /**
     * Gets the cashOrganizationReversionCategoryLogic attribute.
     *
     * @return Returns the cashOrganizationReversionCategoryLogic.
     */
    public OrganizationReversionCategoryLogic getCashOrganizationReversionCategoryLogic() {
        return cashOrganizationReversionCategoryLogic;
    }

    /**
     * Sets the cashOrganizationReversionCategoryLogic attribute value.
     *
     * @param cashOrganizationReversionCategoryLogic The cashOrganizationReversionCategoryLogic to set.
     */
    public void setCashOrganizationReversionCategoryLogic(OrganizationReversionCategoryLogic cashOrganizationReversionCategoryLogic) {
        this.cashOrganizationReversionCategoryLogic = cashOrganizationReversionCategoryLogic;
    }


    /**
     * Gets the batchFileDirectoryName attribute.
     *
     * @return Returns the batchFileDirectoryName.
     */
    public String getBatchFileDirectoryName() {
        return batchFileDirectoryName;
    }

    /**
     * Sets the batchFileDirectoryName attribute value.
     *
     * @param batchFileDirectoryName The batchFileDirectoryName to set.
     */
    public void setBatchFileDirectoryName(String batchFileDirectoryName) {
        this.batchFileDirectoryName = batchFileDirectoryName;
    }

    /**
     * Sets the jobParameters attribute value.
     *
     * @param jobParameters The jobParameters to set.
     */
    public void setJobParameters(Map jobParameters) {
        this.jobParameters = jobParameters;
    }

    /**
     * Sets the organizationReversionCounts attribute value.
     *
     * @param organizationReversionCounts The organizationReversionCounts to set.
     */
    public void setOrganizationReversionCounts(Map<String, Integer> organizationReversionCounts) {
        this.organizationReversionCounts = organizationReversionCounts;
    }

}
