View Javadoc
1   /*
2    * Copyright 2005-2006 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.ole.gl.batch.service.impl;
17  
18  import java.util.ArrayList;
19  import java.util.Date;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.kuali.ole.coa.businessobject.A21SubAccount;
25  import org.kuali.ole.coa.businessobject.Account;
26  import org.kuali.ole.coa.businessobject.IndirectCostRecoveryExclusionAccount;
27  import org.kuali.ole.coa.businessobject.IndirectCostRecoveryExclusionType;
28  import org.kuali.ole.coa.businessobject.ObjectCode;
29  import org.kuali.ole.coa.dataaccess.IndirectCostRecoveryExclusionAccountDao;
30  import org.kuali.ole.coa.dataaccess.IndirectCostRecoveryExclusionTypeDao;
31  import org.kuali.ole.gl.GeneralLedgerConstants;
32  import org.kuali.ole.gl.batch.PosterIndirectCostRecoveryEntriesStep;
33  import org.kuali.ole.gl.batch.service.AccountingCycleCachingService;
34  import org.kuali.ole.gl.batch.service.IndirectCostRecoveryService;
35  import org.kuali.ole.gl.batch.service.PostTransaction;
36  import org.kuali.ole.gl.businessobject.ExpenditureTransaction;
37  import org.kuali.ole.gl.businessobject.Transaction;
38  import org.kuali.ole.sys.Message;
39  import org.kuali.ole.sys.OLEConstants;
40  import org.kuali.ole.sys.OLEPropertyConstants;
41  import org.kuali.ole.sys.context.SpringContext;
42  import org.kuali.ole.sys.service.ReportWriterService;
43  import org.kuali.rice.core.api.parameter.ParameterEvaluatorService;
44  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
45  import org.kuali.rice.krad.service.BusinessObjectService;
46  import org.kuali.rice.krad.service.PersistenceStructureService;
47  import org.kuali.rice.krad.util.ObjectUtils;
48  import org.springframework.transaction.annotation.Transactional;
49  import org.springframework.util.StringUtils;
50  
51  /**
52   * This implementation of PostTransaction creates ExpenditureTransactions, temporary records used
53   * for ICR generation
54   */
55  @Transactional
56  public class PostExpenditureTransaction implements IndirectCostRecoveryService, PostTransaction {
57      private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PostExpenditureTransaction.class);
58  
59      private static final String INDIRECT_COST_TYPES_PARAMETER = "INDIRECT_COST_TYPES";
60      private static final String INDIRECT_COST_FISCAL_PERIODS_PARAMETER = "INDIRECT_COST_FISCAL_PERIODS";
61      private static final String ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_PARAMETER_NAME = "ICR_EXCLUSIONS_AT_TRANSACTION_AND_TOP_LEVEL_ONLY_IND";
62  
63      private IndirectCostRecoveryExclusionAccountDao indirectCostRecoveryExclusionAccountDao;
64      private IndirectCostRecoveryExclusionTypeDao indirectCostRecoveryExclusionTypeDao;
65      private AccountingCycleCachingService accountingCycleCachingService;
66      private PersistenceStructureService persistenceStructureService;
67      private ParameterService parameterService;
68  
69      public void setIndirectCostRecoveryExclusionAccountDao(IndirectCostRecoveryExclusionAccountDao icrea) {
70          indirectCostRecoveryExclusionAccountDao = icrea;
71      }
72  
73      public void setIndirectCostRecoveryExclusionTypeDao(IndirectCostRecoveryExclusionTypeDao icrea) {
74          indirectCostRecoveryExclusionTypeDao = icrea;
75      }
76  
77      /**
78       * Creates a PostExpenditureTransaction instance
79       */
80      public PostExpenditureTransaction() {
81          super();
82      }
83  
84      /**
85       * This will determine if this transaction is an ICR eligible transaction
86       *
87       * @param transaction the transaction which is being determined to be ICR or not
88       * @param objectType the object type of the transaction
89       * @param account the account of the transaction
90       * @param objectCode the object code of the transaction
91       * @return true if the transaction is an ICR transaction and therefore should have an expenditure transaction created for it; false if otherwise
92       */
93      @Override
94      public boolean isIcrTransaction(Transaction transaction, ReportWriterService reportWriterService) {
95          if (LOG.isDebugEnabled()) {
96              LOG.debug("isIcrTransaction() started");
97          }
98  
99          // 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 }