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