/* * 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.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.kuali.kfs.coa.businessobject.A21SubAccount; import org.kuali.kfs.coa.businessobject.Account; import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryExclusionAccount; import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryExclusionType; import org.kuali.kfs.coa.businessobject.ObjectCode; import org.kuali.kfs.coa.dataaccess.IndirectCostRecoveryExclusionAccountDao; import org.kuali.kfs.coa.dataaccess.IndirectCostRecoveryExclusionTypeDao; import org.kuali.kfs.gl.GeneralLedgerConstants; import org.kuali.kfs.gl.batch.PosterIndirectCostRecoveryEntriesStep; import org.kuali.kfs.gl.batch.service.AccountingCycleCachingService; import org.kuali.kfs.gl.batch.service.IndirectCostRecoveryService; import org.kuali.kfs.gl.batch.service.PostTransaction; import org.kuali.kfs.gl.businessobject.ExpenditureTransaction; import org.kuali.kfs.gl.businessobject.Transaction; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.kfs.sys.Message; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.service.ReportWriterService; import org.kuali.kfs.sys.service.impl.KfsParameterConstants; import org.kuali.rice.core.api.parameter.ParameterEvaluatorService; import org.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.service.PersistenceStructureService; import org.kuali.rice.krad.util.ObjectUtils; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; /** * This implementation of PostTransaction creates ExpenditureTransactions, temporary records used * for ICR generation */ @Transactional public class PostExpenditureTransaction implements IndirectCostRecoveryService, PostTransaction { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PostExpenditureTransaction.class); private static final String INDIRECT_COST_FISCAL_PERIODS_PARAMETER = "INDIRECT_COST_FISCAL_PERIODS"; private static final String ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_PARAMETER_NAME = "ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_IND"; private IndirectCostRecoveryExclusionAccountDao indirectCostRecoveryExclusionAccountDao; private IndirectCostRecoveryExclusionTypeDao indirectCostRecoveryExclusionTypeDao; private AccountingCycleCachingService accountingCycleCachingService; private PersistenceStructureService persistenceStructureService; private ParameterService parameterService; public void setIndirectCostRecoveryExclusionAccountDao(IndirectCostRecoveryExclusionAccountDao icrea) { indirectCostRecoveryExclusionAccountDao = icrea; } public void setIndirectCostRecoveryExclusionTypeDao(IndirectCostRecoveryExclusionTypeDao icrea) { indirectCostRecoveryExclusionTypeDao = icrea; } /** * Creates a PostExpenditureTransaction instance */ public PostExpenditureTransaction() { super(); } /** * This will determine if this transaction is an ICR eligible transaction * * @param transaction the transaction which is being determined to be ICR or not * @param objectType the object type of the transaction * @param account the account of the transaction * @param objectCode the object code of the transaction * @return true if the transaction is an ICR transaction and therefore should have an expenditure transaction created for it; false if otherwise */ @Override public boolean isIcrTransaction(Transaction transaction, ReportWriterService reportWriterService) { if (LOG.isDebugEnabled()) { LOG.debug("isIcrTransaction() started"); } // Is the ICR indicator set? // Is the period code a non-balance period, as specified by KFS-GL / Poster Indirect Cost Recoveries Step / INDIRECT_COST_FISCAL_PERIODS? If so, continue, if not, we aren't posting this transaction if (transaction.getObjectType().isFinObjectTypeIcrSelectionIndicator() && /*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(PosterIndirectCostRecoveryEntriesStep.class, PostExpenditureTransaction.INDIRECT_COST_FISCAL_PERIODS_PARAMETER, transaction.getUniversityFiscalPeriodCode()).evaluationSucceeds()) { // Continue on the posting process // Check the sub account type code. A21 sub-accounts with the type of CS don't get posted A21SubAccount a21SubAccount = accountingCycleCachingService.getA21SubAccount(transaction.getAccount().getChartOfAccountsCode(), transaction.getAccount().getAccountNumber(), transaction.getSubAccountNumber()); String financialIcrSeriesIdentifier; String indirectCostRecoveryTypeCode; // first, do a check to ensure that if the sub-account is set up for ICR, that the account is also set up for ICR if (a21SubAccount != null) { if (StringUtils.hasText(a21SubAccount.getFinancialIcrSeriesIdentifier()) && StringUtils.hasText(a21SubAccount.getIndirectCostRecoveryTypeCode())) { // the sub account is set up for ICR, make sure that the corresponding account is as well, just for validation purposes if (!StringUtils.hasText(transaction.getAccount().getFinancialIcrSeriesIdentifier()) || !StringUtils.hasText(transaction.getAccount().getAcctIndirectCostRcvyTypeCd())) { List<Message> warnings = new ArrayList<Message>(); warnings.add(new Message("Warning - excluding transaction from Indirect Cost Recovery because Sub-Account is set up for ICR, but Account is not.", Message.TYPE_WARNING)); reportWriterService.writeError(transaction, warnings); } } if (StringUtils.hasText(a21SubAccount.getFinancialIcrSeriesIdentifier()) && StringUtils.hasText(a21SubAccount.getIndirectCostRecoveryTypeCode())) { // A21SubAccount info set up correctly financialIcrSeriesIdentifier = a21SubAccount.getFinancialIcrSeriesIdentifier(); indirectCostRecoveryTypeCode = a21SubAccount.getIndirectCostRecoveryTypeCode(); } else { // we had an A21SubAccount, but it was not set up for ICR, use account values instead financialIcrSeriesIdentifier = transaction.getAccount().getFinancialIcrSeriesIdentifier(); indirectCostRecoveryTypeCode = transaction.getAccount().getAcctIndirectCostRcvyTypeCd(); } } else { // no A21SubAccount found, default to using Account financialIcrSeriesIdentifier = transaction.getAccount().getFinancialIcrSeriesIdentifier(); indirectCostRecoveryTypeCode = transaction.getAccount().getAcctIndirectCostRcvyTypeCd(); } // the ICR Series identifier set? if (!StringUtils.hasText(financialIcrSeriesIdentifier)) { LOG.debug("isIcrTransaction() Not ICR Account"); return false; } if ((a21SubAccount != null) && KFSConstants.SubAccountType.COST_SHARE.equals(a21SubAccount.getSubAccountTypeCode())) { // No need to post this LOG.debug("isIcrTransaction() A21 subaccounts with type of CS - not posted"); return false; } // do we have an exclusion by type or by account? then we don't have to post no expenditure transaction final boolean selfAndTopLevelOnly = getParameterService().getParameterValueAsBoolean(PosterIndirectCostRecoveryEntriesStep.class, PostExpenditureTransaction.ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_PARAMETER_NAME); if (excludedByType(indirectCostRecoveryTypeCode, transaction.getFinancialObject(), selfAndTopLevelOnly)) { return false; } if (excludedByAccount(transaction.getAccount(), transaction.getFinancialObject(), selfAndTopLevelOnly)) { return false; } return true; // still here? then I guess we don't have an exclusion } else { // Don't need to post anything LOG.debug("isIcrTransaction() invalid period code - not posted"); return false; } } /** * Determines if there's an exclusion by type record existing for the given ICR type code and object code or object codes within the object code's reportsTo hierarchy * @param indirectCostRecoveryTypeCode the ICR type code to check * @param objectCode the object code to check for, as well as check the reports-to hierarchy * @param selfAndTopLevelOnly whether only the given object code and the top level object code should be checked * @return true if the transaction with the given ICR type code and object code have an exclusion by type record, false otherwise */ protected boolean excludedByType(String indirectCostRecoveryTypeCode, ObjectCode objectCode, boolean selfAndTopLevelOnly) { // If the ICR type code is empty or excluded by the KFS-GL / Batch / INDIRECT_COST_TYPES parameter, don't post if ((!StringUtils.hasText(indirectCostRecoveryTypeCode)) || !/*REFACTORME*/SpringContext.getBean(ParameterEvaluatorService.class).getParameterEvaluator(KfsParameterConstants.GENERAL_LEDGER_BATCH.class, GeneralLedgerConstants.INDIRECT_COST_TYPES_PARAMETER, indirectCostRecoveryTypeCode).evaluationSucceeds()) { // No need to post this if (LOG.isDebugEnabled()) { LOG.debug("isIcrTransaction() ICR type is null or excluded by the KFS-GL / Poster Indirect Cost Recoveries Step / INDIRECT_COST_TYPES parameter - not posted"); } return true; } if (hasExclusionByType(indirectCostRecoveryTypeCode, objectCode)) { return true; } ObjectCode currentObjectCode = getReportsToObjectCode(objectCode); while (currentObjectCode != null && !currentObjectCode.isReportingToSelf()) { if (!selfAndTopLevelOnly && hasExclusionByType(indirectCostRecoveryTypeCode, currentObjectCode)) { return true; } currentObjectCode = getReportsToObjectCode(currentObjectCode); } if (currentObjectCode != null && hasExclusionByType(indirectCostRecoveryTypeCode, currentObjectCode)) { return true; // we must be top level if the object code isn't null } return false; } /** * Determines if the given object code and indirect cost recovery type code have an exclusion by type record associated with them * @param indirectCostRecoveryTypeCode the indirect cost recovery type code to check * @param objectCode the object code to check * @return true if there's an exclusion by type record for this type code and object code */ protected boolean hasExclusionByType(String indirectCostRecoveryTypeCode, ObjectCode objectCode) { Map<String, Object> keys = new HashMap<String, Object>(); keys.put(KFSPropertyConstants.ACCOUNT_INDIRECT_COST_RECOVERY_TYPE_CODE, indirectCostRecoveryTypeCode); keys.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, objectCode.getChartOfAccountsCode()); keys.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, objectCode.getFinancialObjectCode()); final IndirectCostRecoveryExclusionType excType = SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(IndirectCostRecoveryExclusionType.class, keys); return !ObjectUtils.isNull(excType) && excType.isActive(); } /** * Determine if the given account and object code have an exclusion by account associated which should prevent this transaction from posting an ExpenditureTransaction * @param account account to check * @param objectCode object code to check * @param selfAndTopLevelOnly if only the given object code and the top level object code should seek exclusion by account records or not * @return true if the given account and object code have an associated exclusion by account, false otherwise */ protected boolean excludedByAccount(Account account, ObjectCode objectCode, boolean selfAndTopLevelOnly) { if (hasExclusionByAccount(account, objectCode)) { return true; } ObjectCode currentObjectCode = getReportsToObjectCode(objectCode); while (currentObjectCode != null && !currentObjectCode.isReportingToSelf()) { if (!selfAndTopLevelOnly && hasExclusionByAccount(account, currentObjectCode)) { return true; } currentObjectCode = getReportsToObjectCode(currentObjectCode); } if (currentObjectCode != null && hasExclusionByAccount(account, currentObjectCode)) { return true; // we must be top level if we got this far } return false; } /** * Determines if there's an exclusion by account record for the given account and object code * @param account the account to check * @param objectCode the object code to check * @return true if the given account and object code have an exclusion by account record, false otherwise */ protected boolean hasExclusionByAccount(Account account, ObjectCode objectCode) { Map<String, Object> keys = new HashMap<String, Object>(); keys.put(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE, account.getChartOfAccountsCode()); keys.put(KFSPropertyConstants.ACCOUNT_NUMBER, account.getAccountNumber()); keys.put(KFSPropertyConstants.FINANCIAL_OBJECT_CHART_OF_ACCOUNT_CODE, objectCode.getChartOfAccountsCode()); keys.put(KFSPropertyConstants.FINANCIAL_OBJECT_CODE, objectCode.getFinancialObjectCode()); final IndirectCostRecoveryExclusionAccount excAccount = SpringContext.getBean(BusinessObjectService.class).findByPrimaryKey(IndirectCostRecoveryExclusionAccount.class, keys); return !ObjectUtils.isNull(excAccount); } /** * Determines if the given object code has a valid reports-to hierarchy * @param objectCode the object code to check * @return true if the object code has a valid reports-to hierarchy with no nulls; false otherwise */ protected boolean hasValidObjectCodeReportingHierarchy(ObjectCode objectCode) { ObjectCode currentObjectCode = objectCode; while (hasValidReportsToFields(currentObjectCode) && !currentObjectCode.isReportingToSelf()) { currentObjectCode = getReportsToObjectCode(currentObjectCode); if (ObjectUtils.isNull(currentObjectCode) || !currentObjectCode.isActive()) { return false; } } if (!hasValidReportsToFields(currentObjectCode)) { return false; } return true; } /** * Determines if the given object code has all the fields it would need for a strong and healthy reports to hierarchy * @param objectCode the object code to give a little check * @return true if everything is good, false if the object code has a bad, rotted reports to hierarchy */ protected boolean hasValidReportsToFields(ObjectCode objectCode) { return !org.apache.commons.lang.StringUtils.isBlank(objectCode.getReportsToChartOfAccountsCode()) && !org.apache.commons.lang.StringUtils.isBlank(objectCode.getReportsToFinancialObjectCode()); } /** * Uses the caching DAO instead of regular OJB to find the reports-to object code * @param objectCode the object code to get the reporter of * @return the reports to object code, or, if that is impossible, null */ protected ObjectCode getReportsToObjectCode(ObjectCode objectCode) { return accountingCycleCachingService.getObjectCode(objectCode.getUniversityFiscalYear(), objectCode.getReportsToChartOfAccountsCode(), objectCode.getReportsToFinancialObjectCode()); } /** * If the transaction is a valid ICR transaction, posts an expenditure transaction record for the transaction * * @param t the transaction which is being posted * @param mode the mode the poster is currently running in * @param postDate the date this transaction should post to * @param posterReportWriterService the writer service where the poster is writing its report * @return the accomplished post type * @see org.kuali.kfs.gl.batch.service.PostTransaction#post(org.kuali.kfs.gl.businessobject.Transaction, int, java.util.Date) */ @Override public String post(Transaction t, int mode, Date postDate, ReportWriterService posterReportWriterService) { LOG.debug("post() started"); if (ObjectUtils.isNull(t.getFinancialObject()) || !hasValidObjectCodeReportingHierarchy(t.getFinancialObject())) { // I agree with the commenter below...this seems totally lame return GeneralLedgerConstants.ERROR_CODE + ": Warning - excluding transaction from Indirect Cost Recovery because "+t.getUniversityFiscalYear().toString()+"-"+t.getChartOfAccountsCode()+"-"+t.getFinancialObjectCode()+" has an invalid reports to hierarchy (either has an non-existent object or an inactive object)"; } else if (isIcrTransaction(t, posterReportWriterService)) { return postTransaction(t, mode); } return GeneralLedgerConstants.EMPTY_CODE; } /** * Actually posts the transaction to the appropriate expenditure transaction record * * @param t the transaction to post * @param mode the mode of the poster as it is currently running * @return the accomplished post type */ protected String postTransaction(Transaction t, int mode) { LOG.debug("postTransaction() started"); String returnCode = GeneralLedgerConstants.UPDATE_CODE; ExpenditureTransaction et = accountingCycleCachingService.getExpenditureTransaction(t); if (et == null) { LOG.debug("Posting expenditure transation"); et = new ExpenditureTransaction(t); returnCode = GeneralLedgerConstants.INSERT_CODE; } if (org.apache.commons.lang.StringUtils.isBlank(t.getOrganizationReferenceId())) { et.setOrganizationReferenceId(GeneralLedgerConstants.getDashOrganizationReferenceId()); } if (KFSConstants.GL_DEBIT_CODE.equals(t.getTransactionDebitCreditCode()) || KFSConstants.GL_BUDGET_CODE.equals(t.getTransactionDebitCreditCode())) { et.setAccountObjectDirectCostAmount(et.getAccountObjectDirectCostAmount().add(t.getTransactionLedgerEntryAmount())); } else { et.setAccountObjectDirectCostAmount(et.getAccountObjectDirectCostAmount().subtract(t.getTransactionLedgerEntryAmount())); } if (returnCode.equals(GeneralLedgerConstants.INSERT_CODE)) { //TODO: remove this log statement. Added to troubleshoot FSKD-194. LOG.info("Inserting a GLEX record. Transaction:"+t); accountingCycleCachingService.insertExpenditureTransaction(et); } else { //TODO: remove this log statement. Added to troubleshoot FSKD-194. LOG.info("Updating a GLEX record. Transaction:"+t); accountingCycleCachingService.updateExpenditureTransaction(et); } return returnCode; } /** * @see org.kuali.kfs.gl.batch.service.PostTransaction#getDestinationName() */ @Override public String getDestinationName() { return persistenceStructureService.getTableName(ExpenditureTransaction.class); } public void setAccountingCycleCachingService(AccountingCycleCachingService accountingCycleCachingService) { this.accountingCycleCachingService = accountingCycleCachingService; } public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) { this.persistenceStructureService = persistenceStructureService; } /** * Gets the parameterService attribute. * @return Returns the parameterService. */ public ParameterService getParameterService() { return parameterService; } /** * Sets the parameterService attribute value. * @param parameterService The parameterService to set. */ public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } }