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 }