/* * 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.batch.service.impl; import java.sql.Date; 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.Account; import org.kuali.kfs.coa.service.AccountService; import org.kuali.kfs.gl.GeneralLedgerConstants; import org.kuali.kfs.gl.batch.SufficientFundsAccountUpdateStep; import org.kuali.kfs.gl.batch.service.SufficientFundsAccountUpdateService; import org.kuali.kfs.gl.businessobject.Balance; import org.kuali.kfs.gl.businessobject.SufficientFundBalances; import org.kuali.kfs.gl.businessobject.SufficientFundRebuild; import org.kuali.kfs.gl.dataaccess.BalanceDao; import org.kuali.kfs.gl.dataaccess.SufficientFundBalancesDao; import org.kuali.kfs.gl.dataaccess.SufficientFundRebuildDao; import org.kuali.kfs.gl.service.SufficientFundsService; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSKeyConstants; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.kfs.sys.Message; import org.kuali.kfs.sys.businessobject.SystemOptions; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.service.ReportWriterService; 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.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.krad.service.BusinessObjectService; import org.springframework.transaction.annotation.Transactional; /** * The default implementation of SufficientFundsAccountUpdateService */ @Transactional public class SufficientFundsAccountUpdateServiceImpl implements SufficientFundsAccountUpdateService { private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SufficientFundsAccountUpdateServiceImpl.class); private DateTimeService dateTimeService; private ConfigurationService kualiConfigurationService; private BalanceDao balanceDao; private SufficientFundBalancesDao sufficientFundBalancesDao; private SufficientFundRebuildDao sufficientFundRebuildDao; private SufficientFundsService sufficientFundsService; private AccountService accountService; private ReportWriterService reportWriterService; private BusinessObjectService boService; private Date runDate; private SystemOptions options; Map batchError; List reportSummary; List<Message> transactionErrors; private Integer universityFiscalYear; private int sfrbRecordsConvertedCount = 0; private int sfrbRecordsReadCount = 0; private int sfrbRecordsDeletedCount = 0; private int sfrbNotDeletedCount = 0; private int sfblDeletedCount = 0; private int sfblInsertedCount = 0; private int sfblUpdatedCount = 0; private int warningCount = 0; private SufficientFundBalances currentSfbl = null; /** * Constructs a SufficientFundsAccountUpdateServiceImpl instance */ public SufficientFundsAccountUpdateServiceImpl() { super(); } /** * Returns the fiscal year, set in a parameter, of sufficient funds to rebuild * * @return the fiscal year */ protected Integer getFiscalYear() { String val = SpringContext.getBean(ParameterService.class).getParameterValueAsString(SufficientFundsAccountUpdateStep.class, GeneralLedgerConstants.FISCAL_YEAR_PARM); return Integer.parseInt(val); } /** * Rebuilds all necessary sufficient funds balances. * @see org.kuali.kfs.gl.batch.service.SufficientFundsAccountUpdateService#rebuildSufficientFunds() */ public void rebuildSufficientFunds() { // driver List <SufficientFundRebuild> rebuildSfrbList = new ArrayList<SufficientFundRebuild>(); LOG.debug("rebuildSufficientFunds() started"); universityFiscalYear = getFiscalYear(); initService(); //need to add time info - batch util? runDate = dateTimeService.getCurrentSqlDate(); // Get all the O types and convert them to A types if (LOG.isDebugEnabled()) { LOG.debug("rebuildSufficientFunds() Converting O types to A types"); } Map criteria = new HashMap(); criteria.put(KFSPropertyConstants.ACCOUNT_FINANCIAL_OBJECT_TYPE_CODE, KFSConstants.SF_TYPE_OBJECT); for (Iterator iter = boService.findMatching(SufficientFundRebuild.class, criteria).iterator(); iter.hasNext();) { SufficientFundRebuild sfrb = (SufficientFundRebuild) iter.next(); ++sfrbRecordsReadCount; transactionErrors = new ArrayList<Message>(); convertOtypeToAtypes(sfrb); if (transactionErrors.size() > 0) { reportWriterService.writeError(sfrb, transactionErrors); rebuildSfrbList.add(sfrb); } } criteria.clear(); // Get all the A types and process them LOG.debug("rebuildSufficientFunds() Calculating SF balances for all A types"); criteria.put(KFSPropertyConstants.ACCOUNT_FINANCIAL_OBJECT_TYPE_CODE, KFSConstants.SF_TYPE_ACCOUNT); for (Iterator iter = boService.findMatching(SufficientFundRebuild.class, criteria).iterator(); iter.hasNext();) { SufficientFundRebuild sfrb = (SufficientFundRebuild) iter.next(); ++sfrbRecordsReadCount; transactionErrors = new ArrayList<Message>(); calculateSufficientFundsByAccount(sfrb); if (transactionErrors.size() > 0) { reportWriterService.writeError(sfrb, transactionErrors); rebuildSfrbList.add(sfrb); } } sufficientFundRebuildDao.purgeSufficientFundRebuild(); boService.save( rebuildSfrbList); // Look at all the left over rows. There shouldn't be any left if all are O's and A's without error. // Write out error messages for any that aren't A or O LOG.debug("rebuildSufficientFunds() Handle any non-A and non-O types"); for (Iterator iter = boService.findAll(SufficientFundRebuild.class).iterator(); iter.hasNext();) { SufficientFundRebuild sfrb = (SufficientFundRebuild) iter.next(); if ((!KFSConstants.SF_TYPE_ACCOUNT.equals(sfrb.getAccountFinancialObjectTypeCode())) && (!KFSConstants.SF_TYPE_OBJECT.equals(sfrb.getAccountFinancialObjectTypeCode()))) { ++sfrbRecordsReadCount; transactionErrors = new ArrayList<Message>(); addTransactionError(kualiConfigurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_INVALID_SF_OBJECT_TYPE_CODE)); ++warningCount; ++sfrbNotDeletedCount; reportWriterService.writeError(sfrb, transactionErrors); } } // write out report and errors if (LOG.isDebugEnabled()) { LOG.debug("rebuildSufficientFunds() Create report"); } // write out statistics reportWriterService.writeStatisticLine(" SFRB RECORDS CONVERTED FROM OBJECT TO ACCOUNT %,9d\n", sfrbRecordsConvertedCount); reportWriterService.writeStatisticLine(" POST CONVERSION SFRB RECORDS READ %,9d\n", sfrbRecordsReadCount); reportWriterService.writeStatisticLine(" SFRB RECORDS DELETED %,9d\n", sfrbRecordsDeletedCount); reportWriterService.writeStatisticLine(" SFRB RECORDS KEPT DUE TO ERRORS %,9d\n", sfrbNotDeletedCount); reportWriterService.writeStatisticLine(" SFBL RECORDS DELETED %,9d\n", sfblDeletedCount); reportWriterService.writeStatisticLine(" SFBL RECORDS ADDED %,9d\n", sfblInsertedCount); reportWriterService.writeStatisticLine(" SFBL RECORDS UDPATED %,9d\n", sfblUpdatedCount); } /** * Initializes the process at the beginning of a run. */ protected void initService() { batchError = new HashMap(); reportSummary = new ArrayList(); runDate = new Date(dateTimeService.getCurrentDate().getTime()); options = (SystemOptions)boService.findBySinglePrimaryKey(SystemOptions.class, universityFiscalYear); if (options == null) { throw new IllegalStateException(kualiConfigurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_UNIV_DATE_NOT_FOUND)); } } /** * Given an O SF rebuild type, it will look up all of the matching balances in the table and add each account it finds as an A * SF rebuild type. * * @param sfrb the sufficient fund rebuild record to convert */ public void convertOtypeToAtypes(SufficientFundRebuild sfrb) { ++sfrbRecordsConvertedCount; Collection fundBalances = sufficientFundBalancesDao.getByObjectCode(universityFiscalYear, sfrb.getChartOfAccountsCode(), sfrb.getAccountNumberFinancialObjectCode()); Map criteria = new HashMap(); for (Iterator fundBalancesIter = fundBalances.iterator(); fundBalancesIter.hasNext();) { SufficientFundBalances sfbl = (SufficientFundBalances) fundBalancesIter.next(); criteria.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, sfbl.getChartOfAccountsCode()); criteria.put(KFSPropertyConstants.ACCOUNT_FINANCIAL_OBJECT_TYPE_CODE, KFSConstants.SF_TYPE_ACCOUNT); criteria.put(KFSPropertyConstants.ACCOUNT_NUMBER_FINANCIAL_OBJECT_CODE, sfbl.getAccountNumber()); SufficientFundRebuild altSfrb = (SufficientFundRebuild)boService.findByPrimaryKey(SufficientFundRebuild.class, criteria); if (altSfrb == null) { altSfrb = new SufficientFundRebuild(); altSfrb.setAccountFinancialObjectTypeCode(KFSConstants.SF_TYPE_ACCOUNT); altSfrb.setAccountNumberFinancialObjectCode(sfbl.getAccountNumber()); altSfrb.setChartOfAccountsCode(sfbl.getChartOfAccountsCode()); boService.save(altSfrb); } criteria.clear(); } } /** * Updates sufficient funds balances for the given account * * @param sfrb the sufficient fund rebuild record, with a chart and account number */ public void calculateSufficientFundsByAccount(SufficientFundRebuild sfrb) { Account sfrbAccount = accountService.getByPrimaryId(sfrb.getChartOfAccountsCode(), sfrb.getAccountNumberFinancialObjectCode()); if (sfrbAccount == null) { String msg = "Account found in SufficientFundsRebuild table that is not in Accounts table [" + sfrb.getChartOfAccountsCode() + "-" + sfrb.getAccountNumberFinancialObjectCode() + "]."; LOG.error(msg); throw new RuntimeException(msg); } if ((sfrbAccount.getAccountSufficientFundsCode() != null) && (KFSConstants.SF_TYPE_ACCOUNT.equals(sfrbAccount.getAccountSufficientFundsCode()) || KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(sfrbAccount.getAccountSufficientFundsCode()) || KFSConstants.SF_TYPE_CONSOLIDATION.equals(sfrbAccount.getAccountSufficientFundsCode()) || KFSConstants.SF_TYPE_LEVEL.equals(sfrbAccount.getAccountSufficientFundsCode()) || KFSConstants.SF_TYPE_OBJECT.equals(sfrbAccount.getAccountSufficientFundsCode()) || KFSConstants.SF_TYPE_NO_CHECKING.equals(sfrbAccount.getAccountSufficientFundsCode()))) { ++sfrbRecordsDeletedCount; sfblDeletedCount += sufficientFundBalancesDao.deleteByAccountNumber(universityFiscalYear, sfrb.getChartOfAccountsCode(), sfrbAccount.getAccountNumber()); if (KFSConstants.SF_TYPE_NO_CHECKING.equalsIgnoreCase(sfrbAccount.getAccountSufficientFundsCode())) { // nothing to do here, no errors either, just return return; } Iterator balancesIterator = balanceDao.findAccountBalances(universityFiscalYear, sfrb.getChartOfAccountsCode(), sfrb.getAccountNumberFinancialObjectCode(), sfrbAccount.getAccountSufficientFundsCode()); if (balancesIterator == null) { addTransactionError(kualiConfigurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_BALANCE_NOT_FOUND_FOR) + universityFiscalYear + ")"); ++warningCount; ++sfrbNotDeletedCount; return; } String currentFinObjectCd = ""; while (balancesIterator.hasNext()) { Balance balance = (Balance) balancesIterator.next(); String tempFinObjectCd = sufficientFundsService.getSufficientFundsObjectCode(balance.getFinancialObject(), sfrbAccount.getAccountSufficientFundsCode()); if (!tempFinObjectCd.equals(currentFinObjectCd)) { // we have a change or are on the last record, write out the data if there is any currentFinObjectCd = tempFinObjectCd; if (currentSfbl != null && amountsAreNonZero(currentSfbl)) { boService.save(currentSfbl); ++sfblInsertedCount; } currentSfbl = new SufficientFundBalances(); currentSfbl.setUniversityFiscalYear(universityFiscalYear); currentSfbl.setChartOfAccountsCode(sfrb.getChartOfAccountsCode()); currentSfbl.setAccountNumber(sfrbAccount.getAccountNumber()); currentSfbl.setFinancialObjectCode(currentFinObjectCd); currentSfbl.setAccountSufficientFundsCode(sfrbAccount.getAccountSufficientFundsCode()); currentSfbl.setAccountActualExpenditureAmt(KualiDecimal.ZERO); currentSfbl.setAccountEncumbranceAmount(KualiDecimal.ZERO); currentSfbl.setCurrentBudgetBalanceAmount(KualiDecimal.ZERO); } if (sfrbAccount.isForContractsAndGrants()) { balance.setAccountLineAnnualBalanceAmount(balance.getAccountLineAnnualBalanceAmount().add(balance.getContractsGrantsBeginningBalanceAmount())); } if (KFSConstants.SF_TYPE_CASH_AT_ACCOUNT.equals(sfrbAccount.getAccountSufficientFundsCode())) { processCash(sfrbAccount, balance); } else { processObjectOrAccount(sfrbAccount, balance); } } // save the last one if (currentSfbl != null && amountsAreNonZero(currentSfbl)) { boService.save(currentSfbl); ++sfblInsertedCount; } } else { addTransactionError(kualiConfigurationService.getPropertyValueAsString(KFSKeyConstants.ERROR_INVALID_ACCOUNT_SF_CODE_FOR)); ++warningCount; ++sfrbNotDeletedCount; return; } } /** * Determines if all sums associated with a sufficient funds balance are zero * * @param sfbl the sufficient funds balance to check * @return true if all sums in the balance are zero, false otherwise */ protected boolean amountsAreNonZero(SufficientFundBalances sfbl) { boolean zero = true; zero &= KualiDecimal.ZERO.equals(sfbl.getAccountActualExpenditureAmt()); zero &= KualiDecimal.ZERO.equals(sfbl.getAccountEncumbranceAmount()); zero &= KualiDecimal.ZERO.equals(sfbl.getCurrentBudgetBalanceAmount()); return !zero; } /** * Determines how best to process the given balance * * @param sfrbAccount the account of the current sufficient funds balance rebuild record * @param balance the cash encumbrance balance to update the sufficient funds balance with */ protected void processObjectOrAccount(Account sfrbAccount, Balance balance) { if (options.getFinObjTypeExpenditureexpCd().equals(balance.getObjectTypeCode()) || options.getFinObjTypeExpendNotExpCode().equals(balance.getObjectTypeCode()) || options.getFinObjTypeExpNotExpendCode().equals(balance.getObjectTypeCode()) || options.getFinancialObjectTypeTransferExpenseCd().equals(balance.getObjectTypeCode())) { if (options.getActualFinancialBalanceTypeCd().equals(balance.getBalanceTypeCode())) { processObjtAcctActual(balance); } else if (options.getExtrnlEncumFinBalanceTypCd().equals(balance.getBalanceTypeCode()) || options.getIntrnlEncumFinBalanceTypCd().equals(balance.getBalanceTypeCode()) || options.getPreencumbranceFinBalTypeCd().equals(balance.getBalanceTypeCode()) || options.getCostShareEncumbranceBalanceTypeCd().equals(balance.getBalanceTypeCode())) { processObjtAcctEncmbrnc(balance); } else if (options.getBudgetCheckingBalanceTypeCd().equals(balance.getBalanceTypeCode())) { processObjtAcctBudget(balance); } } } /** * Updates the current sufficient fund balance record with a non-cash actual balance * * @param balance the cash encumbrance balance to update the sufficient funds balance with */ protected void processObjtAcctActual(Balance balance) { currentSfbl.setAccountActualExpenditureAmt(currentSfbl.getAccountActualExpenditureAmt().add(balance.getAccountLineAnnualBalanceAmount())); } /** * Updates the current sufficient fund balance record with a non-cash encumbrance balance * * @param balance the cash encumbrance balance to update the sufficient funds balance with */ protected void processObjtAcctEncmbrnc(Balance balance) { currentSfbl.setAccountEncumbranceAmount(currentSfbl.getAccountEncumbranceAmount().add(balance.getAccountLineAnnualBalanceAmount())); currentSfbl.setAccountEncumbranceAmount(currentSfbl.getAccountEncumbranceAmount().add(balance.getBeginningBalanceLineAmount())); } /** * Updates the current sufficient fund balance record with a non-cash budget balance * * @param balance the cash encumbrance balance to update the sufficient funds balance with */ protected void processObjtAcctBudget(Balance balance) { currentSfbl.setCurrentBudgetBalanceAmount(currentSfbl.getCurrentBudgetBalanceAmount().add(balance.getAccountLineAnnualBalanceAmount())); currentSfbl.setCurrentBudgetBalanceAmount(currentSfbl.getCurrentBudgetBalanceAmount().add(balance.getBeginningBalanceLineAmount())); } /** * Determines how best to process a cash balance * * @param sfrbAccount the account of the current sufficient funds balance record * @param balance the cash encumbrance balance to update the sufficient funds balance with */ protected void processCash(Account sfrbAccount, Balance balance) { if (balance.getBalanceTypeCode().equals(options.getActualFinancialBalanceTypeCd())) { if (balance.getObjectCode().equals(sfrbAccount.getChartOfAccounts().getFinancialCashObjectCode()) || balance.getObjectCode().equals(sfrbAccount.getChartOfAccounts().getFinAccountsPayableObjectCode())) { processCashActual(sfrbAccount, balance); } } else if (balance.getBalanceTypeCode().equals(options.getExtrnlEncumFinBalanceTypCd()) || balance.getBalanceTypeCode().equals(options.getIntrnlEncumFinBalanceTypCd()) || balance.getBalanceTypeCode().equals(options.getPreencumbranceFinBalTypeCd()) || options.getCostShareEncumbranceBalanceTypeCd().equals(balance.getBalanceTypeCode())) { if (balance.getObjectTypeCode().equals(options.getFinObjTypeExpenditureexpCd()) || balance.getObjectTypeCode().equals(options.getFinObjTypeExpendNotExpCode()) || options.getFinancialObjectTypeTransferExpenseCd().equals(balance.getObjectTypeCode()) || options.getFinObjTypeExpNotExpendCode().equals(balance.getObjectTypeCode())) { processCashEncumbrance(balance); } } } /** * Updates the current sufficient fund balance record with a cash actual balance * * @param sfrbAccount the account of the current sufficient funds balance record * @param balance the cash encumbrance balance to update the sufficient funds balance with */ protected void processCashActual(Account sfrbAccount, Balance balance) { if (balance.getObjectCode().equals(sfrbAccount.getChartOfAccounts().getFinancialCashObjectCode())) { currentSfbl.setCurrentBudgetBalanceAmount(currentSfbl.getCurrentBudgetBalanceAmount().add(balance.getAccountLineAnnualBalanceAmount())); currentSfbl.setCurrentBudgetBalanceAmount(currentSfbl.getCurrentBudgetBalanceAmount().add(balance.getBeginningBalanceLineAmount())); } if (balance.getObjectCode().equals(sfrbAccount.getChartOfAccounts().getFinAccountsPayableObjectCode())) { currentSfbl.setCurrentBudgetBalanceAmount(currentSfbl.getCurrentBudgetBalanceAmount().subtract(balance.getAccountLineAnnualBalanceAmount())); currentSfbl.setCurrentBudgetBalanceAmount(currentSfbl.getCurrentBudgetBalanceAmount().subtract(balance.getBeginningBalanceLineAmount())); } } /** * Updates the current sufficient funds balance with a cash encumbrance balance * * @param balance the cash encumbrance balance to update the sufficient funds balance with */ protected void processCashEncumbrance(Balance balance) { currentSfbl.setAccountEncumbranceAmount(currentSfbl.getAccountEncumbranceAmount().add(balance.getAccountLineAnnualBalanceAmount())); currentSfbl.setAccountEncumbranceAmount(currentSfbl.getAccountEncumbranceAmount().add(balance.getBeginningBalanceLineAmount())); } /** * Adds an error message to this instance's List of error messages * @param errorMessage the error message to keep */ protected void addTransactionError(String errorMessage) { transactionErrors.add(new Message(errorMessage, Message.TYPE_WARNING)); } public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } public void setConfigurationService(ConfigurationService kualiConfigurationService) { this.kualiConfigurationService = kualiConfigurationService; } public void setBalanceDao(BalanceDao balanceDao) { this.balanceDao = balanceDao; } public void setSufficientFundBalancesDao(SufficientFundBalancesDao sufficientFundBalancesDao) { this.sufficientFundBalancesDao = sufficientFundBalancesDao; } public void setReportWriterService(ReportWriterService sfrs) { reportWriterService = sfrs; } public void setAccountService(AccountService accountService) { this.accountService = accountService; } public void setSufficientFundsService(SufficientFundsService sfs) { sufficientFundsService = sfs; } public void setBusinessObjectService(BusinessObjectService bos) { boService = bos; } public void setSufficientFundRebuildDao(SufficientFundRebuildDao sufficientFundRebuildDao) { this.sufficientFundRebuildDao = sufficientFundRebuildDao; } }