/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.ld.batch.service.impl; import static org.kuali.kfs.module.ld.LaborConstants.DestinationNames.ORIGN_ENTRY; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.PrintStream; import java.sql.Date; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.gl.GeneralLedgerConstants; import org.kuali.kfs.gl.batch.service.PostTransaction; import org.kuali.kfs.gl.batch.service.VerifyTransaction; import org.kuali.kfs.gl.report.LedgerSummaryReport; import org.kuali.kfs.gl.report.TransactionListingReport; import org.kuali.kfs.gl.service.OriginEntryGroupService; import org.kuali.kfs.module.ld.LaborConstants; import org.kuali.kfs.module.ld.LaborConstants.Poster; import org.kuali.kfs.module.ld.batch.LaborPosterStep; import org.kuali.kfs.module.ld.batch.service.LaborPosterService; import org.kuali.kfs.module.ld.businessobject.LaborOriginEntry; import org.kuali.kfs.module.ld.document.validation.impl.TransactionFieldValidator; import org.kuali.kfs.module.ld.service.LaborOriginEntryService; import org.kuali.kfs.module.ld.service.LaborTransactionDescriptionService; import org.kuali.kfs.module.ld.util.LaborLedgerUnitOfWork; import org.kuali.kfs.module.ld.util.LaborOriginEntryFileIterator; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.Message; import org.kuali.kfs.sys.MessageBuilder; import org.kuali.kfs.sys.service.ReportWriterService; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.krad.util.ObjectUtils; import org.springframework.transaction.annotation.Transactional; /** * The Labor Ledger Poster accepts pending entries generated by Labor Ledger e-docs (such as Salary Expense Transfer and Benefit * Expense Transfer), and combines them with entries from external systems. It edits the entries for validity. Invalid entries can * be marked for Labor Ledger Error Correction process. The Poster writes valid entries to the Labor Ledger Entry table, updates * balances in the Labor Ledger Balance table, and summarizes the entries for posting to the General Ledger. */ @Transactional public class LaborPosterServiceImpl implements LaborPosterService { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(LaborPosterServiceImpl.class); private LaborOriginEntryService laborOriginEntryService; private OriginEntryGroupService originEntryGroupService; private LaborTransactionDescriptionService laborTransactionDescriptionService; private ReportWriterService reportWriterService; private ReportWriterService errorListingReportWriterService; private ReportWriterService ledgerSummaryReportWriterService; private ReportWriterService laborGlEntryStatisticsReportWriterService; private DateTimeService dateTimeService; private VerifyTransaction laborPosterTransactionValidator; private ParameterService parameterService; private PostTransaction laborLedgerEntryPoster; private PostTransaction laborLedgerBalancePoster; private PostTransaction laborGLLedgerEntryPoster; private int numberOfErrorOriginEntry; private String batchFileDirectoryName; private PrintStream POSTER_OUTPUT_ERR_FILE_ps; /** * @see org.kuali.kfs.module.ld.batch.service.LaborPosterService#postMainEntries() */ public void postMainEntries() { LOG.debug("postMainEntries() started"); Date runDate = dateTimeService.getCurrentSqlDate(); this.postLaborLedgerEntries(runDate); } /** * post the qualified origin entries into Labor Ledger tables * * @param validGroup the origin entry group that holds the valid transactions * @param invalidGroup the origin entry group that holds the invalid transactions * @param runDate the data when the process is running */ protected void postLaborLedgerEntries(Date runDate) { LOG.debug("postLaborLedgerEntries() started.........................."); numberOfErrorOriginEntry = 0; // change file name to FIS String postInputFileName = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.POSTER_INPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION; String postErrFileName = batchFileDirectoryName + File.separator + LaborConstants.BatchFileSystem.POSTER_ERROR_OUTPUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION; FileReader INPUT_GLE_FILE = null; try { INPUT_GLE_FILE = new FileReader(postInputFileName); } catch (FileNotFoundException e) { throw new RuntimeException(e); } try { POSTER_OUTPUT_ERR_FILE_ps = new PrintStream(postErrFileName); } catch (IOException e) { LOG.error("postLaborLedgerEntries cannot open file: " + e.getMessage(), e); throw new RuntimeException(e); } int lineNumber = 0; int loadedCount = 0; int numberOfSelectedOriginEntry = 0; LaborLedgerUnitOfWork laborLedgerUnitOfWork = new LaborLedgerUnitOfWork(); LedgerSummaryReport ledgerSummaryReport = new LedgerSummaryReport(); Map<String, Integer> reportSummary = this.constructPosterReportSummary(); Map<String, Integer> glEntryReportSummary = this.constructGlEntryReportSummary(); try { BufferedReader INPUT_GLE_FILE_br = new BufferedReader(INPUT_GLE_FILE); String currentLine = INPUT_GLE_FILE_br.readLine(); while (currentLine != null) { LaborOriginEntry laborOriginEntry = null; try { lineNumber++; if (!StringUtils.isEmpty(currentLine) && !StringUtils.isBlank(currentLine.trim())) { laborOriginEntry = new LaborOriginEntry(); // checking parsing process and stop poster when it has errors. List<Message> parsingError = new ArrayList<Message>(); parsingError = laborOriginEntry.setFromTextFileForBatch(currentLine, lineNumber); if (parsingError.size() > 0) { throw new RuntimeException("Exception happened from parsing process"); } loadedCount++; if (loadedCount % 1000 == 0) { LOG.info(loadedCount + " " + laborOriginEntry.toString()); } boolean isPostable = this.postSingleEntryIntoLaborLedger(laborOriginEntry, reportSummary, runDate, currentLine); if (isPostable) { this.updateReportSummary(glEntryReportSummary, ORIGN_ENTRY, KFSConstants.OperationType.READ); this.writeLaborGLEntry(laborOriginEntry, laborLedgerUnitOfWork, runDate, lineNumber, glEntryReportSummary); ledgerSummaryReport.summarizeEntry(laborOriginEntry); numberOfSelectedOriginEntry++; laborOriginEntry = null; } } currentLine = INPUT_GLE_FILE_br.readLine(); } catch (RuntimeException re) { // catch here again, it should be from postSingleEntryIntoLaborLedger LOG.error("postLaborLedgerEntries stopped due to: " + re.getMessage() + " on line number : " + loadedCount, re); LOG.error("laborOriginEntry failure occured on: " + laborOriginEntry == null ? null : laborOriginEntry.toString()); throw new RuntimeException("Unable to execute: " + re.getMessage() + " on line number : " + loadedCount, re); } } this.writeLaborGLEntry(null, laborLedgerUnitOfWork, runDate, lineNumber, glEntryReportSummary); INPUT_GLE_FILE_br.close(); INPUT_GLE_FILE.close(); POSTER_OUTPUT_ERR_FILE_ps.close(); this.fillPosterReportWriter(lineNumber, reportSummary, glEntryReportSummary); this.fillGlEntryReportWriter(glEntryReportSummary); // Generate Error Listing Report ledgerSummaryReport.writeReport(ledgerSummaryReportWriterService); new TransactionListingReport().generateReport(errorListingReportWriterService, new LaborOriginEntryFileIterator(new File(postErrFileName))); } catch (IOException ioe) { LOG.error("postLaborLedgerEntries stopped due to: " + ioe.getMessage(), ioe); throw new RuntimeException("Unable to execute: " + ioe.getMessage() + " on line number : " + loadedCount, ioe); } } /** * post the given entry into the labor ledger tables if the entry is qualified; otherwise report error * * @param originEntry the given origin entry, a transaction * @param reportSummary the report summary object that need to be update when a transaction is posted * @param runDate the data when the process is running * @return true if the given transaction is posted into ledger tables; otherwise, return false */ protected boolean postSingleEntryIntoLaborLedger(LaborOriginEntry originEntry, Map<String, Integer> reportSummary, Date runDate, String line) { // reject the invalid entry so that it can be available for error correction List<Message> errors = new ArrayList<Message>(); try { errors = this.validateEntry(originEntry); } catch (Exception e) { errors.add(new Message(e.toString() + " occurred for this record.", Message.TYPE_FATAL)); } if (errors != null && !errors.isEmpty()) { reportWriterService.writeError(originEntry, errors); numberOfErrorOriginEntry += errors.size(); writeErrorEntry(line); return false; } String operationOnLedgerEntry = postAsLedgerEntry(originEntry, runDate); updateReportSummary(reportSummary, laborLedgerEntryPoster.getDestinationName(), operationOnLedgerEntry); String operationOnLedgerBalance = updateLedgerBalance(originEntry, runDate); updateReportSummary(reportSummary, laborLedgerBalancePoster.getDestinationName(), operationOnLedgerBalance); return true; } /** * validate the given entry, and generate an error list if the entry cannot meet the business rules * * @param originEntry the given origin entry, a transcation * @return error message list. If the given transaction is invalid, the list has message(s); otherwise, it is empty */ protected List<Message> validateEntry(LaborOriginEntry originEntry) { return laborPosterTransactionValidator.verifyTransaction(originEntry); } /** * post the given entry to the labor entry table * * @param originEntry the given origin entry, a transaction * @param postDate the data when the transaction is processes return the operation type of the process */ protected String postAsLedgerEntry(LaborOriginEntry originEntry, Date postDate) { return laborLedgerEntryPoster.post(originEntry, 0, postDate, null); } /** * update the labor ledger balance for the given entry * * @param originEntry the given origin entry, a transaction * @param postDate the data when the transaction is processes return the operation type of the process */ protected String updateLedgerBalance(LaborOriginEntry originEntry, Date postDate) { return laborLedgerBalancePoster.post(originEntry, 0, postDate, null); } /** * determine if the given origin entry can be posted back to Labor GL entry * * @param originEntry the given origin entry, atransaction * @return a message list. The list has message(s) if the given origin entry cannot be posted back to Labor GL entry; otherwise, * it is empty */ protected List<Message> isPostableForLaborGLEntry(LaborOriginEntry originEntry) { List<Message> errors = new ArrayList<Message>(); MessageBuilder.addMessageIntoList(errors, TransactionFieldValidator.checkPostablePeridCode(originEntry, getPeriodCodesNotProcessed())); MessageBuilder.addMessageIntoList(errors, TransactionFieldValidator.checkPostableBalanceTypeCode(originEntry, getBalanceTypesNotProcessed())); MessageBuilder.addMessageIntoList(errors, TransactionFieldValidator.checkZeroTotalAmount(originEntry)); return errors; } // construct a poster report summary object protected void fillPosterReportWriter(int lineNumber, Map<String, Integer> reportSummary, Map<String, Integer> glEntryReportSummary) { reportWriterService.writeStatisticLine("SEQUENTIAL RECORDS READ %,9d", lineNumber); reportWriterService.writeStatisticLine("LLEN RECORDS INSERTED (LD_LDGR_ENTR_T) %,9d", reportSummary.get(laborLedgerEntryPoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT)); reportWriterService.writeStatisticLine("LLBL RECORDS INSERTED (LD_LDGR_BAL_T) %,9d", reportSummary.get(laborLedgerBalancePoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT)); reportWriterService.writeStatisticLine("LLBL RECORDS UPDATED (LD_LDGR_BAL_T) %,9d", reportSummary.get(laborLedgerBalancePoster.getDestinationName() + "," + KFSConstants.OperationType.UPDATE)); reportWriterService.writeStatisticLine("LLGL RECORDS INSERTED (LD_LBR_GL_ENTRY_T) %,9d", glEntryReportSummary.get(laborGLLedgerEntryPoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT)); reportWriterService.writeStatisticLine("WARNING RECORDS WRITTEN %,9d", numberOfErrorOriginEntry); } // fill the poster report writer with the collected data protected Map<String, Integer> constructPosterReportSummary() { Map<String, Integer> reportSummary = new HashMap<String, Integer>(); reportSummary.put(laborLedgerEntryPoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT, 0); reportSummary.put(laborLedgerBalancePoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT, 0); reportSummary.put(laborLedgerBalancePoster.getDestinationName() + "," + KFSConstants.OperationType.UPDATE, 0); reportSummary.put(laborGLLedgerEntryPoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT, 0); return reportSummary; } // construct a gl entry report summary object protected Map<String, Integer> constructGlEntryReportSummary() { Map<String, Integer> glEntryReportSummary = new HashMap<String, Integer>(); glEntryReportSummary.put(ORIGN_ENTRY + "," + KFSConstants.OperationType.READ, 0); glEntryReportSummary.put(ORIGN_ENTRY + "," + KFSConstants.OperationType.BYPASS, 0); glEntryReportSummary.put(ORIGN_ENTRY + "," + KFSConstants.OperationType.SELECT, 0); glEntryReportSummary.put(ORIGN_ENTRY + "," + KFSConstants.OperationType.REPORT_ERROR, 0); glEntryReportSummary.put(laborGLLedgerEntryPoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT, 0); return glEntryReportSummary; } // fill the gl entry report writer with the collected data protected void fillGlEntryReportWriter(Map<String, Integer> glEntryReportSummary) { laborGlEntryStatisticsReportWriterService.writeStatisticLine("NUMBER OF RECORDS READ %,9d", glEntryReportSummary.get(ORIGN_ENTRY + "," + KFSConstants.OperationType.READ)); laborGlEntryStatisticsReportWriterService.writeStatisticLine("NUMBER OF RECORDS BYPASSED %,9d", glEntryReportSummary.get(ORIGN_ENTRY + "," + KFSConstants.OperationType.BYPASS)); laborGlEntryStatisticsReportWriterService.writeStatisticLine("NUMBER OF RECORDS SELECTED %,9d", glEntryReportSummary.get(ORIGN_ENTRY + "," + KFSConstants.OperationType.SELECT)); laborGlEntryStatisticsReportWriterService.writeStatisticLine("NUMBER OF RECORDS IN ERROR %,9d", glEntryReportSummary.get(ORIGN_ENTRY + "," + KFSConstants.OperationType.REPORT_ERROR)); laborGlEntryStatisticsReportWriterService.writeStatisticLine("NUMBER OF RECORDS INSERTED %,9d", glEntryReportSummary.get(laborGLLedgerEntryPoster.getDestinationName() + "," + KFSConstants.OperationType.INSERT)); } /** * summary the valid origin entries for the General Ledger * * @param laborOriginEntry the current entry to check for summarization * @param laborLedgerUnitOfWork the current (in process) summarized entry for the GL * @param runDate the data when the process is running * @param lineNumber the line in the input file (used for error message only) */ protected LaborOriginEntry summarizeLaborGLEntries(LaborOriginEntry laborOriginEntry, LaborLedgerUnitOfWork laborLedgerUnitOfWork, Date runDate, int lineNumber, Map<String, Integer> glEntryReportSummary) { // KFSMI-5308: Description update moved here due to requirement for this to happen before consolidation if(ObjectUtils.isNotNull(laborOriginEntry)) { String description = laborTransactionDescriptionService.getTransactionDescription(laborOriginEntry); if(StringUtils.isNotEmpty(description)) { laborOriginEntry.setTransactionLedgerEntryDescription(description); } } LaborOriginEntry summarizedEntry = null; if (laborLedgerUnitOfWork.canContain(laborOriginEntry)) { laborLedgerUnitOfWork.addEntryIntoUnit(laborOriginEntry); updateReportSummary(glEntryReportSummary, ORIGN_ENTRY, KFSConstants.OperationType.SELECT); } else { summarizedEntry = laborLedgerUnitOfWork.getWorkingEntry(); laborLedgerUnitOfWork.resetLaborLedgerUnitOfWork(laborOriginEntry); } return summarizedEntry; } protected void writeLaborGLEntry(LaborOriginEntry laborOriginEntry, LaborLedgerUnitOfWork laborLedgerUnitOfWork, Date runDate, int lineNumber, Map<String, Integer> glEntryReportSummary) { LaborOriginEntry summarizedEntry = summarizeLaborGLEntries(laborOriginEntry, laborLedgerUnitOfWork, runDate, lineNumber, glEntryReportSummary); if (summarizedEntry == null) { return; } try { List<Message> errors = this.isPostableForLaborGLEntry(summarizedEntry); if (errors == null || errors.isEmpty()) { String operationType = laborGLLedgerEntryPoster.post(summarizedEntry, 0, runDate, null); updateReportSummary(glEntryReportSummary, laborGLLedgerEntryPoster.getDestinationName(), operationType); } else { updateReportSummary(glEntryReportSummary, ORIGN_ENTRY, KFSConstants.OperationType.BYPASS); } } catch (RuntimeException ioe) { // catch here again, it should be from postSingleEntryIntoLaborLedger LOG.error("postLaborGLEntries stopped due to: " + ioe.getMessage() + " on line number : " + lineNumber, ioe); throw new RuntimeException("Unable to execute: " + ioe.getMessage() + " on line number : " + lineNumber, ioe); } } protected void updateReportSummary(Map<String, Integer> reportSummary, String destination, String operation) { String key = destination + "," + operation; if (reportSummary.containsKey(key)) { Integer count = reportSummary.get(key); reportSummary.put(key, count + 1); } else { reportSummary.put(key, 1); } } protected void writeErrorEntry(String line) { try { POSTER_OUTPUT_ERR_FILE_ps.printf("%s\n", line); } catch (Exception e) { LOG.error("postAsProcessedOriginEntry stopped due to: " + e.getMessage(), e); throw new RuntimeException("Unable to execute: " + e.getMessage(), e); } } /** * Get a set of the balance type codes that are bypassed by Labor Poster * * @return a set of the balance type codes that are bypassed by Labor Poster */ public Collection<String> getBalanceTypesNotProcessed() { return parameterService.getParameterValuesAsString(LaborPosterStep.class, Poster.BALANCE_TYPES_NOT_PROCESSED); } /** * Get a set of the fiscal period codes that are bypassed by Labor Poster * * @return a set of the fiscal period codes that are bypassed by Labor Poster */ public Collection<String> getPeriodCodesNotProcessed() { return parameterService.getParameterValuesAsString(LaborPosterStep.class, Poster.PERIOD_CODES_NOT_PROCESSED); } /** * Sets the dateTimeService attribute value. * * @param dateTimeService The dateTimeService to set. */ public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } /** * Sets the laborLedgerBalancePoster attribute value. * * @param laborLedgerBalancePoster The laborLedgerBalancePoster to set. */ public void setLaborLedgerBalancePoster(PostTransaction laborLedgerBalancePoster) { this.laborLedgerBalancePoster = laborLedgerBalancePoster; } /** * Sets the laborGLLedgerEntryPoster attribute value. * * @param laborGLLedgerEntryPoster The laborGLLedgerEntryPoster to set. */ public void setLaborGLLedgerEntryPoster(PostTransaction laborGLLedgerEntryPoster) { this.laborGLLedgerEntryPoster = laborGLLedgerEntryPoster; } /** * Sets the laborLedgerEntryPoster attribute value. * * @param laborLedgerEntryPoster The laborLedgerEntryPoster to set. */ public void setLaborLedgerEntryPoster(PostTransaction laborLedgerEntryPoster) { this.laborLedgerEntryPoster = laborLedgerEntryPoster; } /** * Sets the laborOriginEntryService attribute value. * * @param laborOriginEntryService The laborOriginEntryService to set. */ public void setLaborOriginEntryService(LaborOriginEntryService laborOriginEntryService) { this.laborOriginEntryService = laborOriginEntryService; } /** * Sets the originEntryGroupService attribute value. * * @param originEntryGroupService The originEntryGroupService to set. */ public void setOriginEntryGroupService(OriginEntryGroupService originEntryGroupService) { this.originEntryGroupService = originEntryGroupService; } /** * Sets the laborTransactionDescriptionService attribute value. * @param laborTransactionDescriptionService The laborTransactionDescriptionService to set. */ public void setLaborTransactionDescriptionService(LaborTransactionDescriptionService laborTransactionDescriptionService) { this.laborTransactionDescriptionService = laborTransactionDescriptionService; } /** * Sets the reportWriterService * * @param reportWriterService The reportWriterService to set. */ public void setReportWriterService(ReportWriterService reportWriterService) { this.reportWriterService = reportWriterService; } /** * Sets the errorListingReportWriterService * * @param errorListingReportWriterService The errorListingReportWriterService to set. */ public void setErrorListingReportWriterService(ReportWriterService errorListingReportWriterService) { this.errorListingReportWriterService = errorListingReportWriterService; } /** * Sets the ledgerSummaryReportWriterService * * @param ledgerSummaryReportWriterService The ledgerSummaryReportWriterService to set. */ public void setLedgerSummaryReportWriterService(ReportWriterService ledgerSummaryReportWriterService) { this.ledgerSummaryReportWriterService = ledgerSummaryReportWriterService; } /** * Sets the laborPosterTransactionValidator attribute value. * * @param laborPosterTransactionValidator The laborPosterTransactionValidator to set. */ public void setLaborPosterTransactionValidator(VerifyTransaction laborPosterTransactionValidator) { this.laborPosterTransactionValidator = laborPosterTransactionValidator; } /** * Sets the parameterService attribute value. * * @param parameterService The parameterService to set. */ public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } /** * Sets the laborGlEntryStatisticsReportWriterService attribute value. * * @param laborGlEntryStatisticsReportWriterService The laborGlEntryStatisticsReportWriterService to set. */ public void setLaborGlEntryStatisticsReportWriterService(ReportWriterService laborGlEntryStatisticsReportWriterService) { this.laborGlEntryStatisticsReportWriterService = laborGlEntryStatisticsReportWriterService; } /** * Sets the batchFileDirectoryName attribute value. * * @param batchFileDirectoryName The batchFileDirectoryName to set. */ public void setBatchFileDirectoryName(String batchFileDirectoryName) { this.batchFileDirectoryName = batchFileDirectoryName; } }