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}