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.batch.service.impl; 017 018 import java.util.ArrayList; 019 import java.util.Date; 020 import java.util.List; 021 022 import org.kuali.kfs.coa.businessobject.A21SubAccount; 023 import org.kuali.kfs.coa.businessobject.Account; 024 import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryExclusionAccount; 025 import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryExclusionType; 026 import org.kuali.kfs.coa.businessobject.ObjectCode; 027 import org.kuali.kfs.coa.businessobject.ObjectType; 028 import org.kuali.kfs.coa.dataaccess.IndirectCostRecoveryExclusionAccountDao; 029 import org.kuali.kfs.coa.dataaccess.IndirectCostRecoveryExclusionTypeDao; 030 import org.kuali.kfs.gl.GeneralLedgerConstants; 031 import org.kuali.kfs.gl.batch.PosterIndirectCostRecoveryEntriesStep; 032 import org.kuali.kfs.gl.batch.service.AccountingCycleCachingService; 033 import org.kuali.kfs.gl.batch.service.IndirectCostRecoveryService; 034 import org.kuali.kfs.gl.batch.service.PostTransaction; 035 import org.kuali.kfs.gl.businessobject.ExpenditureTransaction; 036 import org.kuali.kfs.gl.businessobject.Transaction; 037 import org.kuali.kfs.sys.KFSConstants; 038 import org.kuali.kfs.sys.Message; 039 import org.kuali.kfs.sys.service.ReportWriterService; 040 import org.kuali.rice.kns.service.ParameterService; 041 import org.kuali.rice.kns.service.PersistenceStructureService; 042 import org.kuali.rice.kns.util.ObjectUtils; 043 import org.springframework.transaction.annotation.Transactional; 044 import org.springframework.util.StringUtils; 045 046 /** 047 * This implementation of PostTransaction creates ExpenditureTransactions, temporary records used 048 * for ICR generation 049 */ 050 @Transactional 051 public class PostExpenditureTransaction implements IndirectCostRecoveryService, PostTransaction { 052 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PostExpenditureTransaction.class); 053 054 private static final String INDIRECT_COST_TYPES_PARAMETER = "INDIRECT_COST_TYPES"; 055 private static final String INDIRECT_COST_FISCAL_PERIODS_PARAMETER = "INDIRECT_COST_FISCAL_PERIODS"; 056 private static final String ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_PARAMETER_NAME = "ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_IND"; 057 058 private IndirectCostRecoveryExclusionAccountDao indirectCostRecoveryExclusionAccountDao; 059 private IndirectCostRecoveryExclusionTypeDao indirectCostRecoveryExclusionTypeDao; 060 private AccountingCycleCachingService accountingCycleCachingService; 061 private PersistenceStructureService persistenceStructureService; 062 private ParameterService parameterService; 063 064 public void setIndirectCostRecoveryExclusionAccountDao(IndirectCostRecoveryExclusionAccountDao icrea) { 065 indirectCostRecoveryExclusionAccountDao = icrea; 066 } 067 068 public void setIndirectCostRecoveryExclusionTypeDao(IndirectCostRecoveryExclusionTypeDao icrea) { 069 indirectCostRecoveryExclusionTypeDao = icrea; 070 } 071 072 /** 073 * Creates a PostExpenditureTransaction instance 074 */ 075 public PostExpenditureTransaction() { 076 super(); 077 } 078 079 /** 080 * This will determine if this transaction is an ICR eligible transaction 081 * 082 * @param transaction the transaction which is being determined to be ICR or not 083 * @param objectType the object type of the transaction 084 * @param account the account of the transaction 085 * @param objectCode the object code of the transaction 086 * @return true if the transaction is an ICR transaction and therefore should have an expenditure transaction created for it; false if otherwise 087 */ 088 public boolean isIcrTransaction(Transaction transaction, ReportWriterService reportWriterService) { 089 if (LOG.isDebugEnabled()) { 090 LOG.debug("isIcrTransaction() started"); 091 } 092 093 // Is the ICR indicator set? 094 // 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 095 if (transaction.getObjectType().isFinObjectTypeIcrSelectionIndicator() && getParameterService().getParameterEvaluator(PosterIndirectCostRecoveryEntriesStep.class, PostExpenditureTransaction.INDIRECT_COST_FISCAL_PERIODS_PARAMETER, transaction.getUniversityFiscalPeriodCode()).evaluationSucceeds()) { 096 // Continue on the posting process 097 098 // Check the sub account type code. A21 sub-accounts with the type of CS don't get posted 099 A21SubAccount a21SubAccount = accountingCycleCachingService.getA21SubAccount(transaction.getAccount().getChartOfAccountsCode(), transaction.getAccount().getAccountNumber(), transaction.getSubAccountNumber()); 100 String financialIcrSeriesIdentifier; 101 String indirectCostRecoveryTypeCode; 102 103 // 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 104 if (a21SubAccount != null) { 105 if (StringUtils.hasText(a21SubAccount.getFinancialIcrSeriesIdentifier()) && StringUtils.hasText(a21SubAccount.getIndirectCostRecoveryTypeCode())) { 106 // the sub account is set up for ICR, make sure that the corresponding account is as well, just for validation purposes 107 if (!StringUtils.hasText(transaction.getAccount().getFinancialIcrSeriesIdentifier()) || !StringUtils.hasText(transaction.getAccount().getAcctIndirectCostRcvyTypeCd())) { 108 List<Message> warnings = new ArrayList<Message>(); 109 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)); 110 reportWriterService.writeError(transaction, warnings); 111 } 112 } 113 114 if (StringUtils.hasText(a21SubAccount.getFinancialIcrSeriesIdentifier()) && StringUtils.hasText(a21SubAccount.getIndirectCostRecoveryTypeCode())) { 115 // A21SubAccount info set up correctly 116 financialIcrSeriesIdentifier = a21SubAccount.getFinancialIcrSeriesIdentifier(); 117 indirectCostRecoveryTypeCode = a21SubAccount.getIndirectCostRecoveryTypeCode(); 118 } 119 else { 120 // we had an A21SubAccount, but it was not set up for ICR, use account values instead 121 financialIcrSeriesIdentifier = transaction.getAccount().getFinancialIcrSeriesIdentifier(); 122 indirectCostRecoveryTypeCode = transaction.getAccount().getAcctIndirectCostRcvyTypeCd(); 123 } 124 } 125 else { 126 // no A21SubAccount found, default to using Account 127 financialIcrSeriesIdentifier = transaction.getAccount().getFinancialIcrSeriesIdentifier(); 128 indirectCostRecoveryTypeCode = transaction.getAccount().getAcctIndirectCostRcvyTypeCd(); 129 } 130 131 // the ICR Series identifier set? 132 if (!StringUtils.hasText(financialIcrSeriesIdentifier)) { 133 LOG.debug("isIcrTransaction() Not ICR Account"); 134 return false; 135 } 136 137 if ((a21SubAccount != null) && KFSConstants.SubAccountType.COST_SHARE.equals(a21SubAccount.getSubAccountTypeCode())) { 138 // No need to post this 139 LOG.debug("isIcrTransaction() A21 subaccounts with type of CS - not posted"); 140 return false; 141 } 142 143 // do we have an exclusion by type or by account? then we don't have to post no expenditure transaction 144 final boolean selfAndTopLevelOnly = getParameterService().getIndicatorParameter(PosterIndirectCostRecoveryEntriesStep.class, PostExpenditureTransaction.ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_PARAMETER_NAME); 145 if (excludedByType(indirectCostRecoveryTypeCode, transaction.getFinancialObject(), selfAndTopLevelOnly)) return false; 146 if (excludedByAccount(transaction.getAccount(), transaction.getFinancialObject(), selfAndTopLevelOnly)) return false; 147 148 return true; // still here? then I guess we don't have an exclusion 149 } 150 else { 151 // Don't need to post anything 152 LOG.debug("isIcrTransaction() invalid period code - not posted"); 153 return false; 154 } 155 } 156 157 /** 158 * 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 159 * @param indirectCostRecoveryTypeCode the ICR type code to check 160 * @param objectCode the object code to check for, as well as check the reports-to hierarchy 161 * @param selfAndTopLevelOnly whether only the given object code and the top level object code should be checked 162 * @return true if the transaction with the given ICR type code and object code have an exclusion by type record, false otherwise 163 */ 164 protected boolean excludedByType(String indirectCostRecoveryTypeCode, ObjectCode objectCode, boolean selfAndTopLevelOnly) { 165 // If the ICR type code is empty or excluded by the KFS-GL / Poster Indirect Cost Recoveries Step / INDIRECT_COST_TYPES parameter, don't post 166 if ((!StringUtils.hasText(indirectCostRecoveryTypeCode)) || !getParameterService().getParameterEvaluator(PosterIndirectCostRecoveryEntriesStep.class, PostExpenditureTransaction.INDIRECT_COST_TYPES_PARAMETER, indirectCostRecoveryTypeCode).evaluationSucceeds()) { 167 // No need to post this 168 if (LOG.isDebugEnabled()) { 169 LOG.debug("isIcrTransaction() ICR type is null or excluded by the KFS-GL / Poster Indirect Cost Recoveries Step / INDIRECT_COST_TYPES parameter - not posted"); 170 } 171 return true; 172 } 173 174 if (hasExclusionByType(indirectCostRecoveryTypeCode, objectCode)) return true; 175 176 ObjectCode currentObjectCode = getReportsToObjectCode(objectCode); 177 while (currentObjectCode != null && !currentObjectCode.isReportingToSelf()) { 178 if (!selfAndTopLevelOnly && hasExclusionByType(indirectCostRecoveryTypeCode, currentObjectCode)) return true; 179 180 currentObjectCode = getReportsToObjectCode(currentObjectCode); 181 } 182 if (currentObjectCode != null && hasExclusionByType(indirectCostRecoveryTypeCode, currentObjectCode)) return true; // we must be top level if the object code isn't null 183 184 return false; 185 } 186 187 /** 188 * Determines if the given object code and indirect cost recovery type code have an exclusion by type record associated with them 189 * @param indirectCostRecoveryTypeCode the indirect cost recovery type code to check 190 * @param objectCode the object code to check 191 * @return true if there's an exclusion by type record for this type code and object code 192 */ 193 protected boolean hasExclusionByType(String indirectCostRecoveryTypeCode, ObjectCode objectCode) { 194 final IndirectCostRecoveryExclusionType excType = indirectCostRecoveryExclusionTypeDao.getByPrimaryKey(indirectCostRecoveryTypeCode, objectCode.getChartOfAccountsCode(), objectCode.getFinancialObjectCode()); 195 return !ObjectUtils.isNull(excType) && excType.isActive(); 196 } 197 198 /** 199 * Determine if the given account and object code have an exclusion by account associated which should prevent this transaction from posting an ExpenditureTransaction 200 * @param account account to check 201 * @param objectCode object code to check 202 * @param selfAndTopLevelOnly if only the given object code and the top level object code should seek exclusion by account records or not 203 * @return true if the given account and object code have an associated exclusion by account, false otherwise 204 */ 205 protected boolean excludedByAccount(Account account, ObjectCode objectCode, boolean selfAndTopLevelOnly) { 206 if (hasExclusionByAccount(account, objectCode)) return true; 207 208 ObjectCode currentObjectCode = getReportsToObjectCode(objectCode); 209 while (currentObjectCode != null && !currentObjectCode.isReportingToSelf()) { 210 if (!selfAndTopLevelOnly && hasExclusionByAccount(account, currentObjectCode)) return true; 211 212 currentObjectCode = getReportsToObjectCode(currentObjectCode); 213 } 214 if (currentObjectCode != null && hasExclusionByAccount(account, currentObjectCode)) return true; // we must be top level if we got this far 215 216 return false; 217 } 218 219 /** 220 * Determines if there's an exclusion by account record for the given account and object code 221 * @param account the account to check 222 * @param objectCode the object code to check 223 * @return true if the given account and object code have an exclusion by account record, false otherwise 224 */ 225 protected boolean hasExclusionByAccount(Account account, ObjectCode objectCode) { 226 final IndirectCostRecoveryExclusionAccount excAccount = indirectCostRecoveryExclusionAccountDao.getByPrimaryKey(account.getChartOfAccountsCode(), account.getAccountNumber(), objectCode.getChartOfAccountsCode(), objectCode.getFinancialObjectCode()); 227 return !ObjectUtils.isNull(excAccount); 228 } 229 230 /** 231 * Determines if the given object code has a valid reports-to hierarchy 232 * @param objectCode the object code to check 233 * @return true if the object code has a valid reports-to hierarchy with no nulls; false otherwise 234 */ 235 protected boolean hasValidObjectCodeReportingHierarchy(ObjectCode objectCode) { 236 ObjectCode currentObjectCode = objectCode; 237 while (hasValidReportsToFields(currentObjectCode) && !currentObjectCode.isReportingToSelf()) { 238 currentObjectCode = getReportsToObjectCode(currentObjectCode); 239 if (ObjectUtils.isNull(currentObjectCode) || !currentObjectCode.isActive()) { 240 return false; 241 } 242 } 243 if (!hasValidReportsToFields(currentObjectCode)) return false; 244 return true; 245 } 246 247 /** 248 * Determines if the given object code has all the fields it would need for a strong and healthy reports to hierarchy 249 * @param objectCode the object code to give a little check 250 * @return true if everything is good, false if the object code has a bad, rotted reports to hierarchy 251 */ 252 protected boolean hasValidReportsToFields(ObjectCode objectCode) { 253 return !org.apache.commons.lang.StringUtils.isBlank(objectCode.getReportsToChartOfAccountsCode()) && !org.apache.commons.lang.StringUtils.isBlank(objectCode.getReportsToFinancialObjectCode()); 254 } 255 256 /** 257 * Uses the caching DAO instead of regular OJB to find the reports-to object code 258 * @param objectCode the object code to get the reporter of 259 * @return the reports to object code, or, if that is impossible, null 260 */ 261 protected ObjectCode getReportsToObjectCode(ObjectCode objectCode) { 262 return accountingCycleCachingService.getObjectCode(objectCode.getUniversityFiscalYear(), objectCode.getReportsToChartOfAccountsCode(), objectCode.getReportsToFinancialObjectCode()); 263 } 264 265 /** 266 * If the transaction is a valid ICR transaction, posts an expenditure transaction record for the transaction 267 * 268 * @param t the transaction which is being posted 269 * @param mode the mode the poster is currently running in 270 * @param postDate the date this transaction should post to 271 * @param posterReportWriterService the writer service where the poster is writing its report 272 * @return the accomplished post type 273 * @see org.kuali.kfs.gl.batch.service.PostTransaction#post(org.kuali.kfs.gl.businessobject.Transaction, int, java.util.Date) 274 */ 275 public String post(Transaction t, int mode, Date postDate, ReportWriterService posterReportWriterService) { 276 LOG.debug("post() started"); 277 278 if (ObjectUtils.isNull(t.getFinancialObject()) || !hasValidObjectCodeReportingHierarchy(t.getFinancialObject())) { 279 // I agree with the commenter below...this seems totally lame 280 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)"; 281 } 282 else if (isIcrTransaction(t, posterReportWriterService)) { 283 return postTransaction(t, mode); 284 } 285 return GeneralLedgerConstants.EMPTY_CODE; 286 } 287 288 /** 289 * Actually posts the transaction to the appropriate expenditure transaction record 290 * 291 * @param t the transaction to post 292 * @param mode the mode of the poster as it is currently running 293 * @return the accomplished post type 294 */ 295 protected String postTransaction(Transaction t, int mode) { 296 LOG.debug("postTransaction() started"); 297 298 String returnCode = GeneralLedgerConstants.UPDATE_CODE; 299 ExpenditureTransaction et = accountingCycleCachingService.getExpenditureTransaction(t); 300 if (et == null) { 301 LOG.debug("Posting expenditure transation"); 302 et = new ExpenditureTransaction(t); 303 returnCode = GeneralLedgerConstants.INSERT_CODE; 304 } 305 306 if (org.apache.commons.lang.StringUtils.isBlank(t.getOrganizationReferenceId())) { 307 et.setOrganizationReferenceId(GeneralLedgerConstants.getDashOrganizationReferenceId()); 308 } 309 310 if (KFSConstants.GL_DEBIT_CODE.equals(t.getTransactionDebitCreditCode()) || KFSConstants.GL_BUDGET_CODE.equals(t.getTransactionDebitCreditCode())) { 311 et.setAccountObjectDirectCostAmount(et.getAccountObjectDirectCostAmount().add(t.getTransactionLedgerEntryAmount())); 312 } 313 else { 314 et.setAccountObjectDirectCostAmount(et.getAccountObjectDirectCostAmount().subtract(t.getTransactionLedgerEntryAmount())); 315 } 316 317 if (returnCode.equals(GeneralLedgerConstants.INSERT_CODE)) { 318 //TODO: remove this log statement. Added to troubleshoot FSKD-194. 319 LOG.info("Inserting a GLEX record. Transaction:"+t); 320 accountingCycleCachingService.insertExpenditureTransaction(et); 321 } else { 322 //TODO: remove this log statement. Added to troubleshoot FSKD-194. 323 LOG.info("Updating a GLEX record. Transaction:"+t); 324 accountingCycleCachingService.updateExpenditureTransaction(et); 325 } 326 327 return returnCode; 328 } 329 330 /** 331 * @see org.kuali.kfs.gl.batch.service.PostTransaction#getDestinationName() 332 */ 333 public String getDestinationName() { 334 return persistenceStructureService.getTableName(ExpenditureTransaction.class); 335 } 336 337 public void setAccountingCycleCachingService(AccountingCycleCachingService accountingCycleCachingService) { 338 this.accountingCycleCachingService = accountingCycleCachingService; 339 } 340 341 public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) { 342 this.persistenceStructureService = persistenceStructureService; 343 } 344 345 /** 346 * Gets the parameterService attribute. 347 * @return Returns the parameterService. 348 */ 349 public ParameterService getParameterService() { 350 return parameterService; 351 } 352 353 /** 354 * Sets the parameterService attribute value. 355 * @param parameterService The parameterService to set. 356 */ 357 public void setParameterService(ParameterService parameterService) { 358 this.parameterService = parameterService; 359 } 360 361 }