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    }