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 }