/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 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.service.impl; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.kuali.kfs.coa.businessobject.ObjectCode; import org.kuali.kfs.coa.service.AccountService; import org.kuali.kfs.coa.service.ObjectLevelService; import org.kuali.kfs.coa.service.ObjectTypeService; import org.kuali.kfs.fp.document.YearEndDocument; import org.kuali.kfs.gl.batch.dataaccess.SufficientFundsDao; import org.kuali.kfs.gl.businessobject.SufficientFundBalances; import org.kuali.kfs.gl.businessobject.SufficientFundRebuild; import org.kuali.kfs.gl.businessobject.Transaction; import org.kuali.kfs.gl.dataaccess.SufficientFundBalancesDao; import org.kuali.kfs.gl.service.SufficientFundsService; import org.kuali.kfs.gl.service.SufficientFundsServiceConstants; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.kfs.sys.businessobject.SufficientFundsItem; import org.kuali.kfs.sys.businessobject.SystemOptions; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.document.GeneralLedgerPostingDocument; import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService; import org.kuali.kfs.sys.service.OptionsService; import org.kuali.rice.core.api.config.property.ConfigurationService; import org.kuali.rice.core.api.util.type.KualiDecimal; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.util.ObjectUtils; import org.springframework.transaction.annotation.Transactional; /** * The base implementation of SufficientFundsService */ @Transactional public class SufficientFundsServiceImpl implements SufficientFundsService, SufficientFundsServiceConstants { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SufficientFundsServiceImpl.class); private AccountService accountService; private ObjectLevelService objectLevelService; private ConfigurationService kualiConfigurationService; private SufficientFundsDao sufficientFundsDao; private SufficientFundBalancesDao sufficientFundBalancesDao; private OptionsService optionsService; private GeneralLedgerPendingEntryService generalLedgerPendingEntryService; private BusinessObjectService businessObjectService; /** * Default constructor */ public SufficientFundsServiceImpl() { super(); } /** * This operation derives the acct_sf_finobj_cd which is used to populate the General Ledger Pending entry table, so that later * we can do Suff Fund checking against that entry * * @param financialObject the object code being checked against * @param accountSufficientFundsCode the kind of sufficient funds checking turned on in this system * @return the object code that should be used for the sufficient funds inquiry, or a blank String * @see org.kuali.kfs.gl.service.SufficientFundsService#getSufficientFundsObjectCode(org.kuali.kfs.coa.businessobject.ObjectCode, * java.lang.String) */ public String getSufficientFundsObjectCode(ObjectCode financialObject, String accountSufficientFundsCode) { if (KFSConstants.SF_TYPE_NO_CHECKING.equals(accountSufficientFundsCode)) { return KFSConstants.NOT_AVAILABLE_STRING; } else if (KFSConstants.SF_TYPE_ACCOUNT.equals(accountSufficientFundsCode)) { return " "; } else if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(accountSufficientFundsCode)) { return " "; } else if (KFSConstants.SF_TYPE_OBJECT.equals(accountSufficientFundsCode)) { return financialObject.getFinancialObjectCode(); } else if (KFSConstants.SF_TYPE_LEVEL.equals(accountSufficientFundsCode)) { return financialObject.getFinancialObjectLevelCode(); } else if (KFSConstants.SF_TYPE_CONSOLIDATION.equals(accountSufficientFundsCode)) { financialObject.refreshReferenceObject("financialObjectLevel"); return financialObject.getFinancialObjectLevel().getFinancialConsolidationObjectCode(); } else { throw new IllegalArgumentException("Invalid Sufficient Funds Code: " + accountSufficientFundsCode); } } /** * Checks for sufficient funds on a single document * * @param document document to check * @return Empty List if has sufficient funds for all accounts, List of SufficientFundsItem if not * @see org.kuali.kfs.gl.service.SufficientFundsService#checkSufficientFunds(org.kuali.rice.krad.document.FinancialDocument) */ public List<SufficientFundsItem> checkSufficientFunds(GeneralLedgerPostingDocument document) { LOG.debug("checkSufficientFunds() started"); return checkSufficientFunds((List<? extends Transaction>) document.getPendingLedgerEntriesForSufficientFundsChecking()); } /** * checks to see if a document is a <code>YearEndDocument</code> * * @param documentClass the class of a Document to check * @return true if the class implements <code>YearEndDocument</code> */ @SuppressWarnings("unchecked") protected boolean isYearEndDocument(Class documentClass) { return YearEndDocument.class.isAssignableFrom(documentClass); } /** * Checks for sufficient funds on a list of transactions * * @param transactions list of transactions * @return Empty List if has sufficient funds for all accounts, List of SufficientFundsItem if not * @see org.kuali.kfs.gl.service.SufficientFundsService#checkSufficientFunds(java.util.List) */ @SuppressWarnings("unchecked") public List<SufficientFundsItem> checkSufficientFunds(List<? extends Transaction> transactions) { LOG.debug("checkSufficientFunds() started"); for (Transaction e : transactions) { e.refreshNonUpdateableReferences(); } List<SufficientFundsItem> summaryItems = summarizeTransactions(transactions); for (Iterator iter = summaryItems.iterator(); iter.hasNext();) { SufficientFundsItem item = (SufficientFundsItem) iter.next(); if ( LOG.isDebugEnabled() ) { LOG.debug("checkSufficientFunds() " + item.toString()); } if (hasSufficientFundsOnItem(item)) { iter.remove(); } } return summaryItems; } /** * For each transaction, fetches the appropriate sufficient funds item to check against * * @param transactions a list of Transactions * @return a List of corresponding SufficientFundsItem */ @SuppressWarnings("unchecked") protected List<SufficientFundsItem> summarizeTransactions(List<? extends Transaction> transactions) { Map<String, SufficientFundsItem> items = new HashMap<String, SufficientFundsItem>(); SystemOptions currentYear = optionsService.getCurrentYearOptions(); // loop over the given transactions, grouping into SufficientFundsItem objects // which are keyed by the appropriate chart/account/SF type, and derived object value // see getSufficientFundsObjectCode() for the "object" used for grouping for (Iterator iter = transactions.iterator(); iter.hasNext();) { Transaction tran = (Transaction) iter.next(); SystemOptions year = tran.getOption(); if (year == null) { year = currentYear; } if (ObjectUtils.isNull(tran.getAccount())) { throw new IllegalArgumentException("Invalid account: " + tran.getChartOfAccountsCode() + "-" + tran.getAccountNumber()); } SufficientFundsItem sfi = new SufficientFundsItem(year, tran, getSufficientFundsObjectCode(tran.getFinancialObject(), tran.getAccount().getAccountSufficientFundsCode())); sfi.setDocumentTypeCode(tran.getFinancialDocumentTypeCode()); if (items.containsKey(sfi.getKey())) { SufficientFundsItem item = (SufficientFundsItem) items.get(sfi.getKey()); item.add(tran); } else { items.put(sfi.getKey(), sfi); } } return new ArrayList<SufficientFundsItem>(items.values()); } /** * Given a sufficient funds item record, determines if there are sufficient funds available for the transaction * * @param item the item to check * @return true if there are sufficient funds available, false otherwise */ protected boolean hasSufficientFundsOnItem(SufficientFundsItem item) { if (item.getAmount().equals(KualiDecimal.ZERO)) { LOG.debug("hasSufficientFundsOnItem() Transactions with zero amounts shold pass"); return true; } if (!item.getYear().isBudgetCheckingOptionsCode()) { LOG.debug("hasSufficientFundsOnItem() No sufficient funds checking"); return true; } if (!item.getAccount().isPendingAcctSufficientFundsIndicator()) { if ( LOG.isDebugEnabled() ) { LOG.debug("hasSufficientFundsOnItem() No checking on eDocs for account " + item.getAccount().getChartOfAccountsCode() + "-" + item.getAccount().getAccountNumber()); } return true; } // exit sufficient funds checking if not enabled for an account if (KFSConstants.SF_TYPE_NO_CHECKING.equals(item.getAccountSufficientFundsCode())) { if ( LOG.isDebugEnabled() ) { LOG.debug("hasSufficientFundsOnItem() sufficient funds not enabled for account " + item.getAccount().getChartOfAccountsCode() + "-" + item.getAccount().getAccountNumber()); } return true; } ObjectTypeService objectTypeService = (ObjectTypeService) SpringContext.getBean(ObjectTypeService.class); List<String> expenseObjectTypes = objectTypeService.getCurrentYearExpenseObjectTypes(); if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode()) && !item.getFinancialObject().getChartOfAccounts().getFinancialCashObjectCode().equals(item.getFinancialObject().getFinancialObjectCode())) { LOG.debug("hasSufficientFundsOnItem() SF checking is cash and transaction is not cash"); return true; } else if (!KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode()) && !expenseObjectTypes.contains(item.getFinancialObjectType().getCode())) { LOG.debug("hasSufficientFundsOnItem() SF checking is budget and transaction is not expense"); return true; } Map<String, Object> keys = new HashMap<String, Object>(); keys.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, item.getYear().getUniversityFiscalYear()); keys.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, item.getAccount().getChartOfAccountsCode()); keys.put(KFSPropertyConstants.ACCOUNT_NUMBER, item.getAccount().getAccountNumber()); keys.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, item.getSufficientFundsObjectCode()); SufficientFundBalances sfBalance = (SufficientFundBalances)businessObjectService.findByPrimaryKey(SufficientFundBalances.class, keys); if (sfBalance == null) { Map criteria = new HashMap(); criteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, item.getAccount().getChartOfAccountsCode()); criteria.put(KFSPropertyConstants.ACCOUNT_NUMBER_FINANCIAL_OBJECT_CODE, item.getAccount().getAccountNumber()); Collection sufficientFundRebuilds = businessObjectService.findMatching(SufficientFundRebuild.class, criteria); if (sufficientFundRebuilds != null && sufficientFundRebuilds.size() > 0) { LOG.debug("hasSufficientFundsOnItem() No balance record and waiting on rebuild, no sufficient funds"); return false; } else { sfBalance = new SufficientFundBalances(); sfBalance.setAccountActualExpenditureAmt(KualiDecimal.ZERO); sfBalance.setAccountEncumbranceAmount(KualiDecimal.ZERO); sfBalance.setCurrentBudgetBalanceAmount(KualiDecimal.ZERO); } } KualiDecimal balanceAmount = item.getAmount(); if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode()) || item.getYear().getBudgetCheckingBalanceTypeCd().equals(item.getBalanceTyp().getCode())) { // We need to change the sign on the amount because the amount in the item is an increase in cash. We only care // about decreases in cash. // Also, negating if this is a balance type code of budget checking and the transaction is a budget transaction. balanceAmount = balanceAmount.negated(); } if (balanceAmount.isNegative()) { LOG.debug("hasSufficientFundsOnItem() balanceAmount is negative, allow transaction to proceed"); return true; } PendingAmounts priorYearPending = new PendingAmounts(); // if we're checking the CASH_AT_ACCOUNT type, then we need to consider the prior year pending transactions // if the balance forwards have not been run if ((KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode())) && (!item.getYear().isFinancialBeginBalanceLoadInd())) { priorYearPending = getPriorYearSufficientFundsBalanceAmount(item); } PendingAmounts pending = getPendingBalanceAmount(item); KualiDecimal availableBalance = null; if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode())) { // if the beginning balances have not loaded for the transaction FY, pull the remaining balance from last year if (!item.getYear().isFinancialBeginBalanceLoadInd()) { availableBalance = sfBalance.getCurrentBudgetBalanceAmount() .add(priorYearPending.budget) // add the remaining budget from last year (assumed to carry to this year's) .add(pending.actual) // any pending expenses (remember sense is negated) .subtract(sfBalance.getAccountEncumbranceAmount()) // subtract the encumbrances (not reflected in cash yet) .subtract(priorYearPending.encumbrance); } else { // balance forwards have been run, don't need to consider prior year remaining budget availableBalance = sfBalance.getCurrentBudgetBalanceAmount() .add(pending.actual) .subtract(sfBalance.getAccountEncumbranceAmount()); } } else { availableBalance = sfBalance.getCurrentBudgetBalanceAmount() // current budget balance .add(pending.budget) // pending budget entries .subtract(sfBalance.getAccountActualExpenditureAmt()) // minus all current and pending actuals and encumbrances .subtract(pending.actual) .subtract(sfBalance.getAccountEncumbranceAmount()) .subtract(pending.encumbrance); } if ( LOG.isDebugEnabled() ) { LOG.debug("hasSufficientFundsOnItem() balanceAmount: " + balanceAmount + " availableBalance: " + availableBalance); } if (balanceAmount.compareTo(availableBalance) > 0) { LOG.debug("hasSufficientFundsOnItem() no sufficient funds"); return false; } LOG.debug("hasSufficientFundsOnItem() has sufficient funds"); return true; } /** * An inner class to hold summary totals of pending ledger entry amounts */ protected class PendingAmounts { public KualiDecimal budget = KualiDecimal.ZERO; public KualiDecimal actual = KualiDecimal.ZERO; public KualiDecimal encumbrance = KualiDecimal.ZERO; } /** * Given a sufficient funds item to check, gets the prior year sufficient funds balance to check against * * @param item the sufficient funds item to check against * @return a PendingAmounts record with the pending budget and encumbrance */ protected PendingAmounts getPriorYearSufficientFundsBalanceAmount(SufficientFundsItem item) { PendingAmounts amounts = new PendingAmounts(); // This only gets called for sufficient funds type of Cash at Account (H). The object code in the table for this type is // always // 4 spaces. Map<String, Object> keys = new HashMap<String, Object>(); keys.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, Integer.valueOf(item.getYear().getUniversityFiscalYear().intValue() - 1)); keys.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, item.getAccount().getChartOfAccountsCode()); keys.put(KFSPropertyConstants.ACCOUNT_NUMBER, item.getAccount().getAccountNumber()); keys.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, " "); SufficientFundBalances bal = (SufficientFundBalances)businessObjectService.findByPrimaryKey(SufficientFundBalances.class, keys); if (bal != null) { amounts.budget = bal.getCurrentBudgetBalanceAmount(); amounts.encumbrance = bal.getAccountEncumbranceAmount(); } if ( LOG.isDebugEnabled() ) { LOG.debug("getPriorYearSufficientFundsBalanceAmount() budget " + amounts.budget); LOG.debug("getPriorYearSufficientFundsBalanceAmount() encumbrance " + amounts.encumbrance); } return amounts; } /** * Totals the amounts of actual, encumbrance, and budget amounts from related pending entries * * @param item a sufficient funds item to find pending amounts for * @return the totals encapsulated in a PendingAmounts object */ @SuppressWarnings("unchecked") protected PendingAmounts getPendingBalanceAmount(SufficientFundsItem item) { LOG.debug("getPendingBalanceAmount() started"); Integer fiscalYear = item.getYear().getUniversityFiscalYear(); String chart = item.getAccount().getChartOfAccountsCode(); String account = item.getAccount().getAccountNumber(); String sfCode = item.getAccount().getAccountSufficientFundsCode(); PendingAmounts amounts = new PendingAmounts(); if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(sfCode)) { // Cash checking List years = new ArrayList(); years.add(item.getYear().getUniversityFiscalYear()); // If the beginning balance isn't loaded, we need to include cash from // the previous fiscal year if (!item.getYear().isFinancialBeginBalanceLoadInd()) { years.add(item.getYear().getUniversityFiscalYear() - 1); } // Calculate the pending actual amount // Get Cash (debit amount - credit amount) amounts.actual = generalLedgerPendingEntryService.getCashSummary(years, chart, account, true); amounts.actual = amounts.actual.subtract(generalLedgerPendingEntryService.getCashSummary(years, chart, account, false)); // Get Payables (credit amount - debit amount) amounts.actual = amounts.actual.add(generalLedgerPendingEntryService.getActualSummary(years, chart, account, true)); amounts.actual = amounts.actual.subtract(generalLedgerPendingEntryService.getActualSummary(years, chart, account, false)); } else { // Non-Cash checking // Get expenditure (debit - credit) amounts.actual = generalLedgerPendingEntryService.getExpenseSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), true, item.getDocumentTypeCode().startsWith("YE")); amounts.actual = amounts.actual.subtract(generalLedgerPendingEntryService.getExpenseSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), false, item.getDocumentTypeCode().startsWith("YE"))); // Get budget amounts.budget = generalLedgerPendingEntryService.getBudgetSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), item.getDocumentTypeCode().startsWith("YE")); // Get encumbrance (debit - credit) amounts.encumbrance = generalLedgerPendingEntryService.getEncumbranceSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), true, item.getDocumentTypeCode().startsWith("YE")); amounts.encumbrance = amounts.encumbrance.subtract(generalLedgerPendingEntryService.getEncumbranceSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), false, item.getDocumentTypeCode().startsWith("YE"))); } if ( LOG.isDebugEnabled() ) { LOG.debug("getPendingBalanceAmount() actual " + amounts.actual); LOG.debug("getPendingBalanceAmount() budget " + amounts.budget); LOG.debug("getPendingBalanceAmount() encumbrance " + amounts.encumbrance); } return amounts; } /** * Purge the sufficient funds balance table by year/chart * * @param chart the chart of sufficient fund balances to purge * @param year the fiscal year of sufficient fund balances to purge */ public void purgeYearByChart(String chart, int year) { sufficientFundsDao.purgeYearByChart(chart, year); } public void setAccountService(AccountService accountService) { this.accountService = accountService; } public void setGeneralLedgerPendingEntryService(GeneralLedgerPendingEntryService generalLedgerPendingEntryService) { this.generalLedgerPendingEntryService = generalLedgerPendingEntryService; } public void setConfigurationService(ConfigurationService kualiConfigurationService) { this.kualiConfigurationService = kualiConfigurationService; } public void setObjectLevelService(ObjectLevelService objectLevelService) { this.objectLevelService = objectLevelService; } public void setOptionsService(OptionsService optionsService) { this.optionsService = optionsService; } public void setSufficientFundBalancesDao(SufficientFundBalancesDao sufficientFundBalancesDao) { this.sufficientFundBalancesDao = sufficientFundBalancesDao; } public void setSufficientFundsDao(SufficientFundsDao sufficientFundsDao) { this.sufficientFundsDao = sufficientFundsDao; } public void setBusinessObjectService(BusinessObjectService businessObjectService) { this.businessObjectService = businessObjectService; } }