001 /* 002 * Copyright 2011 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.kfs.gl.service.impl; 017 018 import java.util.ArrayList; 019 import java.util.Collection; 020 import java.util.HashMap; 021 import java.util.Iterator; 022 import java.util.List; 023 import java.util.Map; 024 025 import org.kuali.kfs.coa.businessobject.ObjectCode; 026 import org.kuali.kfs.coa.service.AccountService; 027 import org.kuali.kfs.coa.service.ObjectLevelService; 028 import org.kuali.kfs.coa.service.ObjectTypeService; 029 import org.kuali.kfs.fp.document.YearEndDocument; 030 import org.kuali.kfs.gl.batch.dataaccess.SufficientFundsDao; 031 import org.kuali.kfs.gl.businessobject.SufficientFundBalances; 032 import org.kuali.kfs.gl.businessobject.SufficientFundRebuild; 033 import org.kuali.kfs.gl.businessobject.Transaction; 034 import org.kuali.kfs.gl.dataaccess.SufficientFundBalancesDao; 035 import org.kuali.kfs.gl.service.SufficientFundsService; 036 import org.kuali.kfs.gl.service.SufficientFundsServiceConstants; 037 import org.kuali.kfs.sys.KFSConstants; 038 import org.kuali.kfs.sys.KFSPropertyConstants; 039 import org.kuali.kfs.sys.businessobject.SufficientFundsItem; 040 import org.kuali.kfs.sys.businessobject.SystemOptions; 041 import org.kuali.kfs.sys.context.SpringContext; 042 import org.kuali.kfs.sys.document.GeneralLedgerPostingDocument; 043 import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService; 044 import org.kuali.kfs.sys.service.OptionsService; 045 import org.kuali.rice.kns.service.BusinessObjectService; 046 import org.kuali.rice.kns.service.KualiConfigurationService; 047 import org.kuali.rice.kns.util.KualiDecimal; 048 import org.kuali.rice.kns.util.ObjectUtils; 049 import org.springframework.transaction.annotation.Transactional; 050 051 /** 052 * The base implementation of SufficientFundsService 053 */ 054 @Transactional 055 public class SufficientFundsServiceImpl implements SufficientFundsService, SufficientFundsServiceConstants { 056 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SufficientFundsServiceImpl.class); 057 058 private AccountService accountService; 059 private ObjectLevelService objectLevelService; 060 private KualiConfigurationService kualiConfigurationService; 061 private SufficientFundsDao sufficientFundsDao; 062 private SufficientFundBalancesDao sufficientFundBalancesDao; 063 private OptionsService optionsService; 064 private GeneralLedgerPendingEntryService generalLedgerPendingEntryService; 065 private BusinessObjectService businessObjectService; 066 067 /** 068 * Default constructor 069 */ 070 public SufficientFundsServiceImpl() { 071 super(); 072 } 073 074 /** 075 * This operation derives the acct_sf_finobj_cd which is used to populate the General Ledger Pending entry table, so that later 076 * we can do Suff Fund checking against that entry 077 * 078 * @param financialObject the object code being checked against 079 * @param accountSufficientFundsCode the kind of sufficient funds checking turned on in this system 080 * @return the object code that should be used for the sufficient funds inquiry, or a blank String 081 * @see org.kuali.kfs.gl.service.SufficientFundsService#getSufficientFundsObjectCode(org.kuali.kfs.coa.businessobject.ObjectCode, 082 * java.lang.String) 083 */ 084 public String getSufficientFundsObjectCode(ObjectCode financialObject, String accountSufficientFundsCode) { 085 086 if (KFSConstants.SF_TYPE_NO_CHECKING.equals(accountSufficientFundsCode)) { 087 return KFSConstants.NOT_AVAILABLE_STRING; 088 } 089 else if (KFSConstants.SF_TYPE_ACCOUNT.equals(accountSufficientFundsCode)) { 090 return " "; 091 } 092 else if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(accountSufficientFundsCode)) { 093 return " "; 094 } 095 else if (KFSConstants.SF_TYPE_OBJECT.equals(accountSufficientFundsCode)) { 096 return financialObject.getFinancialObjectCode(); 097 } 098 else if (KFSConstants.SF_TYPE_LEVEL.equals(accountSufficientFundsCode)) { 099 return financialObject.getFinancialObjectLevelCode(); 100 } 101 else if (KFSConstants.SF_TYPE_CONSOLIDATION.equals(accountSufficientFundsCode)) { 102 financialObject.refreshReferenceObject("financialObjectLevel"); 103 return financialObject.getFinancialObjectLevel().getFinancialConsolidationObjectCode(); 104 } 105 else { 106 throw new IllegalArgumentException("Invalid Sufficient Funds Code: " + accountSufficientFundsCode); 107 } 108 } 109 110 /** 111 * Checks for sufficient funds on a single document 112 * 113 * @param document document to check 114 * @return Empty List if has sufficient funds for all accounts, List of SufficientFundsItem if not 115 * @see org.kuali.kfs.gl.service.SufficientFundsService#checkSufficientFunds(org.kuali.rice.kns.document.FinancialDocument) 116 */ 117 public List<SufficientFundsItem> checkSufficientFunds(GeneralLedgerPostingDocument document) { 118 LOG.debug("checkSufficientFunds() started"); 119 120 return checkSufficientFunds((List<? extends Transaction>) document.getPendingLedgerEntriesForSufficientFundsChecking()); 121 } 122 123 /** 124 * checks to see if a document is a <code>YearEndDocument</code> 125 * 126 * @param documentClass the class of a Document to check 127 * @return true if the class implements <code>YearEndDocument</code> 128 */ 129 @SuppressWarnings("unchecked") 130 protected boolean isYearEndDocument(Class documentClass) { 131 return YearEndDocument.class.isAssignableFrom(documentClass); 132 } 133 134 /** 135 * Checks for sufficient funds on a list of transactions 136 * 137 * @param transactions list of transactions 138 * @return Empty List if has sufficient funds for all accounts, List of SufficientFundsItem if not 139 * @see org.kuali.kfs.gl.service.SufficientFundsService#checkSufficientFunds(java.util.List) 140 */ 141 @SuppressWarnings("unchecked") 142 public List<SufficientFundsItem> checkSufficientFunds(List<? extends Transaction> transactions) { 143 LOG.debug("checkSufficientFunds() started"); 144 145 for (Transaction e : transactions) { 146 e.refreshNonUpdateableReferences(); 147 } 148 149 List<SufficientFundsItem> summaryItems = summarizeTransactions(transactions); 150 for (Iterator iter = summaryItems.iterator(); iter.hasNext();) { 151 SufficientFundsItem item = (SufficientFundsItem) iter.next(); 152 if ( LOG.isDebugEnabled() ) { 153 LOG.debug("checkSufficientFunds() " + item.toString()); 154 } 155 if (hasSufficientFundsOnItem(item)) { 156 iter.remove(); 157 } 158 } 159 160 return summaryItems; 161 } 162 163 /** 164 * For each transaction, fetches the appropriate sufficient funds item to check against 165 * 166 * @param transactions a list of Transactions 167 * @return a List of corresponding SufficientFundsItem 168 */ 169 @SuppressWarnings("unchecked") 170 protected List<SufficientFundsItem> summarizeTransactions(List<? extends Transaction> transactions) { 171 Map<String, SufficientFundsItem> items = new HashMap<String, SufficientFundsItem>(); 172 173 SystemOptions currentYear = optionsService.getCurrentYearOptions(); 174 175 // loop over the given transactions, grouping into SufficientFundsItem objects 176 // which are keyed by the appropriate chart/account/SF type, and derived object value 177 // see getSufficientFundsObjectCode() for the "object" used for grouping 178 for (Iterator iter = transactions.iterator(); iter.hasNext();) { 179 Transaction tran = (Transaction) iter.next(); 180 181 SystemOptions year = tran.getOption(); 182 if (year == null) { 183 year = currentYear; 184 } 185 if (ObjectUtils.isNull(tran.getAccount())) { 186 throw new IllegalArgumentException("Invalid account: " + tran.getChartOfAccountsCode() + "-" + tran.getAccountNumber()); 187 } 188 SufficientFundsItem sfi = new SufficientFundsItem(year, tran, getSufficientFundsObjectCode(tran.getFinancialObject(), tran.getAccount().getAccountSufficientFundsCode())); 189 sfi.setDocumentTypeCode(tran.getFinancialDocumentTypeCode()); 190 191 if (items.containsKey(sfi.getKey())) { 192 SufficientFundsItem item = (SufficientFundsItem) items.get(sfi.getKey()); 193 item.add(tran); 194 } 195 else { 196 items.put(sfi.getKey(), sfi); 197 } 198 } 199 200 return new ArrayList<SufficientFundsItem>(items.values()); 201 } 202 203 /** 204 * Given a sufficient funds item record, determines if there are sufficient funds available for the transaction 205 * 206 * @param item the item to check 207 * @return true if there are sufficient funds available, false otherwise 208 */ 209 protected boolean hasSufficientFundsOnItem(SufficientFundsItem item) { 210 211 if (item.getAmount().equals(KualiDecimal.ZERO)) { 212 LOG.debug("hasSufficientFundsOnItem() Transactions with zero amounts shold pass"); 213 return true; 214 } 215 216 if (!item.getYear().isBudgetCheckingOptionsCode()) { 217 LOG.debug("hasSufficientFundsOnItem() No sufficient funds checking"); 218 return true; 219 } 220 221 if (!item.getAccount().isPendingAcctSufficientFundsIndicator()) { 222 if ( LOG.isDebugEnabled() ) { 223 LOG.debug("hasSufficientFundsOnItem() No checking on eDocs for account " + item.getAccount().getChartOfAccountsCode() + "-" + item.getAccount().getAccountNumber()); 224 } 225 return true; 226 } 227 228 // exit sufficient funds checking if not enabled for an account 229 if (KFSConstants.SF_TYPE_NO_CHECKING.equals(item.getAccountSufficientFundsCode())) { 230 if ( LOG.isDebugEnabled() ) { 231 LOG.debug("hasSufficientFundsOnItem() sufficient funds not enabled for account " + item.getAccount().getChartOfAccountsCode() + "-" + item.getAccount().getAccountNumber()); 232 } 233 return true; 234 } 235 236 ObjectTypeService objectTypeService = (ObjectTypeService) SpringContext.getBean(ObjectTypeService.class); 237 List<String> expenseObjectTypes = objectTypeService.getCurrentYearExpenseObjectTypes(); 238 239 if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode()) 240 && !item.getFinancialObject().getChartOfAccounts().getFinancialCashObjectCode().equals(item.getFinancialObject().getFinancialObjectCode())) { 241 LOG.debug("hasSufficientFundsOnItem() SF checking is cash and transaction is not cash"); 242 return true; 243 } 244 245 else if (!KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode()) 246 && !expenseObjectTypes.contains(item.getFinancialObjectType().getCode())) { 247 LOG.debug("hasSufficientFundsOnItem() SF checking is budget and transaction is not expense"); 248 return true; 249 } 250 251 SufficientFundBalances sfBalance = sufficientFundBalancesDao.getByPrimaryId(item.getYear().getUniversityFiscalYear(), item.getAccount().getChartOfAccountsCode(), item.getAccount().getAccountNumber(), item.getSufficientFundsObjectCode()); 252 253 if (sfBalance == null) { 254 Map criteria = new HashMap(); 255 criteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, item.getAccount().getChartOfAccountsCode()); 256 criteria.put(KFSPropertyConstants.ACCOUNT_NUMBER_FINANCIAL_OBJECT_CODE, item.getAccount().getAccountNumber()); 257 258 Collection sufficientFundRebuilds = businessObjectService.findMatching(SufficientFundRebuild.class, criteria); 259 if (sufficientFundRebuilds != null && sufficientFundRebuilds.size() > 0) { 260 LOG.debug("hasSufficientFundsOnItem() No balance record and waiting on rebuild, no sufficient funds"); 261 return false; 262 } 263 else { 264 sfBalance = new SufficientFundBalances(); 265 sfBalance.setAccountActualExpenditureAmt(KualiDecimal.ZERO); 266 sfBalance.setAccountEncumbranceAmount(KualiDecimal.ZERO); 267 sfBalance.setCurrentBudgetBalanceAmount(KualiDecimal.ZERO); 268 } 269 } 270 271 KualiDecimal balanceAmount = item.getAmount(); 272 if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode()) 273 || item.getYear().getBudgetCheckingBalanceTypeCd().equals(item.getBalanceTyp().getCode())) { 274 // We need to change the sign on the amount because the amount in the item is an increase in cash. We only care 275 // about decreases in cash. 276 277 // Also, negating if this is a balance type code of budget checking and the transaction is a budget transaction. 278 279 balanceAmount = balanceAmount.negated(); 280 } 281 282 if (balanceAmount.isNegative()) { 283 LOG.debug("hasSufficientFundsOnItem() balanceAmount is negative, allow transaction to proceed"); 284 return true; 285 } 286 287 PendingAmounts priorYearPending = new PendingAmounts(); 288 // if we're checking the CASH_AT_ACCOUNT type, then we need to consider the prior year pending transactions 289 // if the balance forwards have not been run 290 if ((KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode())) 291 && (!item.getYear().isFinancialBeginBalanceLoadInd())) { 292 priorYearPending = getPriorYearSufficientFundsBalanceAmount(item); 293 } 294 295 PendingAmounts pending = getPendingBalanceAmount(item); 296 297 KualiDecimal availableBalance = null; 298 if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(item.getAccount().getAccountSufficientFundsCode())) { 299 // if the beginning balances have not loaded for the transaction FY, pull the remaining balance from last year 300 if (!item.getYear().isFinancialBeginBalanceLoadInd()) { 301 availableBalance = sfBalance.getCurrentBudgetBalanceAmount() 302 .add(priorYearPending.budget) // add the remaining budget from last year (assumed to carry to this year's) 303 .add(pending.actual) // any pending expenses (remember sense is negated) 304 .subtract(sfBalance.getAccountEncumbranceAmount()) // subtract the encumbrances (not reflected in cash yet) 305 .subtract(priorYearPending.encumbrance); 306 } else { // balance forwards have been run, don't need to consider prior year remaining budget 307 availableBalance = sfBalance.getCurrentBudgetBalanceAmount() 308 .add(pending.actual) 309 .subtract(sfBalance.getAccountEncumbranceAmount()); 310 } 311 } 312 else { 313 availableBalance = sfBalance.getCurrentBudgetBalanceAmount() // current budget balance 314 .add(pending.budget) // pending budget entries 315 .subtract(sfBalance.getAccountActualExpenditureAmt()) // minus all current and pending actuals and encumbrances 316 .subtract(pending.actual) 317 .subtract(sfBalance.getAccountEncumbranceAmount()) 318 .subtract(pending.encumbrance); 319 } 320 321 if ( LOG.isDebugEnabled() ) { 322 LOG.debug("hasSufficientFundsOnItem() balanceAmount: " + balanceAmount + " availableBalance: " + availableBalance); 323 } 324 if (balanceAmount.compareTo(availableBalance) > 0) { 325 LOG.debug("hasSufficientFundsOnItem() no sufficient funds"); 326 return false; 327 } 328 329 LOG.debug("hasSufficientFundsOnItem() has sufficient funds"); 330 return true; 331 } 332 333 /** 334 * An inner class to hold summary totals of pending ledger entry amounts 335 */ 336 protected class PendingAmounts { 337 public KualiDecimal budget = KualiDecimal.ZERO; 338 public KualiDecimal actual = KualiDecimal.ZERO; 339 public KualiDecimal encumbrance = KualiDecimal.ZERO; 340 } 341 342 /** 343 * Given a sufficient funds item to check, gets the prior year sufficient funds balance to check against 344 * 345 * @param item the sufficient funds item to check against 346 * @return a PendingAmounts record with the pending budget and encumbrance 347 */ 348 protected PendingAmounts getPriorYearSufficientFundsBalanceAmount(SufficientFundsItem item) { 349 PendingAmounts amounts = new PendingAmounts(); 350 351 // This only gets called for sufficient funds type of Cash at Account (H). The object code in the table for this type is 352 // always 353 // 4 spaces. 354 SufficientFundBalances bal = sufficientFundBalancesDao.getByPrimaryId(Integer.valueOf(item.getYear().getUniversityFiscalYear().intValue() - 1), item.getAccount().getChartOfAccountsCode(), item.getAccount().getAccountNumber(), " "); 355 356 if (bal != null) { 357 amounts.budget = bal.getCurrentBudgetBalanceAmount(); 358 amounts.encumbrance = bal.getAccountEncumbranceAmount(); 359 } 360 361 if ( LOG.isDebugEnabled() ) { 362 LOG.debug("getPriorYearSufficientFundsBalanceAmount() budget " + amounts.budget); 363 LOG.debug("getPriorYearSufficientFundsBalanceAmount() encumbrance " + amounts.encumbrance); 364 } 365 return amounts; 366 } 367 368 /** 369 * Totals the amounts of actual, encumbrance, and budget amounts from related pending entries 370 * 371 * @param item a sufficient funds item to find pending amounts for 372 * @return the totals encapsulated in a PendingAmounts object 373 */ 374 @SuppressWarnings("unchecked") 375 protected PendingAmounts getPendingBalanceAmount(SufficientFundsItem item) { 376 LOG.debug("getPendingBalanceAmount() started"); 377 378 Integer fiscalYear = item.getYear().getUniversityFiscalYear(); 379 String chart = item.getAccount().getChartOfAccountsCode(); 380 String account = item.getAccount().getAccountNumber(); 381 String sfCode = item.getAccount().getAccountSufficientFundsCode(); 382 383 PendingAmounts amounts = new PendingAmounts(); 384 385 if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(sfCode)) { 386 // Cash checking 387 List years = new ArrayList(); 388 years.add(item.getYear().getUniversityFiscalYear()); 389 390 // If the beginning balance isn't loaded, we need to include cash from 391 // the previous fiscal year 392 if (!item.getYear().isFinancialBeginBalanceLoadInd()) { 393 years.add(item.getYear().getUniversityFiscalYear() - 1); 394 } 395 396 // Calculate the pending actual amount 397 // Get Cash (debit amount - credit amount) 398 amounts.actual = generalLedgerPendingEntryService.getCashSummary(years, chart, account, true); 399 amounts.actual = amounts.actual.subtract(generalLedgerPendingEntryService.getCashSummary(years, chart, account, false)); 400 401 // Get Payables (credit amount - debit amount) 402 amounts.actual = amounts.actual.add(generalLedgerPendingEntryService.getActualSummary(years, chart, account, true)); 403 amounts.actual = amounts.actual.subtract(generalLedgerPendingEntryService.getActualSummary(years, chart, account, false)); 404 } 405 else { 406 // Non-Cash checking 407 408 // Get expenditure (debit - credit) 409 amounts.actual = generalLedgerPendingEntryService.getExpenseSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), true, item.getDocumentTypeCode().startsWith("YE")); 410 amounts.actual = amounts.actual.subtract(generalLedgerPendingEntryService.getExpenseSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), false, item.getDocumentTypeCode().startsWith("YE"))); 411 412 // Get budget 413 amounts.budget = generalLedgerPendingEntryService.getBudgetSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), item.getDocumentTypeCode().startsWith("YE")); 414 415 // Get encumbrance (debit - credit) 416 amounts.encumbrance = generalLedgerPendingEntryService.getEncumbranceSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), true, item.getDocumentTypeCode().startsWith("YE")); 417 amounts.encumbrance = amounts.encumbrance.subtract(generalLedgerPendingEntryService.getEncumbranceSummary(fiscalYear, chart, account, item.getSufficientFundsObjectCode(), false, item.getDocumentTypeCode().startsWith("YE"))); 418 } 419 420 if ( LOG.isDebugEnabled() ) { 421 LOG.debug("getPendingBalanceAmount() actual " + amounts.actual); 422 LOG.debug("getPendingBalanceAmount() budget " + amounts.budget); 423 LOG.debug("getPendingBalanceAmount() encumbrance " + amounts.encumbrance); 424 } 425 return amounts; 426 } 427 428 /** 429 * Purge the sufficient funds balance table by year/chart 430 * 431 * @param chart the chart of sufficient fund balances to purge 432 * @param year the fiscal year of sufficient fund balances to purge 433 */ 434 public void purgeYearByChart(String chart, int year) { 435 sufficientFundsDao.purgeYearByChart(chart, year); 436 } 437 438 public void setAccountService(AccountService accountService) { 439 this.accountService = accountService; 440 } 441 442 public void setGeneralLedgerPendingEntryService(GeneralLedgerPendingEntryService generalLedgerPendingEntryService) { 443 this.generalLedgerPendingEntryService = generalLedgerPendingEntryService; 444 } 445 446 public void setKualiConfigurationService(KualiConfigurationService kualiConfigurationService) { 447 this.kualiConfigurationService = kualiConfigurationService; 448 } 449 450 public void setObjectLevelService(ObjectLevelService objectLevelService) { 451 this.objectLevelService = objectLevelService; 452 } 453 454 public void setOptionsService(OptionsService optionsService) { 455 this.optionsService = optionsService; 456 } 457 458 public void setSufficientFundBalancesDao(SufficientFundBalancesDao sufficientFundBalancesDao) { 459 this.sufficientFundBalancesDao = sufficientFundBalancesDao; 460 } 461 462 public void setSufficientFundsDao(SufficientFundsDao sufficientFundsDao) { 463 this.sufficientFundsDao = sufficientFundsDao; 464 } 465 466 public void setBusinessObjectService(BusinessObjectService businessObjectService) { 467 this.businessObjectService = businessObjectService; 468 } 469 }