001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     * 
008     * http://www.opensource.org/licenses/ecl2.php
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.kfs.gl.batch.service.impl;
017    
018    import java.io.File;
019    import java.io.IOException;
020    import java.io.PrintStream;
021    import java.sql.Date;
022    import java.util.ArrayList;
023    import java.util.Collection;
024    import java.util.Iterator;
025    import java.util.LinkedHashMap;
026    import java.util.Map;
027    
028    import org.apache.commons.lang.StringUtils;
029    import org.kuali.kfs.gl.GeneralLedgerConstants;
030    import org.kuali.kfs.gl.businessobject.Entry;
031    import org.kuali.kfs.gl.businessobject.OriginEntryFull;
032    import org.kuali.kfs.gl.businessobject.OriginEntryInformation;
033    import org.kuali.kfs.gl.businessobject.PendingEntrySummary;
034    import org.kuali.kfs.gl.report.LedgerSummaryReport;
035    import org.kuali.kfs.gl.service.NightlyOutService;
036    import org.kuali.kfs.gl.service.OriginEntryGroupService;
037    import org.kuali.kfs.gl.service.OriginEntryService;
038    import org.kuali.kfs.sys.KFSConstants;
039    import org.kuali.kfs.sys.KFSPropertyConstants;
040    import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
041    import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
042    import org.kuali.kfs.sys.service.ReportWriterService;
043    import org.kuali.rice.kns.service.DataDictionaryService;
044    import org.kuali.rice.kns.service.DateTimeService;
045    import org.kuali.rice.kns.util.KualiDecimal;
046    import org.kuali.rice.kns.util.ObjectUtils;
047    import org.kuali.rice.kns.web.format.CurrencyFormatter;
048    import org.springframework.transaction.annotation.Transactional;
049    
050    /**
051     * This class implements the nightly out batch job.
052     */
053    @Transactional
054    public class NightlyOutServiceImpl implements NightlyOutService {
055        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(NightlyOutServiceImpl.class);
056    
057        private GeneralLedgerPendingEntryService generalLedgerPendingEntryService;
058        private OriginEntryService originEntryService;
059        private DateTimeService dateTimeService;
060        private OriginEntryGroupService originEntryGroupService;
061        private String batchFileDirectoryName;
062        private ReportWriterService pendingEntryListReportWriterService;
063        private ReportWriterService pendingEntrySummaryReportWriterService;
064        private DataDictionaryService dataDictionaryService;
065        
066        /**
067         * Constructs a NightlyOutServiceImpl instance
068         */
069        public NightlyOutServiceImpl() {
070        }
071    
072        /**
073         * Deletes all the pending general ledger entries that have now been copied to origin entries
074         * @see org.kuali.kfs.gl.service.NightlyOutService#deleteCopiedPendingLedgerEntries()
075         */
076        public void deleteCopiedPendingLedgerEntries() {
077            LOG.debug("deleteCopiedPendingLedgerEntries() started");
078    
079            generalLedgerPendingEntryService.deleteByFinancialDocumentApprovedCode(KFSConstants.PENDING_ENTRY_APPROVED_STATUS_CODE.PROCESSED);
080        }
081    
082        /**
083         * Copies the approved pending ledger entries to origin entry table and generates a report
084         * @see org.kuali.kfs.gl.service.NightlyOutService#copyApprovedPendingLedgerEntries()
085         */
086        public void copyApprovedPendingLedgerEntries() {
087            if (LOG.isDebugEnabled()) {
088                LOG.debug("copyApprovedPendingLedgerEntries() started");
089            }
090            Date today = new Date(dateTimeService.getCurrentTimestamp().getTime());
091            
092            Iterator pendingEntries = generalLedgerPendingEntryService.findApprovedPendingLedgerEntries();
093            String outputFile = batchFileDirectoryName + File.separator + GeneralLedgerConstants.BatchFileSystem.NIGHTLY_OUT_FILE + GeneralLedgerConstants.BatchFileSystem.EXTENSION ;
094            PrintStream outputFilePs;
095            
096            try {
097                outputFilePs  = new PrintStream(outputFile);
098            }
099            catch (IOException ioe) {
100                throw new RuntimeException("Cannot open output file "+outputFile+" for writing", ioe);
101            }
102            
103            EntryListReport entryListReport = new EntryListReport();
104            LedgerSummaryReport nightlyOutLedgerSummaryReport = new LedgerSummaryReport();
105            
106            Collection<OriginEntryFull> group = new ArrayList();
107            while (pendingEntries.hasNext()) {
108                // get one pending entry
109                GeneralLedgerPendingEntry pendingEntry = (GeneralLedgerPendingEntry) pendingEntries.next();
110                
111                OriginEntryFull entry = new OriginEntryFull(pendingEntry);
112                
113                // write entry to reports
114                entryListReport.writeEntry(entry, pendingEntryListReportWriterService);
115                nightlyOutLedgerSummaryReport.summarizeEntry(entry);
116    
117                group.add(entry);
118                
119                // copy the pending entry to text file
120                try {
121                    outputFilePs.printf("%s\n", entry.getLine());
122                } catch (Exception e) {
123                    throw new RuntimeException(e);
124                }
125    
126                // update the pending entry to indicate it has been copied
127                pendingEntry.setFinancialDocumentApprovedCode(KFSConstants.PENDING_ENTRY_APPROVED_STATUS_CODE.PROCESSED);
128                pendingEntry.setTransactionDate(today);
129                
130                generalLedgerPendingEntryService.save(pendingEntry);
131            }
132            
133            outputFilePs.close();
134            
135            //create done file    
136            String doneFileName = outputFile.replace(GeneralLedgerConstants.BatchFileSystem.EXTENSION, GeneralLedgerConstants.BatchFileSystem.DONE_FILE_EXTENSION);
137            File doneFile = new File (doneFileName);
138            if (!doneFile.exists()){
139                try {
140                    doneFile.createNewFile();
141                } catch (IOException e) {
142                    throw new RuntimeException();
143                }
144            }
145            
146            // finish writing reports
147            entryListReport.writeReportFooter(pendingEntryListReportWriterService);
148            nightlyOutLedgerSummaryReport.writeReport(pendingEntrySummaryReportWriterService);
149        }
150        
151        
152    
153        public void setGeneralLedgerPendingEntryService(GeneralLedgerPendingEntryService generalLedgerPendingEntryService) {
154            this.generalLedgerPendingEntryService = generalLedgerPendingEntryService;
155        }
156    
157        public void setOriginEntryService(OriginEntryService originEntryService) {
158            this.originEntryService = originEntryService;
159        }
160    
161        public void setOriginEntryGroupService(OriginEntryGroupService originEntryGroupService) {
162            this.originEntryGroupService = originEntryGroupService;
163        }
164    
165        public void setDateTimeService(DateTimeService dateTimeService) {
166            this.dateTimeService = dateTimeService;
167        }
168    
169        public void setBatchFileDirectoryName(String batchFileDirectoryName) {
170            this.batchFileDirectoryName = batchFileDirectoryName;
171        }
172        
173        /**
174         * Gets the pendingEntryListReportWriterService attribute. 
175         * @return Returns the pendingEntryListReportWriterService.
176         */
177        public ReportWriterService getPendingEntryListReportWriterService() {
178            return pendingEntryListReportWriterService;
179        }
180    
181        /**
182         * Sets the pendingEntryListReportWriterService attribute value.
183         * @param pendingEntryListReportWriterService The pendingEntryListReportWriterService to set.
184         */
185        public void setPendingEntryListReportWriterService(ReportWriterService pendingEntryListReportWriterService) {
186            this.pendingEntryListReportWriterService = pendingEntryListReportWriterService;
187        }
188    
189        /**
190         * Gets the pendingEntrySummaryReportWriterService attribute. 
191         * @return Returns the pendingEntrySummaryReportWriterService.
192         */
193        public ReportWriterService getPendingEntrySummaryReportWriterService() {
194            return pendingEntrySummaryReportWriterService;
195        }
196    
197        /**
198         * Sets the pendingEntrySummaryReportWriterService attribute value.
199         * @param pendingEntrySummaryReportWriterService The pendingEntrySummaryReportWriterService to set.
200         */
201        public void setPendingEntrySummaryReportWriterService(ReportWriterService pendingEntrySummaryReportWriterService) {
202            this.pendingEntrySummaryReportWriterService = pendingEntrySummaryReportWriterService;
203        }
204    
205        /**
206         * Gets the dataDictionaryService attribute. 
207         * @return Returns the dataDictionaryService.
208         */
209        public DataDictionaryService getDataDictionaryService() {
210            return dataDictionaryService;
211        }
212    
213        /**
214         * Sets the dataDictionaryService attribute value.
215         * @param dataDictionaryService The dataDictionaryService to set.
216         */
217        public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
218            this.dataDictionaryService = dataDictionaryService;
219        }
220    
221        /**
222         * A helper class which writes out the nightly out entry list report
223         */
224        protected class EntryListReport {
225            private PendingEntrySummary pendingEntrySummary;
226            private EntryReportTotalLine totalLine;
227            private Map<String, EntryReportDocumentTypeTotalLine> documentTypeTotals;
228            private EntryReportDocumentNumberTotalLine documentNumberTotal;
229            private int entryCount = 0;
230            private String suppressKey = "";
231            
232            /**
233             * Constructs a NightlyOutServiceImpl
234             */
235            public EntryListReport() {
236                pendingEntrySummary = new PendingEntrySummary();
237                totalLine = new EntryReportTotalLine();
238                documentTypeTotals = new LinkedHashMap<String, EntryReportDocumentTypeTotalLine>();
239            }
240            
241            /**
242             * Writes an entry to the list report
243             * @param entry the entry to write
244             * @param reportWriterService the reportWriterService to write the entry to
245             */
246            public void writeEntry(OriginEntryInformation entry, ReportWriterService reportWriterService) {
247                pendingEntrySummary.setOriginEntry(entry);
248                if (pendingEntrySummary.getSuppressableFieldsAsKey().equals(suppressKey)) {
249                    pendingEntrySummary.suppressCommonFields(true);
250                }
251                else if (StringUtils.isNotBlank(suppressKey)) {
252                    writeDocumentTotalLine(documentNumberTotal, reportWriterService);
253                    documentNumberTotal = new EntryReportDocumentNumberTotalLine(pendingEntrySummary.getConstantDocumentNumber());
254                }
255                
256                if (StringUtils.isBlank(suppressKey)) {
257                    documentNumberTotal = new EntryReportDocumentNumberTotalLine(pendingEntrySummary.getConstantDocumentNumber());
258                    reportWriterService.writeTableHeader(pendingEntrySummary);
259                }
260                suppressKey = pendingEntrySummary.getSuppressableFieldsAsKey();
261                
262                reportWriterService.writeTableRow(pendingEntrySummary);
263                
264                addPendingEntryToDocumentType(pendingEntrySummary, documentTypeTotals);
265                addSummaryToTotal(pendingEntrySummary, documentNumberTotal);
266                addSummaryToTotal(pendingEntrySummary, totalLine);
267                entryCount += 1;
268            }
269            
270            /**
271             * Adds the given pending entry summary to the appropriate doc type's line total
272             * @param pendingEntrySummary the pending entry summary to add
273             * @param docTypeTotals the Map of doc type line total helpers to add the summary to
274             */
275            protected void addPendingEntryToDocumentType(PendingEntrySummary pendingEntrySummary, Map<String, EntryReportDocumentTypeTotalLine> docTypeTotals) {
276                EntryReportDocumentTypeTotalLine docTypeTotal = docTypeTotals.get(pendingEntrySummary.getConstantDocumentTypeCode());
277                if (docTypeTotal == null) {
278                    docTypeTotal = new EntryReportDocumentTypeTotalLine(pendingEntrySummary.getConstantDocumentTypeCode());
279                    docTypeTotals.put(pendingEntrySummary.getConstantDocumentTypeCode(), docTypeTotal);
280                }
281                addSummaryToTotal(pendingEntrySummary, docTypeTotal);
282            }
283            
284            
285            /**
286             * Adds the given summary to the correct credit, debit, or budget total in the total line
287             * @param pendingEntrySummary the summary to add
288             * @param totalLine the entry report total line which holds debit, credit, and budget sum totals
289             */
290            protected void addSummaryToTotal(PendingEntrySummary pendingEntrySummary, EntryReportTotalLine totalLine) {
291                if (pendingEntrySummary.getDebitAmount() != null) {
292                    totalLine.addDebitAmount(pendingEntrySummary.getDebitAmount());
293                }
294                if (pendingEntrySummary.getCreditAmount() != null) {
295                    totalLine.addCreditAmount(pendingEntrySummary.getCreditAmount());
296                }
297                if (pendingEntrySummary.getBudgetAmount() != null) {
298                    totalLine.addBudgetAmount(pendingEntrySummary.getBudgetAmount());
299                }
300            }
301            
302            /**
303             * Writes totals for the document number we just finished writing out
304             * 
305             * @param documentNumberTotal EntryReportDocumentNumberTotalLine containing totals to write
306             * @param reportWriterService ReportWriterService for writing output to report
307             */
308            protected void writeDocumentTotalLine(EntryReportDocumentNumberTotalLine documentNumberTotal, ReportWriterService reportWriterService) {
309                final CurrencyFormatter formatter = new CurrencyFormatter();
310                final int amountLength = getDataDictionaryService().getAttributeMaxLength(Entry.class, KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_AMOUNT);
311                
312                reportWriterService.writeNewLines(1);
313                reportWriterService.writeFormattedMessageLine("                                          Total: %"+amountLength+"s %"+amountLength+"s %"+amountLength+"s", formatter.format(documentNumberTotal.getCreditAmount()), formatter.format(documentNumberTotal.getDebitAmount()), formatter.format(documentNumberTotal.getBudgetAmount()));
314                reportWriterService.writeNewLines(1);
315            }
316            
317            /**
318             * Completes the footer summary information for the report
319             * @param reportWriterService the reportWriterService to write the footer to
320             */
321            public void writeReportFooter(ReportWriterService reportWriterService) {
322                // write the last total line for the last entry, because we have yet to write it
323                if (documentNumberTotal != null) {
324                    // dNT may have been null if no entries were processed for the batch
325                    writeDocumentTotalLine(documentNumberTotal, reportWriterService);
326                }
327                
328                final CurrencyFormatter formatter = new CurrencyFormatter();
329                final int amountLength = getDataDictionaryService().getAttributeMaxLength(Entry.class, KFSPropertyConstants.TRANSACTION_LEDGER_ENTRY_AMOUNT);
330              
331                reportWriterService.writeNewLines(1);
332                for (String documentTypeCode : documentTypeTotals.keySet()) {
333                    final EntryReportDocumentTypeTotalLine docTypeTotal = documentTypeTotals.get(documentTypeCode);
334                    reportWriterService.writeFormattedMessageLine("       Totals for Document Type %4s Cnt %6d: %"+amountLength+"s %"+amountLength+"s %"+amountLength+"s",documentTypeCode, docTypeTotal.getEntryCount(), formatter.format(docTypeTotal.getCreditAmount()), formatter.format(docTypeTotal.getDebitAmount()), formatter.format(docTypeTotal.getBudgetAmount()));
335                }
336                
337                reportWriterService.writeNewLines(1);
338                reportWriterService.writeFormattedMessageLine("                        Grand Totals Cnt %6d: %"+amountLength+"s %"+amountLength+"s %"+amountLength+"s", new Integer(entryCount), formatter.format(totalLine.getCreditAmount()), formatter.format(totalLine.getDebitAmount()), formatter.format(totalLine.getBudgetAmount()));
339            }
340            
341            /**
342             * Summarizes entries for the pending entry view
343             */
344            protected class EntryReportTotalLine {
345                private KualiDecimal debitAmount = new KualiDecimal("0");
346                private KualiDecimal creditAmount = new KualiDecimal("0");
347                private KualiDecimal budgetAmount = new KualiDecimal("0");
348                
349                /**
350                 * @return the debit total
351                 */
352                public KualiDecimal getDebitAmount() {
353                    return debitAmount;
354                }
355                
356                /**
357                 * @return the credit total
358                 */
359                public KualiDecimal getCreditAmount() {
360                    return creditAmount;
361                }
362                
363                /**
364                 * @return the budget total
365                 */
366                public KualiDecimal getBudgetAmount() {
367                    return budgetAmount;
368                }
369                
370                /**
371                 * Adds the given amount to the debit total
372                 * @param debitAmount the amount to add to the debit total
373                 */
374                public void addDebitAmount(KualiDecimal debitAmount) {
375                    this.debitAmount = this.debitAmount.add(debitAmount);
376                }
377                
378                /**
379                 * Adds the given amount to the credit total
380                 * @param creditAmount the amount to add to the credit total
381                 */
382                public void addCreditAmount(KualiDecimal creditAmount) {
383                    this.creditAmount = this.creditAmount.add(creditAmount);
384                }
385                
386                /**
387                 * Adds the given amount to the budget total
388                 * @param budgetAmount the amount to add to the budget total
389                 */
390                public void addBudgetAmount(KualiDecimal budgetAmount) {
391                    this.budgetAmount = this.budgetAmount.add(budgetAmount);
392                }
393            }
394            
395            /**
396             * Summarizes pending entry data per document type
397             */
398            protected class EntryReportDocumentTypeTotalLine extends EntryReportTotalLine {
399                private String documentTypeCode;
400                private int entryCount = 0;
401                
402                /**
403                 * Constructs a NightlyOutServiceImpl
404                 * @param documentTypeCode the document type code to 
405                 */
406                public EntryReportDocumentTypeTotalLine(String documentTypeCode) {
407                    this.documentTypeCode = documentTypeCode;
408                }
409                
410                /**
411                 * @return the document type associated with this summarizer
412                 */
413                public String getDocumentTypeCode() {
414                    return this.documentTypeCode;
415                }
416                
417                /**
418                 * @return the number of entries associated with the current document type
419                 */
420                public int getEntryCount() {
421                    return this.entryCount;
422                }
423    
424                /**
425                 * Overridden to automagically udpate the entry count
426                 * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addBudgetAmount(org.kuali.rice.kns.util.KualiDecimal)
427                 */
428                @Override
429                public void addBudgetAmount(KualiDecimal budgetAmount) {
430                    super.addBudgetAmount(budgetAmount);
431                    entryCount += 1;
432                }
433    
434                /**
435                 * Overridden to automagically update the entry count
436                 * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addCreditAmount(org.kuali.rice.kns.util.KualiDecimal)
437                 */
438                @Override
439                public void addCreditAmount(KualiDecimal creditAmount) {
440                    super.addCreditAmount(creditAmount);
441                    entryCount += 1;
442                }
443    
444                /**
445                 * Overridden to automagically update the entry count
446                 * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addDebitAmount(org.kuali.rice.kns.util.KualiDecimal)
447                 */
448                @Override
449                public void addDebitAmount(KualiDecimal debitAmount) {
450                    super.addDebitAmount(debitAmount);
451                    entryCount += 1;
452                }
453            }
454            
455            /**
456             * Summarizes pending entry data per document number
457             */
458            protected class EntryReportDocumentNumberTotalLine extends EntryReportTotalLine {
459                private String documentNumber;
460                private int entryCount = 0;
461                
462                /**
463                 * Constructs a NightlyOutServiceImpl
464                 * @param documentNumber the document number to total
465                 */
466                public EntryReportDocumentNumberTotalLine(String documentNumber) {
467                    this.documentNumber = documentNumber;
468                }
469                
470                /**
471                 * @return the document number associated with this summarizer
472                 */
473                public String getDocumentNumber() {
474                    return this.documentNumber;
475                }
476                
477                /**
478                 * @return the number of entries associated with the current document number
479                 */
480                public int getEntryCount() {
481                    return this.entryCount;
482                }
483    
484                /**
485                 * Overridden to automagically udpate the entry count
486                 * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addBudgetAmount(org.kuali.rice.kns.util.KualiDecimal)
487                 */
488                @Override
489                public void addBudgetAmount(KualiDecimal budgetAmount) {
490                    super.addBudgetAmount(budgetAmount);
491                    entryCount += 1;
492                }
493    
494                /**
495                 * Overridden to automagically update the entry count
496                 * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addCreditAmount(org.kuali.rice.kns.util.KualiDecimal)
497                 */
498                @Override
499                public void addCreditAmount(KualiDecimal creditAmount) {
500                    super.addCreditAmount(creditAmount);
501                    entryCount += 1;
502                }
503    
504                /**
505                 * Overridden to automagically update the entry count
506                 * @see org.kuali.kfs.gl.batch.service.impl.NightlyOutServiceImpl.EntryReportTotalLine#addDebitAmount(org.kuali.rice.kns.util.KualiDecimal)
507                 */
508                @Override
509                public void addDebitAmount(KualiDecimal debitAmount) {
510                    super.addDebitAmount(debitAmount);
511                    entryCount += 1;
512                }
513            }
514        }
515    }