001/*
002 * Copyright 2006-2009 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
017package org.kuali.ole.gl.batch.service.impl;
018
019import java.io.BufferedReader;
020import java.io.File;
021import java.io.FileNotFoundException;
022import java.io.FileReader;
023import java.io.IOException;
024import java.io.PrintStream;
025import java.math.BigDecimal;
026import java.sql.Date;
027import java.text.DecimalFormat;
028import java.text.MessageFormat;
029import java.text.SimpleDateFormat;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.HashMap;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036
037import org.apache.commons.lang.StringUtils;
038import org.kuali.ole.coa.businessobject.A21IndirectCostRecoveryAccount;
039import org.kuali.ole.coa.businessobject.A21SubAccount;
040import org.kuali.ole.coa.businessobject.Account;
041import org.kuali.ole.coa.businessobject.AccountingPeriod;
042import org.kuali.ole.coa.businessobject.IndirectCostRecoveryAccount;
043import org.kuali.ole.coa.businessobject.IndirectCostRecoveryRate;
044import org.kuali.ole.coa.businessobject.IndirectCostRecoveryRateDetail;
045import org.kuali.ole.coa.businessobject.ObjectCode;
046import org.kuali.ole.coa.businessobject.OffsetDefinition;
047import org.kuali.ole.coa.businessobject.SubAccount;
048import org.kuali.ole.coa.dataaccess.IndirectCostRecoveryRateDetailDao;
049import org.kuali.ole.coa.service.AccountingPeriodService;
050import org.kuali.ole.coa.service.ObjectCodeService;
051import org.kuali.ole.coa.service.OffsetDefinitionService;
052import org.kuali.ole.coa.service.SubAccountService;
053import org.kuali.ole.gl.GeneralLedgerConstants;
054import org.kuali.ole.gl.batch.PosterIndirectCostRecoveryEntriesStep;
055import org.kuali.ole.gl.batch.service.AccountingCycleCachingService;
056import org.kuali.ole.gl.batch.service.PostTransaction;
057import org.kuali.ole.gl.batch.service.PosterService;
058import org.kuali.ole.gl.batch.service.RunDateService;
059import org.kuali.ole.gl.batch.service.VerifyTransaction;
060import org.kuali.ole.gl.businessobject.ExpenditureTransaction;
061import org.kuali.ole.gl.businessobject.OriginEntryFull;
062import org.kuali.ole.gl.businessobject.OriginEntryInformation;
063import org.kuali.ole.gl.businessobject.Reversal;
064import org.kuali.ole.gl.businessobject.Transaction;
065import org.kuali.ole.gl.dataaccess.ExpenditureTransactionDao;
066import org.kuali.ole.gl.dataaccess.ReversalDao;
067import org.kuali.ole.gl.report.LedgerSummaryReport;
068import org.kuali.ole.gl.report.TransactionListingReport;
069import org.kuali.ole.gl.service.OriginEntryGroupService;
070import org.kuali.ole.gl.service.OriginEntryService;
071import org.kuali.ole.sys.OLEConstants;
072import org.kuali.ole.sys.OLEKeyConstants;
073import org.kuali.ole.sys.OLEPropertyConstants;
074import org.kuali.ole.sys.Message;
075import org.kuali.ole.sys.businessobject.SystemOptions;
076import org.kuali.ole.sys.businessobject.UniversityDate;
077import org.kuali.ole.sys.context.SpringContext;
078import org.kuali.ole.sys.dataaccess.UniversityDateDao;
079import org.kuali.ole.sys.exception.InvalidFlexibleOffsetException;
080import org.kuali.ole.sys.service.FlexibleOffsetAccountService;
081import org.kuali.ole.sys.service.ReportWriterService;
082import org.kuali.ole.sys.service.impl.OleParameterConstants;
083import org.kuali.rice.core.api.config.property.ConfigurationService;
084import org.kuali.rice.core.api.datetime.DateTimeService;
085import org.kuali.rice.core.api.util.type.KualiDecimal;
086import org.kuali.rice.coreservice.framework.parameter.ParameterService;
087import org.kuali.rice.kns.service.DataDictionaryService;
088import org.kuali.rice.krad.service.BusinessObjectService;
089import org.kuali.rice.krad.service.PersistenceService;
090import org.kuali.rice.krad.service.PersistenceStructureService;
091import org.kuali.rice.krad.util.ObjectUtils;
092import org.springframework.transaction.annotation.Transactional;
093
094/**
095 * The base implementation of PosterService
096 */
097@Transactional
098public class PosterServiceImpl implements PosterService {
099    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(PosterServiceImpl.class);
100
101    public static final KualiDecimal WARNING_MAX_DIFFERENCE = new KualiDecimal("0.03");
102    public static final String DATE_FORMAT_STRING = "yyyyMMdd";
103
104    private List transactionPosters;
105    private VerifyTransaction verifyTransaction;
106    private OriginEntryService originEntryService;
107    private OriginEntryGroupService originEntryGroupService;
108    private DateTimeService dateTimeService;
109    private ReversalDao reversalDao;
110    private UniversityDateDao universityDateDao;
111    private AccountingPeriodService accountingPeriodService;
112    private ExpenditureTransactionDao expenditureTransactionDao;
113    private IndirectCostRecoveryRateDetailDao indirectCostRecoveryRateDetailDao;
114    private ObjectCodeService objectCodeService;
115    private ParameterService parameterService;
116    private ConfigurationService configurationService;
117    private FlexibleOffsetAccountService flexibleOffsetAccountService;
118    private RunDateService runDateService;
119    private SubAccountService subAccountService;
120    private OffsetDefinitionService offsetDefinitionService;
121    private DataDictionaryService dataDictionaryService;
122    private BusinessObjectService businessObjectService;
123    private PersistenceStructureService persistenceStructureService;
124    private ReportWriterService reportWriterService;
125    private ReportWriterService errorListingReportWriterService;
126    private ReportWriterService reversalReportWriterService;
127    private ReportWriterService ledgerSummaryReportWriterService;
128    
129    //private File OUTPUT_ERR_FILE;
130    //private PrintStream OUTPUT_ERR_FILE_ps;
131    //private PrintStream OUTPUT_GLE_FILE_ps;
132    private String batchFileDirectoryName;
133    //private BufferedReader INPUT_GLE_FILE_br = null;
134    //private FileReader INPUT_GLE_FILE = null;
135    private AccountingCycleCachingService accountingCycleCachingService;
136
137    /**
138     * Post scrubbed GL entries to GL tables.
139     */
140    public void postMainEntries() {
141        LOG.debug("postMainEntries() started");
142        Date runDate = dateTimeService.getCurrentSqlDate();
143        try{
144            FileReader INPUT_GLE_FILE = new FileReader(batchFileDirectoryName + File.separator +  GeneralLedgerConstants.BatchFileSystem.POSTER_INPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
145            File OUTPUT_ERR_FILE = new File(batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.POSTER_ERROR_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION);    
146            
147            postEntries(PosterService.MODE_ENTRIES, INPUT_GLE_FILE, null, OUTPUT_ERR_FILE);
148            
149            INPUT_GLE_FILE.close();
150        } catch (FileNotFoundException e1) {
151            e1.printStackTrace();
152            throw new RuntimeException("PosterMainEntries Stopped: " + e1.getMessage(), e1);
153        } catch (IOException ioe) {
154            LOG.error("postMainEntries stopped due to: " + ioe.getMessage(), ioe);
155            throw new RuntimeException(ioe);
156        }
157    }
158
159    /**
160     * Post reversal GL entries to GL tables.
161     */
162    public void postReversalEntries() {
163        LOG.debug("postReversalEntries() started");
164        Date runDate = dateTimeService.getCurrentSqlDate();
165        try{
166            PrintStream OUTPUT_GLE_FILE_ps = new PrintStream(batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.REVERSAL_POSTER_VALID_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
167            File OUTPUT_ERR_FILE = new File(batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.REVERSAL_POSTER_ERROR_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
168            
169            postEntries(PosterService.MODE_REVERSAL, null, OUTPUT_GLE_FILE_ps, OUTPUT_ERR_FILE);
170            
171            OUTPUT_GLE_FILE_ps.close();
172        } catch (FileNotFoundException e1) {
173            e1.printStackTrace();
174            throw new RuntimeException("PosterReversalEntries Stopped: " + e1.getMessage(), e1);
175        }
176    }
177
178    /**
179     * Post ICR GL entries to GL tables.
180     */
181    public void postIcrEntries() {
182        LOG.debug("postIcrEntries() started");
183        Date runDate = dateTimeService.getCurrentSqlDate();
184        try{
185            FileReader INPUT_GLE_FILE = new FileReader(batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.ICR_POSTER_INPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
186            File OUTPUT_ERR_FILE = new File(batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.ICR_POSTER_ERROR_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
187        
188            postEntries(PosterService.MODE_ICR, INPUT_GLE_FILE, null, OUTPUT_ERR_FILE);
189            
190            INPUT_GLE_FILE.close();
191        } catch (FileNotFoundException e1) {
192            e1.printStackTrace();
193            throw new RuntimeException("PosterIcrEntries Stopped: " + e1.getMessage(), e1);
194        } catch (IOException ioe) {
195            LOG.error("postIcrEntries stopped due to: " + ioe.getMessage(), ioe);
196            throw new RuntimeException(ioe);
197        }
198    }
199
200    /**
201     * Actually post the entries. The mode variable decides which entries to post.
202     * 
203     * @param mode the poster's current run mode
204     */
205    protected void postEntries(int mode, FileReader INPUT_GLE_FILE, PrintStream OUTPUT_GLE_FILE_ps, File OUTPUT_ERR_FILE) throws FileNotFoundException {
206        if (LOG.isDebugEnabled()) {
207            LOG.debug("postEntries() started");
208        }
209        
210        PrintStream OUTPUT_ERR_FILE_ps = new PrintStream(OUTPUT_ERR_FILE);
211        BufferedReader INPUT_GLE_FILE_br = null;
212        if (INPUT_GLE_FILE != null) {
213            INPUT_GLE_FILE_br = new BufferedReader(INPUT_GLE_FILE);
214        }
215        
216        String GLEN_RECORD;
217        Date executionDate = new Date(dateTimeService.getCurrentDate().getTime());
218        Date runDate = new Date(runDateService.calculateRunDate(executionDate).getTime());
219        UniversityDate runUniversityDate = (UniversityDate)SpringContext.getBean(BusinessObjectService.class).findBySinglePrimaryKey(UniversityDate.class, runDate);       
220        LedgerSummaryReport ledgerSummaryReport = new LedgerSummaryReport();
221
222        // Build the summary map so all the possible combinations of destination & operation
223        // are included in the summary part of the report.
224        Map reportSummary = new HashMap();
225        for (Iterator posterIter = transactionPosters.iterator(); posterIter.hasNext();) {
226            PostTransaction poster = (PostTransaction) posterIter.next();
227            reportSummary.put(poster.getDestinationName() + "," + GeneralLedgerConstants.DELETE_CODE, new Integer(0));
228            reportSummary.put(poster.getDestinationName() + "," + GeneralLedgerConstants.INSERT_CODE, new Integer(0));
229            reportSummary.put(poster.getDestinationName() + "," + GeneralLedgerConstants.UPDATE_CODE, new Integer(0));
230        }
231        int ecount = 0;
232        
233        OriginEntryFull tran = null;
234        Transaction reversalTransaction = null;
235        try {
236            if ((mode == PosterService.MODE_ENTRIES) || (mode == PosterService.MODE_ICR)) {
237                LOG.debug("postEntries() Processing groups");
238                while ((GLEN_RECORD = INPUT_GLE_FILE_br.readLine()) != null) {
239                    if (!org.apache.commons.lang.StringUtils.isEmpty(GLEN_RECORD) && !org.apache.commons.lang.StringUtils.isBlank(GLEN_RECORD.trim())) {
240                        ecount++;
241
242                        GLEN_RECORD = org.apache.commons.lang.StringUtils.rightPad(GLEN_RECORD, 183, ' ');
243                        tran = new OriginEntryFull();
244
245                        // checking parsing process and stop poster when it has errors. 
246                        List<Message> parsingError = new ArrayList();
247                        parsingError = tran.setFromTextFileForBatch(GLEN_RECORD, ecount);
248                        if (parsingError.size() > 0) {
249                            String messages = "";
250                            for(Message msg : parsingError) {messages += msg + " ";}
251                            throw new RuntimeException("Exception happened from parsing process: " + messages);
252                        }
253                        // need to pass ecount for building better message
254                        addReporting(reportSummary, "SEQUENTIAL", GeneralLedgerConstants.SELECT_CODE);
255                        postTransaction(tran, mode, reportSummary, ledgerSummaryReport, OUTPUT_ERR_FILE_ps, runUniversityDate, GLEN_RECORD, OUTPUT_GLE_FILE_ps);
256                        
257                        if (ecount % 1000 == 0) {
258                            LOG.info("postEntries() Posted Entry " + ecount);
259                        }
260                    }
261                }
262                if (INPUT_GLE_FILE_br != null) {    
263                    INPUT_GLE_FILE_br.close();
264                }
265                OUTPUT_ERR_FILE_ps.close();
266                reportWriterService.writeStatisticLine("SEQUENTIAL RECORDS READ                    %,9d", reportSummary.get("SEQUENTIAL,S"));
267            }
268            else {
269                if (LOG.isDebugEnabled()) {
270                    LOG.debug("postEntries() Processing reversal transactions");
271                }
272                
273                final String GL_REVERSAL_T = getPersistenceStructureService().getTableName(Reversal.class);
274                Iterator reversalTransactions = reversalDao.getByDate(runDate);
275                TransactionListingReport reversalListingReport = new TransactionListingReport();
276                while (reversalTransactions.hasNext()) {
277                    ecount++;
278                    reversalTransaction = (Transaction) reversalTransactions.next();
279                    addReporting(reportSummary, GL_REVERSAL_T, GeneralLedgerConstants.SELECT_CODE);
280
281                    boolean posted = postTransaction(reversalTransaction, mode, reportSummary, ledgerSummaryReport, OUTPUT_ERR_FILE_ps, runUniversityDate, GL_REVERSAL_T, OUTPUT_GLE_FILE_ps);
282                    
283                    if (posted) {
284                        reversalListingReport.generateReport(reversalReportWriterService, reversalTransaction);
285                    }
286                    
287                    if (ecount % 1000 == 0) {
288                        LOG.info("postEntries() Posted Entry " + ecount);
289                    }
290                }
291                
292                OUTPUT_ERR_FILE_ps.close();
293                
294                reportWriterService.writeStatisticLine("GLRV RECORDS READ (GL_REVERSAL_T)          %,9d", reportSummary.get("GL_REVERSAL_T,S"));
295                reversalListingReport.generateStatistics(reversalReportWriterService);
296            }
297            
298            //PDF version had this abstracted to print I/U/D for each table in 7 posters, but some statistics are meaningless (i.e. GLEN is never updated), so un-abstracted here
299            reportWriterService.writeStatisticLine("GLEN RECORDS INSERTED (GL_ENTRY_T)         %,9d", reportSummary.get("GL_ENTRY_T,I"));
300            reportWriterService.writeStatisticLine("GLBL RECORDS INSERTED (GL_BALANCE_T)       %,9d", reportSummary.get("GL_BALANCE_T,I"));
301            reportWriterService.writeStatisticLine("GLBL RECORDS UPDATED  (GL_BALANCE_T)       %,9d", reportSummary.get("GL_BALANCE_T,U"));
302            reportWriterService.writeStatisticLine("GLEX RECORDS INSERTED (GL_EXPEND_TRN_MT)    %,9d", reportSummary.get("GL_EXPEND_TRN_MT,I"));
303            reportWriterService.writeStatisticLine("GLEX RECORDS UPDATED  (GL_EXPEND_TRN_MT)    %,9d", reportSummary.get("GL_EXPEND_TRN_MT,U"));
304            reportWriterService.writeStatisticLine("GLEC RECORDS INSERTED (GL_ENCUMBRANCE_T)   %,9d", reportSummary.get("GL_ENCUMBRANCE_T,I"));
305            reportWriterService.writeStatisticLine("GLEC RECORDS UPDATED  (GL_ENCUMBRANCE_T)   %,9d", reportSummary.get("GL_ENCUMBRANCE_T,U"));
306            reportWriterService.writeStatisticLine("GLRV RECORDS INSERTED (GL_REVERSAL_T)      %,9d", reportSummary.get("GL_REVERSAL_T,I"));
307            reportWriterService.writeStatisticLine("GLRV RECORDS DELETED  (GL_REVERSAL_T)      %,9d", reportSummary.get("GL_REVERSAL_T,D"));
308            reportWriterService.writeStatisticLine("SFBL RECORDS INSERTED (GL_SF_BALANCES_T)   %,9d", reportSummary.get("GL_SF_BALANCES_T,I"));
309            reportWriterService.writeStatisticLine("SFBL RECORDS UPDATED  (GL_SF_BALANCES_T)   %,9d", reportSummary.get("GL_SF_BALANCES_T,U"));
310            reportWriterService.writeStatisticLine("ACBL RECORDS INSERTED (GL_ACCT_BALANCES_T) %,9d", reportSummary.get("GL_ACCT_BALANCES_T,I"));
311            reportWriterService.writeStatisticLine("ACBL RECORDS UPDATED  (GL_ACCT_BALANCES_T) %,9d", reportSummary.get("GL_ACCT_BALANCES_T,U"));
312            reportWriterService.writeStatisticLine("ERROR RECORDS WRITTEN                      %,9d", reportSummary.get("WARNING,I"));
313        }
314        catch (RuntimeException re) {
315            LOG.error("postEntries stopped due to: " + re.getMessage() + " on line number : " + ecount, re);
316            LOG.error("tran failure occured on: " + tran == null ? null : tran.toString());
317            LOG.error("reversalTransaction failure occured on: " + reversalTransaction == null ? null : reversalTransaction.toString());
318            throw new RuntimeException("PosterService Stopped: " + re.getMessage(), re);
319        }
320        catch (IOException e) {
321            LOG.error("postEntries stopped due to: " + e.getMessage(), e);
322            throw new RuntimeException(e);
323        }
324        catch (Exception e) {
325            // do nothing - handled in postTransaction method.
326        }
327
328        LOG.info("postEntries() done, total count = " + ecount);
329        // Generate the reports
330        ledgerSummaryReport.writeReport(ledgerSummaryReportWriterService);
331        new TransactionListingReport().generateReport(errorListingReportWriterService, new OriginEntryFileIterator(OUTPUT_ERR_FILE));
332    }
333
334    /**
335     * Runs the given transaction through each transaction posting algorithms associated with this instance
336     * 
337     * @param tran a transaction to post
338     * @param mode the mode the poster is running in
339     * @param reportSummary a Map of summary counts generated by the posting process
340     * @param ledgerSummaryReport for summary reporting
341     * @param invalidGroup the group to save invalid entries to
342     * @param runUniversityDate the university date of this poster run
343     * @param line
344     * @return whether the transaction was posted or not. Useful if calling class attempts to report on the transaction
345     */
346    protected boolean postTransaction(Transaction tran, int mode, Map<String,Integer> reportSummary, LedgerSummaryReport ledgerSummaryReport, PrintStream invalidGroup, UniversityDate runUniversityDate, String line, PrintStream OUTPUT_GLE_FILE_ps) {
347
348        List<Message> errors = new ArrayList();
349        Transaction originalTransaction = tran;
350
351        try {
352            final String GL_ORIGIN_ENTRY_T = getPersistenceStructureService().getTableName(OriginEntryFull.class);
353
354            // Update select count in the report
355            if ((mode == PosterService.MODE_ENTRIES) || (mode == PosterService.MODE_ICR)) {
356                addReporting(reportSummary, GL_ORIGIN_ENTRY_T, GeneralLedgerConstants.SELECT_CODE);
357            }
358            // If these are reversal entries, we need to reverse the entry and
359            // modify a few fields
360            if (mode == PosterService.MODE_REVERSAL) {
361                Reversal reversal = new Reversal(tran);
362                // Reverse the debit/credit code
363                if (OLEConstants.GL_DEBIT_CODE.equals(reversal.getTransactionDebitCreditCode())) {
364                    reversal.setTransactionDebitCreditCode(OLEConstants.GL_CREDIT_CODE);
365                }
366                else if (OLEConstants.GL_CREDIT_CODE.equals(reversal.getTransactionDebitCreditCode())) {
367                    reversal.setTransactionDebitCreditCode(OLEConstants.GL_DEBIT_CODE);
368                }
369                UniversityDate udate = (UniversityDate)SpringContext.getBean(BusinessObjectService.class).findBySinglePrimaryKey(UniversityDate.class, reversal.getFinancialDocumentReversalDate());       
370                
371                if (udate != null) {
372                    reversal.setUniversityFiscalYear(udate.getUniversityFiscalYear());
373                    reversal.setUniversityFiscalPeriodCode(udate.getUniversityFiscalAccountingPeriod());
374                    AccountingPeriod ap = accountingPeriodService.getByPeriod(reversal.getUniversityFiscalPeriodCode(), reversal.getUniversityFiscalYear());
375                    if (ap != null) {
376                        if (!ap.isActive()) { // Make sure accounting period is closed
377                            reversal.setUniversityFiscalYear(runUniversityDate.getUniversityFiscalYear());
378                            reversal.setUniversityFiscalPeriodCode(runUniversityDate.getUniversityFiscalAccountingPeriod());
379                        }
380                        reversal.setFinancialDocumentReversalDate(null);
381                        String newDescription = OLEConstants.GL_REVERSAL_DESCRIPTION_PREFIX + reversal.getTransactionLedgerEntryDescription();
382                        if (newDescription.length() > 40) {
383                            newDescription = newDescription.substring(0, 40);
384                        }
385                        reversal.setTransactionLedgerEntryDescription(newDescription);
386                    }
387                    else {
388                        errors.add(new Message(configurationService.getPropertyValueAsString(OLEKeyConstants.ERROR_UNIV_DATE_NOT_IN_ACCOUNTING_PERIOD_TABLE), Message.TYPE_WARNING));
389                    }
390                }
391                else {
392                    errors.add(new Message (configurationService.getPropertyValueAsString(OLEKeyConstants.ERROR_REVERSAL_DATE_NOT_IN_UNIV_DATE_TABLE) , Message.TYPE_WARNING));
393                }
394                // Make sure the row will be unique when adding to the entries table by adjusting the transaction sequence id
395                int maxSequenceId = accountingCycleCachingService.getMaxSequenceNumber(reversal);
396                reversal.setTransactionLedgerEntrySequenceNumber(new Integer(maxSequenceId + 1));
397
398                PersistenceService ps = SpringContext.getBean(PersistenceService.class);
399                ps.retrieveNonKeyFields(reversal);
400                tran = reversal;
401            }
402            else {
403                tran.setChart(accountingCycleCachingService.getChart(tran.getChartOfAccountsCode()));
404                tran.setAccount(accountingCycleCachingService.getAccount(tran.getChartOfAccountsCode(), tran.getAccountNumber()));
405                tran.setObjectType(accountingCycleCachingService.getObjectType(tran.getFinancialObjectTypeCode()));
406                tran.setBalanceType(accountingCycleCachingService.getBalanceType(tran.getFinancialBalanceTypeCode()));
407                tran.setOption(accountingCycleCachingService.getSystemOptions(tran.getUniversityFiscalYear()));
408
409                ObjectCode objectCode = accountingCycleCachingService.getObjectCode(tran.getUniversityFiscalYear(), tran.getChartOfAccountsCode(), tran.getFinancialObjectCode());
410                if (ObjectUtils.isNull(objectCode)) {
411                    LOG.warn(configurationService.getPropertyValueAsString(OLEKeyConstants.ERROR_OBJECT_CODE_NOT_FOUND_FOR) + tran.getUniversityFiscalYear() + "," + tran.getChartOfAccountsCode() + "," + tran.getFinancialObjectCode());                    
412                    errors.add(new Message(configurationService.getPropertyValueAsString(OLEKeyConstants.ERROR_OBJECT_CODE_NOT_FOUND_FOR) + tran.getUniversityFiscalYear() + "," + tran.getChartOfAccountsCode() + "," + tran.getFinancialObjectCode(), Message.TYPE_WARNING));                    
413                }
414                else {
415                    tran.setFinancialObject(accountingCycleCachingService.getObjectCode(tran.getUniversityFiscalYear(), tran.getChartOfAccountsCode(), tran.getFinancialObjectCode()));
416                }
417
418                // Make sure the row will be unique when adding to the entries table by adjusting the transaction sequence id
419                int maxSequenceId = accountingCycleCachingService.getMaxSequenceNumber(tran);
420                ((OriginEntryFull) tran).setTransactionLedgerEntrySequenceNumber(new Integer(maxSequenceId + 1));
421            }
422
423            // verify accounting period
424            AccountingPeriod originEntryAccountingPeriod = accountingCycleCachingService.getAccountingPeriod(tran.getUniversityFiscalYear(), tran.getUniversityFiscalPeriodCode());
425            if (originEntryAccountingPeriod == null) {
426                errors.add(new Message(configurationService.getPropertyValueAsString(OLEKeyConstants.ERROR_ACCOUNTING_PERIOD_NOT_FOUND) + " for " + tran.getUniversityFiscalYear() + "/" + tran.getUniversityFiscalPeriodCode(),  Message.TYPE_FATAL));
427            }
428
429            if (errors.size() == 0) {
430                try {
431                    errors = verifyTransaction.verifyTransaction(tran);
432                }
433                catch (Exception e) {
434                    errors.add(new Message(e.toString() + " occurred for this record.", Message.TYPE_FATAL));
435                }
436            }
437
438            if (errors.size() > 0) {
439                // Error on this transaction
440                reportWriterService.writeError(tran, errors);
441                addReporting(reportSummary, "WARNING", GeneralLedgerConstants.INSERT_CODE);
442                try {
443                    writeErrorEntry(line, invalidGroup);
444                }
445                catch (IOException ioe) {
446                    LOG.error("PosterServiceImpl Stopped: " + ioe.getMessage(), ioe);
447                    throw new RuntimeException("PosterServiceImpl Stopped: " + ioe.getMessage(), ioe);
448                }
449            }
450            else {
451                // No error so post it
452                for (Iterator posterIter = transactionPosters.iterator(); posterIter.hasNext();) {
453                    PostTransaction poster = (PostTransaction) posterIter.next();
454                    String actionCode = poster.post(tran, mode, runUniversityDate.getUniversityDate(), reportWriterService);
455
456                    if (actionCode.startsWith(GeneralLedgerConstants.ERROR_CODE)) {
457                        errors = new ArrayList<Message>();
458                        errors.add(new Message(actionCode, Message.TYPE_WARNING));
459                        reportWriterService.writeError(tran, errors);
460                    }
461                    else if (actionCode.indexOf(GeneralLedgerConstants.INSERT_CODE) >= 0) {
462                        addReporting(reportSummary, poster.getDestinationName(), GeneralLedgerConstants.INSERT_CODE);
463                    }
464                    else if (actionCode.indexOf(GeneralLedgerConstants.UPDATE_CODE) >= 0) {
465                        addReporting(reportSummary, poster.getDestinationName(), GeneralLedgerConstants.UPDATE_CODE);
466                    }
467                    else if (actionCode.indexOf(GeneralLedgerConstants.DELETE_CODE) >= 0) {
468                        addReporting(reportSummary, poster.getDestinationName(), GeneralLedgerConstants.DELETE_CODE);
469                    }
470                    else if (actionCode.indexOf(GeneralLedgerConstants.SELECT_CODE) >= 0) {
471                        addReporting(reportSummary, poster.getDestinationName(), GeneralLedgerConstants.SELECT_CODE);
472                    }
473                }
474                if (errors.size() == 0) {
475                    // Delete the reversal entry
476                    if (mode == PosterService.MODE_REVERSAL) {
477                        createOutputEntry(tran, OUTPUT_GLE_FILE_ps);
478                        reversalDao.delete((Reversal) originalTransaction);
479                        addReporting(reportSummary, getPersistenceStructureService().getTableName(Reversal.class), GeneralLedgerConstants.DELETE_CODE);
480                    }
481                    
482                    ledgerSummaryReport.summarizeEntry(new OriginEntryFull(tran));
483                    return true;
484                }
485            }
486            
487            return false;
488        }
489        catch (IOException ioe) {
490            LOG.error("PosterServiceImpl Stopped: " + ioe.getMessage(), ioe);
491            throw new RuntimeException("PosterServiceImpl Stopped: " + ioe.getMessage(), ioe);
492
493        }
494        catch (RuntimeException re) {
495            LOG.error("PosterServiceImpl Stopped: " + re.getMessage(), re);
496            throw new RuntimeException("PosterServiceImpl Stopped: " + re.getMessage(), re);
497        }
498    }
499
500    /**
501     * This step reads the expenditure table and uses the data to generate Indirect Cost Recovery transactions.
502     */
503    public void generateIcrTransactions() {
504        LOG.debug("generateIcrTransactions() started");
505
506        Date executionDate = dateTimeService.getCurrentSqlDate();
507        Date runDate = new Date(runDateService.calculateRunDate(executionDate).getTime());
508
509        try {
510            PrintStream OUTPUT_GLE_FILE_ps = new PrintStream(batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.ICR_TRANSACTIONS_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION);
511            
512            int reportExpendTranRetrieved = 0;
513            int reportExpendTranDeleted = 0;
514            int reportExpendTranKept = 0;
515            int reportOriginEntryGenerated = 0;
516            Iterator expenditureTransactions;
517            
518            try {
519                expenditureTransactions = expenditureTransactionDao.getAllExpenditureTransactions();
520            }
521            catch (RuntimeException re) {
522                LOG.error("generateIcrTransactions Stopped: " + re.getMessage());
523                throw new RuntimeException("generateIcrTransactions Stopped: " + re.getMessage(), re);
524            }
525
526            while (expenditureTransactions.hasNext()) {
527                ExpenditureTransaction et = new ExpenditureTransaction();
528                try {
529                    et = (ExpenditureTransaction) expenditureTransactions.next();
530                    reportExpendTranRetrieved++;
531
532                    KualiDecimal transactionAmount = et.getAccountObjectDirectCostAmount();
533                    KualiDecimal distributionAmount = KualiDecimal.ZERO;
534
535                    if (shouldIgnoreExpenditureTransaction(et)) {
536                        // Delete expenditure record
537                        expenditureTransactionDao.delete(et);
538                        reportExpendTranDeleted++;
539                        continue;
540                    }
541                    
542                    IndirectCostRecoveryGenerationMetadata icrGenerationMetadata = retrieveSubAccountIndirectCostRecoveryMetadata(et);
543                    if (icrGenerationMetadata == null) {
544                        // ICR information was not set up properly for sub-account, default to using ICR information from the account
545                        icrGenerationMetadata = retrieveAccountIndirectCostRecoveryMetadata(et);
546                    }
547
548                    Collection<IndirectCostRecoveryRateDetail> automatedEntries = indirectCostRecoveryRateDetailDao.getActiveRateDetailsByRate(et.getUniversityFiscalYear(), icrGenerationMetadata.getFinancialIcrSeriesIdentifier());
549                    int automatedEntriesCount = automatedEntries.size();
550          
551                    if (automatedEntriesCount > 0) {
552                        for (Iterator icrIter = automatedEntries.iterator(); icrIter.hasNext();) {
553                            IndirectCostRecoveryRateDetail icrEntry = (IndirectCostRecoveryRateDetail) icrIter.next();
554                            KualiDecimal generatedTransactionAmount = null;
555
556                            if (!icrIter.hasNext()) {
557                                generatedTransactionAmount = distributionAmount;
558
559                                // Log differences that are over WARNING_MAX_DIFFERENCE
560                                if (getPercentage(transactionAmount, icrEntry.getAwardIndrCostRcvyRatePct()).subtract(distributionAmount).abs().isGreaterThan(WARNING_MAX_DIFFERENCE)) {
561                                    List<Message> warnings = new ArrayList<Message>();
562                                    warnings.add(new Message("ADJUSTMENT GREATER THAN " + WARNING_MAX_DIFFERENCE, Message.TYPE_WARNING));
563                                    reportWriterService.writeError(et, warnings);
564                                }
565                            }
566                            else if (icrEntry.getTransactionDebitIndicator().equals(OLEConstants.GL_DEBIT_CODE)) {
567                                generatedTransactionAmount = getPercentage(transactionAmount, icrEntry.getAwardIndrCostRcvyRatePct());
568                                distributionAmount = distributionAmount.add(generatedTransactionAmount);
569                            }
570                            else if (icrEntry.getTransactionDebitIndicator().equals(OLEConstants.GL_CREDIT_CODE)) {
571                                generatedTransactionAmount = getPercentage(transactionAmount, icrEntry.getAwardIndrCostRcvyRatePct());
572                                distributionAmount = distributionAmount.subtract(generatedTransactionAmount);
573                            }
574                            else {
575                                // Log if D / C code not found
576                                List<Message> warnings = new ArrayList<Message>();
577                                warnings.add(new Message("DEBIT OR CREDIT CODE NOT FOUND", Message.TYPE_FATAL));
578                                reportWriterService.writeError(et, warnings);
579                            }
580                            //KFSMI-5614 CHANGED 
581                            generateTransactionsBySymbol(et, icrEntry, generatedTransactionAmount, runDate, OUTPUT_GLE_FILE_ps, icrGenerationMetadata);
582                            
583                            reportOriginEntryGenerated = reportOriginEntryGenerated + 2;
584                        }
585                    }
586                    // Delete expenditure record
587                    expenditureTransactionDao.delete(et);
588                    reportExpendTranDeleted++;
589
590                }
591                catch (RuntimeException re) {
592                    LOG.error("generateIcrTransactions Stopped: " + re.getMessage());
593                    throw new RuntimeException("generateIcrTransactions Stopped: " + re.getMessage(), re);
594                }
595                catch (Exception e) {
596                    List errorList = new ArrayList();
597                    errorList.add(new Message(e.toString() + " occurred for this record.", Message.TYPE_FATAL));
598                    reportWriterService.writeError(et, errorList);
599                }
600            }
601            OUTPUT_GLE_FILE_ps.close();
602            reportWriterService.writeStatisticLine("GLEX RECORDS READ               (GL_EXPEND_TRN_MT) %,9d", reportExpendTranRetrieved);
603            reportWriterService.writeStatisticLine("GLEX RECORDS DELETED            (GL_EXPEND_TRN_MT) %,9d", reportExpendTranDeleted);
604            reportWriterService.writeStatisticLine("GLEX RECORDS KEPT DUE TO ERRORS (GL_EXPEND_TRN_MT) %,9d", reportExpendTranKept);
605            reportWriterService.writeStatisticLine("TRANSACTIONS GENERATED                            %,9d", reportOriginEntryGenerated);
606        }
607        catch (FileNotFoundException e) {
608            throw new RuntimeException("generateIcrTransactions Stopped: " + e.getMessage(), e);
609        }
610    }
611
612    /**
613     * Wrapper function to allow for internal iterations on ICR account distribution collection if determined to use ICR from account
614     * 
615     * @param et
616     * @param icrRateDetail
617     * @param generatedTransactionAmount
618     * @param runDate
619     * @param group
620     * @param icrGenerationMetadata
621     */
622    private void generateTransactionsBySymbol(ExpenditureTransaction et, IndirectCostRecoveryRateDetail icrRateDetail, KualiDecimal generatedTransactionAmount, Date runDate, PrintStream group, IndirectCostRecoveryGenerationMetadata icrGenerationMetadata) {
623        
624        KualiDecimal icrTransactionAmount;
625        KualiDecimal unappliedTransactionAmount = new KualiDecimal(generatedTransactionAmount.bigDecimalValue());
626        
627        //if symbol is denoted to use ICR from account 
628        if (GeneralLedgerConstants.PosterService.SYMBOL_USE_ICR_FROM_ACCOUNT.equals(icrRateDetail.getAccountNumber())) {
629            
630            int icrCount = icrGenerationMetadata.getAccountLists().size();
631            
632            for (IndirectCostRecoveryAccountDistributionMetadata meta : icrGenerationMetadata.getAccountLists()){
633
634                //set a new icr meta data for transaction processing
635                IndirectCostRecoveryGenerationMetadata icrMeta = new IndirectCostRecoveryGenerationMetadata(icrGenerationMetadata.getIndirectCostRecoveryTypeCode(),
636                        icrGenerationMetadata.getFinancialIcrSeriesIdentifier());
637                icrMeta.setIndirectCostRcvyFinCoaCode(meta.getIndirectCostRecoveryFinCoaCode());
638                icrMeta.setIndirectCostRecoveryAcctNbr(meta.getIndirectCostRecoveryAccountNumber());
639                
640              //change the transaction amount base on ICR percentage
641                if (icrCount-- == 1) {
642                    // Deplete the rest of un-applied transaction amount
643                    icrTransactionAmount = unappliedTransactionAmount;
644                }
645                else {
646                    // Normal transaction amount is calculated by icr account line percentage 
647                    icrTransactionAmount = getPercentage(generatedTransactionAmount, meta.getAccountLinePercent());
648                    unappliedTransactionAmount = unappliedTransactionAmount.subtract(icrTransactionAmount);
649                }
650
651                //perform the actual transaction generation
652                generateTransactions(et, icrRateDetail, icrTransactionAmount, runDate, group, icrMeta);
653            }
654        }else{
655            
656            //non-ICR; process as usual
657            generateTransactions(et, icrRateDetail, generatedTransactionAmount, runDate, group, icrGenerationMetadata);
658        }
659
660    }
661    
662    /**
663     * Generate a transfer transaction and an offset transaction
664     * 
665     * @param et an expenditure transaction
666     * @param icrEntry the indirect cost recovery entry
667     * @param generatedTransactionAmount the amount of the transaction
668     * @param runDate the transaction date for the newly created origin entry
669     * @param group the group to save the origin entry to
670     */
671    protected void generateTransactions(ExpenditureTransaction et, IndirectCostRecoveryRateDetail icrRateDetail, KualiDecimal generatedTransactionAmount, Date runDate, PrintStream group, IndirectCostRecoveryGenerationMetadata icrGenerationMetadata) {
672
673        BigDecimal pct = new BigDecimal(icrRateDetail.getAwardIndrCostRcvyRatePct().toString());
674        pct = pct.divide(BDONEHUNDRED);
675
676        OriginEntryFull e = new OriginEntryFull();
677        e.setTransactionLedgerEntrySequenceNumber(0);
678
679        // SYMBOL_USE_EXPENDITURE_ENTRY means we use the field from the expenditure entry, SYMBOL_USE_IRC_FROM_ACCOUNT
680        // means we use the ICR field from the account record, otherwise, use the field in the icrRateDetail
681        if (GeneralLedgerConstants.PosterService.SYMBOL_USE_EXPENDITURE_ENTRY.equals(icrRateDetail.getFinancialObjectCode()) || GeneralLedgerConstants.PosterService.SYMBOL_USE_ICR_FROM_ACCOUNT.equals(icrRateDetail.getFinancialObjectCode())) {
682            e.setFinancialObjectCode(et.getObjectCode());
683            e.setFinancialSubObjectCode(et.getSubObjectCode());
684        }
685        else {
686            e.setFinancialObjectCode(icrRateDetail.getFinancialObjectCode());
687                e.setFinancialSubObjectCode(icrRateDetail.getFinancialSubObjectCode());
688            }
689
690        if (GeneralLedgerConstants.PosterService.SYMBOL_USE_EXPENDITURE_ENTRY.equals(icrRateDetail.getAccountNumber())) {
691            e.setAccountNumber(et.getAccountNumber());
692            e.setChartOfAccountsCode(et.getChartOfAccountsCode());
693            e.setSubAccountNumber(et.getSubAccountNumber());
694        }
695        else if (GeneralLedgerConstants.PosterService.SYMBOL_USE_ICR_FROM_ACCOUNT.equals(icrRateDetail.getAccountNumber())) {
696            e.setAccountNumber(icrGenerationMetadata.getIndirectCostRecoveryAcctNbr());
697            e.setChartOfAccountsCode(icrGenerationMetadata.getIndirectCostRcvyFinCoaCode());
698            e.setSubAccountNumber(OLEConstants.getDashSubAccountNumber());
699        }
700        else {
701            e.setAccountNumber(icrRateDetail.getAccountNumber());
702            e.setSubAccountNumber(icrRateDetail.getSubAccountNumber());
703            e.setChartOfAccountsCode(icrRateDetail.getChartOfAccountsCode());
704            // TODO Reporting thing line 1946
705        }
706        // take care of infinite recursive error case - do not generate entries
707        if  ((et.getAccountNumber().equals(e.getAccountNumber() )) &&
708                ( et.getChartOfAccountsCode().equals(e.getChartOfAccountsCode())) &&
709                (et.getSubAccountNumber().equals(e.getSubAccountNumber())) &&
710                (et.getObjectCode().equals(e.getFinancialObjectCode())) &&
711                (et.getSubObjectCode().equals(e.getFinancialSubObjectCode()))) {
712            List<Message> warnings = new ArrayList<Message>();
713            warnings.add(new Message("Infinite recursive encumbrance error " +  et.getChartOfAccountsCode() + " " + et.getAccountNumber() + " " + et.getSubAccountNumber() + " " + et.getObjectCode() + " " + et.getSubObjectCode(), Message.TYPE_WARNING));
714            reportWriterService.writeError(et, warnings);
715            return;
716        } 
717
718        e.setFinancialDocumentTypeCode(parameterService.getParameterValueAsString(PosterIndirectCostRecoveryEntriesStep.class, OLEConstants.SystemGroupParameterNames.GL_INDIRECT_COST_RECOVERY));
719        e.setFinancialSystemOriginationCode(parameterService.getParameterValueAsString(OleParameterConstants.GENERAL_LEDGER_BATCH.class, OLEConstants.SystemGroupParameterNames.GL_ORIGINATION_CODE));
720        SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_STRING);
721        e.setDocumentNumber(sdf.format(runDate));
722        if (OLEConstants.GL_DEBIT_CODE.equals(icrRateDetail.getTransactionDebitIndicator())) {
723            e.setTransactionLedgerEntryDescription(getChargeDescription(pct, et.getObjectCode(), icrGenerationMetadata.getIndirectCostRecoveryTypeCode(), et.getAccountObjectDirectCostAmount().abs()));
724        }
725        else {
726            e.setTransactionLedgerEntryDescription(getOffsetDescription(pct, et.getAccountObjectDirectCostAmount().abs(), et.getChartOfAccountsCode(), et.getAccountNumber()));
727        }
728        e.setTransactionDate(new java.sql.Date(runDate.getTime()));
729        e.setTransactionDebitCreditCode(icrRateDetail.getTransactionDebitIndicator());
730        e.setFinancialBalanceTypeCode(et.getBalanceTypeCode());
731        e.setUniversityFiscalYear(et.getUniversityFiscalYear());
732        e.setUniversityFiscalPeriodCode(et.getUniversityFiscalAccountingPeriod());
733
734        ObjectCode oc = objectCodeService.getByPrimaryId(e.getUniversityFiscalYear(), e.getChartOfAccountsCode(), e.getFinancialObjectCode());
735        if (oc == null) {
736            LOG.warn(configurationService.getPropertyValueAsString(OLEKeyConstants.ERROR_OBJECT_CODE_NOT_FOUND_FOR) + e.getUniversityFiscalYear() + "," + e.getChartOfAccountsCode() + "," + e.getFinancialObjectCode());
737            e.setFinancialObjectCode(icrRateDetail.getFinancialObjectCode()); // this will be written out the ICR file. Then, when that file attempts to post, the transaction won't validate and will end up in the icr error file
738        } else {
739            e.setFinancialObjectTypeCode(oc.getFinancialObjectTypeCode());
740        }
741
742        if (generatedTransactionAmount.isNegative()) {
743            if (OLEConstants.GL_DEBIT_CODE.equals(icrRateDetail.getTransactionDebitIndicator())) {
744                e.setTransactionDebitCreditCode(OLEConstants.GL_CREDIT_CODE);
745            }
746            else {
747                e.setTransactionDebitCreditCode(OLEConstants.GL_DEBIT_CODE);
748            }
749            e.setTransactionLedgerEntryAmount(generatedTransactionAmount.negated());
750        }
751        else {
752            e.setTransactionLedgerEntryAmount(generatedTransactionAmount);
753        }
754
755        if (et.getBalanceTypeCode().equals(et.getOption().getExtrnlEncumFinBalanceTypCd()) || et.getBalanceTypeCode().equals(et.getOption().getIntrnlEncumFinBalanceTypCd()) || et.getBalanceTypeCode().equals(et.getOption().getPreencumbranceFinBalTypeCd()) || et.getBalanceTypeCode().equals(et.getOption().getCostShareEncumbranceBalanceTypeCd())) {
756            e.setDocumentNumber(parameterService.getParameterValueAsString(PosterIndirectCostRecoveryEntriesStep.class, OLEConstants.SystemGroupParameterNames.GL_INDIRECT_COST_RECOVERY));
757        }
758        e.setProjectCode(et.getProjectCode());
759        if (GeneralLedgerConstants.getDashOrganizationReferenceId().equals(et.getOrganizationReferenceId())) {
760            e.setOrganizationReferenceId(null);
761        }
762        else {
763            e.setOrganizationReferenceId(et.getOrganizationReferenceId());
764        }
765        // TODO 2031-2039
766        try {
767            createOutputEntry(e, group);
768        }
769        catch (IOException ioe) {
770            LOG.error("generateTransactions Stopped: " + ioe.getMessage());
771            throw new RuntimeException("generateTransactions Stopped: " + ioe.getMessage(), ioe);
772        }
773
774        // Now generate Offset
775        e = new OriginEntryFull(e);
776        if (OLEConstants.GL_DEBIT_CODE.equals(e.getTransactionDebitCreditCode())) {
777            e.setTransactionDebitCreditCode(OLEConstants.GL_CREDIT_CODE);
778        }
779        else {
780            e.setTransactionDebitCreditCode(OLEConstants.GL_DEBIT_CODE);
781        }
782        e.setFinancialSubObjectCode(OLEConstants.getDashFinancialSubObjectCode());
783
784        String offsetBalanceSheetObjectCodeNumber = determineIcrOffsetBalanceSheetObjectCodeNumber(e, et, icrRateDetail);
785        e.setFinancialObjectCode(offsetBalanceSheetObjectCodeNumber);
786        ObjectCode balSheetObjectCode = objectCodeService.getByPrimaryId(icrRateDetail.getUniversityFiscalYear(), e.getChartOfAccountsCode(), offsetBalanceSheetObjectCodeNumber);
787        if (balSheetObjectCode == null) {
788            List<Message> warnings = new ArrayList<Message>();
789            warnings.add(new Message(configurationService.getPropertyValueAsString(OLEKeyConstants.ERROR_INVALID_OFFSET_OBJECT_CODE) + icrRateDetail.getUniversityFiscalYear() + "-" + e.getChartOfAccountsCode() + "-" +offsetBalanceSheetObjectCodeNumber, Message.TYPE_WARNING));
790            reportWriterService.writeError(et, warnings);
791            
792        }
793        else {
794            e.setFinancialObjectTypeCode(balSheetObjectCode.getFinancialObjectTypeCode());
795        }
796
797        if (OLEConstants.GL_DEBIT_CODE.equals(icrRateDetail.getTransactionDebitIndicator())) {
798            e.setTransactionLedgerEntryDescription(getChargeDescription(pct, et.getObjectCode(), icrGenerationMetadata.getIndirectCostRecoveryTypeCode(), et.getAccountObjectDirectCostAmount().abs()));
799        }
800        else {
801            e.setTransactionLedgerEntryDescription(getOffsetDescription(pct, et.getAccountObjectDirectCostAmount().abs(), et.getChartOfAccountsCode(), et.getAccountNumber()));
802        }
803
804        try {
805            flexibleOffsetAccountService.updateOffset(e);
806        }
807        catch (InvalidFlexibleOffsetException ex) {
808            List<Message> warnings = new ArrayList<Message>();
809            warnings.add(new Message("FAILED TO GENERATE FLEXIBLE OFFSETS " + ex.getMessage(), Message.TYPE_WARNING));
810            reportWriterService.writeError(et, warnings);
811            LOG.warn("FAILED TO GENERATE FLEXIBLE OFFSETS FOR EXPENDITURE TRANSACTION " + et.toString(), ex);
812        }
813
814        try {
815            createOutputEntry(e, group);
816        }
817        catch (IOException ioe) {
818            LOG.error("generateTransactions Stopped: " + ioe.getMessage());
819            throw new RuntimeException("generateTransactions Stopped: " + ioe.getMessage(), ioe);
820        }
821    }
822
823    public final static KualiDecimal ONEHUNDRED = new KualiDecimal("100");
824    public final static DecimalFormat DFPCT = new DecimalFormat("#0.000");
825    public final static DecimalFormat DFAMT = new DecimalFormat("##########.00");
826    public final static BigDecimal BDONEHUNDRED = new BigDecimal("100");
827
828    /**
829     * Returns ICR Generation Metadata based on SubAccount information if the SubAccount on the expenditure transaction is properly
830     * set up for ICR
831     * 
832     * @param et
833     * @param reportErrors
834     * @return null if the ET does not have a SubAccount properly set up for ICR
835     */
836    protected IndirectCostRecoveryGenerationMetadata retrieveSubAccountIndirectCostRecoveryMetadata(ExpenditureTransaction et) {
837        SubAccount subAccount = accountingCycleCachingService.getSubAccount(et.getChartOfAccountsCode(), et.getAccountNumber(), et.getSubAccountNumber());
838        if (ObjectUtils.isNotNull(subAccount)) {
839            subAccount.setA21SubAccount(accountingCycleCachingService.getA21SubAccount(et.getChartOfAccountsCode(), et.getAccountNumber(), et.getSubAccountNumber()));
840        }
841
842        if (ObjectUtils.isNotNull(subAccount) && ObjectUtils.isNotNull(subAccount.getA21SubAccount())) {
843            A21SubAccount a21SubAccount = subAccount.getA21SubAccount();
844            if (StringUtils.isBlank(a21SubAccount.getIndirectCostRecoveryTypeCode()) && StringUtils.isBlank(a21SubAccount.getFinancialIcrSeriesIdentifier()) &&
845                    a21SubAccount.getA21ActiveIndirectCostRecoveryAccounts().isEmpty()) {
846                // all ICR fields were blank, therefore, this sub account was not set up for ICR
847                return null;
848            }
849            // refresh the indirect cost recovery account, accounting cycle style!
850            Account refreshSubAccount = null;
851            if (!StringUtils.isBlank(a21SubAccount.getChartOfAccountsCode()) && !StringUtils.isBlank(a21SubAccount.getAccountNumber())) {
852                refreshSubAccount = accountingCycleCachingService.getAccount(a21SubAccount.getChartOfAccountsCode(), a21SubAccount.getAccountNumber());
853            }
854
855            // these fields will be used to construct warning messages
856            String warningMessagePattern = configurationService.getPropertyValueAsString(OLEKeyConstants.WARNING_ICR_GENERATION_PROBLEM_WITH_A21SUBACCOUNT_FIELD_BLANK_INVALID);
857            String subAccountBOLabel = dataDictionaryService.getDataDictionary().getBusinessObjectEntry(SubAccount.class.getName()).getObjectLabel();
858            String subAccountValue = subAccount.getChartOfAccountsCode() + "-" + subAccount.getAccountNumber() + "-" + subAccount.getSubAccountNumber();
859            String accountBOLabel = dataDictionaryService.getDataDictionary().getBusinessObjectEntry(Account.class.getName()).getObjectLabel();
860            String accountValue = et.getChartOfAccountsCode() + "-" + et.getAccountNumber();
861
862            boolean subAccountOK = true;
863
864            // there were some ICR fields that were filled in, make sure they're all filled in and are valid values
865            a21SubAccount.setIndirectCostRecoveryType(accountingCycleCachingService.getIndirectCostRecoveryType(a21SubAccount.getIndirectCostRecoveryTypeCode()));
866            if (StringUtils.isBlank(a21SubAccount.getIndirectCostRecoveryTypeCode()) || ObjectUtils.isNull(a21SubAccount.getIndirectCostRecoveryType())) {
867                String errorFieldName = dataDictionaryService.getAttributeShortLabel(A21SubAccount.class, OLEPropertyConstants.INDIRECT_COST_RECOVERY_TYPE_CODE);
868                String warningMessage = MessageFormat.format(warningMessagePattern, errorFieldName, subAccountBOLabel, subAccountValue, accountBOLabel, accountValue);
869                reportWriterService.writeError(et, new Message(warningMessage, Message.TYPE_WARNING));
870                subAccountOK = false;
871            }
872
873            if (StringUtils.isBlank(a21SubAccount.getFinancialIcrSeriesIdentifier())) {
874                Map<String, Object> icrRatePkMap = new HashMap<String, Object>();
875                icrRatePkMap.put(OLEPropertyConstants.UNIVERSITY_FISCAL_YEAR, et.getUniversityFiscalYear());
876                icrRatePkMap.put(OLEPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, a21SubAccount.getFinancialIcrSeriesIdentifier());
877                IndirectCostRecoveryRate indirectCostRecoveryRate = (IndirectCostRecoveryRate) businessObjectService.findByPrimaryKey(IndirectCostRecoveryRate.class, icrRatePkMap);
878                if (indirectCostRecoveryRate == null) {
879                    String errorFieldName = dataDictionaryService.getAttributeShortLabel(A21SubAccount.class, OLEPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER);
880                    String warningMessage = MessageFormat.format(warningMessagePattern, errorFieldName, subAccountBOLabel, subAccountValue, accountBOLabel, accountValue);
881                    reportWriterService.writeError(et, new Message(warningMessage, Message.TYPE_WARNING));
882                    subAccountOK = false;
883                }
884            }
885
886            if (a21SubAccount.getA21ActiveIndirectCostRecoveryAccounts().isEmpty() || ObjectUtils.isNull(refreshSubAccount)) {
887                String errorFieldName = dataDictionaryService.getAttributeShortLabel(A21IndirectCostRecoveryAccount.class, OLEPropertyConstants.ICR_CHART_OF_ACCOUNTS_CODE) + "/" + dataDictionaryService.getAttributeShortLabel(A21IndirectCostRecoveryAccount.class, OLEPropertyConstants.ICR_ACCOUNT_NUMBER);
888                String warningMessage = MessageFormat.format(warningMessagePattern, errorFieldName, subAccountBOLabel, subAccountValue, accountBOLabel, accountValue);
889                reportWriterService.writeError(et, new Message(warningMessage, Message.TYPE_WARNING));
890                subAccountOK = false;
891            }
892
893            if (subAccountOK) {
894                IndirectCostRecoveryGenerationMetadata metadata = new IndirectCostRecoveryGenerationMetadata(a21SubAccount.getIndirectCostRecoveryTypeCode(),
895                        a21SubAccount.getFinancialIcrSeriesIdentifier());
896                
897                List<IndirectCostRecoveryAccountDistributionMetadata> icrAccountList = metadata.getAccountLists();
898                for (A21IndirectCostRecoveryAccount a21 : a21SubAccount.getA21ActiveIndirectCostRecoveryAccounts()){
899                    icrAccountList.add(new IndirectCostRecoveryAccountDistributionMetadata(a21));
900                }
901                return metadata;
902            }
903        }
904        return null;
905    }
906
907
908    protected IndirectCostRecoveryGenerationMetadata retrieveAccountIndirectCostRecoveryMetadata(ExpenditureTransaction et) {
909        Account account = et.getAccount();
910
911        IndirectCostRecoveryGenerationMetadata metadata = new IndirectCostRecoveryGenerationMetadata(account.getAcctIndirectCostRcvyTypeCd(),
912                account.getFinancialIcrSeriesIdentifier());
913        
914        List<IndirectCostRecoveryAccountDistributionMetadata> icrAccountList = metadata.getAccountLists();
915        for (IndirectCostRecoveryAccount icr : account.getActiveIndirectCostRecoveryAccounts()){
916            icrAccountList.add(new IndirectCostRecoveryAccountDistributionMetadata(icr));
917        }
918
919        return metadata;
920    }
921
922    /**
923     * Generates a percent of a KualiDecimal amount (great for finding out how much of an origin entry should be recouped by
924     * indirect cost recovery)
925     * 
926     * @param amount the original amount
927     * @param percent the percentage of that amount to calculate
928     * @return the percent of the amount
929     */
930    protected KualiDecimal getPercentage(KualiDecimal amount, BigDecimal percent) {
931        BigDecimal result = amount.bigDecimalValue().multiply(percent).divide(BDONEHUNDRED, 2, BigDecimal.ROUND_DOWN);
932        return new KualiDecimal(result);
933    }
934
935    /**
936     * Generates the description of a charge
937     * 
938     * @param rate the ICR rate for this entry
939     * @param objectCode the object code of this entry
940     * @param type the ICR type code of this entry's account
941     * @param amount the amount of this entry
942     * @return a description for the charge entry
943     */
944    protected String getChargeDescription(BigDecimal rate, String objectCode, String type, KualiDecimal amount) {
945        BigDecimal newRate = rate.multiply(PosterServiceImpl.BDONEHUNDRED);
946
947        StringBuffer desc = new StringBuffer("CHG ");
948        if (newRate.doubleValue() < 10) {
949            desc.append(" ");
950        }
951        desc.append(DFPCT.format(newRate));
952        desc.append("% ON ");
953        desc.append(objectCode);
954        desc.append(" (");
955        desc.append(type);
956        desc.append(")  ");
957        String amt = DFAMT.format(amount);
958        while (amt.length() < 13) {
959            amt = " " + amt;
960        }
961        desc.append(amt);
962        return desc.toString();
963    }
964
965    /**
966     * Returns the description of a debit origin entry created by generateTransactions
967     * 
968     * @param rate the ICR rate that relates to this entry
969     * @param amount the amount of this entry
970     * @param chartOfAccountsCode the chart codce of the debit entry
971     * @param accountNumber the account number of the debit entry
972     * @return a description for the debit entry
973     */
974    protected String getOffsetDescription(BigDecimal rate, KualiDecimal amount, String chartOfAccountsCode, String accountNumber) {
975        BigDecimal newRate = rate.multiply(PosterServiceImpl.BDONEHUNDRED);
976
977        StringBuffer desc = new StringBuffer("RCV ");
978        if (newRate.doubleValue() < 10) {
979            desc.append(" ");
980        }
981        desc.append(DFPCT.format(newRate));
982        desc.append("% ON ");
983        String amt = DFAMT.format(amount);
984        while (amt.length() < 13) {
985            amt = " " + amt;
986        }
987        desc.append(amt);
988        desc.append(" FRM ");
989        // desc.append(chartOfAccountsCode);
990        // desc.append("-");
991        desc.append(accountNumber);
992        return desc.toString();
993    }
994
995    /**
996     * Increments a named count holding statistics about posted transactions
997     * 
998     * @param reporting a Map of counts generated by this process
999     * @param destination the destination of a given transaction
1000     * @param operation the operation being performed on the transaction
1001     */
1002    protected void addReporting(Map reporting, String destination, String operation) {
1003        String key = destination + "," + operation;
1004        //TODO: remove this if block. Added to troubleshoot FSKD-194.
1005        if("GL_EXPEND_TRN_MT".equals(destination)){
1006            LOG.info("Counting GLEX operation: "+operation);
1007        }
1008        if (reporting.containsKey(key)) {
1009            Integer c = (Integer) reporting.get(key);
1010            reporting.put(key, new Integer(c.intValue() + 1));
1011        }
1012        else {
1013            reporting.put(key, new Integer(1));
1014        }
1015    }
1016
1017    protected String determineIcrOffsetBalanceSheetObjectCodeNumber(OriginEntryInformation offsetEntry, ExpenditureTransaction et, IndirectCostRecoveryRateDetail icrRateDetail) {
1018        String icrEntryDocumentType = parameterService.getParameterValueAsString(PosterIndirectCostRecoveryEntriesStep.class, OLEConstants.SystemGroupParameterNames.GL_INDIRECT_COST_RECOVERY);
1019        OffsetDefinition offsetDefinition = offsetDefinitionService.getByPrimaryId(offsetEntry.getUniversityFiscalYear(), offsetEntry.getChartOfAccountsCode(), icrEntryDocumentType, et.getBalanceTypeCode());
1020        if (!ObjectUtils.isNull(offsetDefinition)) {
1021            return offsetDefinition.getFinancialObjectCode();
1022        } else {
1023            return null;
1024        }
1025    }
1026    
1027    public void setVerifyTransaction(VerifyTransaction vt) {
1028        verifyTransaction = vt;
1029    }
1030
1031    public void setTransactionPosters(List p) {
1032        transactionPosters = p;
1033    }
1034
1035    public void setOriginEntryService(OriginEntryService oes) {
1036        originEntryService = oes;
1037    }
1038
1039    public void setOriginEntryGroupService(OriginEntryGroupService oes) {
1040        originEntryGroupService = oes;
1041    }
1042
1043    public void setDateTimeService(DateTimeService dts) {
1044        dateTimeService = dts;
1045    }
1046
1047    public void setReversalDao(ReversalDao red) {
1048        reversalDao = red;
1049    }
1050
1051    public void setUniversityDateDao(UniversityDateDao udd) {
1052        universityDateDao = udd;
1053    }
1054
1055    public void setAccountingPeriodService(AccountingPeriodService aps) {
1056        accountingPeriodService = aps;
1057    }
1058
1059    public void setExpenditureTransactionDao(ExpenditureTransactionDao etd) {
1060        expenditureTransactionDao = etd;
1061    }
1062
1063    public void setIndirectCostRecoveryRateDetailDao(IndirectCostRecoveryRateDetailDao iaed) {
1064        indirectCostRecoveryRateDetailDao = iaed;
1065    }
1066
1067    public void setObjectCodeService(ObjectCodeService ocs) {
1068        objectCodeService = ocs;
1069    }
1070
1071    public void setConfigurationService(ConfigurationService configurationService) {
1072        this.configurationService = configurationService;
1073    }
1074
1075    public void setParameterService(ParameterService parameterService) {
1076        this.parameterService = parameterService;
1077    }
1078
1079    public void setFlexibleOffsetAccountService(FlexibleOffsetAccountService flexibleOffsetAccountService) {
1080        this.flexibleOffsetAccountService = flexibleOffsetAccountService;
1081    }
1082
1083    public RunDateService getRunDateService() {
1084        return runDateService;
1085    }
1086
1087    public void setRunDateService(RunDateService runDateService) {
1088        this.runDateService = runDateService;
1089    }
1090
1091    protected void createOutputEntry(Transaction entry, PrintStream group) throws IOException {
1092        OriginEntryFull oef = new OriginEntryFull();
1093        oef.copyFieldsFromTransaction(entry);
1094        try {
1095            group.printf("%s\n", oef.getLine());
1096        }
1097        catch (Exception e) {
1098            throw new IOException(e.toString());
1099        }
1100    }
1101
1102    protected void writeErrorEntry(String line, PrintStream invaliGroup) throws IOException {
1103        try {
1104            invaliGroup.printf("%s\n", line);
1105        } catch (Exception e) {
1106            throw new IOException(e.toString());
1107        }
1108    }
1109    
1110    public AccountingCycleCachingService getAccountingCycleCachingService() {
1111        return accountingCycleCachingService;
1112    }
1113
1114    public void setAccountingCycleCachingService(AccountingCycleCachingService accountingCycleCachingService) {
1115        this.accountingCycleCachingService = accountingCycleCachingService;
1116    }
1117
1118    public void setSubAccountService(SubAccountService subAccountService) {
1119        this.subAccountService = subAccountService;
1120    }
1121
1122    public void setOffsetDefinitionService(OffsetDefinitionService offsetDefinitionService) {
1123        this.offsetDefinitionService = offsetDefinitionService;
1124    }
1125
1126    protected DataDictionaryService getDataDictionaryService() {
1127        return dataDictionaryService;
1128    }
1129
1130    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
1131        this.dataDictionaryService = dataDictionaryService;
1132    }
1133
1134    protected BusinessObjectService getBusinessObjectService() {
1135        return businessObjectService;
1136    }
1137
1138    public void setBusinessObjectService(BusinessObjectService businessObjectService) {
1139        this.businessObjectService = businessObjectService;
1140    }
1141
1142    protected boolean shouldIgnoreExpenditureTransaction(ExpenditureTransaction et) {
1143        if (ObjectUtils.isNotNull(et.getOption())) {
1144            SystemOptions options = et.getOption();
1145            return StringUtils.isNotBlank(options.getActualFinancialBalanceTypeCd()) && !options.getActualFinancialBalanceTypeCd().equals(et.getBalanceTypeCode());
1146        }
1147        return true;
1148    }
1149
1150    public void setBatchFileDirectoryName(String batchFileDirectoryName) {
1151        this.batchFileDirectoryName = batchFileDirectoryName;
1152    }
1153
1154    public void setPersistenceStructureService(PersistenceStructureService persistenceStructureService) {
1155        this.persistenceStructureService = persistenceStructureService;
1156    }
1157
1158    /**
1159     * Gets the persistenceStructureService attribute. 
1160     * @return Returns the persistenceStructureService.
1161     */
1162    public PersistenceStructureService getPersistenceStructureService() {
1163        return persistenceStructureService;
1164    }
1165
1166    public void setReportWriterService(ReportWriterService reportWriterService) {
1167        this.reportWriterService = reportWriterService;
1168    }
1169
1170    public void setErrorListingReportWriterService(ReportWriterService errorListingReportWriterService) {
1171        this.errorListingReportWriterService = errorListingReportWriterService;
1172    }
1173
1174    public void setReversalReportWriterService(ReportWriterService reversalReportWriterService) {
1175        this.reversalReportWriterService = reversalReportWriterService;
1176    }
1177
1178    public void setLedgerSummaryReportWriterService(ReportWriterService ledgerSummaryReportWriterService) {
1179        this.ledgerSummaryReportWriterService = ledgerSummaryReportWriterService;
1180    }
1181}