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    }