001: /*
002: * Copyright 2007 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.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/ecl1.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.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.kuali.core.document.TransactionalDocument;
025: import org.kuali.core.exceptions.ValidationException;
026: import org.kuali.core.rule.event.KualiDocumentEvent;
027: import org.kuali.core.util.KualiDecimal;
028: import org.kuali.kfs.KFSConstants;
029: import org.kuali.kfs.bo.AccountingLine;
030: import org.kuali.kfs.bo.AccountingLineBase;
031: import org.kuali.kfs.bo.AccountingLineParser;
032: import org.kuali.kfs.bo.AccountingLineParserBase;
033: import org.kuali.kfs.bo.SourceAccountingLine;
034: import org.kuali.kfs.bo.TargetAccountingLine;
035: import org.kuali.kfs.context.SpringContext;
036: import org.kuali.kfs.rule.event.AccountingDocumentSaveWithNoLedgerEntryGenerationEvent;
037: import org.kuali.kfs.rule.event.AccountingLineEvent;
038: import org.kuali.kfs.rule.event.AddAccountingLineEvent;
039: import org.kuali.kfs.rule.event.DeleteAccountingLineEvent;
040: import org.kuali.kfs.rule.event.ReviewAccountingLineEvent;
041: import org.kuali.kfs.rule.event.UpdateAccountingLineEvent;
042: import org.kuali.kfs.service.AccountingLineService;
043: import org.kuali.kfs.service.GeneralLedgerPendingEntryService;
044:
045: import edu.iu.uis.eden.exception.WorkflowException;
046:
047: /**
048: * Base implementation class for financial edocs.
049: */
050: public abstract class AccountingDocumentBase extends
051: GeneralLedgerPostingDocumentBase implements AccountingDocument {
052: private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
053: .getLogger(AccountingDocumentBase.class);
054:
055: protected Integer nextSourceLineNumber;
056: protected Integer nextTargetLineNumber;
057: protected List sourceAccountingLines;
058: protected List targetAccountingLines;
059:
060: /**
061: * Default constructor.
062: */
063: public AccountingDocumentBase() {
064: super ();
065: this .nextSourceLineNumber = new Integer(1);
066: this .nextTargetLineNumber = new Integer(1);
067: setSourceAccountingLines(new ArrayList());
068: setTargetAccountingLines(new ArrayList());
069: }
070:
071: /**
072: * @see org.kuali.kfs.document.AccountingDocument#getSourceAccountingLines()
073: */
074: public List getSourceAccountingLines() {
075: return this .sourceAccountingLines;
076: }
077:
078: /**
079: * @see org.kuali.kfs.document.AccountingDocument#setSourceAccountingLines(java.util.List)
080: */
081: public void setSourceAccountingLines(List sourceLines) {
082: this .sourceAccountingLines = sourceLines;
083: }
084:
085: /**
086: * @see org.kuali.kfs.document.AccountingDocument#getTargetAccountingLines()
087: */
088: public List getTargetAccountingLines() {
089: return this .targetAccountingLines;
090: }
091:
092: /**
093: * @see org.kuali.kfs.document.AccountingDocument#setTargetAccountingLines(java.util.List)
094: */
095: public void setTargetAccountingLines(List targetLines) {
096: this .targetAccountingLines = targetLines;
097: }
098:
099: /**
100: * This implementation sets the sequence number appropriately for the passed in source accounting line using the value that has
101: * been stored in the nextSourceLineNumber variable, adds the accounting line to the list that is aggregated by this object, and
102: * then handles incrementing the nextSourceLineNumber variable for you.
103: *
104: * @see org.kuali.kfs.document.AccountingDocument#addSourceAccountingLine(SourceAccountingLine)
105: */
106: public void addSourceAccountingLine(SourceAccountingLine line) {
107: line.setSequenceNumber(this .getNextSourceLineNumber());
108: this .sourceAccountingLines.add(line);
109: this .nextSourceLineNumber = new Integer(this
110: .getNextSourceLineNumber().intValue() + 1);
111: }
112:
113: /**
114: * This implementation sets the sequence number appropriately for the passed in target accounting line using the value that has
115: * been stored in the nextTargetLineNumber variable, adds the accounting line to the list that is aggregated by this object, and
116: * then handles incrementing the nextTargetLineNumber variable for you.
117: *
118: * @see org.kuali.kfs.document.AccountingDocument#addTargetAccountingLine(TargetAccountingLine)
119: */
120: public void addTargetAccountingLine(TargetAccountingLine line) {
121: line.setSequenceNumber(this .getNextTargetLineNumber());
122: this .targetAccountingLines.add(line);
123: this .nextTargetLineNumber = new Integer(this
124: .getNextTargetLineNumber().intValue() + 1);
125: }
126:
127: /**
128: * This implementation is coupled tightly with some underlying issues that the Struts PojoProcessor plugin has with how objects
129: * get instantiated within lists. The first three lines are required otherwise when the PojoProcessor tries to automatically
130: * 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
131: * instances at indices before that one are not being instantiated. So changing the code below will cause adding lines to break
132: * if you add more than one item to the list.
133: *
134: * @see org.kuali.kfs.document.AccountingDocument#getSourceAccountingLine(int)
135: */
136: public SourceAccountingLine getSourceAccountingLine(int index) {
137: while (getSourceAccountingLines().size() <= index) {
138: try {
139: getSourceAccountingLines().add(
140: getSourceAccountingLineClass().newInstance());
141: } catch (InstantiationException e) {
142: throw new RuntimeException("Unable to get class");
143: } catch (IllegalAccessException e) {
144: throw new RuntimeException("Unable to get class");
145: }
146: }
147: return (SourceAccountingLine) getSourceAccountingLines().get(
148: index);
149: }
150:
151: /**
152: * This implementation is coupled tightly with some underlying issues that the Struts PojoProcessor plugin has with how objects
153: * get instantiated within lists. The first three lines are required otherwise when the PojoProcessor tries to automatically
154: * 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
155: * instances at indices before that one are not being instantiated. So changing the code below will cause adding lines to break
156: * if you add more than one item to the list.
157: *
158: * @see org.kuali.kfs.document.AccountingDocument#getTargetAccountingLine(int)
159: */
160: public TargetAccountingLine getTargetAccountingLine(int index) {
161: while (getTargetAccountingLines().size() <= index) {
162: try {
163: getTargetAccountingLines().add(
164: getTargetAccountingLineClass().newInstance());
165: } catch (InstantiationException e) {
166: throw new RuntimeException("Unable to get class");
167: } catch (IllegalAccessException e) {
168: throw new RuntimeException("Unable to get class");
169: }
170: }
171: return (TargetAccountingLine) getTargetAccountingLines().get(
172: index);
173: }
174:
175: /**
176: * @see org.kuali.kfs.document.AccountingDocument#getSourceAccountingLinesSectionTitle()
177: */
178: public String getSourceAccountingLinesSectionTitle() {
179: return KFSConstants.SOURCE;
180: }
181:
182: /**
183: * @see org.kuali.kfs.document.AccountingDocument#getTargetAccountingLinesSectionTitle()
184: */
185: public String getTargetAccountingLinesSectionTitle() {
186: return KFSConstants.TARGET;
187: }
188:
189: /**
190: * Since one side of the document should match the other and the document should balance, the total dollar amount for the
191: * document should either be the expense line or the income line. This is the default implementation of this interface method so
192: * it should be overridden appropriately if your document cannot make this assumption.
193: *
194: * @return if target total is zero, source total, otherwise target total
195: */
196: public KualiDecimal getTotalDollarAmount() {
197: return getTargetTotal().equals(new KualiDecimal(0)) ? getSourceTotal()
198: : getTargetTotal();
199: }
200:
201: /**
202: * @see org.kuali.kfs.document.AccountingDocument#getSourceTotal()
203: */
204: public KualiDecimal getSourceTotal() {
205: KualiDecimal total = new KualiDecimal(0);
206: AccountingLineBase al = null;
207: Iterator iter = getSourceAccountingLines().iterator();
208: while (iter.hasNext()) {
209: al = (AccountingLineBase) iter.next();
210:
211: KualiDecimal amount = al.getAmount();
212: if (amount != null) {
213: total = total.add(amount);
214: }
215: }
216: return total;
217: }
218:
219: /**
220: * @see org.kuali.kfs.document.AccountingDocument#getTargetTotal()
221: */
222: public KualiDecimal getTargetTotal() {
223: KualiDecimal total = new KualiDecimal(0);
224: AccountingLineBase al = null;
225: Iterator iter = getTargetAccountingLines().iterator();
226: while (iter.hasNext()) {
227: al = (AccountingLineBase) iter.next();
228:
229: KualiDecimal amount = al.getAmount();
230: if (amount != null) {
231: total = total.add(amount);
232: }
233: }
234: return total;
235: }
236:
237: /**
238: * @see org.kuali.kfs.document.AccountingDocument#getNextSourceLineNumber()
239: */
240: public Integer getNextSourceLineNumber() {
241: return this .nextSourceLineNumber;
242: }
243:
244: /**
245: * @see org.kuali.kfs.document.AccountingDocument#setNextSourceLineNumber(java.lang.Integer)
246: */
247: public void setNextSourceLineNumber(Integer nextLineNumber) {
248: this .nextSourceLineNumber = nextLineNumber;
249: }
250:
251: /**
252: * @see org.kuali.kfs.document.AccountingDocument#getNextTargetLineNumber()
253: */
254: public Integer getNextTargetLineNumber() {
255: return this .nextTargetLineNumber;
256: }
257:
258: /**
259: * @see org.kuali.kfs.document.AccountingDocument#setNextTargetLineNumber(java.lang.Integer)
260: */
261: public void setNextTargetLineNumber(Integer nextLineNumber) {
262: this .nextTargetLineNumber = nextLineNumber;
263: }
264:
265: /**
266: * Returns the default Source accounting line class.
267: *
268: * @see org.kuali.kfs.document.AccountingDocument#getSourceAccountingLineClass()
269: */
270: public Class getSourceAccountingLineClass() {
271: return SourceAccountingLine.class;
272: }
273:
274: /**
275: * Returns the default Target accounting line class.
276: *
277: * @see org.kuali.kfs.document.AccountingDocument#getTargetAccountingLineClass()
278: */
279: public Class getTargetAccountingLineClass() {
280: return TargetAccountingLine.class;
281: }
282:
283: /**
284: * Used to get the appropriate <code>{@link AccountingLineParser}</code> for the <code>Document</code>
285: *
286: * @return AccountingLineParser
287: */
288: public AccountingLineParser getAccountingLineParser() {
289: return new AccountingLineParserBase();
290: }
291:
292: public String getSourceAccountingLineEntryName() {
293: return this .getSourceAccountingLineClass().getName();
294: }
295:
296: public String getTargetAccountingLineEntryName() {
297: return this .getTargetAccountingLineClass().getName();
298: }
299:
300: /**
301: * @see org.kuali.kfs.document.GeneralLedgerPostingDocumentBase#toCopy()
302: */
303: @Override
304: public void toCopy() throws WorkflowException {
305: super .toCopy();
306: copyAccountingLines(false);
307: }
308:
309: /**
310: * @see org.kuali.kfs.document.GeneralLedgerPostingDocumentBase#toErrorCorrection()
311: */
312: @Override
313: public void toErrorCorrection() throws WorkflowException {
314: super .toErrorCorrection();
315: copyAccountingLines(true);
316: }
317:
318: /**
319: * Copies accounting lines but sets new document number and version If error correction, reverses line amount.
320: */
321: protected void copyAccountingLines(boolean isErrorCorrection) {
322: if (getSourceAccountingLines() != null) {
323: for (Iterator iter = getSourceAccountingLines().iterator(); iter
324: .hasNext();) {
325: AccountingLineBase sourceLine = (AccountingLineBase) iter
326: .next();
327: sourceLine.setDocumentNumber(getDocumentNumber());
328: sourceLine.setVersionNumber(new Long(1));
329: if (isErrorCorrection) {
330: sourceLine.setAmount(sourceLine.getAmount()
331: .negated());
332: }
333: }
334: }
335:
336: if (getTargetAccountingLines() != null) {
337: for (Iterator iter = getTargetAccountingLines().iterator(); iter
338: .hasNext();) {
339: AccountingLineBase targetLine = (AccountingLineBase) iter
340: .next();
341: targetLine.setDocumentNumber(getDocumentNumber());
342: targetLine.setVersionNumber(new Long(1));
343: if (isErrorCorrection) {
344: targetLine.setAmount(targetLine.getAmount()
345: .negated());
346: }
347: }
348: }
349: }
350:
351: /**
352: * @see org.kuali.core.document.DocumentBase#buildListOfDeletionAwareLists()
353: */
354: @Override
355: public List buildListOfDeletionAwareLists() {
356: List managedLists = super .buildListOfDeletionAwareLists();
357:
358: managedLists.add(getSourceAccountingLines());
359: managedLists.add(getTargetAccountingLines());
360:
361: return managedLists;
362: }
363:
364: public void prepareForSave(KualiDocumentEvent event) {
365: if (!(event instanceof AccountingDocumentSaveWithNoLedgerEntryGenerationEvent)) { // only generate entries if the rule event specifically allows us to
366: if (!SpringContext.getBean(
367: GeneralLedgerPendingEntryService.class)
368: .generateGeneralLedgerPendingEntries(this )) {
369: logErrors();
370: throw new ValidationException(
371: "general ledger GLPE generation failed");
372: }
373: }
374: super .prepareForSave(event);
375: }
376:
377: @Override
378: public List generateSaveEvents() {
379: List events = new ArrayList();
380:
381: // foreach (source, target)
382: // 1. retrieve persisted accountingLines for document
383: // 2. retrieve current accountingLines from given document
384: // 3. compare, creating add/delete/update events as needed
385: // 4. apply rules as appropriate returned events
386: List persistedSourceLines = getPersistedSourceAccountingLinesForComparison();
387: List currentSourceLines = getSourceAccountingLinesForComparison();
388:
389: List sourceEvents = generateEvents(
390: persistedSourceLines,
391: currentSourceLines,
392: KFSConstants.DOCUMENT_PROPERTY_NAME
393: + "."
394: + KFSConstants.EXISTING_SOURCE_ACCT_LINE_PROPERTY_NAME,
395: this );
396: for (Iterator i = sourceEvents.iterator(); i.hasNext();) {
397: AccountingLineEvent sourceEvent = (AccountingLineEvent) i
398: .next();
399: events.add(sourceEvent);
400: }
401:
402: List persistedTargetLines = getPersistedTargetAccountingLinesForComparison();
403: List currentTargetLines = getTargetAccountingLinesForComparison();
404:
405: List targetEvents = generateEvents(
406: persistedTargetLines,
407: currentTargetLines,
408: KFSConstants.DOCUMENT_PROPERTY_NAME
409: + "."
410: + KFSConstants.EXISTING_TARGET_ACCT_LINE_PROPERTY_NAME,
411: this );
412: for (Iterator i = targetEvents.iterator(); i.hasNext();) {
413: AccountingLineEvent targetEvent = (AccountingLineEvent) i
414: .next();
415: events.add(targetEvent);
416: }
417:
418: return events;
419: }
420:
421: /**
422: * This method gets the Target Accounting Lines that will be used in comparisons
423: *
424: * @return
425: */
426: protected List getTargetAccountingLinesForComparison() {
427: return getTargetAccountingLines();
428: }
429:
430: /**
431: * This method gets the Persisted Target Accounting Lines that will be used in comparisons
432: *
433: * @return
434: */
435: protected List getPersistedTargetAccountingLinesForComparison() {
436: return SpringContext.getBean(AccountingLineService.class)
437: .getByDocumentHeaderId(getTargetAccountingLineClass(),
438: getDocumentNumber());
439: }
440:
441: /**
442: * This method gets the Source Accounting Lines that will be used in comparisons
443: *
444: * @return
445: */
446: protected List getSourceAccountingLinesForComparison() {
447: return getSourceAccountingLines();
448: }
449:
450: /**
451: * This method gets the Persisted Source Accounting Lines that will be used in comparisons
452: *
453: * @return
454: */
455: protected List getPersistedSourceAccountingLinesForComparison() {
456: return SpringContext.getBean(AccountingLineService.class)
457: .getByDocumentHeaderId(getSourceAccountingLineClass(),
458: getDocumentNumber());
459: }
460:
461: /**
462: * Generates a List of instances of AccountingLineEvent subclasses, one for each accountingLine in the union of the
463: * persistedLines and currentLines lists. Events in the list will be grouped in order by event-type (review, update, add,
464: * delete).
465: *
466: * @param persistedLines
467: * @param currentLines
468: * @param errorPathPrefix
469: * @param document
470: * @return List of AccountingLineEvent subclass instances
471: */
472: private List generateEvents(List persistedLines, List currentLines,
473: String errorPathPrefix, TransactionalDocument document) {
474: List addEvents = new ArrayList();
475: List updateEvents = new ArrayList();
476: List reviewEvents = new ArrayList();
477: List deleteEvents = new ArrayList();
478:
479: //
480: // generate events
481: Map persistedLineMap = buildAccountingLineMap(persistedLines);
482:
483: // (iterate through current lines to detect additions and updates, removing affected lines from persistedLineMap as we go
484: // so deletions can be detected by looking at whatever remains in persistedLineMap)
485: int index = 0;
486: for (Iterator i = currentLines.iterator(); i.hasNext(); index++) {
487: String indexedErrorPathPrefix = errorPathPrefix + "["
488: + index + "]";
489: AccountingLine currentLine = (AccountingLine) i.next();
490: Integer key = currentLine.getSequenceNumber();
491:
492: AccountingLine persistedLine = (AccountingLine) persistedLineMap
493: .get(key);
494: // if line is both current and persisted...
495: if (persistedLine != null) {
496: // ...check for updates
497: if (!currentLine.isLike(persistedLine)) {
498: UpdateAccountingLineEvent updateEvent = new UpdateAccountingLineEvent(
499: indexedErrorPathPrefix, document,
500: persistedLine, currentLine);
501: updateEvents.add(updateEvent);
502: } else {
503: ReviewAccountingLineEvent reviewEvent = new ReviewAccountingLineEvent(
504: indexedErrorPathPrefix, document,
505: currentLine);
506: reviewEvents.add(reviewEvent);
507: }
508:
509: persistedLineMap.remove(key);
510: } else {
511: // it must be a new addition
512: AddAccountingLineEvent addEvent = new AddAccountingLineEvent(
513: indexedErrorPathPrefix, document, currentLine);
514: addEvents.add(addEvent);
515: }
516: }
517:
518: // detect deletions
519: for (Iterator i = persistedLineMap.entrySet().iterator(); i
520: .hasNext();) {
521: // the deleted line is not displayed on the page, so associate the error with the whole group
522: String groupErrorPathPrefix = errorPathPrefix
523: + KFSConstants.ACCOUNTING_LINE_GROUP_SUFFIX;
524: Map.Entry e = (Map.Entry) i.next();
525: AccountingLine persistedLine = (AccountingLine) e
526: .getValue();
527: DeleteAccountingLineEvent deleteEvent = new DeleteAccountingLineEvent(
528: groupErrorPathPrefix, document, persistedLine, true);
529: deleteEvents.add(deleteEvent);
530: }
531:
532: //
533: // merge the lists
534: List lineEvents = new ArrayList();
535: lineEvents.addAll(reviewEvents);
536: lineEvents.addAll(updateEvents);
537: lineEvents.addAll(addEvents);
538: lineEvents.addAll(deleteEvents);
539:
540: return lineEvents;
541: }
542:
543: /**
544: * @param accountingLines
545: * @return Map containing accountingLines from the given List, indexed by their sequenceNumber
546: */
547: private Map buildAccountingLineMap(List accountingLines) {
548: Map lineMap = new HashMap();
549:
550: for (Iterator i = accountingLines.iterator(); i.hasNext();) {
551: AccountingLine accountingLine = (AccountingLine) i.next();
552: Integer sequenceNumber = accountingLine.getSequenceNumber();
553:
554: Object oldLine = lineMap
555: .put(sequenceNumber, accountingLine);
556:
557: // verify that sequence numbers are unique...
558: if (oldLine != null) {
559: throw new IllegalStateException(
560: "sequence number collision detected for sequence number "
561: + sequenceNumber);
562: }
563: }
564:
565: return lineMap;
566: }
567: }
|