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.sys.document;
017    
018    import java.util.ArrayList;
019    import java.util.HashMap;
020    import java.util.Iterator;
021    import java.util.List;
022    import java.util.Map;
023    
024    import org.apache.commons.lang.StringUtils;
025    import org.kuali.kfs.sys.KFSConstants;
026    import org.kuali.kfs.sys.businessobject.AccountingLine;
027    import org.kuali.kfs.sys.businessobject.AccountingLineBase;
028    import org.kuali.kfs.sys.businessobject.AccountingLineParser;
029    import org.kuali.kfs.sys.businessobject.AccountingLineParserBase;
030    import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
031    import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
032    import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail;
033    import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
034    import org.kuali.kfs.sys.businessobject.TargetAccountingLine;
035    import org.kuali.kfs.sys.context.SpringContext;
036    import org.kuali.kfs.sys.document.datadictionary.FinancialSystemTransactionalDocumentEntry;
037    import org.kuali.kfs.sys.document.validation.event.AccountingDocumentSaveWithNoLedgerEntryGenerationEvent;
038    import org.kuali.kfs.sys.document.validation.event.AccountingLineEvent;
039    import org.kuali.kfs.sys.document.validation.event.AddAccountingLineEvent;
040    import org.kuali.kfs.sys.document.validation.event.DeleteAccountingLineEvent;
041    import org.kuali.kfs.sys.document.validation.event.ReviewAccountingLineEvent;
042    import org.kuali.kfs.sys.document.validation.event.UpdateAccountingLineEvent;
043    import org.kuali.kfs.sys.service.AccountingLineService;
044    import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
045    import org.kuali.rice.kew.exception.WorkflowException;
046    import org.kuali.rice.kns.document.TransactionalDocument;
047    import org.kuali.rice.kns.exception.ValidationException;
048    import org.kuali.rice.kns.rule.event.KualiDocumentEvent;
049    import org.kuali.rice.kns.service.DataDictionaryService;
050    import org.kuali.rice.kns.util.KualiDecimal;
051    
052    /**
053     * Base implementation class for financial edocs.
054     */
055    public abstract class AccountingDocumentBase extends GeneralLedgerPostingDocumentBase implements AccountingDocument, GeneralLedgerPendingEntrySource {
056        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AccountingDocumentBase.class);
057    
058        protected Integer nextSourceLineNumber;
059        protected Integer nextTargetLineNumber;
060        protected List sourceAccountingLines;
061        protected List targetAccountingLines;
062    
063        protected transient FinancialSystemTransactionalDocumentEntry dataDictionaryEntry;
064        protected transient Class sourceAccountingLineClass;
065        protected transient Class targetAccountingLineClass;
066    
067        /**
068         * Default constructor.
069         */
070        public AccountingDocumentBase() {
071            super();
072            this.nextSourceLineNumber = new Integer(1);
073            this.nextTargetLineNumber = new Integer(1);
074            setSourceAccountingLines(new ArrayList());
075            setTargetAccountingLines(new ArrayList());
076        }
077    
078        /**
079         * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceAccountingLines()
080         */
081        public List getSourceAccountingLines() {
082            return this.sourceAccountingLines;
083        }
084    
085        /**
086         * @see org.kuali.kfs.sys.document.AccountingDocument#setSourceAccountingLines(java.util.List)
087         */
088        public void setSourceAccountingLines(List sourceLines) {
089            this.sourceAccountingLines = sourceLines;
090        }
091    
092        /**
093         * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetAccountingLines()
094         */
095        public List getTargetAccountingLines() {
096            return this.targetAccountingLines;
097        }
098    
099        /**
100         * @see org.kuali.kfs.sys.document.AccountingDocument#setTargetAccountingLines(java.util.List)
101         */
102        public void setTargetAccountingLines(List targetLines) {
103            this.targetAccountingLines = targetLines;
104        }
105    
106        /**
107         * This implementation sets the sequence number appropriately for the passed in source accounting line using the value that has
108         * been stored in the nextSourceLineNumber variable, adds the accounting line to the list that is aggregated by this object, and
109         * then handles incrementing the nextSourceLineNumber variable for you.
110         * 
111         * @see org.kuali.kfs.sys.document.AccountingDocument#addSourceAccountingLine(SourceAccountingLine)
112         */
113        public void addSourceAccountingLine(SourceAccountingLine line) {
114            line.setSequenceNumber(this.getNextSourceLineNumber());
115            this.sourceAccountingLines.add(line);
116            this.nextSourceLineNumber = new Integer(this.getNextSourceLineNumber().intValue() + 1);
117        }
118    
119        /**
120         * This implementation sets the sequence number appropriately for the passed in target accounting line using the value that has
121         * been stored in the nextTargetLineNumber variable, adds the accounting line to the list that is aggregated by this object, and
122         * then handles incrementing the nextTargetLineNumber variable for you.
123         * 
124         * @see org.kuali.kfs.sys.document.AccountingDocument#addTargetAccountingLine(TargetAccountingLine)
125         */
126        public void addTargetAccountingLine(TargetAccountingLine line) {
127            line.setSequenceNumber(this.getNextTargetLineNumber());
128            this.targetAccountingLines.add(line);
129            this.nextTargetLineNumber = new Integer(this.getNextTargetLineNumber().intValue() + 1);
130        }
131    
132        /**
133         * This implementation is coupled tightly with some underlying issues that the Struts PojoProcessor plugin has with how objects
134         * get instantiated within lists. The first three lines are required otherwise when the PojoProcessor tries to automatically
135         * inject values into the list, it will get an index out of bounds error if the instance at an index is being called and prior
136         * instances at indices before that one are not being instantiated. So changing the code below will cause adding lines to break
137         * if you add more than one item to the list.
138         * 
139         * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceAccountingLine(int)
140         */
141        public SourceAccountingLine getSourceAccountingLine(int index) {
142            while (getSourceAccountingLines().size() <= index) {
143                try {
144                    getSourceAccountingLines().add(getSourceAccountingLineClass().newInstance());
145                }
146                catch (InstantiationException e) {
147                    throw new RuntimeException("Unable to get class");
148                }
149                catch (IllegalAccessException e) {
150                    throw new RuntimeException("Unable to get class");
151                }
152            }
153            return (SourceAccountingLine) getSourceAccountingLines().get(index);
154        }
155    
156        /**
157         * This implementation is coupled tightly with some underlying issues that the Struts PojoProcessor plugin has with how objects
158         * get instantiated within lists. The first three lines are required otherwise when the PojoProcessor tries to automatically
159         * inject values into the list, it will get an index out of bounds error if the instance at an index is being called and prior
160         * instances at indices before that one are not being instantiated. So changing the code below will cause adding lines to break
161         * if you add more than one item to the list.
162         * 
163         * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetAccountingLine(int)
164         */
165        public TargetAccountingLine getTargetAccountingLine(int index) {
166            while (getTargetAccountingLines().size() <= index) {
167                try {
168                    getTargetAccountingLines().add(getTargetAccountingLineClass().newInstance());
169                }
170                catch (InstantiationException e) {
171                    throw new RuntimeException("Unable to get class");
172                }
173                catch (IllegalAccessException e) {
174                    throw new RuntimeException("Unable to get class");
175                }
176            }
177            return (TargetAccountingLine) getTargetAccountingLines().get(index);
178        }
179    
180        /**
181         * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceAccountingLinesSectionTitle()
182         */
183        public String getSourceAccountingLinesSectionTitle() {
184            return KFSConstants.SOURCE;
185        }
186    
187        /**
188         * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetAccountingLinesSectionTitle()
189         */
190        public String getTargetAccountingLinesSectionTitle() {
191            return KFSConstants.TARGET;
192        }
193    
194        /**
195         * Since one side of the document should match the other and the document should balance, the total dollar amount for the
196         * document should either be the expense line or the income line. This is the default implementation of this interface method so
197         * it should be overridden appropriately if your document cannot make this assumption.
198         * 
199         * @return if target total is zero, source total, otherwise target total
200         */
201        public KualiDecimal getTotalDollarAmount() {
202            return getTargetTotal().equals(KualiDecimal.ZERO) ? getSourceTotal() : getTargetTotal();
203        }
204    
205        /**
206         * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceTotal()
207         */
208        public KualiDecimal getSourceTotal() {
209            KualiDecimal total = KualiDecimal.ZERO;
210            AccountingLineBase al = null;
211            Iterator iter = getSourceAccountingLines().iterator();
212            while (iter.hasNext()) {
213                al = (AccountingLineBase) iter.next();
214    
215                KualiDecimal amount = al.getAmount();
216                if (amount != null) {
217                    total = total.add(amount);
218                }
219            }
220            return total;
221        }
222    
223        /**
224         * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetTotal()
225         */
226        public KualiDecimal getTargetTotal() {
227            KualiDecimal total = KualiDecimal.ZERO;
228            AccountingLineBase al = null;
229            Iterator iter = getTargetAccountingLines().iterator();
230            while (iter.hasNext()) {
231                al = (AccountingLineBase) iter.next();
232    
233                KualiDecimal amount = al.getAmount();
234                if (amount != null) {
235                    total = total.add(amount);
236                }
237            }
238            return total;
239        }
240    
241        /**
242         * @see org.kuali.kfs.sys.document.AccountingDocument#getNextSourceLineNumber()
243         */
244        public Integer getNextSourceLineNumber() {
245            return this.nextSourceLineNumber;
246        }
247    
248        /**
249         * @see org.kuali.kfs.sys.document.AccountingDocument#setNextSourceLineNumber(java.lang.Integer)
250         */
251        public void setNextSourceLineNumber(Integer nextLineNumber) {
252            this.nextSourceLineNumber = nextLineNumber;
253        }
254    
255        /**
256         * @see org.kuali.kfs.sys.document.AccountingDocument#getNextTargetLineNumber()
257         */
258        public Integer getNextTargetLineNumber() {
259            return this.nextTargetLineNumber;
260        }
261    
262        /**
263         * @see org.kuali.kfs.sys.document.AccountingDocument#setNextTargetLineNumber(java.lang.Integer)
264         */
265        public void setNextTargetLineNumber(Integer nextLineNumber) {
266            this.nextTargetLineNumber = nextLineNumber;
267        }
268    
269        /**
270         * Returns the default Source accounting line class.
271         * 
272         * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceAccountingLineClass()
273         */
274        public Class getSourceAccountingLineClass() {
275            if (sourceAccountingLineClass == null) {
276                sourceAccountingLineClass = (getDataDictionaryEntry().getAccountingLineGroups() != null && getDataDictionaryEntry().getAccountingLineGroups().containsKey("source") && getDataDictionaryEntry().getAccountingLineGroups().get("source").getAccountingLineClass() != null) ? getDataDictionaryEntry().getAccountingLineGroups().get("source").getAccountingLineClass() : SourceAccountingLine.class;
277            }
278            return sourceAccountingLineClass;
279        }
280    
281        /**
282         * Returns the default Target accounting line class.
283         * 
284         * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetAccountingLineClass()
285         */
286        public Class getTargetAccountingLineClass() {
287            if (targetAccountingLineClass == null) {
288                targetAccountingLineClass = (getDataDictionaryEntry().getAccountingLineGroups() != null && getDataDictionaryEntry().getAccountingLineGroups().containsKey("target") && getDataDictionaryEntry().getAccountingLineGroups().get("target").getAccountingLineClass() != null) ? getDataDictionaryEntry().getAccountingLineGroups().get("target").getAccountingLineClass() : TargetAccountingLine.class;
289            }
290            return targetAccountingLineClass;
291        }
292    
293        /**
294         * Used to get the appropriate <code>{@link AccountingLineParser}</code> for the <code>Document</code>
295         * 
296         * @return AccountingLineParser
297         */
298        public AccountingLineParser getAccountingLineParser() {
299            try {
300                if (getDataDictionaryEntry().getImportedLineParserClass() != null) {
301                    return getDataDictionaryEntry().getImportedLineParserClass().newInstance();
302                }
303            }
304            catch (InstantiationException ie) {
305                throw new IllegalStateException("Accounting Line Parser class " + getDataDictionaryEntry().getImportedLineParserClass().getName() + " cannot be instantiated", ie);
306            }
307            catch (IllegalAccessException iae) {
308                throw new IllegalStateException("Illegal Access Exception while attempting to instantiate Accounting Line Parser class " + getDataDictionaryEntry().getImportedLineParserClass().getName(), iae);
309            }
310            return new AccountingLineParserBase();
311        }
312    
313        /**
314         * @return the data dictionary entry for this document
315         */
316        public FinancialSystemTransactionalDocumentEntry getDataDictionaryEntry() {
317            if (dataDictionaryEntry == null) {
318                dataDictionaryEntry = (FinancialSystemTransactionalDocumentEntry) SpringContext.getBean(DataDictionaryService.class).getDataDictionary().getDocumentEntry(SpringContext.getBean(DataDictionaryService.class).getValidDocumentTypeNameByClass(getClass()));
319            }
320            return dataDictionaryEntry;
321        }
322    
323        public String getSourceAccountingLineEntryName() {
324            return this.getSourceAccountingLineClass().getName();
325        }
326    
327        public String getTargetAccountingLineEntryName() {
328            return this.getTargetAccountingLineClass().getName();
329        }
330    
331        public List<GeneralLedgerPendingEntrySourceDetail> getGeneralLedgerPendingEntrySourceDetails() {
332            List<GeneralLedgerPendingEntrySourceDetail> accountingLines = new ArrayList<GeneralLedgerPendingEntrySourceDetail>();
333            if (getSourceAccountingLines() != null) {
334                Iterator iter = getSourceAccountingLines().iterator();
335                while (iter.hasNext()) {
336                    accountingLines.add((GeneralLedgerPendingEntrySourceDetail) iter.next());
337                }
338            }
339            if (getTargetAccountingLines() != null) {
340                Iterator iter = getTargetAccountingLines().iterator();
341                while (iter.hasNext()) {
342                    accountingLines.add((GeneralLedgerPendingEntrySourceDetail) iter.next());
343                }
344            }
345            return accountingLines;
346        }
347    
348        public void customizeExplicitGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail postable, GeneralLedgerPendingEntry explicitEntry) {
349        }
350    
351        public boolean customizeOffsetGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail accountingLine, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
352            return true;
353        }
354    
355        /**
356         * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#toCopy()
357         */
358        @Override
359        public void toCopy() throws WorkflowException {
360            super.toCopy();
361            copyAccountingLines(false);
362            updatePostingYearForAccountingLines(getSourceAccountingLines());
363            updatePostingYearForAccountingLines(getTargetAccountingLines());
364        }
365    
366        /**
367         * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#toErrorCorrection()
368         */
369        @Override
370        public void toErrorCorrection() throws WorkflowException {
371            super.toErrorCorrection();
372            copyAccountingLines(true);
373        }
374    
375        /**
376         * Copies accounting lines but sets new document number and version If error correction, reverses line amount.
377         */
378        protected void copyAccountingLines(boolean isErrorCorrection) {
379            if (getSourceAccountingLines() != null) {
380                for (Iterator iter = getSourceAccountingLines().iterator(); iter.hasNext();) {
381                    AccountingLineBase sourceLine = (AccountingLineBase) iter.next();
382                    sourceLine.setDocumentNumber(getDocumentNumber());
383                    sourceLine.setVersionNumber(new Long(1));
384                    if (isErrorCorrection) {
385                        sourceLine.setAmount(sourceLine.getAmount().negated());
386                    }
387                }
388            }
389    
390            if (getTargetAccountingLines() != null) {
391                for (Iterator iter = getTargetAccountingLines().iterator(); iter.hasNext();) {
392                    AccountingLineBase targetLine = (AccountingLineBase) iter.next();
393                    targetLine.setDocumentNumber(getDocumentNumber());
394                    targetLine.setVersionNumber(new Long(1));
395                    if (isErrorCorrection) {
396                        targetLine.setAmount(targetLine.getAmount().negated());
397                    }
398                }
399            }
400        }
401    
402        /**
403         * Updates the posting year on accounting lines to be the current posting year
404         * 
405         * @param lines a List of accounting lines to update
406         */
407        protected void updatePostingYearForAccountingLines(List<AccountingLine> lines) {
408            if (lines != null) {
409                for (AccountingLine line : lines) {
410                    if (!line.getPostingYear().equals(getPostingYear())) {
411                        line.setPostingYear(getPostingYear());
412                    }
413                }
414            }
415        }
416    
417        /**
418         * @see org.kuali.rice.kns.document.DocumentBase#buildListOfDeletionAwareLists()
419         */
420        @Override
421        public List buildListOfDeletionAwareLists() {
422            List managedLists = super.buildListOfDeletionAwareLists();
423    
424            managedLists.add(getSourceAccountingLines());
425            managedLists.add(getTargetAccountingLines());
426    
427            return managedLists;
428        }
429    
430        public void prepareForSave(KualiDocumentEvent event) {
431            if (!(event instanceof AccountingDocumentSaveWithNoLedgerEntryGenerationEvent)) { // only generate entries if the rule event
432                                                                                              // specifically allows us to
433                if (!SpringContext.getBean(GeneralLedgerPendingEntryService.class).generateGeneralLedgerPendingEntries(this)) {
434                    logErrors();
435                    throw new ValidationException("general ledger GLPE generation failed");
436                }
437            }
438            super.prepareForSave(event);
439        }
440    
441        @Override
442        public List generateSaveEvents() {
443            List events = new ArrayList();
444    
445            // foreach (source, target)
446            // 1. retrieve persisted accountingLines for document
447            // 2. retrieve current accountingLines from given document
448            // 3. compare, creating add/delete/update events as needed
449            // 4. apply rules as appropriate returned events
450            List persistedSourceLines = getPersistedSourceAccountingLinesForComparison();
451            List currentSourceLines = getSourceAccountingLinesForComparison();
452    
453            List sourceEvents = generateEvents(persistedSourceLines, currentSourceLines, KFSConstants.DOCUMENT_PROPERTY_NAME + "." + KFSConstants.SOURCE_ACCOUNTING_LINE_ERRORS, this);
454            for (Iterator i = sourceEvents.iterator(); i.hasNext();) {
455                AccountingLineEvent sourceEvent = (AccountingLineEvent) i.next();
456                events.add(sourceEvent);
457            }
458    
459            List persistedTargetLines = getPersistedTargetAccountingLinesForComparison();
460            List currentTargetLines = getTargetAccountingLinesForComparison();
461    
462            List targetEvents = generateEvents(persistedTargetLines, currentTargetLines, KFSConstants.DOCUMENT_PROPERTY_NAME + "." + KFSConstants.TARGET_ACCOUNTING_LINE_ERRORS, this);
463            for (Iterator i = targetEvents.iterator(); i.hasNext();) {
464                AccountingLineEvent targetEvent = (AccountingLineEvent) i.next();
465                events.add(targetEvent);
466            }
467    
468            return events;
469        }
470    
471        /**
472         * This method gets the Target Accounting Lines that will be used in comparisons
473         * 
474         * @return
475         */
476        protected List getTargetAccountingLinesForComparison() {
477            return getTargetAccountingLines();
478        }
479    
480        /**
481         * This method gets the Persisted Target Accounting Lines that will be used in comparisons
482         * 
483         * @return
484         */
485        protected List getPersistedTargetAccountingLinesForComparison() {
486            return SpringContext.getBean(AccountingLineService.class).getByDocumentHeaderId(getTargetAccountingLineClass(), getDocumentNumber());
487        }
488    
489        /**
490         * This method gets the Source Accounting Lines that will be used in comparisons
491         * 
492         * @return
493         */
494        protected List getSourceAccountingLinesForComparison() {
495            return getSourceAccountingLines();
496        }
497    
498        /**
499         * This method gets the Persisted Source Accounting Lines that will be used in comparisons
500         * 
501         * @return
502         */
503        protected List getPersistedSourceAccountingLinesForComparison() {
504            return SpringContext.getBean(AccountingLineService.class).getByDocumentHeaderId(getSourceAccountingLineClass(), getDocumentNumber());
505        }
506    
507        /**
508         * Generates a List of instances of AccountingLineEvent subclasses, one for each accountingLine in the union of the
509         * persistedLines and currentLines lists. Events in the list will be grouped in order by event-type (review, update, add,
510         * delete).
511         * 
512         * @param persistedLines
513         * @param currentLines
514         * @param errorPathPrefix
515         * @param document
516         * @return List of AccountingLineEvent subclass instances
517         */
518        protected List generateEvents(List persistedLines, List currentLines, String errorPathPrefix, TransactionalDocument document) {
519            List addEvents = new ArrayList();
520            List updateEvents = new ArrayList();
521            List reviewEvents = new ArrayList();
522            List deleteEvents = new ArrayList();
523    
524            //
525            // generate events
526            Map persistedLineMap = buildAccountingLineMap(persistedLines);
527    
528            // (iterate through current lines to detect additions and updates, removing affected lines from persistedLineMap as we go
529            // so deletions can be detected by looking at whatever remains in persistedLineMap)
530            int index = 0;
531            for (Iterator i = currentLines.iterator(); i.hasNext(); index++) {
532                String indexedErrorPathPrefix = errorPathPrefix + "[" + index + "]";
533                AccountingLine currentLine = (AccountingLine) i.next();
534                Integer key = currentLine.getSequenceNumber();
535    
536                AccountingLine persistedLine = (AccountingLine) persistedLineMap.get(key);
537                // if line is both current and persisted...
538                if (persistedLine != null) {
539                    // ...check for updates
540                    if (!currentLine.isLike(persistedLine)) {
541                        UpdateAccountingLineEvent updateEvent = new UpdateAccountingLineEvent(indexedErrorPathPrefix, document, persistedLine, currentLine);
542                        updateEvents.add(updateEvent);
543                    }
544                    else {
545                        ReviewAccountingLineEvent reviewEvent = new ReviewAccountingLineEvent(indexedErrorPathPrefix, document, currentLine);
546                        reviewEvents.add(reviewEvent);
547                    }
548    
549                    persistedLineMap.remove(key);
550                }
551                else {
552                    // it must be a new addition
553                    AddAccountingLineEvent addEvent = new AddAccountingLineEvent(indexedErrorPathPrefix, document, currentLine);
554                    addEvents.add(addEvent);
555                }
556            }
557    
558            // detect deletions
559            for (Iterator i = persistedLineMap.entrySet().iterator(); i.hasNext();) {
560                // the deleted line is not displayed on the page, so associate the error with the whole group
561                String groupErrorPathPrefix = errorPathPrefix + KFSConstants.ACCOUNTING_LINE_GROUP_SUFFIX;
562                Map.Entry e = (Map.Entry) i.next();
563                AccountingLine persistedLine = (AccountingLine) e.getValue();
564                DeleteAccountingLineEvent deleteEvent = new DeleteAccountingLineEvent(groupErrorPathPrefix, document, persistedLine, true);
565                deleteEvents.add(deleteEvent);
566            }
567    
568    
569            //
570            // merge the lists
571            List lineEvents = new ArrayList();
572            lineEvents.addAll(reviewEvents);
573            lineEvents.addAll(updateEvents);
574            lineEvents.addAll(addEvents);
575            lineEvents.addAll(deleteEvents);
576    
577            return lineEvents;
578        }
579    
580    
581        /**
582         * @param accountingLines
583         * @return Map containing accountingLines from the given List, indexed by their sequenceNumber
584         */
585        protected Map buildAccountingLineMap(List accountingLines) {
586            Map lineMap = new HashMap();
587    
588            for (Iterator i = accountingLines.iterator(); i.hasNext();) {
589                AccountingLine accountingLine = (AccountingLine) i.next();
590                Integer sequenceNumber = accountingLine.getSequenceNumber();
591    
592                Object oldLine = lineMap.put(sequenceNumber, accountingLine);
593    
594                // verify that sequence numbers are unique...
595                if (oldLine != null) {
596                    throw new IllegalStateException("sequence number collision detected for sequence number " + sequenceNumber);
597                }
598            }
599    
600            return lineMap;
601        }
602    
603        /**
604         * Perform business rules common to all transactional documents when generating general ledger pending entries.
605         * 
606         * @see org.kuali.rice.kns.rule.GenerateGeneralLedgerPendingEntriesRule#processGenerateGeneralLedgerPendingEntries(org.kuali.rice.kns.document.AccountingDocument,
607         *      org.kuali.rice.kns.bo.AccountingLine, org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper)
608         */
609        public boolean generateGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
610            LOG.debug("processGenerateGeneralLedgerPendingEntries(AccountingDocument, AccountingLine, GeneralLedgerPendingEntrySequenceHelper) - start");
611    
612            // handle the explicit entry
613            // create a reference to the explicitEntry to be populated, so we can pass to the offset method later
614            GeneralLedgerPendingEntry explicitEntry = new GeneralLedgerPendingEntry();
615            processExplicitGeneralLedgerPendingEntry(sequenceHelper, glpeSourceDetail, explicitEntry);
616    
617            // increment the sequence counter
618            sequenceHelper.increment();
619    
620            // handle the offset entry
621            GeneralLedgerPendingEntry offsetEntry = new GeneralLedgerPendingEntry(explicitEntry);
622            boolean success = processOffsetGeneralLedgerPendingEntry(sequenceHelper, glpeSourceDetail, explicitEntry, offsetEntry);
623    
624            LOG.debug("processGenerateGeneralLedgerPendingEntries(AccountingDocument, AccountingLine, GeneralLedgerPendingEntrySequenceHelper) - end");
625            return success;
626        }
627    
628        /**
629         * This method processes all necessary information to build an explicit general ledger entry, and then adds that to the
630         * document.
631         * 
632         * @param accountingDocument
633         * @param sequenceHelper
634         * @param accountingLine
635         * @param explicitEntry
636         * @return boolean True if the explicit entry generation was successful, false otherwise.
637         */
638        protected void processExplicitGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntry explicitEntry) {
639            LOG.debug("processExplicitGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry) - start");
640    
641            // populate the explicit entry
642            SpringContext.getBean(GeneralLedgerPendingEntryService.class).populateExplicitGeneralLedgerPendingEntry(this, glpeSourceDetail, sequenceHelper, explicitEntry);
643    
644            // hook for children documents to implement document specific GLPE field mappings
645            customizeExplicitGeneralLedgerPendingEntry(glpeSourceDetail, explicitEntry);
646    
647            addPendingEntry(explicitEntry);
648    
649            LOG.debug("processExplicitGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry) - end");
650        }
651    
652        /**
653         * This method processes an accounting line's information to build an offset entry, and then adds that to the document.
654         * 
655         * @param accountingDocument
656         * @param sequenceHelper
657         * @param accountingLine
658         * @param explicitEntry
659         * @param offsetEntry
660         * @return boolean True if the offset generation is successful.
661         */
662        protected boolean processOffsetGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail postable, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
663            LOG.debug("processOffsetGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry, GeneralLedgerPendingEntry) - start");
664    
665            // populate the offset entry
666            boolean success = SpringContext.getBean(GeneralLedgerPendingEntryService.class).populateOffsetGeneralLedgerPendingEntry(getPostingYear(), explicitEntry, sequenceHelper, offsetEntry);
667    
668            // hook for children documents to implement document specific field mappings for the GLPE
669            success &= customizeOffsetGeneralLedgerPendingEntry(postable, explicitEntry, offsetEntry);
670    
671            addPendingEntry(offsetEntry);
672    
673            LOG.debug("processOffsetGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry, GeneralLedgerPendingEntry) - end");
674            return success;
675        }
676    
677        /**
678         * Returns one of the two given String's; if the preferred String is not null and has a length > 0, then it is returned,
679         * otherwise the second String is returned
680         * 
681         * @param preferredString the String you're hoping isn't blank so you can get it back
682         * @param secondaryString the "rebound" String, which you'll end up with if the preferred String is blank
683         * @return one of the String's
684         */
685        protected String getEntryValue(String preferredString, String secondaryString) {
686            return (StringUtils.isNotBlank(preferredString) ? preferredString : secondaryString);
687        }
688    
689        /**
690         * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#isDebit(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail)
691         */
692        public abstract boolean isDebit(GeneralLedgerPendingEntrySourceDetail postable);
693    
694        /**
695         * Most accounting documents don't need to generate document level GLPE's, so don't do anything in the default implementation
696         * 
697         * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#processGenerateDocumentGeneralLedgerPendingEntries(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper)
698         * @return always true, because we've always successfully not generating anything
699         */
700        public boolean generateDocumentGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
701            return true;
702        }
703    
704        /**
705         * GLPE amounts are ALWAYS positive, so just take the absolute value of the accounting line's amount.
706         * 
707         * @param accountingLine
708         * @return KualiDecimal The amount that will be used to populate the GLPE.
709         */
710        public KualiDecimal getGeneralLedgerPendingEntryAmountForDetail(GeneralLedgerPendingEntrySourceDetail postable) {
711            LOG.debug("getGeneralLedgerPendingEntryAmountForAccountingLine(AccountingLine) - start");
712    
713            KualiDecimal returnKualiDecimal = postable.getAmount().abs();
714            LOG.debug("getGeneralLedgerPendingEntryAmountForAccountingLine(AccountingLine) - end");
715            return returnKualiDecimal;
716        }
717        
718        public Class<? extends AccountingDocument> getDocumentClassForAccountingLineValueAllowedValidation() {
719            return this.getClass();
720        }
721    }